Skip to content

Commit dd58106

Browse files
authored
Merge pull request #1548 from lowcoder-org/feature/echarts
boxplot/parallel/line3d chart
2 parents aa27381 + c40b5e0 commit dd58106

26 files changed

+3946
-10
lines changed

client/packages/lowcoder-comps/package.json

+25
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
"agora-rtm-sdk": "^1.5.1",
2424
"big.js": "^6.2.1",
2525
"echarts-extension-gmap": "^1.6.0",
26+
"echarts-gl": "^2.0.9",
2627
"echarts-wordcloud": "^2.1.0",
2728
"lowcoder-cli": "workspace:^",
2829
"lowcoder-sdk": "workspace:^",
@@ -90,6 +91,30 @@
9091
"h": 40
9192
}
9293
},
94+
"boxplotChart": {
95+
"name": "Boxplot Chart",
96+
"icon": "./icons/icon-chart.svg",
97+
"layoutInfo": {
98+
"w": 12,
99+
"h": 40
100+
}
101+
},
102+
"parallelChart": {
103+
"name": "Parallel Chart",
104+
"icon": "./icons/icon-chart.svg",
105+
"layoutInfo": {
106+
"w": 12,
107+
"h": 40
108+
}
109+
},
110+
"line3dChart": {
111+
"name": "Line3D Chart",
112+
"icon": "./icons/icon-chart.svg",
113+
"layoutInfo": {
114+
"w": 12,
115+
"h": 40
116+
}
117+
},
93118
"imageEditor": {
94119
"name": "Image Editor",
95120
"icon": "./icons/icon-chart.svg",

client/packages/lowcoder-comps/src/comps/basicChartComp/reactEcharts/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import * as echarts from "echarts";
2+
import "echarts-gl";
23
import "echarts-wordcloud";
34
import { EChartsReactProps, EChartsInstance, EChartsOptionWithMap } from "./types";
45
import EChartsReactCore from "./core";
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,282 @@
1+
import {
2+
changeChildAction,
3+
changeValueAction,
4+
CompAction,
5+
CompActionTypes,
6+
wrapChildAction,
7+
} from "lowcoder-core";
8+
import { AxisFormatterComp, EchartsAxisType } from "../basicChartComp/chartConfigs/cartesianAxisConfig";
9+
import { boxplotChartChildrenMap, ChartSize, getDataKeys } from "./boxplotChartConstants";
10+
import { boxplotChartPropertyView } from "./boxplotChartPropertyView";
11+
import _ from "lodash";
12+
import { useContext, useEffect, useMemo, useRef, useState } from "react";
13+
import ReactResizeDetector from "react-resize-detector";
14+
import ReactECharts from "../basicChartComp/reactEcharts";
15+
import * as echarts from "echarts";
16+
import {
17+
childrenToProps,
18+
depsConfig,
19+
genRandomKey,
20+
NameConfig,
21+
UICompBuilder,
22+
withDefault,
23+
withExposingConfigs,
24+
withViewFn,
25+
ThemeContext,
26+
chartColorPalette,
27+
getPromiseAfterDispatch,
28+
dropdownControl,
29+
} from "lowcoder-sdk";
30+
import { getEchartsLocale, i18nObjs, trans } from "i18n/comps";
31+
import {
32+
echartsConfigOmitChildren,
33+
getEchartsConfig,
34+
getSelectedPoints,
35+
} from "./boxplotChartUtils";
36+
import 'echarts-extension-gmap';
37+
import log from "loglevel";
38+
39+
let clickEventCallback = () => {};
40+
41+
const chartModeOptions = [
42+
{
43+
label: "UI",
44+
value: "ui",
45+
}
46+
] as const;
47+
48+
let BoxplotChartTmpComp = (function () {
49+
return new UICompBuilder({mode:dropdownControl(chartModeOptions,'ui'),...boxplotChartChildrenMap}, () => null)
50+
.setPropertyViewFn(boxplotChartPropertyView)
51+
.build();
52+
})();
53+
54+
BoxplotChartTmpComp = withViewFn(BoxplotChartTmpComp, (comp) => {
55+
const mode = comp.children.mode.getView();
56+
const onUIEvent = comp.children.onUIEvent.getView();
57+
const onEvent = comp.children.onEvent.getView();
58+
const echartsCompRef = useRef<ReactECharts | null>();
59+
const [chartSize, setChartSize] = useState<ChartSize>();
60+
const firstResize = useRef(true);
61+
const theme = useContext(ThemeContext);
62+
const defaultChartTheme = {
63+
color: chartColorPalette,
64+
backgroundColor: "#fff",
65+
};
66+
67+
let themeConfig = defaultChartTheme;
68+
try {
69+
themeConfig = theme?.theme.chart ? JSON.parse(theme?.theme.chart) : defaultChartTheme;
70+
} catch (error) {
71+
log.error('theme chart error: ', error);
72+
}
73+
74+
const triggerClickEvent = async (dispatch: any, action: CompAction<JSONValue>) => {
75+
await getPromiseAfterDispatch(
76+
dispatch,
77+
action,
78+
{ autoHandleAfterReduce: true }
79+
);
80+
onEvent('click');
81+
}
82+
83+
useEffect(() => {
84+
const echartsCompInstance = echartsCompRef?.current?.getEchartsInstance();
85+
if (!echartsCompInstance) {
86+
return _.noop;
87+
}
88+
echartsCompInstance?.on("click", (param: any) => {
89+
document.dispatchEvent(new CustomEvent("clickEvent", {
90+
bubbles: true,
91+
detail: {
92+
action: 'click',
93+
data: param.data,
94+
}
95+
}));
96+
triggerClickEvent(
97+
comp.dispatch,
98+
changeChildAction("lastInteractionData", param.data, false)
99+
);
100+
});
101+
return () => {
102+
echartsCompInstance?.off("click");
103+
document.removeEventListener('clickEvent', clickEventCallback)
104+
};
105+
}, []);
106+
107+
useEffect(() => {
108+
// bind events
109+
const echartsCompInstance = echartsCompRef?.current?.getEchartsInstance();
110+
if (!echartsCompInstance) {
111+
return _.noop;
112+
}
113+
echartsCompInstance?.on("selectchanged", (param: any) => {
114+
const option: any = echartsCompInstance?.getOption();
115+
document.dispatchEvent(new CustomEvent("clickEvent", {
116+
bubbles: true,
117+
detail: {
118+
action: param.fromAction,
119+
data: getSelectedPoints(param, option)
120+
}
121+
}));
122+
123+
if (param.fromAction === "select") {
124+
comp.dispatch(changeChildAction("selectedPoints", getSelectedPoints(param, option), false));
125+
onUIEvent("select");
126+
} else if (param.fromAction === "unselect") {
127+
comp.dispatch(changeChildAction("selectedPoints", getSelectedPoints(param, option), false));
128+
onUIEvent("unselect");
129+
}
130+
131+
triggerClickEvent(
132+
comp.dispatch,
133+
changeChildAction("lastInteractionData", getSelectedPoints(param, option), false)
134+
);
135+
});
136+
// unbind
137+
return () => {
138+
echartsCompInstance?.off("selectchanged");
139+
document.removeEventListener('clickEvent', clickEventCallback)
140+
};
141+
}, [onUIEvent]);
142+
143+
const echartsConfigChildren = _.omit(comp.children, echartsConfigOmitChildren);
144+
const childrenProps = childrenToProps(echartsConfigChildren);
145+
146+
const option = useMemo(() => {
147+
return getEchartsConfig(
148+
childrenProps as ToViewReturn<typeof echartsConfigChildren>,
149+
chartSize,
150+
themeConfig
151+
);
152+
}, [theme, childrenProps, chartSize, ...Object.values(echartsConfigChildren)]);
153+
154+
return (
155+
<ReactResizeDetector
156+
onResize={(w, h) => {
157+
if (w && h) {
158+
setChartSize({ w: w, h: h });
159+
}
160+
if (!firstResize.current) {
161+
// ignore the first resize, which will impact the loading animation
162+
echartsCompRef.current?.getEchartsInstance().resize();
163+
} else {
164+
firstResize.current = false;
165+
}
166+
}}
167+
>
168+
<ReactECharts
169+
ref={(e) => (echartsCompRef.current = e)}
170+
style={{ height: "100%" }}
171+
notMerge
172+
lazyUpdate
173+
opts={{ locale: getEchartsLocale() }}
174+
option={option}
175+
mode={mode}
176+
/>
177+
</ReactResizeDetector>
178+
);
179+
});
180+
181+
function getYAxisFormatContextValue(
182+
data: Array<JSONObject>,
183+
yAxisType: EchartsAxisType,
184+
yAxisName?: string
185+
) {
186+
const dataSample = yAxisName && data.length > 0 && data[0][yAxisName];
187+
let contextValue = dataSample;
188+
if (yAxisType === "time") {
189+
// to timestamp
190+
const time =
191+
typeof dataSample === "number" || typeof dataSample === "string"
192+
? new Date(dataSample).getTime()
193+
: null;
194+
if (time) contextValue = time;
195+
}
196+
return contextValue;
197+
}
198+
199+
BoxplotChartTmpComp = class extends BoxplotChartTmpComp {
200+
private lastYAxisFormatContextVal?: JSONValue;
201+
private lastColorContext?: JSONObject;
202+
203+
updateContext(comp: this) {
204+
// the context value of axis format
205+
let resultComp = comp;
206+
const data = comp.children.data.getView();
207+
const yAxisContextValue = getYAxisFormatContextValue(
208+
data,
209+
comp.children.yConfig.children.yAxisType.getView(),
210+
);
211+
if (yAxisContextValue !== comp.lastYAxisFormatContextVal) {
212+
comp.lastYAxisFormatContextVal = yAxisContextValue;
213+
resultComp = comp.setChild(
214+
"yConfig",
215+
comp.children.yConfig.reduce(
216+
wrapChildAction(
217+
"formatter",
218+
AxisFormatterComp.changeContextDataAction({ value: yAxisContextValue })
219+
)
220+
)
221+
);
222+
}
223+
return resultComp;
224+
}
225+
226+
override reduce(action: CompAction): this {
227+
const comp = super.reduce(action);
228+
if (action.type === CompActionTypes.UPDATE_NODES_V2) {
229+
const newData = comp.children.data.getView();
230+
// data changes
231+
if (comp.children.data !== this.children.data) {
232+
setTimeout(() => {
233+
// update x-axis value
234+
const keys = getDataKeys(newData);
235+
if (keys.length > 0 && !keys.includes(comp.children.xAxisKey.getView())) {
236+
comp.children.xAxisKey.dispatch(changeValueAction(keys[0] || ""));
237+
}
238+
if (keys.length > 0 && !keys.includes(comp.children.yAxisKey.getView())) {
239+
comp.children.yAxisKey.dispatch(changeValueAction(keys[1] || ""));
240+
}
241+
}, 0);
242+
}
243+
return this.updateContext(comp);
244+
}
245+
return comp;
246+
}
247+
248+
override autoHeight(): boolean {
249+
return false;
250+
}
251+
};
252+
253+
let BoxplotChartComp = withExposingConfigs(BoxplotChartTmpComp, [
254+
depsConfig({
255+
name: "selectedPoints",
256+
desc: trans("chart.selectedPointsDesc"),
257+
depKeys: ["selectedPoints"],
258+
func: (input) => {
259+
return input.selectedPoints;
260+
},
261+
}),
262+
depsConfig({
263+
name: "lastInteractionData",
264+
desc: trans("chart.lastInteractionDataDesc"),
265+
depKeys: ["lastInteractionData"],
266+
func: (input) => {
267+
return input.lastInteractionData;
268+
},
269+
}),
270+
depsConfig({
271+
name: "data",
272+
desc: trans("chart.dataDesc"),
273+
depKeys: ["data", "mode"],
274+
func: (input) =>[] ,
275+
}),
276+
new NameConfig("title", trans("chart.titleDesc")),
277+
]);
278+
279+
280+
export const BoxplotChartCompWithDefault = withDefault(BoxplotChartComp, {
281+
xAxisKey: "date",
282+
});

0 commit comments

Comments
 (0)