实现功能, 点击排序按钮,按照表格的高度依次排序
再次点击按钮,按照底部的label(01A, 02B...排序)
主要难点,修改完数据后,页面的chart不刷新
解决方案 React.useRef和chart的ref绑定,然后调用chart.update方法
代码如下
import * as React from "react";
import {Chart} from "react-chartjs-2";
import {
Chart as ChartJS,
Title,
Tooltip,
Legend,
CategoryScale,
BarController,
BarElement,
ActiveElement,
ChartData,
} from "chart.js";
import {ILogger} from "../logging/ILogger";
import {from} from "linq-to-typescript";
import {IChartDataList} from "./IChartDataList";
import {ITranslator} from "../translations/ITranslator";
import {ILinkPosition} from "./common";
import {ILink} from "./ILink";
import {ItemWithLoading} from "./ItemWithLoading";
// Register the elements to display ChartJs. Enables tree-shaking reducing bundle size.
// See https://www.chartjs.org/docs/latest/getting-started/integration.html#bundlers-webpack-rollup-etc
ChartJS.register(Title, Tooltip, Legend, CategoryScale, BarController, BarElement);
export interface ISum {
subscript: number;
totalReject: number[];
label: string;
sumOfTotalReject?: number;
}
export interface IBarChartProps {
logger: ILogger;
translator: ITranslator;
/** The state of showing loading icon while sending request */
showLoading: boolean;
/** get link box position */
handleLinkPosition: (linksPosition: ILinkPosition) => void;
/** get link words of link box */
getLinksList: (linksList: ILink[]) => void;
/** height of chart */
chartHeight: number;
/** current bar data */
itemData: IChartDataList;
/** sort data by label or total reject, if true, sort by total reject */
sortByReject: boolean;
}
export const BarChart: React.FC<IBarChartProps> = (props: IBarChartProps) => {
const chartRef = React.useRef<ChartJS>(null);
const comparer = (x: number, y: number) => y - x;
//这个库的方法来排序linq-to-typescript
const sortByRejectOrLabel = (list: any[]) => {
if (props.sortByReject) {
// sort data by sum of total reject (按照高度排序)
list = from(list)
.orderBy<number>((x) => x.sumOfTotalReject as number, comparer)
.toArray();
} else {
// sort data by label(按照label排序(字母顺序))
list = from(list)
.orderBy<string>((x) => x.label)
.toArray();
}
props.logger.debug("sorted list is", list);
return list;
};
/**这里是最复杂的逻辑
* @param chartData 当前chart
* @returns 排好序的新的表格数据
*/
const handleSortByReject = (chart) => {
if (
chart.data.labels !== undefined &&
chart.data.labels.length !== 0 &&
chart.data.datasets !== undefined &&
chart.data.datasets.length !== 0
) {
let labelAndRejectArr: {label: string; sumOfTotalReject: number}[] = [];
// 表格只有一个颜色的数据
if (chart.data.datasets.length === 1) {
chart.data.datasets[0].data.forEach((e, i) => {
const label = chart.data.labels ? (chart.data.labels[i] as string) : "";
const totalRejectValue = e === null ? 0 : (e as number);
labelAndRejectArr.push({sumOfTotalReject: totalRejectValue, label});
});
labelAndRejectArr = sortByRejectOrLabel(labelAndRejectArr);
const labels: string[] = [];
const totalRejects: number[] = [];
labelAndRejectArr.forEach((e) => {
labels.push(e.label);
totalRejects.push(e.sumOfTotalReject);
});
// re-set label
chart.data.labels = labels;
props.logger.debug("new labels is", chart.data.labels);
// re-set data
chart.data.datasets[0].data = totalRejects;
props.logger.debug("new data is", chart.data.datasets);
} else {
//表格有多个颜色的数据
handleSortByRejectForMultipleArray(chart);
}
}
return chart;
};
/**
* 数据整合
* * eg:
* 两个数组:
* ```
* [11, null, null]
* [null, 2, 8]
* ```
* 变成下面这样:
*
* ```
* [
* {subscript: 0, totalReject: [11, 0], label: "1A"},
* {subscript: 1, totalReject: [0, 2], label: "2A"},
* {subscript: 2, totalReject: [0, 8], label: "4D"},
* ]
*
* ```
*/
const integrateTotalReject = (chart: {data: {labels?: string[]; datasets?: any}}) => {
const {datasets} = chart.data;
const newArr: ISum[] = [];
datasets.forEach((e) => {
if (e.data.length !== 0) {
e.data.forEach((j, k) => {
const totalReject = j === null ? 0 : (j as number);
let label = "";
if (chart.data.labels) {
label = chart.data.labels[k];
}
// new item include subscript and totalReject
const values: ISum = {
subscript: k,
totalReject: [totalReject],
label,
};
const sameSubscript = newArr.findIndex((c) => c.subscript === k);
if (sameSubscript !== -1) {
newArr[sameSubscript].totalReject.push(totalReject);
} else {
newArr.push(values);
}
});
}
});
props.logger.debug("integrate totalReject list is", newArr);
return newArr;
};
/** 求和
* ```
* [
* {subscript: 0, totalReject: [11, 0], label: "1A"},
* {subscript: 1, totalReject: [0, 2], label: "2A"},
* {subscript: 2, totalReject: [0, 8], label: "4D"},
* ]
* ```
* 变成
* ```
* [
* {subscript: 0, totalReject: [11, 0], label: "1A", sumOfTotalReject: 11},
* {subscript: 1, totalReject: [0, 2], label: "2A", sumOfTotalReject: 2},
* {subscript: 2, totalReject: [0, 8], label: "4D", sumOfTotalReject: 8},
* ]
* ```
*/
const sumTotalReject = (newArr: ISum[]): ISum[] => {
return newArr.map((e) => {
const sum = e.totalReject.reduce((total, num) => total + num);
return {...e, sumOfTotalReject: sum};
});
};
/**
* 分开数据
* * eg:
* 下面是原数组:
*
* ```
* [
* {subscript: 0, totalReject: [11, 0], label: "1A", sumOfTotalReject: 11},
* {subscript: 1, totalReject: [0, 2], label: "2A", sumOfTotalReject: 2},
* {subscript: 2, totalReject: [0, 8], label: "4D", sumOfTotalReject: 8},
* ]
*
* ```
* 变成下面这样:
* ```
* [11, null, null]
* [null, 2, 8]
* ```
*/
const partTotalReject = (sumArrWithLabels: ISum[]) => {
const arr: {subscript: number; data: (null | number)[]}[] = [];
sumArrWithLabels.forEach((e) => {
e.totalReject.forEach((j, k) => {
const data = j === 0 ? null : j;
const values = {
subscript: k,
data: [data],
};
const sameSubscript = arr.findIndex((c) => c.subscript === k);
if (sameSubscript !== -1) {
arr[sameSubscript].data.push(data);
} else {
arr.push(values);
}
});
});
props.logger.debug("part totalReject list is", arr);
return arr;
};
/**
* @param chart 当前chart
*/
const handleSortByRejectForMultipleArray = (chart) => {
const newData = integrateTotalReject(chart);
let sumArrWithLabels = sumTotalReject(newData);
sumArrWithLabels = sortByRejectOrLabel(sumArrWithLabels);
const labels: string[] = [];
sumArrWithLabels.forEach((e) => {
labels.push(e.label);
});
// re-set label
chart.data.labels = labels;
props.logger.debug("new labels is", chart.data.labels);
const arr = partTotalReject(sumArrWithLabels);
// re-set data
chart.data.datasets.forEach((e, i) => {
e.data = arr[i].data;
});
props.logger.debug("new data is", chart.data.datasets);
};
/**
* @returns options property or bar chart
*/
const getOptionBarChart = (titleKey?: string) => {
return {
maintainAspectRatio: false,
scales: {
x: {
stacked: true,
},
y: {
stacked: true,
},
},
plugins: {
title: {
display: true,
text: props.translator.translate(titleKey as string),
},
},
};
};
/**
* 官方方法
* 1. [update.html](https://www.chartjs.org/docs/latest/developers/updates.html)
* 2. [chartRef-demo](https://react-chartjs-2.netlify.app/examples/chart-ref/)
*/
const updateChartData = (chart) => {
if (chart.data !== null && chart.data !== undefined) {
chart = handleSortByReject(chart);
props.logger.debug("new chart is", chart);
chart.update();
}
};
React.useEffect(() => {
const chart = chartRef.current;
if (chart !== null) {
updateChartData(chart);
}
}, [props.sortByReject, props.itemData.chartData.labels, props.showLoading, props.chartHeight]);
return (
<>
{/** 发送请求时,显示loading, 获取到请求结果后,显示ele中的元素*/}
<ItemWithLoading
height={props.chartHeight}
showLoading={props.showLoading}
ele={
props.itemData.chartData !== undefined &&
props.itemData.chartData.labels &&
props.itemData.chartData.labels.length !== 0 &&
props.itemData.chartData.datasets.length !== 0 && (
/** 表格,可以参考官网
* [chartRef-demo](https://react-chartjs-2.netlify.app/examples/chart-ref/)
*/
<Chart
type="bar"
ref={chartRef}
height={props.chartHeight}
data={props.itemData.chartData}
options={getOptionBarChart(props.itemData.titleKey)}
/>
)
}
/>
</>
);
};
这是ItemWithLoading
import * as React from "react";
import {AppContext} from "../app/IAppContext";
import {Loading} from "../app/Loading";
import "../public/style/datalight/ItemWithLoading.scss";
export interface IItemWithLoadingProps {
height: number;
/** if true, show loading icon */
showLoading: boolean;
ele?: JSX.Element | boolean;
}
/** render no data */
export const ItemWithLoading: React.FC<IItemWithLoadingProps> = (props: IItemWithLoadingProps) => (
// before render element, show loading
<AppContext.Consumer>
{(ctx) =>
props.showLoading ? (
<Loading />
) : props.ele ? (
props.ele
) : (
// if ele not available, show 'no data'
<div className="tes-datalight-noData" style={{height: props.height}}>
{ctx.translator.translate("tes_fe_gallery_filter_nodata")}
</div>
)
}
</AppContext.Consumer>
);