reactjsd3.jsnext.jsnivo-react

implement mixed nivo chart


Can anyone help me how to implement mixed chart (Responsive Bar & Line) chart using nivo chart nivo docs, I've tried to follow some of example like this codesandbox, but the line won't show.

Here is the structure of my data:

"data": [
    {
        "id": "2023-02-01",
        "conversion_rate": 6.9616,
        "capture_rate": 19.0435,
        "traffic": 8071,
        "footfall": 1537,
        "conversion": 107
    },
    {
        "id": "2023-02-02",
        "conversion_rate": 5.9096,
        "capture_rate": 10.3813,
        "traffic": 8313,
        "footfall": 863,
        "conversion": 51
    },
    {
        "id": "2023-02-03",
        "conversion_rate": 6.8789,
        "capture_rate": 7.035,
        "traffic": 13845,
        "footfall": 974,
        "conversion": 67
    },
    {
        "id": "2023-02-04",
        "conversion_rate": 7.9602,
        "capture_rate": 9.9642,
        "traffic": 18155,
        "footfall": 1809,
        "conversion": 144
    },
    {
        "id": "2023-02-05",
        "conversion_rate": 5.6901,
        "capture_rate": 12.8901,
        "traffic": 19224,
        "footfall": 2478,
        "conversion": 141
    },
    {
        "id": "2023-02-06",
        "conversion_rate": 7.3981,
        "capture_rate": 11.7409,
        "traffic": 13585,
        "footfall": 1595,
        "conversion": 118
    },
    {
        "id": "2023-02-07",
        "conversion_rate": 4.9834,
        "capture_rate": 10.7847,
        "traffic": 8373,
        "footfall": 903,
        "conversion": 45
    },
    {
        "id": "2023-02-08",
        "conversion_rate": 6.3354,
        "capture_rate": 9.7352,
        "traffic": 8269,
        "footfall": 805,
        "conversion": 51
    },
    {
        "id": "2023-02-09",
        "conversion_rate": 5.6029,
        "capture_rate": 8.9278,
        "traffic": 9196,
        "footfall": 821,
        "conversion": 46
    },
    {
        "id": "2023-02-10",
        "conversion_rate": 7.3901,
        "capture_rate": 8.1822,
        "traffic": 13065,
        "footfall": 1069,
        "conversion": 79
    },
    {
        "id": "2023-02-11",
        "conversion_rate": 7.2671,
        "capture_rate": 10.2961,
        "traffic": 15637,
        "footfall": 1610,
        "conversion": 117
    },
    {
        "id": "2023-02-12",
        "conversion_rate": 7.7323,
        "capture_rate": 14.9319,
        "traffic": 11606,
        "footfall": 1733,
        "conversion": 134
    },
    {
        "id": "2023-02-13",
        "conversion_rate": 5.4198,
        "capture_rate": 15.3507,
        "traffic": 6130,
        "footfall": 941,
        "conversion": 51
    },
    {
        "id": "2023-02-14",
        "conversion_rate": 4.6314,
        "capture_rate": 16.2869,
        "traffic": 6496,
        "footfall": 1058,
        "conversion": 49
    },
    {
        "id": "2023-02-15",
        "conversion_rate": 4.8889,
        "capture_rate": 12.0757,
        "traffic": 7453,
        "footfall": 900,
        "conversion": 44
    },
    {
        "id": "2023-02-16",
        "conversion_rate": 3.9301,
        "capture_rate": 12.8941,
        "traffic": 7104,
        "footfall": 916,
        "conversion": 36
    },
    {
        "id": "2023-02-17",
        "conversion_rate": 6.1553,
        "capture_rate": 13.4454,
        "traffic": 7854,
        "footfall": 1056,
        "conversion": 65
    },
    {
        "id": "2023-02-18",
        "conversion_rate": 8.0982,
        "capture_rate": 16.5583,
        "traffic": 9844,
        "footfall": 1630,
        "conversion": 132
    },
    {
        "id": "2023-02-19",
        "conversion_rate": 7.6063,
        "capture_rate": 22.4454,
        "traffic": 7966,
        "footfall": 1788,
        "conversion": 136
    },
    {
        "id": "2023-02-20",
        "conversion_rate": 2.8269,
        "capture_rate": 14.9277,
        "traffic": 9479,
        "footfall": 1415,
        "conversion": 40
    },
    {
        "id": "2023-02-21",
        "conversion_rate": 3.4237,
        "capture_rate": 12.5246,
        "traffic": 11194,
        "footfall": 1402,
        "conversion": 48
    },
    {
        "id": "2023-02-22",
        "conversion_rate": 3.7143,
        "capture_rate": 15.9399,
        "traffic": 8783,
        "footfall": 1400,
        "conversion": 52
    },
    {
        "id": "2023-02-23",
        "conversion_rate": 12.8159,
        "capture_rate": 9.5765,
        "traffic": 5785,
        "footfall": 554,
        "conversion": 71
    },
    {
        "id": "2023-02-24",
        "conversion_rate": 13.2128,
        "capture_rate": 7.3994,
        "traffic": 9717,
        "footfall": 719,
        "conversion": 95
    },
    {
        "id": "2023-02-25",
        "conversion_rate": 15.743,
        "capture_rate": 9.514,
        "traffic": 13086,
        "footfall": 1245,
        "conversion": 196
    },
    {
        "id": "2023-02-26",
        "conversion_rate": 18.1818,
        "capture_rate": 10.4057,
        "traffic": 9514,
        "footfall": 990,
        "conversion": 180
    },
    {
        "id": "2023-02-27",
        "conversion_rate": 10.1266,
        "capture_rate": 5.7907,
        "traffic": 10914,
        "footfall": 632,
        "conversion": 64
    },
    {
        "id": "2023-02-28",
        "conversion_rate": 14.1831,
        "capture_rate": 11.12,
        "traffic": 5009,
        "footfall": 557,
        "conversion": 79
    },
    {
        "id": "2023-02-29",
        "conversion_rate": 0,
        "capture_rate": 0,
        "traffic": 0,
        "footfall": 0,
        "conversion": 0
    },
    {
        "id": "2023-02-30",
        "conversion_rate": 0,
        "capture_rate": 0,
        "traffic": 0,
        "footfall": 0,
        "conversion": 0
    },
    {
        "id": "2023-02-31",
        "conversion_rate": 0,
        "capture_rate": 0,
        "traffic": 0,
        "footfall": 0,
        "conversion": 0
    }
]

Here is how I implement it in code

  import React, { useEffect, useState } from 'react';
  import { ResponsiveBar } from '@nivo/bar';
  import { line } from 'd3-shape';
  import { Select } from 'flowbite-react';
  import { useRouter } from 'next/router';
  import { useDispatch, useSelector } from 'react-redux';
  import BrowsingInterestSkeleton from '../skeleton/BrowsingInterest';
  import { getStoreMetricGraph } from '../../services/insightV2/storeMetric';
  import { setStoreMetricGraph } from '../../features/InsightV2/storeMetricGraphSlice';

  const LineConversionRate = ({ bars, xScale, yScale }) => {
    const lineGenerator = line()
      .x((d) => xScale(d.data.index) + d.width / 2)
      .y((d) => yScale(d.data.data.conversion_rate));

    return (
      <path d={lineGenerator(bars)} fill='none' stroke='hsla(24, 100%, 47%, 1)' />
    );
  };

  const LineCaptureRate = ({ bars, xScale, yScale }) => {
    const lineGenerator = line()
      .x((d) => xScale(d.data.index) + d.width / 2)
      .y((d) => yScale(d.data.data.capture_rate));

    return (
      <path d={lineGenerator(bars)} fill='none' stroke='hsla(46, 100%, 47%, 1)' />
    );
  };

  export const StoreInsightSummaryChart = () => {
    const days = [
      { title: 'Daily', value: 'daily' },
      { title: 'Weekly', value: 'weekly' },
      { title: 'Monthly', value: 'monthly' },
    ];

    const { query } = useRouter();
    const dispatch = useDispatch();
    const { store, filterReport, accountMetric, storeMetricGraph } = useSelector(
      (store) => store,
    );
    const { filter, custom_date } = filterReport;
    const { store_metric_chart } = storeMetricGraph;

    const [loading, setLoading] = useState(false);
    const [storeDetail, setStoreDetail] = useState({});

    // console.log('custom_date <<<<<<<<', custom_date);

    //   console.log('<<<<<<', store_metric_chart);

    const storeDetails = () => {
      const splitted =
        query['store-insight']?.length && query['store-insight']?.split('&');

      setStoreDetail((prev) => {
        return {
          ...prev,
          id: splitted[0],
          storeName: splitted[1],
          storeLocation: splitted[2],
          storeLogo: splitted[3],
        };
      });
    };

    const fetchData = async () => {
      try {
        setLoading(true);
        if (filter === 'custom') {
          // console.log('masuk sini <<<<<<');
          const { data } = await getStoreMetricGraph({
            days: filter,
            storeId: storeDetail?.id,
            customStart: custom_date?.custom_start,
            customeEnd: custom_date?.custom_end,
          });

          if (data) {
            dispatch(setStoreMetricGraph(data));
          }
        } else {
          const { data } = await getStoreMetricGraph({
            days: filter,
            storeId: storeDetail?.id,
          });

          if (data) {
            dispatch(setStoreMetricGraph(data));
          }
        }
      } catch (error) {
        console.log('store metric chart log <<<<', error);
      } finally {
        setLoading(false);
      }
    };

    useEffect(() => {
      if (query['store-insight'].length > 0) storeDetails();
    }, [query]);

    useEffect(() => {
      if (storeDetail?.id) fetchData();
    }, [storeDetail?.id, filter, custom_date]);

    return (
      <div className='h-[500px] p-4 flex flex-col border rounded-lg bg-white mt-4'>
        <div className='flex flex-1 flex-row mb-4'>
          <div>
            <label className='text-sm font-bold place-self-center'>
              Overview
            </label>
          </div>
          <div className='flex flex-1 justify-end'>
            <Select
            // onChange={(e) => dispatch(setTimePeriodGraph(e.target.value))}
            >
              {days?.map((v, i) => (
                <option key={v.value} value={v.value}>
                  {v.title}
                </option>
              ))}
            </Select>
          </div>
        </div>
        {loading ? (
          <BrowsingInterestSkeleton />
        ) : (
          <ResponsiveBar
            data={store_metric_chart?.data}
            keys={['traffic', 'footfall', 'conversion']}
            indexBy='id'
            groupMode='grouped'
            margin={{ top: 50, right: 130, bottom: 50, left: 60 }}
            padding={0.3}
            valueScale={{ type: 'linear' }}
            indexScale={{ type: 'band', round: true }}
            colors={{ scheme: 'nivo' }}
            defs={[
              {
                id: 'dots',
                type: 'patternDots',
                background: 'inherit',
                color: '#38bcb2',
                size: 4,
                padding: 1,
                stagger: true,
              },
              {
                id: 'lines',
                type: 'patternLines',
                background: 'inherit',
                color: '#eed312',
                rotation: -45,
                lineWidth: 6,
                spacing: 10,
              },
            ]}
            layers={[
              'grid',
              'axes',
              'bars',
              'markers',
              'legends',
              'annotations',
              LineCaptureRate,
              LineConversionRate,
            ]}
            //   fill={[
            //     {
            //       match: {
            //         id: 'fries',
            //       },
            //       id: 'dots',
            //     },
            //     {
            //       match: {
            //         id: 'sandwich',
            //       },
            //       id: 'lines',
            //     },
            //   ]}
            borderColor={{
              from: 'color',
              modifiers: [['darker', 1.6]],
            }}
            axisTop={null}
            axisRight={null}
            axisBottom={{
              tickSize: 5,
              tickPadding: 5,
              tickRotation: 20,
              legend: '',
              legendPosition: 'middle',
              legendOffset: 32,
            }}
            axisLeft={{
              tickSize: 5,
              tickPadding: 5,
              tickRotation: 0,
              legend: '',
              legendPosition: 'middle',
              legendOffset: -40,
            }}
            labelSkipWidth={12}
            labelSkipHeight={12}
            labelTextColor={{
              from: 'color',
              modifiers: [['darker', 1.6]],
            }}
            legends={[
              {
                dataFrom: 'keys',
                anchor: 'top-right',
                direction: 'column',
                justify: false,
                translateX: 120,
                translateY: 0,
                itemsSpacing: 2,
                itemWidth: 100,
                itemHeight: 20,
                itemDirection: 'left-to-right',
                itemOpacity: 0.85,
                symbolSize: 20,
                effects: [
                  {
                    on: 'hover',
                    style: {
                      itemOpacity: 1,
                    },
                  },
                ],
              },
            ]}
            role='application'
            ariaLabel='Nivo bar chart demo'
            //   barAriaLabel={function (e) {
            //     return (
            //       e.id + ': ' + e.formattedValue + ' in country: ' + e.indexValue
            //     );
            //   }}
          />
        )}
      </div>
    );
  };

Here is the result (Line won't show)!

enter image description here

I'm not sure where did I do wrong...


Solution

  • Try to use indexValue, not index

    const LineConversionRate = ({ bars, xScale, yScale }) => {
        const lineGenerator = line()
            .x((d) => xScale(d.data.indexValue) + d.width / 2)
            .y((d) => yScale(d.data.data.conversion_rate))
    
        return <path d={lineGenerator(bars)} fill="none" stroke="hsla(24, 100%, 47%, 1)" />
    }