1
0
mirror of https://github.com/lensapp/lens.git synced 2025-05-20 05:10:56 +00:00
lens/src/renderer/components/chart/chart.tsx
Janne Savolainen 589472c2b5
Shorten license header to reduce amount of clutter in top of the files (#4709)
Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>
2022-01-18 10:18:10 +02:00

223 lines
5.7 KiB
TypeScript

/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import "./chart.scss";
import React from "react";
import ChartJS from "chart.js";
import { 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<ChartProps> = {
type: ChartKind.DOUGHNUT,
options: {},
showChart: true,
showLegend: true,
legendPosition: "bottom",
plugins: [],
redraw: false,
};
export class Chart extends React.Component<ChartProps> {
static defaultProps = defaultProps as object;
private canvas = React.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() {
const { showChart } = this.props;
if (!showChart) return;
this.renderChart();
}
componentDidUpdate() {
const { showChart, redraw } = this.props;
if (redraw) {
this.chart.destroy();
this.renderChart();
return;
}
if (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) => (
<Badge
key={title}
className="LegendBadge flex gaps align-center"
label={(
<div className="flex items-center">
<StatusBrick style={{ backgroundColor: color }} className="flex-shrink-0"/>
<span>{title}</span>
</div>
)}
tooltip={tooltip}
expandable={false}
/>
);
return (
<div className="legend flex wrap">
{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() {
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 (
<>
<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>
</>
);
}
}