import "./chart.scss"; import React from "react"; import ChartJS from "chart.js"; import { isEqual, remove } from "lodash"; import { cssNames } from "../../utils"; import { StatusBrick } from "../status-brick"; import { Badge } from "../badge"; export interface ChartData extends ChartJS.ChartData { datasets?: ChartDataSets[]; } export interface ChartDataSets extends ChartJS.ChartDataSets { id?: string; tooltip?: string; } export interface ChartProps { data: ChartData; options?: ChartJS.ChartOptions; // Passed to ChartJS instance width?: number | string; height?: number | string; 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 React.Component { static defaultProps = defaultProps as object; private canvas = React.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() { const { showChart } = this.props if (!showChart) return this.renderChart() } componentDidUpdate(prevProps: ChartProps) { 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() { const { data } = this.props this.currentChartData = { ...data, datasets: data.datasets && data.datasets.map(set => { return { ...set } }) } } updateChart() { const { options } = this.props if (!this.chart) return this.chart.options = ChartJS.helpers.configMerge(this.chart.options, options) this.memoizeDataProps() const datasets: ChartDataSets[] = this.chart.config.data.datasets const nextDatasets: ChartDataSets[] = 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, ...props } = next datasets[index] = { ...datasets[index], ...props } } else { datasets[datasetIndex] = next } }) this.chart.update() } renderLegend() { if (!this.props.showLegend) return null const { data, legendColors } = this.props const { labels, datasets } = data const labelElem = (title: string, color: string, tooltip?: string) => ( {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() { 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() { const { width, height, showChart, title, className } = this.props return ( <>
{title &&
{title}
} {showChart &&
} {this.renderLegend()}
) } }