Skip to content

Commit 666e49c

Browse files
authored
Merge pull request #1539 from lowcoder-org/feature/echarts
[WIP]: Implemented Echart variations
2 parents c716ae0 + 437ea26 commit 666e49c

36 files changed

+6125
-70
lines changed

client/packages/lowcoder-comps/package.json

+32
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,38 @@
5858
"h": 40
5959
}
6060
},
61+
"barChart": {
62+
"name": "Bar Chart",
63+
"icon": "./icons/icon-chart.svg",
64+
"layoutInfo": {
65+
"w": 12,
66+
"h": 40
67+
}
68+
},
69+
"lineChart": {
70+
"name": "Line Chart",
71+
"icon": "./icons/icon-chart.svg",
72+
"layoutInfo": {
73+
"w": 12,
74+
"h": 40
75+
}
76+
},
77+
"pieChart": {
78+
"name": "Pie Chart",
79+
"icon": "./icons/icon-chart.svg",
80+
"layoutInfo": {
81+
"w": 12,
82+
"h": 40
83+
}
84+
},
85+
"scatterChart": {
86+
"name": "Scatter Chart",
87+
"icon": "./icons/icon-chart.svg",
88+
"layoutInfo": {
89+
"w": 12,
90+
"h": 40
91+
}
92+
},
6193
"imageEditor": {
6294
"name": "Image Editor",
6395
"icon": "./icons/icon-chart.svg",
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,320 @@
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 { barChartChildrenMap, ChartSize, getDataKeys } from "./barChartConstants";
10+
import { barChartPropertyView } from "./barChartPropertyView";
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 {
16+
childrenToProps,
17+
depsConfig,
18+
genRandomKey,
19+
NameConfig,
20+
UICompBuilder,
21+
withDefault,
22+
withExposingConfigs,
23+
withViewFn,
24+
ThemeContext,
25+
chartColorPalette,
26+
getPromiseAfterDispatch,
27+
dropdownControl,
28+
JSONObject,
29+
} from "lowcoder-sdk";
30+
import { getEchartsLocale, trans } from "i18n/comps";
31+
import { ItemColorComp } from "comps/basicChartComp/chartConfigs/lineChartConfig";
32+
import {
33+
echartsConfigOmitChildren,
34+
getEchartsConfig,
35+
getSelectedPoints,
36+
} from "./barChartUtils";
37+
import 'echarts-extension-gmap';
38+
import log from "loglevel";
39+
40+
let clickEventCallback = () => {};
41+
42+
const chartModeOptions = [
43+
{
44+
label: "ECharts JSON",
45+
value: "json",
46+
}
47+
] as const;
48+
49+
let BarChartTmpComp = (function () {
50+
return new UICompBuilder({mode:dropdownControl(chartModeOptions,'ui'),...barChartChildrenMap}, () => null)
51+
.setPropertyViewFn(barChartPropertyView)
52+
.build();
53+
})();
54+
55+
BarChartTmpComp = withViewFn(BarChartTmpComp, (comp) => {
56+
const mode = comp.children.mode.getView();
57+
const onUIEvent = comp.children.onUIEvent.getView();
58+
const onEvent = comp.children.onEvent.getView();
59+
const echartsCompRef = useRef<ReactECharts | null>();
60+
const [chartSize, setChartSize] = useState<ChartSize>();
61+
const firstResize = useRef(true);
62+
const theme = useContext(ThemeContext);
63+
const defaultChartTheme = {
64+
color: chartColorPalette,
65+
backgroundColor: "#fff",
66+
};
67+
68+
let themeConfig = defaultChartTheme;
69+
try {
70+
themeConfig = theme?.theme.chart ? JSON.parse(theme?.theme.chart) : defaultChartTheme;
71+
} catch (error) {
72+
log.error('theme chart error: ', error);
73+
}
74+
75+
const triggerClickEvent = async (dispatch: any, action: CompAction<JSONValue>) => {
76+
await getPromiseAfterDispatch(
77+
dispatch,
78+
action,
79+
{ autoHandleAfterReduce: true }
80+
);
81+
onEvent('click');
82+
}
83+
84+
useEffect(() => {
85+
const echartsCompInstance = echartsCompRef?.current?.getEchartsInstance();
86+
if (!echartsCompInstance) {
87+
return _.noop;
88+
}
89+
echartsCompInstance?.on("click", (param: any) => {
90+
document.dispatchEvent(new CustomEvent("clickEvent", {
91+
bubbles: true,
92+
detail: {
93+
action: 'click',
94+
data: param.data,
95+
}
96+
}));
97+
triggerClickEvent(
98+
comp.dispatch,
99+
changeChildAction("lastInteractionData", param.data, false)
100+
);
101+
});
102+
return () => {
103+
echartsCompInstance?.off("click");
104+
document.removeEventListener('clickEvent', clickEventCallback)
105+
};
106+
}, []);
107+
108+
useEffect(() => {
109+
// bind events
110+
const echartsCompInstance = echartsCompRef?.current?.getEchartsInstance();
111+
if (!echartsCompInstance) {
112+
return _.noop;
113+
}
114+
echartsCompInstance?.on("selectchanged", (param: any) => {
115+
const option: any = echartsCompInstance?.getOption();
116+
document.dispatchEvent(new CustomEvent("clickEvent", {
117+
bubbles: true,
118+
detail: {
119+
action: param.fromAction,
120+
data: getSelectedPoints(param, option)
121+
}
122+
}));
123+
124+
if (param.fromAction === "select") {
125+
comp.dispatch(changeChildAction("selectedPoints", getSelectedPoints(param, option), false));
126+
onUIEvent("select");
127+
} else if (param.fromAction === "unselect") {
128+
comp.dispatch(changeChildAction("selectedPoints", getSelectedPoints(param, option), false));
129+
onUIEvent("unselect");
130+
}
131+
132+
triggerClickEvent(
133+
comp.dispatch,
134+
changeChildAction("lastInteractionData", getSelectedPoints(param, option), false)
135+
);
136+
});
137+
// unbind
138+
return () => {
139+
echartsCompInstance?.off("selectchanged");
140+
document.removeEventListener('clickEvent', clickEventCallback)
141+
};
142+
}, [onUIEvent]);
143+
144+
const echartsConfigChildren = _.omit(comp.children, echartsConfigOmitChildren);
145+
const childrenProps = childrenToProps(echartsConfigChildren);
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+
useEffect(() => {
155+
comp.children.mapInstance.dispatch(changeValueAction(null, false))
156+
if(comp.children.mapInstance.value) return;
157+
}, [option])
158+
159+
return (
160+
<ReactResizeDetector
161+
onResize={(w, h) => {
162+
if (w && h) {
163+
setChartSize({ w: w, h: h });
164+
}
165+
if (!firstResize.current) {
166+
// ignore the first resize, which will impact the loading animation
167+
echartsCompRef.current?.getEchartsInstance().resize();
168+
} else {
169+
firstResize.current = false;
170+
}
171+
}}
172+
>
173+
<ReactECharts
174+
ref={(e) => (echartsCompRef.current = e)}
175+
style={{ height: "100%" }}
176+
notMerge
177+
lazyUpdate
178+
opts={{ locale: getEchartsLocale() }}
179+
option={option}
180+
mode={mode}
181+
/>
182+
</ReactResizeDetector>
183+
);
184+
});
185+
186+
function getYAxisFormatContextValue(
187+
data: Array<JSONObject>,
188+
yAxisType: EchartsAxisType,
189+
yAxisName?: string
190+
) {
191+
const dataSample = yAxisName && data.length > 0 && data[0][yAxisName];
192+
let contextValue = dataSample;
193+
if (yAxisType === "time") {
194+
// to timestamp
195+
const time =
196+
typeof dataSample === "number" || typeof dataSample === "string"
197+
? new Date(dataSample).getTime()
198+
: null;
199+
if (time) contextValue = time;
200+
}
201+
return contextValue;
202+
}
203+
204+
BarChartTmpComp = class extends BarChartTmpComp {
205+
private lastYAxisFormatContextVal?: JSONValue;
206+
private lastColorContext?: JSONObject;
207+
208+
updateContext(comp: this) {
209+
// the context value of axis format
210+
let resultComp = comp;
211+
const data = comp.children.data.getView();
212+
const sampleSeries = comp.children.series.getView().find((s) => !s.getView().hide);
213+
const yAxisContextValue = getYAxisFormatContextValue(
214+
data,
215+
comp.children.yConfig.children.yAxisType.getView(),
216+
sampleSeries?.children.columnName.getView()
217+
);
218+
if (yAxisContextValue !== comp.lastYAxisFormatContextVal) {
219+
comp.lastYAxisFormatContextVal = yAxisContextValue;
220+
resultComp = comp.setChild(
221+
"yConfig",
222+
comp.children.yConfig.reduce(
223+
wrapChildAction(
224+
"formatter",
225+
AxisFormatterComp.changeContextDataAction({ value: yAxisContextValue })
226+
)
227+
)
228+
);
229+
}
230+
// item color context
231+
const colorContextVal = {
232+
seriesName: sampleSeries?.children.seriesName.getView(),
233+
value: yAxisContextValue,
234+
};
235+
if (
236+
comp.children.chartConfig.children.comp.children.hasOwnProperty("itemColor") &&
237+
!_.isEqual(colorContextVal, comp.lastColorContext)
238+
) {
239+
comp.lastColorContext = colorContextVal;
240+
resultComp = resultComp.setChild(
241+
"chartConfig",
242+
comp.children.chartConfig.reduce(
243+
wrapChildAction(
244+
"comp",
245+
wrapChildAction("itemColor", ItemColorComp.changeContextDataAction(colorContextVal))
246+
)
247+
)
248+
);
249+
}
250+
return resultComp;
251+
}
252+
253+
override reduce(action: CompAction): this {
254+
const comp = super.reduce(action);
255+
if (action.type === CompActionTypes.UPDATE_NODES_V2) {
256+
const newData = comp.children.data.getView();
257+
// data changes
258+
if (comp.children.data !== this.children.data) {
259+
setTimeout(() => {
260+
// update x-axis value
261+
const keys = getDataKeys(newData);
262+
if (keys.length > 0 && !keys.includes(comp.children.xAxisKey.getView())) {
263+
comp.children.xAxisKey.dispatch(changeValueAction(keys[0] || ""));
264+
}
265+
// pass to child series comp
266+
comp.children.series.dispatchDataChanged(newData);
267+
}, 0);
268+
}
269+
return this.updateContext(comp);
270+
}
271+
return comp;
272+
}
273+
274+
override autoHeight(): boolean {
275+
return false;
276+
}
277+
};
278+
279+
let BarChartComp = withExposingConfigs(BarChartTmpComp, [
280+
depsConfig({
281+
name: "selectedPoints",
282+
desc: trans("chart.selectedPointsDesc"),
283+
depKeys: ["selectedPoints"],
284+
func: (input) => {
285+
return input.selectedPoints;
286+
},
287+
}),
288+
depsConfig({
289+
name: "lastInteractionData",
290+
desc: trans("chart.lastInteractionDataDesc"),
291+
depKeys: ["lastInteractionData"],
292+
func: (input) => {
293+
return input.lastInteractionData;
294+
},
295+
}),
296+
depsConfig({
297+
name: "data",
298+
desc: trans("chart.dataDesc"),
299+
depKeys: ["data", "mode"],
300+
func: (input) =>[] ,
301+
}),
302+
new NameConfig("title", trans("chart.titleDesc")),
303+
]);
304+
305+
306+
export const BarChartCompWithDefault = withDefault(BarChartComp, {
307+
xAxisKey: "date",
308+
series: [
309+
{
310+
dataIndex: genRandomKey(),
311+
seriesName: trans("chart.spending"),
312+
columnName: "spending",
313+
},
314+
{
315+
dataIndex: genRandomKey(),
316+
seriesName: trans("chart.budget"),
317+
columnName: "budget",
318+
},
319+
],
320+
});

0 commit comments

Comments
 (0)