import "./chart.scss"; import React, { createRef, PureComponent } from "react"; import isEqual from "lodash/isEqual"; import remove from "lodash/remove"; import * as ChartJS from "chart.js"; import { ChartData as ChartDataOrig, ChartDataSets, ChartOptions } from "chart.js"; import { StatusBrick } from "../status-brick"; import { cssNames } from "../../utils"; import { Badge } from "../badge"; export interface ChartData extends ChartDataOrig { datasets?: ChartDataSet[]; } export interface ChartDataSet extends ChartDataSets { id?: string; borderWidth?: { top: number } | any; tooltip?: string; } export interface ChartProps { data: ChartData; width?: number | string; height?: number | string; options?: ChartOptions; // Passed to ChartJS instance type?: ChartKind; showChart?: boolean; // Possible to show legend only if false showLegend?: boolean; legendPosition?: "bottom"; legendColors?: string[]; // Hex colors for each of the labels in data object plugins?: any[]; redraw?: boolean; // If true - recreate chart instance with no animation title?: string; className?: string; } export enum ChartKind { PIE = "pie", BAR = "bar", LINE = "line", DOUGHNUT = "doughnut" } const defaultProps: Partial = { type: ChartKind.DOUGHNUT, options: {}, showChart: true, showLegend: true, legendPosition: "bottom", plugins: [], redraw: false }; export class Chart extends PureComponent { static defaultProps = defaultProps as object; private canvas = createRef() private chart: ChartJS // ChartJS adds _meta field to any data object passed to it. // We clone new data prop into currentChartData to compare props and prevProps private currentChartData: ChartData componentDidMount(): void { const { showChart } = this.props; if (!showChart) { return; } this.renderChart(); } componentDidUpdate(prevProps: ChartProps): void { const { data, showChart, redraw } = this.props; if (redraw) { this.chart.destroy(); this.renderChart(); return; } if (!isEqual(prevProps.data, data) && showChart) { if (!this.chart) { this.renderChart(); } else { this.updateChart(); } } } memoizeDataProps(): void { const { data } = this.props; this.currentChartData = { ...data, datasets: data.datasets && data.datasets.map(set => ({ ...set })) }; } updateChart(): void { const { options } = this.props; if (!this.chart) { return; } this.chart.options = ChartJS.helpers.configMerge(this.chart.options, options); this.memoizeDataProps(); const datasets: ChartDataSet[] = this.chart.config.data.datasets; const nextDatasets: ChartDataSet[] = this.currentChartData.datasets || []; // Remove stale datasets if they're not available in nextDatasets if (datasets.length > nextDatasets.length) { const sets = [...datasets]; sets.forEach(set => { if (!nextDatasets.find(next => next.id === set.id)) { remove(datasets, (item => item.id === set.id)); } }); } // Mutating inner chart datasets to enable seamless transitions nextDatasets.forEach((next, datasetIndex) => { const index = datasets.findIndex(set => set.id === next.id); if (index !== -1) { datasets[index].data = datasets[index].data.slice(); // "Clean" mobx observables data to use in ChartJS datasets[index].data.splice(next.data.length); next.data.forEach((point: any, dataIndex: number) => { datasets[index].data[dataIndex] = next.data[dataIndex]; }); // Merge other fields const { data: _data, ...props } = next; datasets[index] = { ...datasets[index], ...props }; } else { datasets[datasetIndex] = next; } }); this.chart.update(); } renderLegend(): JSX.Element { if (!this.props.showLegend) { return null; } const { data, legendColors } = this.props; const { labels, datasets } = data; const labelElem = (title: string, color: string, tooltip?: string): JSX.Element => ( {title} )} tooltip={tooltip} /> ); return (
{labels && labels.map((label: string, index) => { const { backgroundColor } = datasets[0] as any; const color = legendColors ? legendColors[index] : backgroundColor[index]; return labelElem(label, color); })} {!labels && datasets.map(({ borderColor, label, tooltip }) => labelElem(label, borderColor as any, tooltip) )}
); } renderChart(): void { const { type, options, plugins } = this.props; this.memoizeDataProps(); this.chart = new ChartJS(this.canvas.current, { type, plugins, options: { ...options, legend: { display: false }, }, data: this.currentChartData, }); } render(): JSX.Element { const { width, height, showChart, title, className } = this.props; return ( <>
{title &&
{title}
} {showChart &&
} {this.renderLegend()}
); } }