Skip to content

Conversation

bernardobelchior
Copy link
Member

@bernardobelchior bernardobelchior commented Jul 8, 2025

Fixes #18516.

This PR adds a symlog scale, but with some differences compared to other scales.

Due to d3/d3-scale#162, by default ticks don't look good on a symlog scale:

image

Basically, symlog computes ticks as if it were a linear scale, but it isn't, so there's many overlaps and ticks don't make it clear that it's a log scale.

To work around this, I've created a wrapper scale that converts the symlog scale into three so that the ticks look better. You can read more about it here.

This might impact performance slightly, but we can try to optimize later if we feel it's a burden.

The ticks looks much better now:

image

Even when zooming, ticks look nice and without overlaps:

Screen.Recording.2025-07-10.at.16.46.47.mov

What I'd like to understand is: are you comfortable with this workaround? Another option would be to implement the proper tick formatting algorithm, which might either be available or we could adapt d3 log scale's.

@bernardobelchior bernardobelchior added type: new feature Expand the scope of the product to solve a new problem. scope: charts Changes related to the charts. labels Jul 8, 2025
@mui-bot
Copy link

mui-bot commented Jul 8, 2025

Deploy preview: https://deploy-preview-18729--material-ui-x.netlify.app/

Updated pages:

Bundle size report

Total Size Change: 🔺+45.7KB(+0.34%) - Total Gzip Change: 🔺+15.5KB(+0.38%)
Files: 122 total (0 added, 0 removed, 31 changed)

Show details for 100 more bundles (22 more not shown)

@mui/x-charts-pro/ChartsToolbarProparsed: 🔺+1.49KB(+2.24%) gzip: 🔺+500B(+2.11%)
@mui/x-charts/ChartContainerparsed: 🔺+1.49KB(+0.98%) gzip: 🔺+508B(+1.09%)
@mui/x-charts/ChartDataProviderparsed: 🔺+1.49KB(+1.02%) gzip: 🔺+494B(+1.10%)
@mui/x-charts/ChartsAxisparsed: 🔺+1.49KB(+2.07%) gzip: 🔺+502B(+1.96%)
@mui/x-charts/ChartsGridparsed: 🔺+1.49KB(+2.49%) gzip: 🔺+504B(+2.35%)
@mui/x-charts/ChartsLegendparsed: 🔺+1.49KB(+2.04%) gzip: 🔺+523B(+2.05%)
@mui/x-charts/ChartsXAxisparsed: 🔺+1.49KB(+2.18%) gzip: 🔺+508B(+2.05%)
@mui/x-charts/ChartsYAxisparsed: 🔺+1.49KB(+2.21%) gzip: 🔺+523B(+2.15%)
@mui/x-charts/ChartsAxisHighlightparsed: 🔺+1.49KB(+2.40%) gzip: 🔺+493B(+2.25%)
@mui/x-charts/ChartsReferenceLineparsed: 🔺+1.49KB(+2.43%) gzip: 🔺+496B(+2.25%)
@mui/x-charts/ChartsSurfaceparsed: 🔺+1.49KB(+2.45%) gzip: 🔺+488B(+2.26%)
@mui/x-charts/Gaugeparsed: 🔺+1.49KB(+1.02%) gzip: 🔺+495B(+1.09%)
@mui/x-charts-pro/BarChartProparsed: 🔺+1.46KB(+0.52%) gzip: 🔺+488B(+0.56%)
@mui/x-charts-pro/ChartDataProviderProparsed: 🔺+1.46KB(+0.83%) gzip: 🔺+513B(+0.93%)
@mui/x-charts-pro/FunnelChartparsed: 🔺+1.46KB(+0.59%) gzip: 🔺+504B(+0.66%)
@mui/x-charts-pro/Heatmapparsed: 🔺+1.46KB(+0.63%) gzip: 🔺+512B(+0.70%)
@mui/x-charts-pro/LineChartProparsed: 🔺+1.46KB(+0.51%) gzip: 🔺+534B(+0.60%)
@mui/x-charts-pro/PieChartProparsed: 🔺+1.46KB(+0.62%) gzip: 🔺+516B(+0.70%)
@mui/x-charts-pro/ScatterChartProparsed: 🔺+1.46KB(+0.54%) gzip: 🔺+493B(+0.57%)
@mui/x-charts/BarChartparsed: 🔺+1.46KB(+0.69%) gzip: 🔺+490B(+0.74%)
@mui/x-charts/ChartsTooltipparsed: 🔺+1.46KB(+1.90%) gzip: 🔺+500B(+1.88%)
@mui/x-charts/LineChartparsed: 🔺+1.46KB(+0.64%) gzip: 🔺+480B(+0.68%)
@mui/x-charts/PieChartparsed: 🔺+1.46KB(+0.73%) gzip: 🔺+509B(+0.83%)
@mui/x-charts/RadarChartparsed: 🔺+1.46KB(+0.75%) gzip: 🔺+498B(+0.83%)
@mui/x-charts/ScatterChartparsed: 🔺+1.46KB(+0.74%) gzip: 🔺+506B(+0.82%)
@mui/x-charts/SparkLineChartparsed: 🔺+1.46KB(+0.69%) gzip: 🔺+496B(+0.76%)
@mui/x-chartsparsed: 🔺+1.46KB(+0.47%) gzip: 🔺+487B(+0.53%)
@mui/x-charts-proparsed: 🔺+1.46KB(+0.38%) gzip: 🔺+495B(+0.42%)
@mui/x-charts-pro/ChartContainerProparsed: 🔺+1.46KB(+0.79%) gzip: 🔺+483B(+0.84%)
@mui/x-charts-pro/ChartZoomSliderparsed: 🔺+1.46KB(+1.37%) gzip: 🔺+498B(+1.39%)
@mui/x-charts-pro/RadarChartProparsed: 🔺+1.46KB(+0.70%) gzip: 🔺+492B(+0.77%)
@mui/x-charts/ChartsClipPathparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-charts/ChartsLabelparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-charts/ChartsLocalizationProviderparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-charts/ChartsOverlayparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-charts/ChartsTextparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-charts/Toolbarparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-data-gridparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-data-grid-premiumparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-data-grid-premium/DataGridPremiumparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-data-grid-proparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-data-grid-pro/DataGridProparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-data-grid/DataGridparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-date-pickersparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-date-pickers-proparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-date-pickers-pro/AdapterDateFnsparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-date-pickers-pro/AdapterDateFnsJalaliparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-date-pickers-pro/AdapterDayjsparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-date-pickers-pro/AdapterLuxonparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-date-pickers-pro/AdapterMomentparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-date-pickers-pro/AdapterMomentHijriparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-date-pickers-pro/AdapterMomentJalaaliparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-date-pickers-pro/DateRangeCalendarparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-date-pickers-pro/DateRangePickerparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-date-pickers-pro/DateRangePickerDayparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-date-pickers-pro/DateRangePickerDay2parsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-date-pickers-pro/DateTimeRangePickerparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-date-pickers-pro/DesktopDateRangePickerparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-date-pickers-pro/DesktopDateTimeRangePickerparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-date-pickers-pro/DesktopTimeRangePickerparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-date-pickers-pro/LocalizationProviderparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-date-pickers-pro/MobileDateRangePickerparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-date-pickers-pro/MobileDateTimeRangePickerparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-date-pickers-pro/MobileTimeRangePickerparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-date-pickers-pro/MultiInputDateRangeFieldparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-date-pickers-pro/MultiInputDateTimeRangeFieldparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-date-pickers-pro/MultiInputTimeRangeFieldparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-date-pickers-pro/PickersRangeCalendarHeaderparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-date-pickers-pro/SingleInputDateRangeFieldparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-date-pickers-pro/SingleInputDateTimeRangeFieldparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-date-pickers-pro/SingleInputTimeRangeFieldparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-date-pickers-pro/StaticDateRangePickerparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-date-pickers-pro/TimeRangePickerparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-date-pickers/AdapterDateFnsparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-date-pickers/AdapterDateFnsBaseparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-date-pickers/AdapterDateFnsJalaliparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-date-pickers/AdapterDayjsparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-date-pickers/AdapterLuxonparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-date-pickers/AdapterMomentparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-date-pickers/AdapterMomentHijriparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-date-pickers/AdapterMomentJalaaliparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-date-pickers/DateCalendarparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-date-pickers/DateFieldparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-date-pickers/DatePickerparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-date-pickers/DateTimeFieldparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-date-pickers/DateTimePickerparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-date-pickers/DayCalendarSkeletonparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-date-pickers/DesktopDatePickerparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-date-pickers/DesktopDateTimePickerparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-date-pickers/DesktopTimePickerparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-date-pickers/DigitalClockparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-date-pickers/LocalizationProviderparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-date-pickers/MobileDatePickerparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-date-pickers/MobileDateTimePickerparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-date-pickers/MobileTimePickerparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-date-pickers/MonthCalendarparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-date-pickers/MultiSectionDigitalClockparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-date-pickers/PickerDay2parsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-date-pickers/PickersActionBarparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-date-pickers/PickersCalendarHeaderparsed: 0B(0.00%) gzip: 0B(0.00%)

Details of bundle changes

Generated by 🚫 dangerJS against 9361d9f

Copy link

codspeed-hq bot commented Jul 8, 2025

CodSpeed Performance Report

Merging #18729 will not alter performance

Comparing bernardobelchior:charts-symlog (9361d9f) with master (de2de2e)

Summary

✅ 10 untouched benchmarks

const { negativeScale, linearScale, positiveScale } = generateScales(scale);

// Workaround for https://github.com/d3/d3-scale/issues/162
scale.ticks = (count?: number) => {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For ticks to work, I'm breaking the symlog scale into three scales:

  • Log scale for the < -constant;
  • Linear scale for the [-constant, constant] interval;
  • Log scale for > constant.

This then mean that we need to call each scale with the appropriate tick number. This is the solution that I quickly made.

Still need to test it more thoroughly, but it seems to work well.

Copy link

This pull request has conflicts, please resolve those before we can evaluate the pull request.

@github-actions github-actions bot added the PR: out-of-date The pull request has merge conflicts and can't be merged. label Jul 10, 2025
@github-actions github-actions bot removed the PR: out-of-date The pull request has merge conflicts and can't be merged. label Jul 10, 2025
@bernardobelchior bernardobelchior marked this pull request as ready for review July 10, 2025 14:51
@@ -77,6 +77,16 @@ You can test all configuration options in the following demo:

{{"demo": "TickPlacementBars.js"}}

### Log scale
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be in /axis page instead of bar/line pages? 🤔

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm afraid it won't be discoverable there. Do you think it'll be easy to find? That was the only reason I added the docs to these line/bar sections

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about introducing the same baseline concept as in the line chart.

The behavior of baseline: 'min' looks perfect for a bar chart. with log scale. Except that the definition of this baseline is probably better in the axis definition than the series to enable stacking strategy 🤔

From that you could have the baseline section in both lines and bar charts pages. And an info point indicating that for log scale with negative and positive values, we also have a sym log scale, with a link to an axis section. And their you have all the space you want to explain symlog scale, advantage and issues in a single place.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What are some use cases for the baseline? I'm failing to come up with one.

If we're worried about usage of the symlog scale, we can have a section in the axis docs and just a small section in bar/line that links there. Then we can explain why you should/shouldn't use the symlog scale. What do you think?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we're worried about usage of the symlog scale, we can have a section in the axis docs and just a small section in bar/line that links there. Then we can explain why you should/shouldn't use the symlog scale. What do you think?

This sounds good

What are some use cases for the baseline? I'm failing to come up with one.

In you're current demo for the bar chart, data varies from 1 to 10.000 which is nice because the symlog has a log scale that goes from 1 to whatever. But if your data goes from 0.01 to 100 (same data but divided by 100. Then a part of it is in the linear scale and another one in the log scale.

image

To get something more meaning full you would need to set constant: 0.01 in the y-axis. Which implies you know what is your minimal/maximal value.

image

My proposal is to use allows setting baseline: 'min' with log scale, such that if the minimal value is for example 0.01235, thanks to nice() the bar chart would display a log scale that starts at 0.01

And we keep the symlog only for bar charts with both positive and negative values. The case with 0 could be solved by skipping display

But that's more a follow-up to allow user better fine tune their choice between log and symlog

Copy link
Member Author

@bernardobelchior bernardobelchior Jul 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This sounds good

Updated 👍

My proposal is to use allows setting baseline: 'min' with log scale, such that if the minimal value is for example 0.01235, thanks to nice() the bar chart would display a log scale that starts at 0.01

We could add that, yes. Baseline 'min' would only work for positive bars, though. If you have a mix of positive and negative bars, you'd need to use the symlog scale, I believe.

Would it also make sense to add a baseline with a specific value? It allows the creation of weird bar charts where the ratio between bars isn't correct, which I don't love, but there might be some valid use cases we aren't thinking of.

Created issue here.

Copy link
Member

@JCQuintas JCQuintas left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see any issue with the implementation itself, left a comment on documentation placing though

@github-actions github-actions bot added the PR: out-of-date The pull request has merge conflicts and can't be merged. label Jul 15, 2025
Copy link

This pull request has conflicts, please resolve those before we can evaluate the pull request.

@github-actions github-actions bot removed the PR: out-of-date The pull request has merge conflicts and can't be merged. label Jul 15, 2025
@alexfauquette
Copy link
Member

alexfauquette commented Jul 15, 2025

The axis highlight does not work. Seems something is missing. Same with the area.

I modified the line demo as follows

<LineChart
  xAxis={[{ scaleType: 'symlog', data: [10, 1_000, 5_000, 10_000, -50_000, 20_000, 100_000, -500], }]}
  yAxis={[{ scaleType: 'symlog', width: 56 }]}
  series={[
    {
      data: [10, 1_000, 5_000, 10_000, -50_000, 20_000, 100_000, -500],
      area: true,
    },
  ]}
  height={600}
/>
image

Otherwise, I'm good with the tick hack.

@bernardobelchior
Copy link
Member Author

The axis highlight does not work. Seems something is missing. Same with the area.

I modified the line demo as follows

<LineChart
  xAxis={[{ scaleType: 'symlog', data: [10, 1_000, 5_000, 10_000, -50_000, 20_000, 100_000, -500], }]}
  yAxis={[{ scaleType: 'symlog', width: 56 }]}
  series={[
    {
      data: [10, 1_000, 5_000, 10_000, -50_000, 20_000, 100_000, -500],
      area: true,
    },
  ]}
  height={600}
/>
image Otherwise, I'm good with the tick hack.

This seems a bug in the line highlight itself when the x-axis data isn't ordered:

Screen.Recording.2025-07-16.at.15.34.59.mov

If the x-axis data is ordered, then it works:

Screen.Recording.2025-07-16.at.15.35.33.mov

@bernardobelchior bernardobelchior merged commit 6d189c4 into mui:master Jul 25, 2025
23 checks passed
@bernardobelchior bernardobelchior deleted the charts-symlog branch July 25, 2025 06:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
scope: charts Changes related to the charts. type: new feature Expand the scope of the product to solve a new problem.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[charts] Consider adding symlog scale
4 participants