1
0
mirror of https://github.com/lensapp/lens.git synced 2025-05-20 05:10:56 +00:00
lens/dashboard/client/components/chart/chart.tsx
Sebastian Malton b1ff34879a cleanup Lens repo with tighter linting
Signed-off-by: Sebastian Malton <smalton@mirantis.com>
2020-07-09 17:00:23 -04:00

212 lines
5.8 KiB
TypeScript

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<ChartProps> = {
type: ChartKind.DOUGHNUT,
options: {},
showChart: true,
showLegend: true,
legendPosition: "bottom",
plugins: [],
redraw: false
};
export class Chart extends PureComponent<ChartProps> {
static defaultProps = defaultProps as object;
private canvas = createRef<HTMLCanvasElement>()
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 => (
<Badge
key={title}
className="flex gaps align-center"
label={(
<div>
<StatusBrick style={{ backgroundColor: color }}/>
<span>{title}</span>
</div>
)}
tooltip={tooltip}
/>
);
return (
<div className="legend flex wrap gaps">
{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)
)}
</div>
);
}
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 (
<>
<div className={cssNames("Chart", className)}>
{title && <div className="chart-title">{title}</div>}
{showChart &&
<div className="chart-container">
<canvas
ref={this.canvas}
width={width}
height={height}
/>
<div className="chartjs-tooltip flex column"></div>
</div>
}
{this.renderLegend()}
</div>
</>
);
}
}