Skip to content

Conversation

JCQuintas
Copy link
Member

@JCQuintas JCQuintas commented Jul 10, 2025

Interactive demo https://deploy-preview-18766--material-ui-x.netlify.app/x/react-charts/axis/#grouped-axes

Resolves #17931

Initial idea is to always rely on a formatting function rather than the data. Which should work for dataset, continuous and discrete axes.

export type AxisGroup = {
  /**
   * The function used to return the value for this group.
   *
   * @param {any} value The value of the axis item.
   * @param {number} dataIndex  The index of the data item.
   * @returns {string | number | Date} The value that will be used to group the axis items.
   */
  getValue: (value: any, dataIndex: number) => string | number | Date;
  /**
   * The size of the tick in pixels.
   * @default 6
   */
  tickSize?: number;
};

I've moved it as part of the band and point scale only

export interface AxisScaleConfig {
  band: {
    scaleType: 'band';
    ...
    /**
     * Each group will have a label that is the stringified value of the group.
     *
     * @example
     * If the axis is grouped by day, month and year.
     *
     * ```tsx
     * [
     *   { getValue: getDate },
     *   { getValue: getMonth },
     *   { getValue: getFullYear }
     * ]
     * ```
     *
     * Then the axis will have three rows, one for each group.
     *
     * ```bash
     * | 31   | 1    | 2    |
     * | Jan  | Feb         |
     * | 2021               |
     * ```
     */
    groups?: AxisGroup[];
  };
  ...
}
Screenshot 2025-08-04 at 22 34 11

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

mui-bot commented Jul 10, 2025

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

Updated pages:

Bundle size report

Bundle Parsed Size Gzip Size
@mui/x-data-grid 0B(0.00%) 0B(0.00%)
@mui/x-data-grid/DataGrid 0B(0.00%) 0B(0.00%)
@mui/x-data-grid-pro 0B(0.00%) 0B(0.00%)
@mui/x-data-grid-pro/DataGridPro 0B(0.00%) 0B(0.00%)
@mui/x-data-grid-premium 0B(0.00%) 0B(0.00%)
@mui/x-data-grid-premium/DataGridPremium 0B(0.00%) 0B(0.00%)
@mui/x-charts 🔺+3.34KB(+1.08%) 🔺+953B(+1.02%)
@mui/x-charts-pro 🔺+3.34KB(+0.85%) 🔺+935B(+0.79%)
@mui/x-date-pickers 0B(0.00%) 0B(0.00%)
@mui/x-date-pickers-pro 0B(0.00%) 0B(0.00%)
@mui/x-tree-view 0B(0.00%) 0B(0.00%)
@mui/x-tree-view-pro 0B(0.00%) 0B(0.00%)

Details of bundle changes

Generated by 🚫 dangerJS against 575c7aa

Copy link

codspeed-hq bot commented Jul 10, 2025

CodSpeed Performance Report

Merging #18766 will not alter performance

Comparing JCQuintas:grouped-axis-2 (575c7aa) with master (d9f860d)1

Summary

✅ 10 untouched benchmarks

Footnotes

  1. No successful run was found on master (08e9af4) during the generation of this report, so d9f860d was used instead as the comparison base. There might be some changes unrelated to this pull request in this report.

key={index}
transform={`translate(${tickOffset}, 0)`}
className={classes.tickContainer}
data-group-index={groupIndex}
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 intend to use this in a styling demo, will be in another PR to not make this too big

import { isInfinity } from '../internals/isInfinity';
import { isBandScale } from '../internals/isBandScale';
import { useChartContext } from '../context/ChartProvider/useChartContext';
import { ChartsXAxisProps } from '../models/axis';
Copy link
Member Author

Choose a reason for hiding this comment

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

This file became just a "selector" to decide which type of axis component to render.

function ChartsXAxis(inProps: ChartsXAxisProps) {
  const { xAxis, xAxisIds } = useXAxes();

  if (xAxis[inProps.axisId ?? xAxisIds[0]].getGrouping) {
    return <ChartsGroupedXAxis {...inProps} />;
  }

  return <ChartsSingleXAxis {...inProps} />;
}

/**
* @ignore - internal component.
*/
function ChartsGroupedXAxis(inProps: ChartsXAxisProps) {
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've removed a lot of checks/functions that are present in the "Single" axis, like shortenLabels and visibility checks.

Comment on lines 437 to 466
/**
* The function used to create axis groups.
*
* For each value in the returned array, a group will be created.
* Each group will have a label that is the stringified value of the group.
* For example, if the function returns `[31, "Jan", 2021]`, `[1, "Feb", 2021]`, `[2, "Feb", 2021]`,
* the axis will be rendered as:
*
* ```bash
* | 31 | 1 | 2 |
* | Jan | Feb |
* | 2021 |
* ```
*
* Each row in the example above is assigned a group index.
*
* @param {any} value The value of the axis item.
* @param {number} dataIndex The index of the axis item in the `data` array.
* @returns {Array<string | number | Date>} The array of values that will be used to group the axis items.
*/
getGrouping?: (value: V, dataIndex: number) => (string | number | Date)[];
/**
* The configuration for the axis grouping.
* Accepts a single object or an array of objects.
* If an array is provided, each group will use the corresponding object in the array.
* A group is defined by the `getGrouping` function.
* For example, if the `getGrouping` function returns `[31, "Jan", 2021]`, `[1, "Feb", 2021]`, `[2, "Feb", 2021]`,
* the group with index 1 will be `["Jan", "Feb", "Feb"]`
*/
groupingConfig?: AxisGroupingConfig | AxisGroupingConfig[];
Copy link
Member Author

@JCQuintas JCQuintas Jul 15, 2025

Choose a reason for hiding this comment

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

I went with a "built-in" props/component as that was easier to start.

But I suppose we could also make it a stand-alone component to be used with slots/composition.

This would allow the props to be passed directly to it.

Copy link
Member Author

Choose a reason for hiding this comment

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

@alexfauquette thoughts on this?

@JCQuintas JCQuintas marked this pull request as ready for review July 15, 2025 15:01
@alexfauquette
Copy link
Member

The band scale look nice. I've more doubt about the other ones.

Just a small edge case due to the fact you link group with their left tick, you get an issue with the last one

image

About point scale, it feels weird on the extremities. Not sure it's worth supporting it. But I don't have that much opinion on it

About continuouse scale, it looks overcomplicated compared to the result

D3 default time formatter has a nice trick for getting similar result.
They test if the tick value has seconds / minutes / hours / day / month. The first test to pass defined the formatting. For example "2025-10-5T10:00:00:0000Z" will use a hour formatted

A similar approach for contiuouse scale could be slightly more complicating but easier to document. Because with the current solution we fully rely on the tick generation. which works well for months/quatter/years, because D3 tends to place ticks on those values.

image

Last point of attention, the link between text position and the tik size create issues for small ticks. Either we define a groupHeight: number | (groupIndex: number) => number to define the height of a group or measure actual text size. But the tick size does not look reliable

image

@JCQuintas
Copy link
Member Author

The band scale look nice. I've more doubt about the other ones.

Just a small edge case due to the fact you link group with their left tick, you get an issue with the last one

image

Yeah, it is a bit annoying. We could add a prop or something to fix that tick's size manually. The issue is that we can't know in which "tick" we should end.

I suppose I can always end in the biggest one by default though? 🤔

About point scale, it feels weird on the extremities. Not sure it's worth supporting it. But I don't have that much opinion on it

We could make it use the same styling as "continuous"

About continuouse scale, it looks overcomplicated compared to the result

D3 default time formatter has a nice trick for getting similar result. They test if the tick value has seconds / minutes / hours / day / month. The first test to pass defined the formatting. For example "2025-10-5T10:00:00:0000Z" will use a hour formatted

A similar approach for contiuouse scale could be slightly more complicating but easier to document. Because with the current solution we fully rely on the tick generation. which works well for months/quatter/years, because D3 tends to place ticks on those values.

image

I don't get what is your proposal here 🤔

Last point of attention, the link between text position and the tik size create issues for small ticks. Either we define a groupHeight: number | (groupIndex: number) => number to define the height of a group or measure actual text size. But the tick size does not look reliable

image

We can't reliably know how big the text is. The user can set an option for each of the groups by using the groupingConfig: [{tickSize: 2}, {tickSize: 20}] instead of tickSize. I just didn't document it yet. The idea is to allow further customisation using the config.

@@ -443,6 +473,14 @@ export type AxisConfig<

export interface AxisConfigExtension {}

export type AxisGroupingConfig = {
/**
* The number of pixels between two groups.
Copy link
Member

Choose a reason for hiding this comment

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

If tickSize is 6px, does this mean that this group will have a 6px margin top, or will it have 3px margin top + 3px margin bottom? Or is it like flex gap where is only applies to the space in between groups?

From the demo, it seems it's the second one, but my point is that it should probably be explained in these docs as I don't think it will be clear to the user.

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 a bit confused by your question. tickSize doesn't add any margin. It just ensures the tick has a specified size, then the label is rendered after the tick ends, just like the regular axis 🤔

Copy link
Member

@bernardobelchior bernardobelchior Jul 18, 2025

Choose a reason for hiding this comment

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

Changing the tick size changes the labels position in the demo, that's why I thought it was related.

So the label changing position is just a consequence of the tick size being larger?

Copy link
Member

Choose a reason for hiding this comment

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

label' y-position is defined by ticksize + a fix gap

y: positionSign * axisHeight,
};

const tickLabels = new Map(Array.from(xTicks).map((item) => [item, item.formattedValue]));
Copy link
Member

Choose a reason for hiding this comment

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

xTicks already seems to be an array:

Suggested change
const tickLabels = new Map(Array.from(xTicks).map((item) => [item, item.formattedValue]));
const tickLabels = new Map(xTicks.map((item) => [item, item.formattedValue]));

Comment on lines +16 to +39
export type GroupedTickItemType = {
/**
* This property is undefined only if it's the tick closing the last band
*/
value?: any;
formattedValue?: string;
offset: number;
labelOffset: number;
/**
* In band scales, we remove some redundant ticks.
*/
ignoreTick?: boolean;
dataIndex?: number;
/**
* The index of the group this tick belongs to. If `getGrouping` returns `[[0, 1], [2, 3]]`
* both ticks with value `0` and `2` will have `groupIndex: 0`, and both ticks with value `1` and `3` will have `groupIndex: 1`.
*/
groupIndex?: number;
/**
* The index of the tick group inside the group. This is useful if you have multiple ticks with the same value,
* but they are divided by other values. Eg: `'Jan', ..., 'Dec', 'Jan'`
* We would have the first `'Jan'` with `syncIndex: 0` and `syncIndex: 12` for the second `'Jan'`.
*/
syncIndex?: number;
};
Copy link
Member

Choose a reason for hiding this comment

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

I wonder if we should return separate TickItem[] and TickLabel[], especially with grouping since tickItems.length !== tickLabels.length.

That way, we wouldn't need ignoreTick nor undefined values.

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 think for now it would not be necessary

Copy link
Member Author

Choose a reason for hiding this comment

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

Like, tickItems is very often not the same tickLabels in band scale, as we always have to add a final tick anyways.

Copy link
Member

Choose a reason for hiding this comment

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

Like, tickItems is very often not the same tickLabels in band scale, as we always have to add a final tick anyways.

Yeah, that's why I was wondering if the should separate them. One array of ticks, one array of tick labels.

Copy link
Member

Choose a reason for hiding this comment

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

We could technically do it since useTicks is an internal hook.
Maybe better to do that in a dedicated PR to avoid mixing too many modification

Copy link
Member

Choose a reason for hiding this comment

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

Yeah, separate PR sounds good

@alexfauquette
Copy link
Member

Yeah, it is a bit annoying. We could add a prop or something to fix that tick's size manually. The issue is that we can't know in which "tick" we should end.

For now we could always set it to the maximal. If we add customisation option we will have to add the sam option to the first tick.

Such that if the first month is March it does not look like it's the beginning of Q1

I don't get what is your proposal here 🤔

In brief my proposal is "don't do continuous scales in this PR.

The reason being:

  • I've not seen request for if and I don't think the data grid integration needs that
  • The usecase is not obviouse
  • The proposed result could be obtained with work-around

I tried to illustrate what a continuous axis grouping could look like. But this would require a completely different API

image

To get the current result, you could transform the tickSize into a function. And implements in something similar to the code below.

function tickSize(tickValue){
  if(tickValue.getDate() > 1){
	return 10; // date
  }
  which(tickValue.getMonth()){
    case 0:
      return 30; // year
    case 3:
    case 6:
    case 9:
      return 20; // quarters
    default:
      return 10; // date
  }
}

That's already how D3 manages to place 2024 and 2025 in those ticks
image

@JCQuintas
Copy link
Member Author

@alexfauquette Yeah I'm trying to go for something like option 1 there.

@alexfauquette
Copy link
Member

alexfauquette commented Jul 18, 2025

Yeah I'm trying to go for something like option 1 there.

You're current approach seems limited to do such thing. Because you start from ticks, which in the case of dates aligne correctly because D3 generates human-readable tick values.

I would not be surprised if we end up with distinct solutions for continuouse/ordinal axes.

Ordianl ones with a solution like you deed because it feels more natural, and for continuous an array of thresholds

@JCQuintas
Copy link
Member Author

I've moved the config to only be present for band and point scales, also reworked the config and added examples for tick size and styling

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

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

@JCQuintas JCQuintas requested a review from arminmeh July 29, 2025 12:07
Copy link
Member

@bernardobelchior bernardobelchior left a comment

Choose a reason for hiding this comment

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

Left some comments

@@ -240,12 +240,33 @@ If two or more axes share the same `position`, they are displayed in the order t

{{"demo": "MultipleAxes.js"}}

### Grouped Axes
## Grouped Axes
Copy link
Member

Choose a reason for hiding this comment

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

Do we want to label this as preview/unstable for now?

Copy link
Member

Choose a reason for hiding this comment

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

The question is "Are we sure the API will not be modified"

The current one seems robust, so I don't see reasons to mark the feature as preview


You can group axes together by rendering more than one axis on the same side.
In order to group axes together, a user can provide a `grouping` property in the axis definition.
Copy link
Member

Choose a reason for hiding this comment

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

Should we mention that this only works for band and point scales? If a user tries to use it with a different scale it'll throw an error

In order to target a specific group, the `data-group-index` attribute can be used as a selector.
The example below has a yellow tick color for the last group and blue text for the first group.

{{"demo": "GroupedAxesStyling.js"}}
Copy link
Member

Choose a reason for hiding this comment

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

I think it would be nice to have different demos, as they current all use the same dataset, but that can be done as a follow-up.

Copy link
Member

Choose a reason for hiding this comment

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

That would be nice for testing the rendering with Argos.

  • One that does not start with Q1 and end with Q4
  • One that does start with Q1 and end with Q4
  • One that uses point scale instead of band scale

Copy link
Member Author

Choose a reason for hiding this comment

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

Good catch, there issues with the first/last ticks 🤔

  • One that does start with Q1 and end with Q4
Screenshot 2025-08-01 at 12 04 07
  • One that does not start with Q1 and end with Q4
Screenshot 2025-08-01 at 12 04 02

We can always use the largest possible tick size at the start&end AG Grid
We can leave as is
We can allow changing the firstTick/lastTick: "largest"|"smallest"|groupIndex

Copy link
Member

Choose a reason for hiding this comment

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

We can always use the largest possible tick size at the start&end AG Grid

Looks like a better first step than the current one which has different behavior between first and last tick 👍

Copy link
Member

@alexfauquette alexfauquette left a comment

Choose a reason for hiding this comment

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

The other all look nice

Comment on lines 248 to 249
The `getGrouping` function receives the axis data value and should return an array of values.
Each value is a group name. Groups are displayed in the order they are defined in the `getGrouping` function.
Copy link
Member

Choose a reason for hiding this comment

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

Should we precise that this function overrides the value formatter?

### Tick size

The tick size can be customized for each group.
To do so, you can provide a `tickSize` property in the `grouping.config` array.
Copy link
Member

Choose a reason for hiding this comment

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

When looking at the config, it feels like the config is the part that will grow, and I don't see what else you will put in grouping

<Chart
  xAxis={[{
    ...,
    grouping: {
      getGrouping,
      config: [{ tickSize: 0 }, { tickSize: 32 }],
    },
  }]}
/>

Maybe another approach could be

<Chart
  xAxis={[{
    ...,
    groups: [
      { getValue: getMonth, tickSize: 0 },
      { getValue: getYear, tickSize: 32 },
    ]
  }]}
/>

The internal processing is similar, but you ensure consistency between the number of groups and their config

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 like the groups approach.

I was going to initially allow other props in grouping, but they did only make sense in the other axis types, but now they are out of scope, it might not be necessary.

Comment on lines +16 to +39
export type GroupedTickItemType = {
/**
* This property is undefined only if it's the tick closing the last band
*/
value?: any;
formattedValue?: string;
offset: number;
labelOffset: number;
/**
* In band scales, we remove some redundant ticks.
*/
ignoreTick?: boolean;
dataIndex?: number;
/**
* The index of the group this tick belongs to. If `getGrouping` returns `[[0, 1], [2, 3]]`
* both ticks with value `0` and `2` will have `groupIndex: 0`, and both ticks with value `1` and `3` will have `groupIndex: 1`.
*/
groupIndex?: number;
/**
* The index of the tick group inside the group. This is useful if you have multiple ticks with the same value,
* but they are divided by other values. Eg: `'Jan', ..., 'Dec', 'Jan'`
* We would have the first `'Jan'` with `syncIndex: 0` and `syncIndex: 12` for the second `'Jan'`.
*/
syncIndex?: number;
};
Copy link
Member

Choose a reason for hiding this comment

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

We could technically do it since useTicks is an internal hook.
Maybe better to do that in a dedicated PR to avoid mixing too many modification

*
* For each value in the returned array, a group will be created.
* Each group will have a label that is the stringified value of the group.
* For example, if the function returns `[31, "Jan", 2021]`, `[1, "Feb", 2021]`, `[2, "Feb", 2021]`,
Copy link
Contributor

Choose a reason for hiding this comment

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

this will play nicely with the grid-charts integration
with row grouping, we keep path for each row that can easily be mapped to this (mapping and in a reverse order - since we keep the top parent on index 0)

Copy link
Member Author

Choose a reason for hiding this comment

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

You liked @alexfauquette comment above #18766 (comment) which regards to grouping vs groups, but this confused me on what you would prefer 🤔

Can you clarify?

grouping: {
  getGrouping,
  config: [{ tickSize: 0 }, { tickSize: 32 }],
}
// VS
groups: [
  { getValue: getMonth, tickSize: 0 },
  { getValue: getYear, tickSize: 32 },
]

Copy link
Contributor

Choose a reason for hiding this comment

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

I see, because it will break the array that I said we could compile pretty easy.

Still, I would prefer

groups: [
  { getValue: getMonth, tickSize: 0 },
  { getValue: getYear, tickSize: 32 },
]

It avoids requirement to track the indexes manually.
The values can still be provided pretty easy by picking the index from our internal array.

Also, in case you want to override defaults for the last index, the first options would have to be something like this?

grouping: {
  getGrouping,
  config: [null, { tickSize: 32 }],
}

Copy link
Member Author

Choose a reason for hiding this comment

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

@arminmeh yeah that would be the case.

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'll convert it to groups: [] then

@JCQuintas JCQuintas changed the title [charts] Add axis grouping [charts] Add groups to band and point axis config Aug 4, 2025
Copy link
Member

@bernardobelchior bernardobelchior left a comment

Choose a reason for hiding this comment

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

It seems the demo is crashing when the groups array is empty.

Screen.Recording.2025-08-05.at.11.36.32.mov

It's a weird edge case, but we should avoid crashing.

Not sure of the implications, but should we only render the grouped axes component when groups.length > 1?

I'm suggesting groups.length > 1 because groups is missing some functionality from normal tick labels, such as overlap handling. When groups.length === 1, the user could have used the normal tick labels which provides more functionality, so it doesn't really make a lot of sense to use the grouping functionality, does it?

position === 'none'
) {
return null;
if ('groups' in xAxis[inProps.axisId ?? xAxisIds[0]]) {
Copy link
Member

Choose a reason for hiding this comment

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

Should we check if groups is an array?

Otherwise there might be edge cases like this:

const a = { 'groups': undefined };
'groups' in a; // true

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've added some more reasonable checks

Copy link
Member Author

Choose a reason for hiding this comment

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

Regarding groups.length > 1

I think it could be more confusing, as in I could just be trying to style the first group in isolation, but then if I add another item in the array everything changes.

I think we can leave it as is for now and check how ppl react

Copy link
Member

Choose a reason for hiding this comment

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

I think it could be more confusing, as in I could just be trying to style the first group in isolation, but then if I add another item in the array everything changes.

True. I'm ok with it as is

) {
return null;
const axis = xAxis[inProps.axisId ?? xAxisIds[0]];
if ('groups' in axis && Array.isArray(axis.groups)) {
Copy link
Member

Choose a reason for hiding this comment

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

Should we also check if length > 0?

Image

People might use an empty array to disable this option. Not sure if there's a big benefit in accepting an empty array here.

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 don't see an issue with it

Comment on lines +16 to +39
export type GroupedTickItemType = {
/**
* This property is undefined only if it's the tick closing the last band
*/
value?: any;
formattedValue?: string;
offset: number;
labelOffset: number;
/**
* In band scales, we remove some redundant ticks.
*/
ignoreTick?: boolean;
dataIndex?: number;
/**
* The index of the group this tick belongs to. If `getGrouping` returns `[[0, 1], [2, 3]]`
* both ticks with value `0` and `2` will have `groupIndex: 0`, and both ticks with value `1` and `3` will have `groupIndex: 1`.
*/
groupIndex?: number;
/**
* The index of the tick group inside the group. This is useful if you have multiple ticks with the same value,
* but they are divided by other values. Eg: `'Jan', ..., 'Dec', 'Jan'`
* We would have the first `'Jan'` with `syncIndex: 0` and `syncIndex: 12` for the second `'Jan'`.
*/
syncIndex?: number;
};
Copy link
Member

Choose a reason for hiding this comment

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

Yeah, separate PR sounds good

Copy link
Member

@alexfauquette alexfauquette left a comment

Choose a reason for hiding this comment

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

I'm good with the API and the global implementation. Left minor comments about the implementation details

Comment on lines +146 to +147
// Step 2: Sort by group index to ensure proper ordering
allTickItems.sort((a, b) => a.groupIndex - b.groupIndex);
Copy link
Member

Choose a reason for hiding this comment

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

You mapped the new groups structure to generate getGrouping and keep this internal logic. BU tit might be simpler to directly use groups

const maxGroupIndex = groups.length -1

const allTickItems = []

for (let groupIndex = 0; groupIndex < groups.length; groupIndex+=1){
  // No need to sort anymore

  tickValues.forEach((tickValue, dataIndex) => {
    const groupValue = groups[groupIndex].getValue(tickValue, dataIndex);

    // ....

    if(isNew){
      // Add a new item
      allTickItems.push( ... )
    } else {
      // Merge it with the last item
      allTickItems[allTickItems.length -1].size += itemSize
    }
  })
}

Copy link
Member Author

Choose a reason for hiding this comment

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

moved it to a new pr, it will change the logic, so it will be easier in a new pr where I can be sure the styles wont be affected

Copy link
Member Author

Choose a reason for hiding this comment

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

Copy link
Member

@bernardobelchior bernardobelchior left a comment

Choose a reason for hiding this comment

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

The functionality seems to be working well, so I'm approving to unblock integration with the data grid. I think we can still do refactors in a follow-up to reduce code duplication and streamline some parts as mentioned in previous comments.

The comments aren't blockers for me, so they can be addressed in a follow-up.

Great job!

In order to group `band` or `point` axes together, a user can provide a `groups` property in the x-axis definition.
This property expects an array of objects with a `getValue` function.

The `getValue` function receives the axis data value and should return a group name.
Copy link
Member

Choose a reason for hiding this comment

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

Should we also explain how grouping is done?

For example, it might not be clear that you can have groups with the same name (e.g., two "Jan") and they'll be separate groups if the next group is different (e.g., 2025 and 2026).

Although, that's shown in the demo, so maybe it'll become clear by example.

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 don't think it is necessary, it should be clear-ish by example.

But also generally, considering the amount of issues we had with dates in the past, users expect the same values to be different if not one after the other

const tickLabel = item.formattedValue;
const ignoreTick = item.ignoreTick ?? false;
const groupIndex = item.groupIndex ?? 0;
const groupConfig = getGroupingConfig(groupingConfig, groupIndex, tickSize);
Copy link
Member

Choose a reason for hiding this comment

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

getGroupingConfig is only being called here and we're only using groupConfig.tickSize.

We can rename getGroupingConfig to calculateTickSize, pass the necessary arguments and return a number instead of an object. Should allow us to remove some lines of code.

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 y axis it will need to parse more options, so I'll leave it here for now

extremities: 0,
end: 1,
middle: 0.5,
tick: 0,
Copy link
Member

Choose a reason for hiding this comment

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

Is tick a valid option? I can't find any reference to it

Copy link
Member Author

Choose a reason for hiding this comment

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

/**
 * The placement of ticks label. Can be the middle of the band, or the tick position.
 * Only used if scale is 'band'.
 * @default 'middle'
 */
tickLabelPlacement?: 'middle' | 'tick';

https://github.com/JCQuintas/mui-x/blob/59b761f8d8942209d7609cfcb5e1fdfac04aed77/packages/x-charts/src/hooks/useTicks.ts#L47

@JCQuintas JCQuintas merged commit 9ce51f6 into mui:master Aug 5, 2025
21 checks passed
@JCQuintas JCQuintas deleted the grouped-axis-2 branch August 5, 2025 17:27
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] Grouped axes
5 participants