Passed
Push — main ( 16ce1d...2a5fa5 )
by Alejandro
25:26 queued 23:10
created

src/visits/helpers/DefaultChart.tsx   A

Complexity

Total Complexity 24
Complexity/F 0

Size

Lines of Code 177
Function Count 0

Duplication

Duplicated Lines 0
Ratio 0 %

Test Coverage

Coverage 80%

Importance

Changes 0
Metric Value
wmc 24
eloc 147
mnd 24
bc 24
fnc 0
dl 0
loc 177
ccs 28
cts 35
cp 0.8
rs 10
bpm 0
cpm 0
noi 0
c 0
b 0
f 0
1
import { useState } from 'react';
2
import { Doughnut, HorizontalBar } from 'react-chartjs-2';
3
import { keys, values } from 'ramda';
4
import classNames from 'classnames';
5
import Chart, { ChartData, ChartDataSets, ChartOptions } from 'chart.js';
6
import { fillTheGaps } from '../../utils/helpers/visits';
7
import { Stats } from '../types';
8
import { prettify } from '../../utils/helpers/numbers';
9
import { pointerOnHover, renderDoughnutChartLabel, renderNonDoughnutChartLabel } from '../../utils/helpers/charts';
10
import './DefaultChart.scss';
11
12
export interface DefaultChartProps {
13
  title: Function | string;
14
  stats: Stats;
15
  isBarChart?: boolean;
16
  max?: number;
17
  highlightedStats?: Stats;
18
  highlightedLabel?: string;
19
  onClick?: (label: string) => void;
20
}
21
22 6
const generateGraphData = (
23
  title: Function | string,
24
  isBarChart: boolean,
25
  labels: string[],
26
  data: number[],
27
  highlightedData?: number[],
28
  highlightedLabel?: string,
29 7
): ChartData => ({
30
  labels,
31
  datasets: [
32
    {
33
      title,
34
      label: highlightedData ? 'Non-selected' : 'Visits',
35
      data,
36
      backgroundColor: isBarChart ? 'rgba(70, 150, 229, 0.4)' : [
37
        '#97BBCD',
38
        '#F7464A',
39
        '#46BFBD',
40
        '#FDB45C',
41
        '#949FB1',
42
        '#57A773',
43
        '#414066',
44
        '#08B2E3',
45
        '#B6C454',
46
        '#DCDCDC',
47
        '#463730',
48
      ],
49
      borderColor: isBarChart ? 'rgba(70, 150, 229, 1)' : 'white',
50
      borderWidth: 2,
51
    },
52
    highlightedData && {
53
      title,
54
      label: highlightedLabel ?? 'Selected',
55
      data: highlightedData,
56
      backgroundColor: 'rgba(247, 127, 40, 0.4)',
57
      borderColor: '#F77F28',
58
      borderWidth: 2,
59
    },
60
  ].filter(Boolean) as ChartDataSets[],
61
});
62
63 14
const dropLabelIfHidden = (label: string) => label.startsWith('hidden') ? '' : label;
64
65 6
const determineHeight = (isBarChart: boolean, labels: string[]): number | undefined => {
66 7
  if (!isBarChart) {
67 1
    return 300;
68
  }
69
70 6
  return isBarChart && labels.length > 20 ? labels.length * 8 : undefined;
71
};
72
73 6
const renderPieChartLegend = ({ config }: Chart) => {
74 4
  const { labels = [], datasets = [] } = config.data ?? {};
75 2
  const { defaultColor } = config.options ?? {} as any;
76
  const [{ backgroundColor: colors }] = datasets;
77
78
  return (
79
    <ul className="default-chart__pie-chart-legend">
80
      {labels.map((label, index) => (
81
        <li key={label as string} className="default-chart__pie-chart-legend-item d-flex">
82
          <div
83
            className="default-chart__pie-chart-legend-item-color"
84
            style={{ backgroundColor: (colors as string[])[index] || defaultColor }}
85
          />
86
          <small className="default-chart__pie-chart-legend-item-text flex-fill">{label}</small>
87
        </li>
88
      ))}
89
    </ul>
90
  );
91
};
92
93 7
const chartElementAtEvent = (onClick?: (label: string) => void) => ([ chart ]: [{ _index: number; _chart: Chart }]) => {
94 4
  if (!onClick || !chart) {
95
    return;
96
  }
97
98
  const { _index, _chart: { data } } = chart;
99
  const { labels } = data;
100
101
  onClick(labels?.[_index] as string);
102
};
103
104 14
const statsAreDefined = (stats: Stats | undefined): stats is Stats => !!stats && Object.keys(stats).length > 0;
105
106 6
const DefaultChart = (
107
  { title, isBarChart = false, stats, max, highlightedStats, highlightedLabel, onClick }: DefaultChartProps,
108
) => {
109 7
  const Component = isBarChart ? HorizontalBar : Doughnut;
110 7
  const labels = keys(stats).map(dropLabelIfHidden);
111 7
  const data = values(
112
    !statsAreDefined(highlightedStats) ? stats : keys(highlightedStats).reduce((acc, highlightedKey) => {
113 5
      if (acc[highlightedKey]) {
114 5
        acc[highlightedKey] -= highlightedStats[highlightedKey];
115
      }
116
117 5
      return acc;
118
    }, { ...stats }),
119
  );
120 7
  const highlightedData = statsAreDefined(highlightedStats) ? fillTheGaps(highlightedStats, labels) : undefined;
121 7
  const [ chartRef, setChartRef ] = useState<HorizontalBar | Doughnut | undefined>()
122
123 7
  const options: ChartOptions = {
124
    legend: { display: false },
125
    legendCallback: !isBarChart && renderPieChartLegend as any,
126
    scales: !isBarChart ? undefined : {
127
      xAxes: [
128
        {
129
          ticks: {
130
            beginAtZero: true,
131
            // @ts-expect-error
132
            precision: 0,
133
            callback: prettify,
134
            max,
135
          },
136
          stacked: true,
137
        },
138
      ],
139
      yAxes: [{ stacked: true }],
140
    },
141
    tooltips: {
142
      intersect: !isBarChart,
143
      // Do not show tooltip on items with empty label when in a bar chart
144 2
      filter: ({ yLabel }) => !isBarChart || yLabel !== '',
145
      callbacks: {
146
        label: isBarChart ? renderNonDoughnutChartLabel('xLabel') : renderDoughnutChartLabel,
147
      },
148
    },
149
    onHover: !isBarChart ? undefined : (pointerOnHover) as any, // TODO Types seem to be incorrectly defined in @types/chart.js
150
  };
151 7
  const graphData = generateGraphData(title, isBarChart, labels, data, highlightedData, highlightedLabel);
152 7
  const height = determineHeight(isBarChart, labels);
153
154
  // Provide a key based on the height, so that every time the dataset changes, a new graph is rendered
155 7
  return (
156
    <div className="row">
157
      <div className={classNames('col-sm-12', { 'col-md-7': !isBarChart })}>
158
        <Component
159 2
          ref={(element) => setChartRef(element ?? undefined)}
160
          key={height}
161
          data={graphData}
162
          options={options}
163
          height={height}
164
          getElementAtEvent={chartElementAtEvent(onClick)}
165
        />
166
      </div>
167
      {!isBarChart && (
168
        <div className="col-sm-12 col-md-5">
169
          {chartRef?.chartInstance.generateLegend()}
170
        </div>
171
      )}
172
    </div>
173
  );
174
};
175
176
export default DefaultChart;
177