mui / mui-x

MUI X: Build complex and data-rich applications using a growing list of advanced React components, like the Data Grid, Date and Time Pickers, Charts, and more!
https://mui.com/x/
4.05k stars 1.25k forks source link

[charts] Add a "seriesIndex" Property to the AxisContent Custom Charts Tooltip in an AreaPlot #13944

Open halfacandan opened 1 month ago

halfacandan commented 1 month ago

Summary

Feature: Add a "seriesIndex" Property to the AxisContent Custom Charts Tooltip

Scenario: User hovers their mouse over a data series in a stacked area chart Given a ResponsiveChartContainer which contains an AreaPlot with a ChartsTooltip And an array of data series called "set1" & "set2" which are stacked in a stack called "total" When the properties on the ChartsTooltip are set to trigger = "axis" and slots = {{ axisContent: formatTooltip }} And the user hovers their mouse over "set2" in the rendered chart Then the formatTooltip function should have access to dataIndex i.e. the x axis's value (which it currently does) And the formatTooltip function should have access to another property which identifies the data series that has focus e.g. "set2"

Examples

Actual Behaviour

Only the dataIndex is available, no reference to the data series which has focus is available: https://stackblitz.com/edit/react-hydnyg-hzvrzt?file=Demo.js

If I stringify the props passed to the "formatTooltip" function (shown below), I have access to the "dataIndex" and "axisValue" properties to identify the x-axis but nothing refers to the data series which is being hovered over. I added some "highlightScope" properties to the data series to show that a reference to the hovered series is stored somewhere.

{
    "axisData": {
        "x": {
            "index": 2,
            "value": "C"
        },
        "y": {
            "value": 0.30000000000000027
        }
    },
    "series": [
        {
            "id": "auto-generated-id-0",
            "color": "#02B2AF",
            "label": "set1",
            "type": "line",
            "area": true,
            "stack": "total",
            "data": [
                1,
                2,
                3,
                2,
                1
            ],
            "highlightScope": {
                "highlighted": "item",
                "faded": "global"
            },
            "stackedData": [
                [
                    0,
                    1
                ],
                [
                    0,
                    2
                ],
                [
                    0,
                    3
                ],
                [
                    0,
                    2
                ],
                [
                    0,
                    1
                ]
            ]
        },
        {
            "id": "auto-generated-id-1",
            "color": "#2E96FF",
            "label": "set2",
            "type": "line",
            "area": true,
            "stack": "total",
            "data": [
                1,
                2,
                3,
                2,
                1
            ],
            "highlightScope": {
                "highlighted": "item",
                "faded": "global"
            },
            "stackedData": [
                [
                    1,
                    2
                ],
                [
                    2,
                    4
                ],
                [
                    3,
                    6
                ],
                [
                    2,
                    4
                ],
                [
                    1,
                    2
                ]
            ]
        }
    ],
    "axis": {
        "categoryGapRatio": 0.2,
        "barGapRatio": 0.1,
        "id": "x-axis-id",
        "data": [
            "A",
            "B",
            "C",
            "D",
            "E"
        ],
        "scaleType": "band",
        "tickNumber": 5
    },
    "dataIndex": 2,
    "axisValue": "C",
    "sx": {
        "mx": 2
    },
    "classes": {
        "root": "MuiChartsTooltip-root",
        "table": "MuiChartsTooltip-table",
        "row": "MuiChartsTooltip-row",
        "cell": "MuiChartsTooltip-cell",
        "mark": "MuiChartsTooltip-mark",
        "markCell": "MuiChartsTooltip-markCell",
        "labelCell": "MuiChartsTooltip-labelCell",
        "valueCell": "MuiChartsTooltip-valueCell"
    },
    "ownerState": {}
}

Example code

import * as React from 'react';
import Box from '@mui/material/Box';
import { ResponsiveChartContainer } from '@mui/x-charts/ResponsiveChartContainer';
import { AreaPlot, MarkPlot } from '@mui/x-charts/LineChart';
import { ChartsTooltip, ChartsXAxis } from '@mui/x-charts';

export default function SwitchSeriesType() {
  const formatTooltip = (props) => {
    console.log(props.dataIndex); // This is the index of the x-axis
    console.log(JSON.stringify(props)); // THe props do not contain any reference to which series is being hovered over
  };

  return (
    <Box sx={{ width: '100%' }}>
      <div>
        <ResponsiveChartContainer
          series={[
            {
              label: 'set1',
              type: 'line',
              area: true,
              stack: 'total',
              data: [1, 2, 3, 2, 1],
              highlightScope: {
                  highlighted: 'item',
                  faded: 'global'
              }
            },
            {
              label: 'set2',
              type: 'line',
              area: true,
              stack: 'total',
              data: [1, 2, 3, 2, 1],
              highlightScope: {
                  highlighted: 'item',
                  faded: 'global'
              }
            },
          ]}
          xAxis={[
            {
              data: ['A', 'B', 'C', 'D', 'E'],
              scaleType: 'band',
              id: 'x-axis-id',
            },
          ]}
          height={200}
        >
          <AreaPlot />
          <ChartsXAxis label="X axis" position="bottom" axisId="x-axis-id" />
          <MarkPlot />
          <ChartsTooltip
            trigger="axis"
            slots={{ axisContent: formatTooltip }}
          />
        </ResponsiveChartContainer>
      </div>
    </Box>
  );
}

Motivation

In the Axis tooltip, I would like to have an option to visually highlight which of the series is currently hovered over by the user's mouse e.g. with emboldened text. This gives users a simple visual prompt which is not dependent on colour or pattern.

Search keywords: ResponsiveChartContainer AreaPlot ChartsTooltip

alexfauquette commented 1 month ago

For now, you can either use

You could consider using useHighlighted() to get information about the current series with highlight. Those hooks are not yet documented

halfacandan commented 1 month ago

Thanks @alexfauquette. I have tried your second suggestion already and documented what I believe is a bugged behaviour: https://github.com/mui/mui-x/issues/13945

Do you have any example code for how to use the "useHighlighted()" function as that sounds ideal?

alexfauquette commented 1 month ago

Do you have any example code for how to use the "useHighlighted()" function as that sounds ideal?

Not yet. We did not decided a strategy about how to document such hooks

halfacandan commented 1 month ago

For anyone else interested in using the "useHighlighted()" function, here's some hacky code that you can use to capture the value

const [highlightedSeriesId, setHighlightedSeriesId] = React.useState<string|null>();
function UseHighlighted(): JSX.Element {
      const { highlightedItem } = useHighlighted();
      setHighlightedSeriesId(highlightedItem?.seriesId?.toString() ?? null);
      return <></>;
  }

They you just nest a element inside your ResponsiveChartContainer e.g.

<ResponsiveChartContainer>
    <UseHighlighted />
    <AreaPlot />
    <ChartsTooltip trigger="axis" slots={{ axisContent: formatTooltip }} />
</ResponsiveChartContainer>

You can then use the captured highlightedSeriesId value to identify a specific data series in your custom tooltip function.

alexfauquette commented 1 month ago

If your custom component is inside the <ResponsiveChartContainer /> you don't need to save it in a dedicated state (it's already in a context)

The slots are expecting components to render. To avoid losing internal state, those components should be declare outside of the function rendering the charts

+ const FormatTooltip = (props) => {
+   console.log(props.dataIndex); // This is the index of the x-axis
+   console.log(JSON.stringify(props)); // THe props do not contain any reference to which series is being hovered over   };
+ }

export default function SwitchSeriesType() {
-   const formatTooltip = (props) => {
-     console.log(props.dataIndex); // This is the index of the x-axis
-     console.log(JSON.stringify(props)); // THe props do not contain any reference to which series is being hovered over
-   };

  return (
    <Box sx={{ width: '100%' }}>
      <div>
        <ResponsiveChartContainer

Here is a quick example, highlighting the current series, by increasing a div size

https://stackblitz.com/edit/react-hydnyg-zja5ef?file=Demo.js,package.json

halfacandan commented 1 month ago

Thank you for the example code @alexfauquette :D

alexfauquette commented 1 month ago

The current "solution" of this issue will probably be easier to create and document after #13819