[charts-premium] Replace dataIndex with name in map identifier#22891
Conversation
Deploy previewBundle size
PerformanceTotal duration: 1,833.50 ms -276.09 ms(-13.1%) | Renders: 63 (+0)
…and 6 more (+15 within noise) — details Check out the code infra dashboard for more information about this PR. |
|
@alexfauquette I started the mapShape identifier change and hit a blocker. I'd like your call on. Option A, keep the field named
Fixing these means adding casts / mapShape exclusions in shared core selectors. Option B, go the Heatmap/Sankey direction: drop I'm leaning towards B, it makes |
|
@sai6855 Option B is effectively the one that make the more sense This will probably require some modification about the keyboard navigation, but noting too complicated |
…sConfig/types/colorProcessor.types.ts
There was a problem hiding this comment.
Pull request overview
This PR updates the mapShape item identifier in x-charts-premium map charts from { dataIndex: number } to { name: string } (feature name), and wires that change through highlighting, tooltips, keyboard navigation, and color resolution.
Changes:
- Replaced
dataIndex-based map shape identifiers withname-based identifiers across map series types and interactions. - Added map-specific identifier serialization/cleaning and highlight/fade creators to avoid the shared
dataIndex-based utilities. - Updated map tooltip, tooltip positioning, keyboard focus handler, and color getter logic to resolve items by
name.
Reviewed changes
Copilot reviewed 19 out of 19 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/x-charts/src/internals/plugins/featurePlugins/useChartHighlight/createIsHighlighted.ts | Excludes mapShape from the shared dataIndex-based highlight creator. |
| packages/x-charts/src/internals/plugins/featurePlugins/useChartHighlight/createIsFaded.ts | Excludes mapShape from the shared dataIndex-based fade creator. |
| packages/x-charts/src/internals/plugins/corePlugins/useChartSeriesConfig/types/colorProcessor.types.ts | Updates mapShape color getter signature to accept name?: string. |
| packages/x-charts/src/internals/createCommonKeyboardFocusHandler.ts | Excludes mapShape from shared dataIndex-based keyboard navigation handler. |
| packages/x-charts/src/internals/commonNextFocusItem.ts | Excludes mapShape from shared dataIndex-based next/prev focus utilities. |
| packages/x-charts-premium/src/models/seriesType/mapShape.ts | Changes MapShapeItemIdentifier from dataIndex to name. |
| packages/x-charts-premium/src/Map/seriesConfig/tooltipPosition.ts | Computes tooltip position using identifier.name instead of dataIndex. |
| packages/x-charts-premium/src/Map/seriesConfig/tooltip.ts | Resolves tooltip data/color using identifier.name. |
| packages/x-charts-premium/src/Map/seriesConfig/seriesProcessor.ts | Applies visibility checks using { name } identifiers for map items. |
| packages/x-charts-premium/src/Map/seriesConfig/keyboardFocusHandler.ts | Replaces common dataIndex navigation with a name-based keyboard handler. |
| packages/x-charts-premium/src/Map/seriesConfig/index.ts | Switches to map-specific serializer/cleaner/highlight implementations. |
| packages/x-charts-premium/src/Map/seriesConfig/identifierSerializer.ts | Adds mapShape identifier serializer based on name. |
| packages/x-charts-premium/src/Map/seriesConfig/identifierCleaner.ts | Adds mapShape identifier cleaner returning { type, seriesId, name }. |
| packages/x-charts-premium/src/Map/seriesConfig/highlight.ts | Adds map-specific highlight/fade creators using { name }. |
| packages/x-charts-premium/src/Map/seriesConfig/getColor.ts | Updates map color lookup to be name-based. |
| packages/x-charts-premium/src/Map/seriesConfig/descriptionGetter.ts | Updates description getter to resolve items by name. |
| packages/x-charts-premium/src/Map/MapShapePlot.tsx | Passes name into MapShape and uses colorGetter(item.name). |
| packages/x-charts-premium/src/Map/MapShape.tsx | Builds interaction identifier using { name } instead of { dataIndex }. |
| packages/x-charts-premium/src/Map/FocusedMapShape.tsx | Resolves focused item’s data row by name. |
Comments suppressed due to low confidence (1)
packages/x-charts-premium/src/Map/seriesConfig/tooltipPosition.ts:11
itemSeriesis assigned but never used after switching mapShape identifiers toname. This will trip linting and is unnecessary—just validate the series exists (if needed) without storing it.
if (!identifier || identifier.name === undefined) {
return null;
}
const itemSeries = series.mapShape?.series[identifier.seriesId];
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| const item = series.data.find((d) => d.name === name); | ||
|
|
||
| if (item.color !== undefined) { | ||
| if (item?.color !== undefined) { | ||
| return item.color; | ||
| } |
| if (!identifier || identifier.name === undefined) { | ||
| return null; | ||
| } | ||
|
|
||
| const point = series.data[identifier.dataIndex]; | ||
| const point = series.data.find((d) => d.name === identifier.name); |
| const order = mapSeries.seriesOrder; | ||
| const position = order.indexOf(currentItem.seriesId); | ||
| for (let i = position + direction; i >= 0 && i < order.length; i += direction) { |
|
This pull request has conflicts, please resolve those before we can evaluate the pull request. |
| const item = series.data.find((d) => d.name === name); | ||
|
|
||
| if (item.color !== undefined) { | ||
| if (!item) { | ||
| return series.color; | ||
| } |
| return (name?: string) => { | ||
| if (name == null) { | ||
| return series.color; | ||
| } | ||
| return series.data[dataIndex].color ?? series.color; | ||
| const item = series.data.find((d) => d.name === name); | ||
| return item?.color ?? series.color; | ||
| }; |
| export type MapShapeProps = Omit<React.SVGProps<SVGPathElement>, 'ref'> & { | ||
| seriesId: SeriesId; | ||
| dataIndex: number; | ||
| name: string; | ||
| d: string; | ||
| color: string; | ||
| }; |
| for (const seriesId of mapSeries.seriesOrder) { | ||
| const data = mapSeries.series[seriesId]?.data; | ||
| if (data && data.length > 0) { | ||
| return toFocusedItem(seriesId, data[0].name); | ||
| } | ||
| } | ||
| return null; |
| const data = getSeriesData(state, currentItem.seriesId); | ||
| const current = data.findIndex((d) => d.name === currentItem.name); | ||
| if (current === -1) { | ||
| return getFirstItem(state); | ||
| } | ||
| const next = current + direction; | ||
| if (next < 0 || next >= data.length) { | ||
| return currentItem; | ||
| } | ||
| return toFocusedItem(currentItem.seriesId, data[next].name); | ||
| } |
| for (let i = position + direction; i >= 0 && i < order.length; i += direction) { | ||
| const data = mapSeries.series[order[i]]?.data; | ||
| if (data && data.length > 0) { | ||
| return toFocusedItem(order[i], data[0].name); | ||
| } | ||
| } | ||
| return currentItem; |
| * Note: hidden shapes/series are not skipped here — the defaultized series read | ||
| * from the state does not carry visibility. Focus may land on a hidden shape. |
| import { typeSerializer, seriesIdSerializer } from '@mui/x-charts/internals'; | ||
| import type { IdentifierSerializer } from '@mui/x-charts/internals'; | ||
|
|
||
| const identifierSerializer: IdentifierSerializer<'mapShape'> = (identifier) => { | ||
| return `${typeSerializer(identifier.type)}${seriesIdSerializer(identifier.seriesId)}(${identifier.name})`; | ||
| }; | ||
|
|
||
| export default identifierSerializer; |
| function stepWithinSeries( | ||
| currentItem: FocusedItemIdentifier<'mapShape'>, | ||
| state: MapState, | ||
| direction: 1 | -1, | ||
| ): FocusedItemIdentifier<'mapShape'> | null { | ||
| const series = getMapSeries(state)?.series[currentItem.seriesId]; | ||
| if (!series) { | ||
| return currentItem; | ||
| } | ||
| const current = series.data.findIndex((d) => d.name === currentItem.name); | ||
| if (current === -1) { | ||
| return getFirstItem(state); | ||
| } | ||
| const next = nextVisibleIndex(series.data, current + direction, direction); | ||
| return next === null ? currentItem : toFocusedItem(currentItem.seriesId, series.data[next].name); | ||
| } |
| export type MapShapeProps = Omit<React.SVGProps<SVGPathElement>, 'ref'> & { | ||
| seriesId: SeriesId; | ||
| dataIndex: number; | ||
| name: string; | ||
| d: string; | ||
| color: string; |
@alexfauquette Updated identifier structure, Now PR is ready for review |
|
|
||
| const descriptionGetter: DescriptionGetter<'mapShape'> = ({ identifier, series }) => { | ||
| const item = series.data[identifier.dataIndex]; | ||
| const item = series.data.find((d) => d.name === identifier.name); |
There was a problem hiding this comment.
How should this work when joining shapes? Like, should we provide an array of items instead?
If I understand correctly, in the current api, a name could be related to multiple shapes/items 🤔
There was a problem hiding this comment.
The map joins data to features through a resolved key (geoFeatureKey, default feature.properties.name), and one key can intentionally map to several shapes for example, the Somalia/Somaliland demo,
If two different features share a same name, the user can give them distinct keys via geoFeatureKey (when the features carry a unique field like an id or ISO code)
There was a problem hiding this comment.
Exactly. Giving the same name to multiple shapes is a way to indicate they should be treated the same way.
Btw this aspect changes nothing, because the dataIndex we replace in this PR is the series data index, and not the feature data index :)
There was a problem hiding this comment.
You mean if we merge the below
[ { name: 'land', population: 3000, }, { name: 'land_disputed', population: 1000, }, ]
we will ignore the population data of land_disputed once merged?
Turns out we can re-use existing keyboard utils, just needed slight tweaks in types. Updated code here dbc3813 |
name in map identifier
alexfauquette
left a comment
There was a problem hiding this comment.
LGTM 👍 @JCQuintas are you ok with the new API?
| const getMapSeries = (state: MapState) => | ||
| selectorChartSeriesProcessed(state as ChartState<[UseChartKeyboardNavigationSignature], []>) | ||
| .mapShape; |
There was a problem hiding this comment.
This function is only used once
| const getMapSeries = (state: MapState) => | |
| selectorChartSeriesProcessed(state as ChartState<[UseChartKeyboardNavigationSignature], []>) | |
| .mapShape; |
| } | ||
|
|
||
| return (currentItem, state) => { | ||
| const mapSeries = getMapSeries(state); |
There was a problem hiding this comment.
| const mapSeries = getMapSeries(state); | |
| const mapSeries = selectorChartSeriesProcessed(state as ChartState<[UseChartKeyboardNavigationSignature], []>).mapShape; |
| * reuse these helpers by translating to a `dataIndex` at their boundary. | ||
| */ | ||
| type WorkingItem = { | ||
| type: Exclude<ChartSeriesType, 'sankey' | 'heatmap'>; |
There was a problem hiding this comment.
Why we need to exclude sankey/heatmap now?
| } | ||
|
|
||
| export function createGetNextIndexFocusedItem< | ||
| InSeriesType extends Exclude<ChartSeriesType, 'sankey' | 'heatmap'>, |
There was a problem hiding this comment.
@JCQuintas Exclusion is actually not a new change, We just moved this type to WorkingItem[type]
…#22891) Signed-off-by: sai chand <[email protected]> Co-authored-by: Copilot Autofix powered by AI <[email protected]> Co-authored-by: alex <[email protected]>

Fix #22943
changelog
● Breaking change
The mapShape item identifier uses feature name instead of dataIndex.
If you read or build a mapShape identifier (for example in
highlightedItem, oronHighlightChange), replacedataIndexby the correspondingname.New requirement: names must be unique
Because shapes are now identified by name, each mapShape data entry must have a unique name within its series —
otherwise duplicates can't be told apart for coloring, tooltips, and highlighting. A dev-time error is thrown when a
duplicate is detected. A single name can still map to multiple shapes on the map (one logical item, several polygons);
use
geoFeatureKeyto give distinct features distinct keys.