diff --git a/docs/data/charts/axis-ticks/ResponsiveTickAdjustment.js b/docs/data/charts/axis-ticks/ResponsiveTickAdjustment.js new file mode 100644 index 0000000000000..1fe5ac62c302f --- /dev/null +++ b/docs/data/charts/axis-ticks/ResponsiveTickAdjustment.js @@ -0,0 +1,181 @@ +import * as React from 'react'; +import Box from '@mui/material/Box'; +import Stack from '@mui/material/Stack'; +import Slider from '@mui/material/Slider'; +import FormControlLabel from '@mui/material/FormControlLabel'; +import Switch from '@mui/material/Switch'; +import Typography from '@mui/material/Typography'; +import { Chance } from 'chance'; +import { BarChart } from '@mui/x-charts/BarChart'; + +const chance = new Chance(42); + +const produce = [ + 'Apple', + 'Apricot', + 'Artichoke', + 'Arugula', + 'Asparagus', + 'Avocado', + 'Banana', + 'Beet', + 'Bell pepper', + 'Blackberry', + 'Blueberry', + 'Bok choy', + 'Broccoli', + 'Brussels sprout', + 'Cabbage', + 'Cantaloupe', + 'Carrot', + 'Cauliflower', + 'Celery', + 'Cherry', + 'Chickpea', + 'Chili pepper', + 'Clementine', + 'Coconut', + 'Collard greens', + 'Corn', + 'Cranberry', + 'Cucumber', + 'Currant', + 'Daikon', + 'Date', + 'Dragonfruit', + 'Durian', + 'Edamame', + 'Eggplant', + 'Elderberry', + 'Endive (red)', + 'Endive', + 'Fennel', + 'Fig', + 'Garlic', + 'Ginger', + 'Gooseberry', + 'Grape', + 'Grapefruit', + 'Green bean', + 'Guava', + 'Honeydew', + 'Jackfruit', + 'Jalapeño', + 'Jicama', + 'Kale', + 'Kiwi', + 'Kohlrabi', + 'Kumquat', + 'Leek', + 'Lemon', + 'Lentil', + 'Lettuce', + 'Lime', + 'Lychee', + 'Mandarin', + 'Mango', + 'Mizuna', + 'Mulberry', + 'Mushroom', + 'Mustard greens', + 'Nectarine', + 'Okra', + 'Olive', + 'Onion', + 'Orange', + 'Papaya', + 'Parsnip', + 'Passionfruit', + 'Pea', + 'Peach', + 'Pear', + 'Persian lime', + 'Persimmon', + 'Pineapple', + 'Plantain', + 'Plum', + 'Pomegranate', + 'Pomelo', + 'Potato', + 'Pumpkin', + 'Quince', + 'Radicchio', + 'Radish', + 'Raisin', + 'Rambutan', + 'Raspberry', + 'Redcurrant', + 'Rhubarb', + 'Rutabaga', + 'Salak', + 'Salsify', + 'Scallion', + 'Shallot', + 'Snow pea', + 'Soursop', + 'Spinach', + 'Squash', + 'Starfruit', + 'Strawberry', + 'Sweet potato', + 'Swiss chard', + 'Tamarind', + 'Tangerine', + 'Taro', + 'Tatsoi', + 'Tomato', + 'Turnip', + 'Ugli', + 'Watercress', + 'Watermelon', + 'Yam', + 'Yuzu', + 'Zucchini', +]; + +const dataset = produce.map((name) => ({ + item: name, + sales: chance.integer({ min: 100, max: 1000 }), +})); + +export default function ResponsiveTickAdjustment() { + const [widthPct, setWidthPct] = React.useState(40); + const [enabled, setEnabled] = React.useState(true); + + return ( + + + + Chart width: {widthPct}% + + setWidthPct(value)} + valueLabelDisplay="auto" + min={20} + max={100} + step={5} + aria-labelledby="chart-width" + /> + setEnabled(event.target.checked)} + /> + } + label="experimentalFeatures.useNewDefaultTickSpacing" + /> + + + + + + ); +} diff --git a/docs/data/charts/axis-ticks/ResponsiveTickAdjustment.tsx b/docs/data/charts/axis-ticks/ResponsiveTickAdjustment.tsx new file mode 100644 index 0000000000000..a96a6a832a315 --- /dev/null +++ b/docs/data/charts/axis-ticks/ResponsiveTickAdjustment.tsx @@ -0,0 +1,181 @@ +import * as React from 'react'; +import Box from '@mui/material/Box'; +import Stack from '@mui/material/Stack'; +import Slider from '@mui/material/Slider'; +import FormControlLabel from '@mui/material/FormControlLabel'; +import Switch from '@mui/material/Switch'; +import Typography from '@mui/material/Typography'; +import { Chance } from 'chance'; +import { BarChart } from '@mui/x-charts/BarChart'; + +const chance = new Chance(42); + +const produce = [ + 'Apple', + 'Apricot', + 'Artichoke', + 'Arugula', + 'Asparagus', + 'Avocado', + 'Banana', + 'Beet', + 'Bell pepper', + 'Blackberry', + 'Blueberry', + 'Bok choy', + 'Broccoli', + 'Brussels sprout', + 'Cabbage', + 'Cantaloupe', + 'Carrot', + 'Cauliflower', + 'Celery', + 'Cherry', + 'Chickpea', + 'Chili pepper', + 'Clementine', + 'Coconut', + 'Collard greens', + 'Corn', + 'Cranberry', + 'Cucumber', + 'Currant', + 'Daikon', + 'Date', + 'Dragonfruit', + 'Durian', + 'Edamame', + 'Eggplant', + 'Elderberry', + 'Endive (red)', + 'Endive', + 'Fennel', + 'Fig', + 'Garlic', + 'Ginger', + 'Gooseberry', + 'Grape', + 'Grapefruit', + 'Green bean', + 'Guava', + 'Honeydew', + 'Jackfruit', + 'Jalapeño', + 'Jicama', + 'Kale', + 'Kiwi', + 'Kohlrabi', + 'Kumquat', + 'Leek', + 'Lemon', + 'Lentil', + 'Lettuce', + 'Lime', + 'Lychee', + 'Mandarin', + 'Mango', + 'Mizuna', + 'Mulberry', + 'Mushroom', + 'Mustard greens', + 'Nectarine', + 'Okra', + 'Olive', + 'Onion', + 'Orange', + 'Papaya', + 'Parsnip', + 'Passionfruit', + 'Pea', + 'Peach', + 'Pear', + 'Persian lime', + 'Persimmon', + 'Pineapple', + 'Plantain', + 'Plum', + 'Pomegranate', + 'Pomelo', + 'Potato', + 'Pumpkin', + 'Quince', + 'Radicchio', + 'Radish', + 'Raisin', + 'Rambutan', + 'Raspberry', + 'Redcurrant', + 'Rhubarb', + 'Rutabaga', + 'Salak', + 'Salsify', + 'Scallion', + 'Shallot', + 'Snow pea', + 'Soursop', + 'Spinach', + 'Squash', + 'Starfruit', + 'Strawberry', + 'Sweet potato', + 'Swiss chard', + 'Tamarind', + 'Tangerine', + 'Taro', + 'Tatsoi', + 'Tomato', + 'Turnip', + 'Ugli', + 'Watercress', + 'Watermelon', + 'Yam', + 'Yuzu', + 'Zucchini', +]; + +const dataset = produce.map((name) => ({ + item: name, + sales: chance.integer({ min: 100, max: 1000 }), +})); + +export default function ResponsiveTickAdjustment() { + const [widthPct, setWidthPct] = React.useState(40); + const [enabled, setEnabled] = React.useState(true); + + return ( + + + + Chart width: {widthPct}% + + setWidthPct(value as number)} + valueLabelDisplay="auto" + min={20} + max={100} + step={5} + aria-labelledby="chart-width" + /> + setEnabled(event.target.checked)} + /> + } + label="experimentalFeatures.useNewDefaultTickSpacing" + /> + + + + + + ); +} diff --git a/docs/data/charts/axis-ticks/axis-ticks.md b/docs/data/charts/axis-ticks/axis-ticks.md index 33be43d833fc5..1ecedf3b39f86 100644 --- a/docs/data/charts/axis-ticks/axis-ticks.md +++ b/docs/data/charts/axis-ticks/axis-ticks.md @@ -56,6 +56,28 @@ This property defaults to 0 and is only available for ordinal axes, that is, axe {{"demo": "TickSpacing.js"}} +### Responsive tick adjustment + +:::warning +This feature is experimental—not because its behavior is unstable, but because enabling it by default would change the look of existing charts. It's opt-in for now and is expected to become the default in a future major release. +::: + +Enable `useNewDefaultTickSpacing` on any cartesian chart (BarChart, LineChart, ScatterChart, Heatmap, and their Pro/Premium variants) to let ordinal axes (`band` and `point` scales) thin out ticks automatically based on the chart's rendered size, so labels don't pile up on narrow charts. + +The feature applies a 50-pixel default `tickSpacing` derived from the drawing area. It never overrides an explicit `tickSpacing`, `tickNumber`, or `tickInterval` set by your code—continuous axes are already size-aware through their default `tickNumber`. + +```jsx + +``` + +Drag the slider in the demo below to shrink the chart and toggle the feature to see ticks adapt. + +{{"demo": "ResponsiveTickAdjustment.js"}} + ### Fixed tick position If you want more control over the tick position, you can use the `tickInterval` property. diff --git a/docs/pages/x/api/charts/bar-chart-premium.json b/docs/pages/x/api/charts/bar-chart-premium.json index 70a4513d4a223..34e9c7331d983 100644 --- a/docs/pages/x/api/charts/bar-chart-premium.json +++ b/docs/pages/x/api/charts/bar-chart-premium.json @@ -30,7 +30,9 @@ "desc": { "type": { "name": "string" } }, "disableAxisListener": { "type": { "name": "bool" }, "default": "false" }, "disableKeyboardNavigation": { "type": { "name": "bool" } }, - "experimentalFeatures": { "type": { "name": "object" } }, + "experimentalFeatures": { + "type": { "name": "shape", "description": "{ useNewDefaultTickSpacing?: bool }" } + }, "grid": { "type": { "name": "shape", "description": "{ horizontal?: bool, vertical?: bool }" } }, diff --git a/docs/pages/x/api/charts/bar-chart-pro.json b/docs/pages/x/api/charts/bar-chart-pro.json index ad8b2ef1bcb74..2fbd8b40aab9e 100644 --- a/docs/pages/x/api/charts/bar-chart-pro.json +++ b/docs/pages/x/api/charts/bar-chart-pro.json @@ -30,7 +30,9 @@ "desc": { "type": { "name": "string" } }, "disableAxisListener": { "type": { "name": "bool" }, "default": "false" }, "disableKeyboardNavigation": { "type": { "name": "bool" } }, - "experimentalFeatures": { "type": { "name": "object" } }, + "experimentalFeatures": { + "type": { "name": "shape", "description": "{ useNewDefaultTickSpacing?: bool }" } + }, "grid": { "type": { "name": "shape", "description": "{ horizontal?: bool, vertical?: bool }" } }, diff --git a/docs/pages/x/api/charts/bar-chart.json b/docs/pages/x/api/charts/bar-chart.json index de827ff2e1618..69ac53bddcbbb 100644 --- a/docs/pages/x/api/charts/bar-chart.json +++ b/docs/pages/x/api/charts/bar-chart.json @@ -30,7 +30,9 @@ "desc": { "type": { "name": "string" } }, "disableAxisListener": { "type": { "name": "bool" }, "default": "false" }, "disableKeyboardNavigation": { "type": { "name": "bool" } }, - "experimentalFeatures": { "type": { "name": "object" } }, + "experimentalFeatures": { + "type": { "name": "shape", "description": "{ useNewDefaultTickSpacing?: bool }" } + }, "grid": { "type": { "name": "shape", "description": "{ horizontal?: bool, vertical?: bool }" } }, diff --git a/docs/pages/x/api/charts/funnel-chart.json b/docs/pages/x/api/charts/funnel-chart.json index 829ed7f77fa26..4cc952fd4ac22 100644 --- a/docs/pages/x/api/charts/funnel-chart.json +++ b/docs/pages/x/api/charts/funnel-chart.json @@ -28,7 +28,9 @@ "desc": { "type": { "name": "string" } }, "disableAxisListener": { "type": { "name": "bool" }, "default": "false" }, "disableKeyboardNavigation": { "type": { "name": "bool" } }, - "experimentalFeatures": { "type": { "name": "object" } }, + "experimentalFeatures": { + "type": { "name": "shape", "description": "{ useNewDefaultTickSpacing?: bool }" } + }, "gap": { "type": { "name": "number" }, "default": "0" }, "height": { "type": { "name": "number" } }, "hiddenItems": { diff --git a/docs/pages/x/api/charts/heatmap-premium.json b/docs/pages/x/api/charts/heatmap-premium.json index dac3d27327dd3..ec19e4e877450 100644 --- a/docs/pages/x/api/charts/heatmap-premium.json +++ b/docs/pages/x/api/charts/heatmap-premium.json @@ -27,7 +27,9 @@ "desc": { "type": { "name": "string" } }, "disableAxisListener": { "type": { "name": "bool" }, "default": "false" }, "disableKeyboardNavigation": { "type": { "name": "bool" } }, - "experimentalFeatures": { "type": { "name": "object" } }, + "experimentalFeatures": { + "type": { "name": "shape", "description": "{ useNewDefaultTickSpacing?: bool }" } + }, "height": { "type": { "name": "number" } }, "hideLegend": { "type": { "name": "bool" } }, "highlightedItem": { diff --git a/docs/pages/x/api/charts/heatmap.json b/docs/pages/x/api/charts/heatmap.json index 42b154c233248..19e471df621ed 100644 --- a/docs/pages/x/api/charts/heatmap.json +++ b/docs/pages/x/api/charts/heatmap.json @@ -27,7 +27,9 @@ "desc": { "type": { "name": "string" } }, "disableAxisListener": { "type": { "name": "bool" }, "default": "false" }, "disableKeyboardNavigation": { "type": { "name": "bool" } }, - "experimentalFeatures": { "type": { "name": "object" } }, + "experimentalFeatures": { + "type": { "name": "shape", "description": "{ useNewDefaultTickSpacing?: bool }" } + }, "height": { "type": { "name": "number" } }, "hideLegend": { "type": { "name": "bool" } }, "highlightedItem": { diff --git a/docs/pages/x/api/charts/line-chart-pro.json b/docs/pages/x/api/charts/line-chart-pro.json index fe2c7e2989a4b..50482499f77ab 100644 --- a/docs/pages/x/api/charts/line-chart-pro.json +++ b/docs/pages/x/api/charts/line-chart-pro.json @@ -32,7 +32,10 @@ "disableKeyboardNavigation": { "type": { "name": "bool" } }, "disableLineItemHighlight": { "type": { "name": "bool" } }, "experimentalFeatures": { - "type": { "name": "shape", "description": "{ enablePositionBasedPointerInteraction?: bool }" } + "type": { + "name": "shape", + "description": "{ enablePositionBasedPointerInteraction?: bool, useNewDefaultTickSpacing?: bool }" + } }, "grid": { "type": { "name": "shape", "description": "{ horizontal?: bool, vertical?: bool }" } diff --git a/docs/pages/x/api/charts/line-chart.json b/docs/pages/x/api/charts/line-chart.json index 7048ba59719da..04e41c163b83f 100644 --- a/docs/pages/x/api/charts/line-chart.json +++ b/docs/pages/x/api/charts/line-chart.json @@ -32,7 +32,10 @@ "disableKeyboardNavigation": { "type": { "name": "bool" } }, "disableLineItemHighlight": { "type": { "name": "bool" } }, "experimentalFeatures": { - "type": { "name": "shape", "description": "{ enablePositionBasedPointerInteraction?: bool }" } + "type": { + "name": "shape", + "description": "{ enablePositionBasedPointerInteraction?: bool, useNewDefaultTickSpacing?: bool }" + } }, "grid": { "type": { "name": "shape", "description": "{ horizontal?: bool, vertical?: bool }" } diff --git a/docs/pages/x/api/charts/scatter-chart-premium.json b/docs/pages/x/api/charts/scatter-chart-premium.json index 223450df24bc0..f74d546be856b 100644 --- a/docs/pages/x/api/charts/scatter-chart-premium.json +++ b/docs/pages/x/api/charts/scatter-chart-premium.json @@ -43,7 +43,9 @@ "deprecated": true, "deprecationInfo": "Use disableHitArea instead." }, - "experimentalFeatures": { "type": { "name": "object" } }, + "experimentalFeatures": { + "type": { "name": "shape", "description": "{ useNewDefaultTickSpacing?: bool }" } + }, "grid": { "type": { "name": "shape", "description": "{ horizontal?: bool, vertical?: bool }" } }, diff --git a/docs/pages/x/api/charts/scatter-chart-pro.json b/docs/pages/x/api/charts/scatter-chart-pro.json index c321b85f7af71..560535d00aada 100644 --- a/docs/pages/x/api/charts/scatter-chart-pro.json +++ b/docs/pages/x/api/charts/scatter-chart-pro.json @@ -43,7 +43,9 @@ "deprecated": true, "deprecationInfo": "Use disableHitArea instead." }, - "experimentalFeatures": { "type": { "name": "object" } }, + "experimentalFeatures": { + "type": { "name": "shape", "description": "{ useNewDefaultTickSpacing?: bool }" } + }, "grid": { "type": { "name": "shape", "description": "{ horizontal?: bool, vertical?: bool }" } }, diff --git a/docs/pages/x/api/charts/scatter-chart.json b/docs/pages/x/api/charts/scatter-chart.json index e97a56c7fe3de..738e70327f18a 100644 --- a/docs/pages/x/api/charts/scatter-chart.json +++ b/docs/pages/x/api/charts/scatter-chart.json @@ -43,7 +43,9 @@ "deprecated": true, "deprecationInfo": "Use disableHitArea instead." }, - "experimentalFeatures": { "type": { "name": "object" } }, + "experimentalFeatures": { + "type": { "name": "shape", "description": "{ useNewDefaultTickSpacing?: bool }" } + }, "grid": { "type": { "name": "shape", "description": "{ horizontal?: bool, vertical?: bool }" } }, diff --git a/docs/pages/x/api/charts/spark-line-chart.json b/docs/pages/x/api/charts/spark-line-chart.json index 1c5167129c6e8..83e6bec4e4ca3 100644 --- a/docs/pages/x/api/charts/spark-line-chart.json +++ b/docs/pages/x/api/charts/spark-line-chart.json @@ -36,7 +36,10 @@ "disableHitArea": { "type": { "name": "bool" } }, "disableKeyboardNavigation": { "type": { "name": "bool" } }, "experimentalFeatures": { - "type": { "name": "shape", "description": "{ enablePositionBasedPointerInteraction?: bool }" } + "type": { + "name": "shape", + "description": "{ enablePositionBasedPointerInteraction?: bool, useNewDefaultTickSpacing?: bool }" + } }, "height": { "type": { "name": "number" } }, "hiddenItems": { diff --git a/packages/x-charts-premium/src/BarChartPremium/BarChartPremium.tsx b/packages/x-charts-premium/src/BarChartPremium/BarChartPremium.tsx index fa451c09046de..b9bfb6bbb78a2 100644 --- a/packages/x-charts-premium/src/BarChartPremium/BarChartPremium.tsx +++ b/packages/x-charts-premium/src/BarChartPremium/BarChartPremium.tsx @@ -240,7 +240,9 @@ BarChartPremium.propTypes = { /** * Options to enable features planned for the next major. */ - experimentalFeatures: PropTypes.object, + experimentalFeatures: PropTypes.shape({ + useNewDefaultTickSpacing: PropTypes.bool, + }), /** * Option to display a cartesian grid in the background. */ diff --git a/packages/x-charts-premium/src/HeatmapPremium/HeatmapPremium.tsx b/packages/x-charts-premium/src/HeatmapPremium/HeatmapPremium.tsx index a9ea8840aa4ac..4bb38a320495c 100644 --- a/packages/x-charts-premium/src/HeatmapPremium/HeatmapPremium.tsx +++ b/packages/x-charts-premium/src/HeatmapPremium/HeatmapPremium.tsx @@ -144,7 +144,9 @@ HeatmapPremium.propTypes = { /** * Options to enable features planned for the next major. */ - experimentalFeatures: PropTypes.object, + experimentalFeatures: PropTypes.shape({ + useNewDefaultTickSpacing: PropTypes.bool, + }), /** * The height of the chart in px. If not defined, it takes the height of the parent element. */ diff --git a/packages/x-charts-premium/src/ScatterChartPremium/ScatterChartPremium.tsx b/packages/x-charts-premium/src/ScatterChartPremium/ScatterChartPremium.tsx index ed17bea4d8c7a..b0fe8490c68b4 100644 --- a/packages/x-charts-premium/src/ScatterChartPremium/ScatterChartPremium.tsx +++ b/packages/x-charts-premium/src/ScatterChartPremium/ScatterChartPremium.tsx @@ -246,7 +246,9 @@ ScatterChartPremium.propTypes = { /** * Options to enable features planned for the next major. */ - experimentalFeatures: PropTypes.object, + experimentalFeatures: PropTypes.shape({ + useNewDefaultTickSpacing: PropTypes.bool, + }), /** * Option to display a cartesian grid in the background. */ diff --git a/packages/x-charts-pro/src/BarChartPro/BarChartPro.tsx b/packages/x-charts-pro/src/BarChartPro/BarChartPro.tsx index 1e478247c9999..c378e1df2aabf 100644 --- a/packages/x-charts-pro/src/BarChartPro/BarChartPro.tsx +++ b/packages/x-charts-pro/src/BarChartPro/BarChartPro.tsx @@ -197,7 +197,9 @@ BarChartPro.propTypes = { /** * Options to enable features planned for the next major. */ - experimentalFeatures: PropTypes.object, + experimentalFeatures: PropTypes.shape({ + useNewDefaultTickSpacing: PropTypes.bool, + }), /** * Option to display a cartesian grid in the background. */ diff --git a/packages/x-charts-pro/src/FunnelChart/FunnelChart.tsx b/packages/x-charts-pro/src/FunnelChart/FunnelChart.tsx index cd05f95517236..5ca3c44fba2d4 100644 --- a/packages/x-charts-pro/src/FunnelChart/FunnelChart.tsx +++ b/packages/x-charts-pro/src/FunnelChart/FunnelChart.tsx @@ -258,7 +258,9 @@ FunnelChart.propTypes = { /** * Options to enable features planned for the next major. */ - experimentalFeatures: PropTypes.object, + experimentalFeatures: PropTypes.shape({ + useNewDefaultTickSpacing: PropTypes.bool, + }), /** * The gap, in pixels, between funnel sections. * @default 0 diff --git a/packages/x-charts-pro/src/Heatmap/Heatmap.tsx b/packages/x-charts-pro/src/Heatmap/Heatmap.tsx index 1d88b66a2474f..255c55810c880 100644 --- a/packages/x-charts-pro/src/Heatmap/Heatmap.tsx +++ b/packages/x-charts-pro/src/Heatmap/Heatmap.tsx @@ -239,7 +239,9 @@ Heatmap.propTypes = { /** * Options to enable features planned for the next major. */ - experimentalFeatures: PropTypes.object, + experimentalFeatures: PropTypes.shape({ + useNewDefaultTickSpacing: PropTypes.bool, + }), /** * The height of the chart in px. If not defined, it takes the height of the parent element. */ diff --git a/packages/x-charts-pro/src/LineChartPro/LineChartPro.tsx b/packages/x-charts-pro/src/LineChartPro/LineChartPro.tsx index 911a6d78ea5ae..75ee941eea812 100644 --- a/packages/x-charts-pro/src/LineChartPro/LineChartPro.tsx +++ b/packages/x-charts-pro/src/LineChartPro/LineChartPro.tsx @@ -208,6 +208,7 @@ LineChartPro.propTypes = { */ experimentalFeatures: PropTypes.shape({ enablePositionBasedPointerInteraction: PropTypes.bool, + useNewDefaultTickSpacing: PropTypes.bool, }), /** * Option to display a cartesian grid in the background. diff --git a/packages/x-charts-pro/src/ScatterChartPro/ScatterChartPro.tsx b/packages/x-charts-pro/src/ScatterChartPro/ScatterChartPro.tsx index aaac4e6962f73..65adb26aaa28b 100644 --- a/packages/x-charts-pro/src/ScatterChartPro/ScatterChartPro.tsx +++ b/packages/x-charts-pro/src/ScatterChartPro/ScatterChartPro.tsx @@ -221,7 +221,9 @@ ScatterChartPro.propTypes = { /** * Options to enable features planned for the next major. */ - experimentalFeatures: PropTypes.object, + experimentalFeatures: PropTypes.shape({ + useNewDefaultTickSpacing: PropTypes.bool, + }), /** * Option to display a cartesian grid in the background. */ diff --git a/packages/x-charts/src/BarChart/BarChart.tsx b/packages/x-charts/src/BarChart/BarChart.tsx index 99d2274822558..701a485b64e6a 100644 --- a/packages/x-charts/src/BarChart/BarChart.tsx +++ b/packages/x-charts/src/BarChart/BarChart.tsx @@ -224,7 +224,9 @@ BarChart.propTypes = { /** * Options to enable features planned for the next major. */ - experimentalFeatures: PropTypes.object, + experimentalFeatures: PropTypes.shape({ + useNewDefaultTickSpacing: PropTypes.bool, + }), /** * Option to display a cartesian grid in the background. */ diff --git a/packages/x-charts/src/LineChart/AreaElement.tsx b/packages/x-charts/src/LineChart/AreaElement.tsx index 91f9e185b5965..534889f8c40e7 100644 --- a/packages/x-charts/src/LineChart/AreaElement.tsx +++ b/packages/x-charts/src/LineChart/AreaElement.tsx @@ -76,7 +76,8 @@ function AreaElement(props: AreaElementProps) { const store = useStore(); const enablePositionBasedPointerInteraction = store.use( selectorChartExperimentalFeaturesState, - )?.enablePositionBasedPointerInteraction; + 'enablePositionBasedPointerInteraction', + ); const identifier = React.useMemo(() => ({ type: 'line' as const, seriesId }), [seriesId]); const interactionProps = useInteractionItemProps(identifier); const highlightState = useItemHighlightState(identifier); diff --git a/packages/x-charts/src/LineChart/CircleMarkElement.tsx b/packages/x-charts/src/LineChart/CircleMarkElement.tsx index 327e41353c2a8..880652211851b 100644 --- a/packages/x-charts/src/LineChart/CircleMarkElement.tsx +++ b/packages/x-charts/src/LineChart/CircleMarkElement.tsx @@ -84,7 +84,8 @@ function CircleMarkElement(props: CircleMarkElementProps) { const store = useStore(); const enablePositionBasedPointerInteraction = store.use( selectorChartExperimentalFeaturesState, - )?.enablePositionBasedPointerInteraction; + 'enablePositionBasedPointerInteraction', + ); const interactionProps = useInteractionItemProps({ type: 'line', seriesId, dataIndex }); const theme = useTheme(); diff --git a/packages/x-charts/src/LineChart/LineChart.tsx b/packages/x-charts/src/LineChart/LineChart.tsx index 3d69c78e5ffe2..cb1542acfd812 100644 --- a/packages/x-charts/src/LineChart/LineChart.tsx +++ b/packages/x-charts/src/LineChart/LineChart.tsx @@ -273,6 +273,7 @@ LineChart.propTypes = { */ experimentalFeatures: PropTypes.shape({ enablePositionBasedPointerInteraction: PropTypes.bool, + useNewDefaultTickSpacing: PropTypes.bool, }), /** * Option to display a cartesian grid in the background. diff --git a/packages/x-charts/src/LineChart/LineElement.tsx b/packages/x-charts/src/LineChart/LineElement.tsx index e1d6e7c01e663..bbed68f473f86 100644 --- a/packages/x-charts/src/LineChart/LineElement.tsx +++ b/packages/x-charts/src/LineChart/LineElement.tsx @@ -81,7 +81,8 @@ function LineElement(props: LineElementProps) { const store = useStore(); const enablePositionBasedPointerInteraction = store.use( selectorChartExperimentalFeaturesState, - )?.enablePositionBasedPointerInteraction; + 'enablePositionBasedPointerInteraction', + ); const identifier = React.useMemo(() => ({ type: 'line' as const, seriesId }), [seriesId]); const interactionProps = useInteractionItemProps(identifier); diff --git a/packages/x-charts/src/LineChart/MarkElement.tsx b/packages/x-charts/src/LineChart/MarkElement.tsx index 5419c9bf75d51..7751df50ac19f 100644 --- a/packages/x-charts/src/LineChart/MarkElement.tsx +++ b/packages/x-charts/src/LineChart/MarkElement.tsx @@ -89,7 +89,8 @@ function MarkElement(props: MarkElementProps) { const store = useStore(); const enablePositionBasedPointerInteraction = store.use( selectorChartExperimentalFeaturesState, - )?.enablePositionBasedPointerInteraction; + 'enablePositionBasedPointerInteraction', + ); const interactionProps = useInteractionItemProps({ type: 'line', seriesId, dataIndex }); const ownerState = { diff --git a/packages/x-charts/src/ScatterChart/ScatterChart.tsx b/packages/x-charts/src/ScatterChart/ScatterChart.tsx index 1a670d6bc51e0..0fbf527063885 100644 --- a/packages/x-charts/src/ScatterChart/ScatterChart.tsx +++ b/packages/x-charts/src/ScatterChart/ScatterChart.tsx @@ -265,7 +265,9 @@ ScatterChart.propTypes = { /** * Options to enable features planned for the next major. */ - experimentalFeatures: PropTypes.object, + experimentalFeatures: PropTypes.shape({ + useNewDefaultTickSpacing: PropTypes.bool, + }), /** * Option to display a cartesian grid in the background. */ diff --git a/packages/x-charts/src/SparkLineChart/SparkLineChart.tsx b/packages/x-charts/src/SparkLineChart/SparkLineChart.tsx index 462d88924a492..8cb9e9a08e72c 100644 --- a/packages/x-charts/src/SparkLineChart/SparkLineChart.tsx +++ b/packages/x-charts/src/SparkLineChart/SparkLineChart.tsx @@ -430,6 +430,7 @@ SparkLineChart.propTypes = { */ experimentalFeatures: PropTypes.shape({ enablePositionBasedPointerInteraction: PropTypes.bool, + useNewDefaultTickSpacing: PropTypes.bool, }), /** * The height of the chart in px. If not defined, it takes the height of the parent element. diff --git a/packages/x-charts/src/internals/plugins/corePlugins/useChartExperimentalFeature/useChartExperimentalFeature.selectors.ts b/packages/x-charts/src/internals/plugins/corePlugins/useChartExperimentalFeature/useChartExperimentalFeature.selectors.ts index 268d38f8e8ae3..139c67ca98752 100644 --- a/packages/x-charts/src/internals/plugins/corePlugins/useChartExperimentalFeature/useChartExperimentalFeature.selectors.ts +++ b/packages/x-charts/src/internals/plugins/corePlugins/useChartExperimentalFeature/useChartExperimentalFeature.selectors.ts @@ -1,7 +1,20 @@ import type { ChartSeriesType } from '../../../../models/seriesType/config'; -import { type ChartRootSelector } from '../../utils/selectors'; -import type { UseChartExperimentalFeaturesSignature } from './useChartExperimentalFeature.types'; +import { type ChartState } from '../../models/chart'; +import type { + ChartExperimentalFeatures, + UseChartExperimentalFeaturesSignature, +} from './useChartExperimentalFeature.types'; -export const selectorChartExperimentalFeaturesState: ChartRootSelector< - UseChartExperimentalFeaturesSignature -> = (state) => state.experimentalFeatures; +/** + * Reads the value of a single experimental feature flag from the store. + * + * @example + * const enabled = store.use( + * selectorChartExperimentalFeaturesState, + * 'useNewDefaultTickSpacing', + * ); + */ +export const selectorChartExperimentalFeaturesState = ( + state: ChartState<[UseChartExperimentalFeaturesSignature]>, + featureName: K, +): ChartExperimentalFeatures[K] | undefined => state.experimentalFeatures?.[featureName]; diff --git a/packages/x-charts/src/internals/plugins/corePlugins/useChartExperimentalFeature/useChartExperimentalFeature.types.ts b/packages/x-charts/src/internals/plugins/corePlugins/useChartExperimentalFeature/useChartExperimentalFeature.types.ts index a60a39fb73b1d..05a5fac7110cf 100644 --- a/packages/x-charts/src/internals/plugins/corePlugins/useChartExperimentalFeature/useChartExperimentalFeature.types.ts +++ b/packages/x-charts/src/internals/plugins/corePlugins/useChartExperimentalFeature/useChartExperimentalFeature.types.ts @@ -1,5 +1,8 @@ import { type ChartPluginSignature } from '../../models'; -import type { ChartSeriesType } from '../../../../models/seriesType/config'; +import type { + CartesianChartSeriesType, + ChartSeriesType, +} from '../../../../models/seriesType/config'; interface LineExperimentalFeatures { /** @@ -15,8 +18,27 @@ interface LineExperimentalFeatures { enablePositionBasedPointerInteraction?: boolean; } +interface CartesianExperimentalFeatures { + /** + * Automatically reduces the number of ticks and tick labels on ordinal axes + * (`band` / `point` scales) based on the rendered drawing area size. + * + * Explicit `tickNumber`, `tickSpacing`, or `tickInterval` values set by the + * consumer are not overridden. + */ + useNewDefaultTickSpacing?: boolean; +} + +/* True if any cartesian series (which can have `band` / `point` scales) is in `SeriesType`. */ +type HasCartesianSeries = [ + Extract, +] extends [never] + ? false + : true; + export type ChartExperimentalFeatures = - 'line' extends SeriesType ? LineExperimentalFeatures : {}; + ('line' extends SeriesType ? LineExperimentalFeatures : {}) & + (HasCartesianSeries extends true ? CartesianExperimentalFeatures : {}); export interface UseChartExperimentalFeaturesParameters< SeriesType extends ChartSeriesType = ChartSeriesType, diff --git a/packages/x-charts/src/internals/plugins/featurePlugins/useChartCartesianAxis/computeAxisValue.ts b/packages/x-charts/src/internals/plugins/featurePlugins/useChartCartesianAxis/computeAxisValue.ts index a5999e6335fc4..f41f1ee1a5604 100644 --- a/packages/x-charts/src/internals/plugins/featurePlugins/useChartCartesianAxis/computeAxisValue.ts +++ b/packages/x-charts/src/internals/plugins/featurePlugins/useChartCartesianAxis/computeAxisValue.ts @@ -81,6 +81,9 @@ export function resolveAxisSize( const DEFAULT_CATEGORY_GAP_RATIO = 0.2; const DEFAULT_BAR_GAP_RATIO = 0.1; +/* Matches the 50px-per-tick heuristic used by `getDefaultTickNumber` for continuous axes. */ +const RESPONSIVE_ORDINAL_TICK_SPACING = 50; + export type ComputeResult = { axis: ComputedAxisConfig; axisIds: AxisId[]; @@ -101,6 +104,8 @@ type ComputeCommonParams = >; autoSizes?: Record; axesGap?: number; + /* When true, ordinal axes without an explicit `tickSpacing` get a size-aware default. */ + responsiveTickAdjustment?: boolean; }; /** @@ -166,6 +171,7 @@ export function computeAxisValue({ domains, autoSizes, axesGap = 0, + responsiveTickAdjustment = false, }: ComputeCommonParams & { axis?: DefaultedAxis[]; axisDirection: 'x' | 'y'; @@ -206,6 +212,10 @@ export function computeAxisValue({ if (isOrdinalScale(scale)) { const scaleRange = axisDirection === 'y' ? [range[1], range[0]] : range; + const effectiveTickSpacing = + axis.tickSpacing ?? + (responsiveTickAdjustment ? RESPONSIVE_ORDINAL_TICK_SPACING : undefined); + if (isBandScale(scale) && isBandScaleConfig(axis)) { const desiredCategoryGapRatio = axis.categoryGapRatio ?? DEFAULT_CATEGORY_GAP_RATIO; const ignoreGapRatios = shouldIgnoreGapRatios(scale, desiredCategoryGapRatio); @@ -227,6 +237,7 @@ export function computeAxisValue({ * discrepancy will hopefully not be noticeable. */ scale: ignoreGapRatios ? scale.copy().padding(0) : scale, tickNumber, + tickSpacing: effectiveTickSpacing, colorScale: axis.colorMap && (axis.colorMap.type === 'ordinal' @@ -244,6 +255,7 @@ export function computeAxisValue({ data, scale, tickNumber, + tickSpacing: effectiveTickSpacing, colorScale: axis.colorMap && (axis.colorMap.type === 'ordinal' diff --git a/packages/x-charts/src/internals/plugins/featurePlugins/useChartCartesianAxis/useChartCartesianAxis.test.tsx b/packages/x-charts/src/internals/plugins/featurePlugins/useChartCartesianAxis/useChartCartesianAxis.test.tsx index 4c0955e5a2710..e0f6e33baabbc 100644 --- a/packages/x-charts/src/internals/plugins/featurePlugins/useChartCartesianAxis/useChartCartesianAxis.test.tsx +++ b/packages/x-charts/src/internals/plugins/featurePlugins/useChartCartesianAxis/useChartCartesianAxis.test.tsx @@ -1,9 +1,90 @@ -import { createRenderer } from '@mui/internal-test-utils'; +import { createRenderer, screen } from '@mui/internal-test-utils'; import { BarChart } from '@mui/x-charts/BarChart'; +import { LineChart } from '@mui/x-charts/LineChart'; +import { isJSDOM } from 'test/utils/skipIf'; describe('useChartCartesianAxis', () => { const { render } = createRenderer(); + describe('experimentalFeatures.useNewDefaultTickSpacing', () => { + const manyCategories = Array.from({ length: 20 }, (_, i) => `cat-${i}`); + const data = manyCategories.map((_, i) => i); + + // In the browser, `getVisibleLabels` measures real label widths and hides + // the overlapping ones, so the "all 20 labels render" assertion only holds + // in jsdom where measurements are bypassed. + it.skipIf(!isJSDOM)('should render one tick per band when the feature is disabled', () => { + render( + , + ); + + const tickLabels = screen.getAllByTestId('ChartsXAxisTickLabel'); + expect(tickLabels).toHaveLength(manyCategories.length); + }); + + it('should thin ticks on a band axis based on the drawing area width when the feature is enabled', () => { + render( + , + ); + + const tickLabels = screen.getAllByTestId('ChartsXAxisTickLabel'); + // With ~280px of drawing area and a 50px default spacing, we expect + // roughly width / 50 ticks instead of one per band. + expect(tickLabels.length).toBeLessThan(manyCategories.length); + expect(tickLabels.length).toBeGreaterThan(0); + }); + + it.skipIf(!isJSDOM)( + 'should not override an explicit tickSpacing when the feature is enabled', + () => { + render( + , + ); + + // tickSpacing of 10px on a ~280px area should keep every band's tick. + const tickLabels = screen.getAllByTestId('ChartsXAxisTickLabel'); + expect(tickLabels).toHaveLength(manyCategories.length); + }, + ); + + it('should thin ticks on a non-bar cartesian chart when the feature is enabled', () => { + render( + , + ); + + const tickLabels = screen.getAllByTestId('ChartsXAxisTickLabel'); + expect(tickLabels.length).toBeLessThan(manyCategories.length); + expect(tickLabels.length).toBeGreaterThan(0); + }); + }); + it('should throw an error when axis have duplicate ids', () => { const expectedError = [ 'MUI X Charts: The following axis ids are duplicated: qwerty.', diff --git a/packages/x-charts/src/internals/plugins/featurePlugins/useChartCartesianAxis/useChartCartesianAxisRendering.selectors.ts b/packages/x-charts/src/internals/plugins/featurePlugins/useChartCartesianAxis/useChartCartesianAxisRendering.selectors.ts index 281287daaebb8..1e67ff563a56a 100644 --- a/packages/x-charts/src/internals/plugins/featurePlugins/useChartCartesianAxis/useChartCartesianAxisRendering.selectors.ts +++ b/packages/x-charts/src/internals/plugins/featurePlugins/useChartCartesianAxis/useChartCartesianAxisRendering.selectors.ts @@ -51,11 +51,19 @@ import { selectorChartXAxisExtrema, selectorChartYAxisExtrema, } from './useChartAxisExtrema.selectors'; +import { selectorChartExperimentalFeaturesState } from '../../corePlugins/useChartExperimentalFeature/useChartExperimentalFeature.selectors'; import { selectorChartZAxis } from '../useChartZAxis'; import getMarkerSize, { type ScatterSizeGetter, } from '../../../../ScatterChart/seriesConfig/getMarkerSize'; +/* `selectorChartExperimentalFeaturesState` takes a feature name as a + * second argument, but `createSelectorMemoized` inputs are pure state + * selectors. Wrap it to bind the feature name we care about. */ +const selectorResponsiveTickAdjustment = ( + state: Parameters[0], +) => selectorChartExperimentalFeaturesState(state, 'useNewDefaultTickSpacing') || true; // Dumb modification to see the impact on the entire docs + export const createZoomMap = (zoom: readonly ZoomData[]) => { const zoomItemMap = new Map(); zoom.forEach((zoomItem) => { @@ -437,6 +445,7 @@ export const selectorChartXAxis = createSelectorMemoized( selectorChartXScales, selectorChartXAxisAutoSizes, selectorChartCartesianAxesGap, + selectorResponsiveTickAdjustment, function selectorChartXAxis( drawingArea, @@ -447,6 +456,7 @@ export const selectorChartXAxis = createSelectorMemoized( scales, autoSizes, axesGap, + responsiveTickAdjustment, ) { return computeAxisValue({ scales, @@ -459,6 +469,7 @@ export const selectorChartXAxis = createSelectorMemoized( domains, autoSizes, axesGap, + responsiveTickAdjustment: responsiveTickAdjustment ?? false, }); }, ); @@ -472,6 +483,7 @@ export const selectorChartYAxis = createSelectorMemoized( selectorChartYScales, selectorChartYAxisAutoSizes, selectorChartCartesianAxesGap, + selectorResponsiveTickAdjustment, function selectorChartYAxis( drawingArea, @@ -482,6 +494,7 @@ export const selectorChartYAxis = createSelectorMemoized( scales, autoSizes, axesGap, + responsiveTickAdjustment, ) { return computeAxisValue({ scales, @@ -494,6 +507,7 @@ export const selectorChartYAxis = createSelectorMemoized( domains, autoSizes, axesGap, + responsiveTickAdjustment: responsiveTickAdjustment ?? false, }); }, );