Skip to content

Instantly share code, notes, and snippets.

@amit08255
Created December 12, 2023 19:50
Show Gist options
  • Save amit08255/027bf480674dae3515f2448974a58adf to your computer and use it in GitHub Desktop.
Save amit08255/027bf480674dae3515f2448974a58adf to your computer and use it in GitHub Desktop.
highcharts custom legends with hover and click effects in React

Highcharts Donut Chart Sample

import * as React from 'react';
import Highcharts, { Chart as HighchartsChart } from 'highcharts';
import HighchartsReact from 'highcharts-react-official';
import { Box } from '@chakra-ui/react';
import DonutChartWrapper from './index.styles';
import Tooltip from '../tooltip';

export type DonutChartData = {
    name: string,
    color?: string,
    y: number,
};

type DataLabel = {
    enabled: boolean,
    useHTML?: boolean,
    format?: string,
    formatter?: (e:any) => string,
    dashedConnector?: boolean,
    distance?: string,
    padding?: number,
    connectorColor?: string,
    style: { [key:string]: string|number },
}

type Legend = {
    align?: 'center' | 'left' | 'right',
    verticalAlign?: 'bottom' | 'top' | 'middle',
    y?: number,
    floating?: boolean,
    enabled?: boolean,
    padding?: number,
    itemMarginTop?: number,
    symbolWidth?: number,
    symbolHeight?: number,
    symbolRadius?: number,
    symbolPadding?: number,
    width?: number,
    itemWidth?: number,
    useHTML?: boolean,
};

type Props = {
    data: DonutChartData[],
    options?: Highcharts.Options,
    borderRadius?: string,
    tooltip?: React.FC<{ data: DonutChartData }>,
    title?: string, // can also provide HTML string
    height?: number,
    width?: number,
    margin?: number[],
    donutHeight?: number,
    donutWidth?: number,
    titlePos?: number,
    legendPos?: number,
    circleSize?: string,
    innerCircleSize?: string,
    dataLabels?: DataLabel,
    legend?: Legend,
    eventName?: string, // this event is dispatched when chart is loaded
};

const ColoredGridGraph = ({
    height, width, margin, titlePos, legendPos, data, options, borderRadius, tooltip, title,
    donutWidth, donutHeight, circleSize, innerCircleSize, dataLabels, legend, eventName,
}:Props) => {
    const TooltipNode = tooltip;
    const [chart, setChart] = React.useState<HighchartsChart | null>(null);
    const callback = React.useCallback((chartData: HighchartsChart) => {
        setChart(chartData);
    }, []);

    React.useEffect(() => {
        if (chart && eventName) {
            window.dispatchEvent(new CustomEvent(eventName, { detail: chart }));
        }
    }, [chart]);

    const chartOptions = {
        chart: {
            type: 'pie',
            padding: 0,
            spacing: [0, 0, 0, 0],
            plotBackgroundColor: null,
            plotBorderWidth: null,
            plotShadow: false,
            margin,
            width: donutWidth,
            height: donutHeight,
            spacingTop: 0,
            spacingBottom: 0,
            spacingLeft: 0,
            spacingRight: 0,
        },
        title: {
            text: title,
            align: 'center',
            verticalAlign: 'middle',
            y: titlePos,
        },
        xAxis: {
            className: 'highcharts-d-none',
            minPadding: 0,
            maxPadding: 0,
        },
        yAxis: {
            className: 'highcharts-d-none',
            minPadding: 0,
            maxPadding: 0,
        },
        legend: {
            align: 'center',
            verticalAlign: 'bottom',
            y: legendPos,
            floating: true,
            enabled: true,
            padding: 3,
            itemMarginTop: 4,
            symbolWidth: 10,
            symbolHeight: 10,
            symbolRadius: 2,
            symbolPadding: 6,
            squareSymbol: true,
            width: donutWidth,
            ...legend,
        },
        plotOptions: {
            pie: {
                shadow: false,
            },
        },
        exporting: {
            enabled: false,
        },
        tooltip: {
            useHTML: true,
            outside: true,
            headerFormat: null,
            followPointer: true,
            enabled: TooltipNode !== null,
            hideDelay: 100,
        },
        credits: {
            enabled: false,
        },
        series: [{
            name: 'PieChart',
            data,
            size: circleSize,
            innerSize: innerCircleSize,
            showInLegend: true,
            dataLabels,
        }],
        ...options,
    };

    return (
        <DonutChartWrapper
            height={height}
            width={width}
            borderRadius={borderRadius}
            dashedConnector={dataLabels?.dashedConnector}
        >
            <HighchartsReact
                highcharts={Highcharts}
                options={chartOptions}
                callback={callback}
            />
            <Tooltip chart={chart}>
                {(formatterContext) => {
                    if (!TooltipNode) {
                        return (
                            <Box display="none" />
                        );
                    }
                    return (
                        <TooltipNode data={{ name: formatterContext.key, y: formatterContext.y }} />
                    );
                }}
            </Tooltip>
        </DonutChartWrapper>
    );
};

ColoredGridGraph.defaultProps = {
    width: 450,
    height: 400,
    donutWidth: 330,
    donutHeight: 390,
    margin: [-110, 0, 0, 0],
    titlePos: -30,
    legendPos: -60,
    circleSize: '88%',
    innerCircleSize: '70%',
    options: {},
    borderRadius: '5px',
    tooltip: null,
    title: '',
    legend: {},
    dataLabels: {
        enabled: false,
    },
    eventName: null,
};

export default ColoredGridGraph;

Custom Legend Component

import * as React from 'react';
import { Box } from '@chakra-ui/react';

const CustomLegends = () => {
    const ref = React.useRef({
        prevCallback: null, callback: null, hover: {},
    });
    const [visible, setVisible] = React.useState({});
    const [chart, setChart] = React.useState(null);
    const [legends, setLegends] = React.useState([]);

    ref.current.callback = (e) => {
        setChart(() => e.detail);

        const legendList = [];

        for (let i = 0; i < e.detail.series[0].data.length; i += 1) {
            const item = e.detail.series[0].data[i];
            legendList.push({
                name: item.name,
                color: item.color,
            });
        }

        setLegends(() => legendList);
    };

    React.useEffect(() => {
        if (ref.current.prevCallback) {
            window.removeEventListener('competitor-wallet-share-chart-update', ref.current.prevCallback);
        }

        window.addEventListener('custom-chart-update', ref.current.callback);
        ref.current.prevCallback = ref.current.callback;
    }, []);

    const onLegendClick = (index) => {
        const data = chart.series[0].data[index];
        const visibleData = { ...visible };

        visibleData[data.name] = visibleData[data.name] !== undefined
            ? !visibleData[data.name] : false;

        data.setVisible(visibleData[data.name]);
        setVisible(() => visibleData);
    };

    const onMouseOver = (index) => {
        const dataList = chart.series[0].data;

        if (dataList[index].visible === false || ref.current.hover[dataList[index].name]) {
            return;
        }

        ref.current.hover[dataList[index].name] = true;

        for (let i = 0; i < dataList.length; i += 1) {
            if (i !== index) {
                dataList[i].update({
                    opacity: 0.5,
                });
            } else {
                dataList[i].update({
                    opacity: 1,
                });

                dataList[i].setState('hover');
            }
        }
    };

    const onMouseOut = () => {
        const dataList = chart.series[0].data;

        for (let i = 0; i < dataList.length; i += 1) {
            if (dataList[i].opacity >= 1) {
                // eslint-disable-next-line no-continue
                continue;
            }

            dataList[i].update({
                opacity: 1,
            });

            dataList[i].setState('normal');
            ref.current.hover[dataList[i].name] = false;
        }
    };

    return (
        <Box w="full" overflow="hidden">
            <Box display="flex" alignItems="center" w="full" flexWrap="wrap">
                {
                    legends.map((item, index) => (
                        <Box
                            cursor="pointer"
                            mr="2"
                            key={`chart-legend-${index + 1}`}
                            display="flex"
                            alignItems="center"
                            mb="2"
                            onClick={() => onLegendClick(index)}
                            onMouseOver={() => onMouseOver(index)}
                            onMouseOut={() => onMouseOut()}
                            opacity={
                                visible[item.name] || visible[item.name] === undefined ? 1 : 0.5
                            }
                        >
                            <Box w="10px" h="10px" borderRadius="50%" bgColor={item.color} mr="2" />
                            <Box fontSize="sm" _hover={{ fontWeight: 'semibold' }}>{item.name}</Box>
                        </Box>
                    ))
                }
            </Box>
        </Box>
    );
};

export default CustomLegends;

Usage

import * as React from 'react';
import { Box, Text } from '@chakra-ui/react';
import DonutChartWrapper from 'components/HighCharts/DonutChart/index.styles';
import DonutChart from 'components/HighCharts/DonutChart';
import CustomLegends from './CustomLegends';

const CustomChartSample = () => (
    <Box w="full" mt="6" p="5" border="1.5px solid" borderColor="gray.300" rounded="md">
        <DonutChartWrapper>
            <DonutChart
                data={[
                    { name: 'test', y: 100 },
                    { name: 'test2', y: 200 },
                    { name: 'test3', y: 300 },
                    { name: 'test4', y: 400 },
                    { name: 'test5', y: 500 },
                    { name: 'test6', y: 600 },
                    { name: 'test7', y: 700 },
                    { name: 'test8', y: 800 },
                    { name: 'test9', y: 900 },
                    { name: 'test10', y: 1000 },
                    { name: 'test11', y: 1100 },
                    { name: 'test12', y: 1200 },
                    { name: 'test13', y: 1300 },
                    { name: 'test14', y: 1400 },
                    { name: 'test15', y: 1500 },
                    { name: 'test16', y: 1600 },
                    { name: 'test17', y: 1700 },
                    { name: 'test18', y: 1800 },
                    { name: 'test19', y: 1900 },
                    { name: 'test20', y: 2000 },
                    { name: 'test21', y: 2100 },
                    { name: 'test22', y: 2200 },
                    { name: 'test23', y: 2300 },
                    { name: 'test24', y: 2400 },
                    { name: 'test25', y: 2500 },
                    { name: 'test26', y: 2600 },
                    { name: 'test27', y: 2700 },
                    { name: 'test28', y: 2800 },
                    { name: 'test29', y: 2900 },
                    { name: 'test30', y: 3000 },
                    { name: 'test31', y: 3100 },
                    { name: 'test32', y: 3200 },
                    { name: 'test33', y: 3300 },
                    { name: 'test34', y: 3400 },
                    { name: 'test35', y: 3500 },
                    { name: 'test36', y: 3600 },
                    { name: 'test37', y: 3700 },
                    { name: 'test38', y: 3800 },
                    { name: 'test39', y: 3900 },
                    { name: 'test40', y: 4000 },
                ]}
                width={300}
                height={300}
                donutWidth={300}
                donutHeight={300}
                margin={[0, 0, 0, 0]}
                titlePos={-30}
                legendPos={0}
                legend={{ enabled: false }}
                eventName="custom-chart-update"
            />
        </DonutChartWrapper>
        <CustomLegends />
    </Box>
);

export default CustomChartSample;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment