Skip to content

Commit 943897e

Browse files
Imiss-U1025dragonpoo
authored andcommittedFeb 26, 2025·
boxplot chart
1 parent df3bbd3 commit 943897e

File tree

9 files changed

+2306
-1
lines changed

9 files changed

+2306
-1
lines changed
 

‎client/packages/lowcoder-comps/package.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,14 @@
9090
"h": 40
9191
}
9292
},
93+
"boxplotChart": {
94+
"name": "Scatter Chart",
95+
"icon": "./icons/icon-chart.svg",
96+
"layoutInfo": {
97+
"w": 12,
98+
"h": 40
99+
}
100+
},
93101
"imageEditor": {
94102
"name": "Image Editor",
95103
"icon": "./icons/icon-chart.svg",
Lines changed: 282 additions & 0 deletions
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+
});
Lines changed: 248 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,248 @@
1+
import {
2+
jsonControl,
3+
stateComp,
4+
toJSONObjectArray,
5+
toObject,
6+
BoolControl,
7+
ColorControl,
8+
withDefault,
9+
StringControl,
10+
NumberControl,
11+
dropdownControl,
12+
list,
13+
eventHandlerControl,
14+
valueComp,
15+
withType,
16+
uiChildren,
17+
clickEvent,
18+
toArray,
19+
styleControl,
20+
EchartDefaultTextStyle,
21+
EchartDefaultChartStyle,
22+
MultiCompBuilder,
23+
} from "lowcoder-sdk";
24+
import { RecordConstructorToComp, RecordConstructorToView } from "lowcoder-core";
25+
import { XAxisConfig, YAxisConfig } from "../basicChartComp/chartConfigs/cartesianAxisConfig";
26+
import { LegendConfig } from "../basicChartComp/chartConfigs/legendConfig";
27+
import { EchartsLegendConfig } from "../basicChartComp/chartConfigs/echartsLegendConfig";
28+
import { EchartsLabelConfig } from "../basicChartComp/chartConfigs/echartsLabelConfig";
29+
import { EChartsOption } from "echarts";
30+
import { i18nObjs, trans } from "i18n/comps";
31+
import {EchartsTitleVerticalConfig} from "../chartComp/chartConfigs/echartsTitleVerticalConfig";
32+
import {EchartsTitleConfig} from "../basicChartComp/chartConfigs/echartsTitleConfig";
33+
34+
export const UIEventOptions = [
35+
{
36+
label: trans("chart.select"),
37+
value: "select",
38+
description: trans("chart.selectDesc"),
39+
},
40+
{
41+
label: trans("chart.unSelect"),
42+
value: "unselect",
43+
description: trans("chart.unselectDesc"),
44+
},
45+
] as const;
46+
47+
export const XAxisDirectionOptions = [
48+
{
49+
label: trans("chart.horizontal"),
50+
value: "horizontal",
51+
},
52+
{
53+
label: trans("chart.vertical"),
54+
value: "vertical",
55+
},
56+
] as const;
57+
58+
export type XAxisDirectionType = ValueFromOption<typeof XAxisDirectionOptions>;
59+
60+
export const noDataAxisConfig = {
61+
animation: false,
62+
xAxis: {
63+
type: "category",
64+
name: trans("chart.noData"),
65+
nameLocation: "middle",
66+
data: [],
67+
axisLine: {
68+
lineStyle: {
69+
color: "#8B8FA3",
70+
},
71+
},
72+
},
73+
yAxis: {
74+
type: "value",
75+
axisLabel: {
76+
color: "#8B8FA3",
77+
},
78+
splitLine: {
79+
lineStyle: {
80+
color: "#F0F0F0",
81+
},
82+
},
83+
},
84+
tooltip: {
85+
show: false,
86+
},
87+
series: [
88+
{
89+
data: [700],
90+
type: "line",
91+
itemStyle: {
92+
opacity: 0,
93+
},
94+
},
95+
],
96+
} as EChartsOption;
97+
98+
export const noDataBoxplotChartConfig = {
99+
animation: false,
100+
tooltip: {
101+
show: false,
102+
},
103+
legend: {
104+
formatter: trans("chart.unknown"),
105+
top: "bottom",
106+
selectedMode: false,
107+
},
108+
color: ["#B8BBCC", "#CED0D9", "#DCDEE6", "#E6E6EB"],
109+
series: [
110+
{
111+
type: "boxplot",
112+
radius: "35%",
113+
center: ["25%", "50%"],
114+
silent: true,
115+
label: {
116+
show: false,
117+
},
118+
data: [
119+
{
120+
name: "1",
121+
value: 70,
122+
},
123+
{
124+
name: "2",
125+
value: 68,
126+
},
127+
{
128+
name: "3",
129+
value: 48,
130+
},
131+
{
132+
name: "4",
133+
value: 40,
134+
},
135+
],
136+
},
137+
{
138+
type: "boxplot",
139+
radius: "35%",
140+
center: ["75%", "50%"],
141+
silent: true,
142+
label: {
143+
show: false,
144+
},
145+
data: [
146+
{
147+
name: "1",
148+
value: 70,
149+
},
150+
{
151+
name: "2",
152+
value: 68,
153+
},
154+
{
155+
name: "3",
156+
value: 48,
157+
},
158+
{
159+
name: "4",
160+
value: 40,
161+
},
162+
],
163+
},
164+
],
165+
} as EChartsOption;
166+
167+
export type ChartSize = { w: number; h: number };
168+
169+
export const getDataKeys = (data: Array<JSONObject>) => {
170+
if (!data) {
171+
return [];
172+
}
173+
const dataKeys: Array<string> = [];
174+
data[0].forEach((key) => {
175+
if (!dataKeys.includes(key)) {
176+
dataKeys.push(key);
177+
}
178+
});
179+
return dataKeys;
180+
};
181+
182+
export const chartUiModeChildren = {
183+
title: withDefault(StringControl, trans("echarts.defaultTitle")),
184+
data: jsonControl(toArray, i18nObjs.defaultDatasourceBoxplot),
185+
xAxisKey: valueComp<string>(""), // x-axis, key from data
186+
xAxisDirection: dropdownControl(XAxisDirectionOptions, "horizontal"),
187+
xAxisData: jsonControl(toArray, []),
188+
yAxisKey: valueComp<string>(""), // x-axis, key from data
189+
xConfig: XAxisConfig,
190+
yConfig: YAxisConfig,
191+
legendConfig: LegendConfig,
192+
onUIEvent: eventHandlerControl(UIEventOptions),
193+
};
194+
195+
let chartJsonModeChildren: any = {
196+
echartsOption: jsonControl(toObject, i18nObjs.defaultEchartsJsonOption),
197+
echartsTitle: withDefault(StringControl, trans("echarts.defaultTitle")),
198+
echartsLegendConfig: EchartsLegendConfig,
199+
echartsLabelConfig: EchartsLabelConfig,
200+
echartsTitleVerticalConfig: EchartsTitleVerticalConfig,
201+
echartsTitleConfig:EchartsTitleConfig,
202+
203+
left:withDefault(NumberControl,trans('chart.defaultLeft')),
204+
right:withDefault(NumberControl,trans('chart.defaultRight')),
205+
top:withDefault(NumberControl,trans('chart.defaultTop')),
206+
bottom:withDefault(NumberControl,trans('chart.defaultBottom')),
207+
208+
tooltip: withDefault(BoolControl, true),
209+
legendVisibility: withDefault(BoolControl, true),
210+
}
211+
212+
if (EchartDefaultChartStyle && EchartDefaultTextStyle) {
213+
chartJsonModeChildren = {
214+
...chartJsonModeChildren,
215+
chartStyle: styleControl(EchartDefaultChartStyle, 'chartStyle'),
216+
titleStyle: styleControl(EchartDefaultTextStyle, 'titleStyle'),
217+
xAxisStyle: styleControl(EchartDefaultTextStyle, 'xAxis'),
218+
yAxisStyle: styleControl(EchartDefaultTextStyle, 'yAxisStyle'),
219+
legendStyle: styleControl(EchartDefaultTextStyle, 'legendStyle'),
220+
}
221+
}
222+
223+
export type UIChartDataType = {
224+
seriesName: string;
225+
// coordinate chart
226+
x?: any;
227+
y?: any;
228+
// boxplot or funnel
229+
itemName?: any;
230+
value?: any;
231+
};
232+
233+
export type NonUIChartDataType = {
234+
name: string;
235+
value: any;
236+
}
237+
238+
export const boxplotChartChildrenMap = {
239+
selectedPoints: stateComp<Array<UIChartDataType>>([]),
240+
lastInteractionData: stateComp<Array<UIChartDataType> | NonUIChartDataType>({}),
241+
onEvent: eventHandlerControl([clickEvent] as const),
242+
...chartUiModeChildren,
243+
...chartJsonModeChildren,
244+
};
245+
246+
const chartUiChildrenMap = uiChildren(boxplotChartChildrenMap);
247+
export type ChartCompPropsType = RecordConstructorToView<typeof chartUiChildrenMap>;
248+
export type ChartCompChildrenType = RecordConstructorToComp<typeof chartUiChildrenMap>;
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import { changeChildAction, CompAction } from "lowcoder-core";
2+
import { ChartCompChildrenType, getDataKeys } from "./boxplotChartConstants";
3+
import {
4+
CustomModal,
5+
Dropdown,
6+
hiddenPropertyView,
7+
Option,
8+
RedButton,
9+
Section,
10+
sectionNames,
11+
controlItem,
12+
} from "lowcoder-sdk";
13+
import { trans } from "i18n/comps";
14+
15+
export function boxplotChartPropertyView(
16+
children: ChartCompChildrenType,
17+
dispatch: (action: CompAction) => void
18+
) {
19+
const columnOptions = getDataKeys(children.data.getView()).map((key) => ({
20+
label: key,
21+
value: key,
22+
}));
23+
24+
const uiModePropertyView = (
25+
<>
26+
<Section name={trans("chart.data")}>
27+
<Dropdown
28+
value={children.xAxisKey.getView()}
29+
options={columnOptions}
30+
label={trans("chart.xAxis")}
31+
onChange={(value) => {
32+
dispatch(changeChildAction("xAxisKey", value));
33+
}}
34+
/>
35+
<Dropdown
36+
value={children.yAxisKey.getView()}
37+
options={columnOptions}
38+
label={trans("chart.yAxis")}
39+
onChange={(value) => {
40+
dispatch(changeChildAction("yAxisKey", value));
41+
}}
42+
/>
43+
</Section>
44+
<Section name={sectionNames.interaction}>
45+
<div style={{display: 'flex', flexDirection: 'column', gap: '8px'}}>
46+
{children.onUIEvent.propertyView({title: trans("chart.chartEventHandlers")})}
47+
</div>
48+
<div style={{display: 'flex', flexDirection: 'column', gap: '8px'}}>
49+
{children.onEvent.propertyView()}
50+
</div>
51+
</Section>
52+
<Section name={sectionNames.layout}>
53+
{children.echartsTitleConfig.getPropertyView()}
54+
{children.echartsTitleVerticalConfig.getPropertyView()}
55+
{children.legendConfig.getPropertyView()}
56+
{children.title.propertyView({ label: trans("chart.title") })}
57+
{children.left.propertyView({ label: trans("chart.left"), tooltip: trans("echarts.leftTooltip") })}
58+
{children.right.propertyView({ label: trans("chart.right"), tooltip: trans("echarts.rightTooltip") })}
59+
{children.top.propertyView({ label: trans("chart.top"), tooltip: trans("echarts.topTooltip") })}
60+
{children.bottom.propertyView({ label: trans("chart.bottom"), tooltip: trans("echarts.bottomTooltip") })}
61+
{hiddenPropertyView(children)}
62+
{children.tooltip.propertyView({label: trans("echarts.tooltip"), tooltip: trans("echarts.tooltipTooltip")})}
63+
</Section>
64+
<Section name={sectionNames.chartStyle}>
65+
{children.chartStyle?.getPropertyView()}
66+
</Section>
67+
<Section name={sectionNames.titleStyle}>
68+
{children.titleStyle?.getPropertyView()}
69+
</Section>
70+
<Section name={sectionNames.xAxisStyle}>
71+
{children.xAxisStyle?.getPropertyView()}
72+
</Section>
73+
<Section name={sectionNames.yAxisStyle}>
74+
{children.yAxisStyle?.getPropertyView()}
75+
</Section>
76+
<Section name={sectionNames.legendStyle}>
77+
{children.legendStyle?.getPropertyView()}
78+
</Section>
79+
<Section name={sectionNames.advanced}>
80+
{children.data.propertyView({
81+
label: trans("chart.data"),
82+
})}
83+
</Section>
84+
</>
85+
);
86+
87+
const getChatConfigByMode = (mode: string) => {
88+
switch(mode) {
89+
case "ui":
90+
return uiModePropertyView;
91+
}
92+
}
93+
return (
94+
<>
95+
{getChatConfigByMode(children.mode.getView())}
96+
</>
97+
);
98+
}
Lines changed: 285 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,285 @@
1+
import {
2+
ChartCompPropsType,
3+
ChartSize,
4+
noDataBoxplotChartConfig,
5+
} from "comps/boxplotChartComp/boxplotChartConstants";
6+
import { EChartsOptionWithMap } from "../basicChartComp/reactEcharts/types";
7+
import _ from "lodash";
8+
import { googleMapsApiUrl } from "../basicChartComp/chartConfigs/chartUrls";
9+
import parseBackground from "../../util/gradientBackgroundColor";
10+
import {chartStyleWrapper, styleWrapper} from "../../util/styleWrapper";
11+
// Define the configuration interface to match the original transform
12+
13+
interface AggregateConfig {
14+
resultDimensions: Array<{
15+
name: string;
16+
from: string;
17+
method?: string; // e.g., 'min', 'Q1', 'median', 'Q3', 'max'
18+
}>;
19+
groupBy: string;
20+
}
21+
22+
// Custom transform function
23+
function customAggregateTransform(params: {
24+
upstream: { source: any[] };
25+
config: AggregateConfig;
26+
}): any[] {
27+
const { upstream, config } = params;
28+
const data = upstream.source;
29+
30+
// Assume data is an array of arrays, with the first row as headers
31+
const headers = data[0];
32+
const rows = data.slice(1);
33+
34+
// Find the index of the groupBy column
35+
const groupByIndex = headers.indexOf(config.groupBy);
36+
if (groupByIndex === -1) {
37+
return [];
38+
}
39+
40+
// Group rows by the groupBy column
41+
const groups: { [key: string]: any[][] } = {};
42+
rows.forEach(row => {
43+
const key = row[groupByIndex];
44+
if (!groups[key]) {
45+
groups[key] = [];
46+
}
47+
groups[key].push(row);
48+
});
49+
50+
// Define aggregation functions
51+
const aggregators: {
52+
[method: string]: (values: number[]) => number;
53+
} = {
54+
min: values => Math.min(...values),
55+
max: values => Math.max(...values),
56+
Q1: values => percentile(values, 25),
57+
median: values => percentile(values, 50),
58+
Q3: values => percentile(values, 75),
59+
};
60+
61+
// Helper function to calculate percentiles (Q1, median, Q3)
62+
function percentile(arr: number[], p: number): number {
63+
const sorted = arr.slice().sort((a, b) => a - b);
64+
const index = (p / 100) * (sorted.length - 1);
65+
const i = Math.floor(index);
66+
const f = index - i;
67+
if (i === sorted.length - 1) {
68+
return sorted[i];
69+
}
70+
return sorted[i] + f * (sorted[i + 1] - sorted[i]);
71+
}
72+
73+
// Prepare output headers from resultDimensions
74+
const outputHeaders = config.resultDimensions.map(dim => dim.name);
75+
76+
// Compute aggregated data for each group
77+
const aggregatedData: any[][] = [];
78+
for (const key in groups) {
79+
const groupRows = groups[key];
80+
const row: any[] = [];
81+
82+
config.resultDimensions.forEach(dim => {
83+
if (dim.from === config.groupBy) {
84+
// Include the group key directly
85+
row.push(key);
86+
} else {
87+
// Find the index of the 'from' column
88+
const fromIndex = headers.indexOf(dim.from);
89+
if (fromIndex === -1) {
90+
return;
91+
}
92+
// Extract values for the 'from' column in this group
93+
const values = groupRows
94+
.map(r => parseFloat(r[fromIndex]))
95+
.filter(v => !isNaN(v));
96+
if (dim.method && aggregators[dim.method]) {
97+
// Apply the aggregation method
98+
row.push(aggregators[dim.method](values));
99+
} else {
100+
return;
101+
}
102+
}
103+
});
104+
105+
aggregatedData.push(row);
106+
}
107+
108+
// Return the transformed data with headers
109+
return [outputHeaders, ...aggregatedData];
110+
}
111+
112+
export const echartsConfigOmitChildren = [
113+
"hidden",
114+
"selectedPoints",
115+
"onUIEvent",
116+
"mapInstance"
117+
] as const;
118+
type EchartsConfigProps = Omit<ChartCompPropsType, typeof echartsConfigOmitChildren[number]>;
119+
120+
// https://echarts.apache.org/en/option.html
121+
export function getEchartsConfig(
122+
props: EchartsConfigProps,
123+
chartSize?: ChartSize,
124+
theme?: any,
125+
): EChartsOptionWithMap {
126+
const gridPos = {
127+
left: `${props?.left}%`,
128+
right: `${props?.right}%`,
129+
bottom: `${props?.bottom}%`,
130+
top: `${props?.top}%`,
131+
};
132+
133+
let config: any = {
134+
title: {
135+
text: props.title,
136+
top: props.echartsTitleVerticalConfig.top,
137+
left:props.echartsTitleConfig.top,
138+
textStyle: {
139+
...styleWrapper(props?.titleStyle, theme?.titleStyle)
140+
}
141+
},
142+
backgroundColor: parseBackground( props?.chartStyle?.background || theme?.chartStyle?.backgroundColor || "#FFFFFF"),
143+
tooltip: props.tooltip && {
144+
trigger: "axis",
145+
axisPointer: {
146+
type: "line",
147+
lineStyle: {
148+
color: "rgba(0,0,0,0.2)",
149+
width: 2,
150+
type: "solid"
151+
}
152+
}
153+
},
154+
grid: {
155+
...gridPos,
156+
containLabel: true,
157+
},
158+
xAxis: {
159+
name: props.xAxisKey,
160+
nameLocation: 'middle',
161+
nameGap: 30,
162+
scale: true
163+
},
164+
yAxis: {
165+
type: "category",
166+
},
167+
dataset: [
168+
{
169+
id: 'raw',
170+
source: customAggregateTransform({upstream: {source: props.data as any[]}, config:{
171+
resultDimensions: [
172+
{ name: 'min', from: props.xAxisKey, method: 'min' },
173+
{ name: 'Q1', from: props.xAxisKey, method: 'Q1' },
174+
{ name: 'median', from: props.xAxisKey, method: 'median' },
175+
{ name: 'Q3', from: props.xAxisKey, method: 'Q3' },
176+
{ name: 'max', from: props.xAxisKey, method: 'max' },
177+
{ name: props.yAxisKey, from: props.yAxisKey }
178+
],
179+
groupBy: props.yAxisKey
180+
}}),
181+
},
182+
{
183+
id: 'finaldataset',
184+
fromDatasetId: 'raw',
185+
transform: [
186+
{
187+
type: 'sort',
188+
config: {
189+
dimension: 'Q3',
190+
order: 'asc'
191+
}
192+
}
193+
]
194+
}
195+
],
196+
};
197+
198+
if (props.data.length <= 0) {
199+
// no data
200+
return {
201+
...config,
202+
...noDataBoxplotChartConfig,
203+
};
204+
}
205+
const yAxisConfig = props.yConfig();
206+
// y-axis is category and time, data doesn't need to aggregate
207+
let transformedData = props.data;
208+
209+
config = {
210+
...config,
211+
series: [{
212+
name: 'boxplot',
213+
type: 'boxplot',
214+
datasetId: 'finaldataset',
215+
itemStyle: {
216+
color: '#b8c5f2'
217+
},
218+
encode: {
219+
x: ['min', 'Q1', 'median', 'Q3', 'max'],
220+
y: props.yAxisKey,
221+
itemName: [props.yAxisKey],
222+
tooltip: ['min', 'Q1', 'median', 'Q3', 'max']
223+
}
224+
}],
225+
};
226+
227+
console.log("Echarts transformedData and config", transformedData, config);
228+
return config;
229+
}
230+
231+
export function getSelectedPoints(param: any, option: any) {
232+
const series = option.series;
233+
const dataSource = _.isArray(option.dataset) && option.dataset[0]?.source;
234+
if (series && dataSource) {
235+
return param.selected.flatMap((selectInfo: any) => {
236+
const seriesInfo = series[selectInfo.seriesIndex];
237+
if (!seriesInfo || !seriesInfo.encode) {
238+
return [];
239+
}
240+
return selectInfo.dataIndex.map((index: any) => {
241+
const commonResult = {
242+
seriesName: seriesInfo.name,
243+
};
244+
if (seriesInfo.encode.itemName && seriesInfo.encode.value) {
245+
return {
246+
...commonResult,
247+
itemName: dataSource[index][seriesInfo.encode.itemName],
248+
value: dataSource[index][seriesInfo.encode.value],
249+
};
250+
} else {
251+
return {
252+
...commonResult,
253+
x: dataSource[index][seriesInfo.encode.x],
254+
y: dataSource[index][seriesInfo.encode.y],
255+
};
256+
}
257+
});
258+
});
259+
}
260+
return [];
261+
}
262+
263+
export function loadGoogleMapsScript(apiKey: string) {
264+
const mapsUrl = `${googleMapsApiUrl}?key=${apiKey}`;
265+
const scripts = document.getElementsByTagName('script');
266+
// is script already loaded
267+
let scriptIndex = _.findIndex(scripts, (script) => script.src.endsWith(mapsUrl));
268+
if(scriptIndex > -1) {
269+
return scripts[scriptIndex];
270+
}
271+
// is script loaded with diff api_key, remove the script and load again
272+
scriptIndex = _.findIndex(scripts, (script) => script.src.startsWith(googleMapsApiUrl));
273+
if(scriptIndex > -1) {
274+
scripts[scriptIndex].remove();
275+
}
276+
277+
const script = document.createElement("script");
278+
script.type = "text/javascript";
279+
script.src = mapsUrl;
280+
script.async = true;
281+
script.defer = true;
282+
window.document.body.appendChild(script);
283+
284+
return script;
285+
}

‎client/packages/lowcoder-comps/src/i18n/comps/locales/en.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -553,6 +553,7 @@ export const en = {
553553
UIMode: "UI Mode",
554554
chartType: "Chart Type",
555555
xAxis: "X-axis",
556+
yAxis: "Y-axis",
556557
chartSeries: "Chart Series",
557558
customSeries: "Custom Series",
558559
add: "Add",

‎client/packages/lowcoder-comps/src/i18n/comps/locales/enObj.tsx

Lines changed: 1381 additions & 1 deletion
Large diffs are not rendered by default.

‎client/packages/lowcoder-comps/src/i18n/comps/locales/types.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,4 +29,5 @@ export type I18nObjects = {
2929
imageEditorLocale?: Record<string, string>;
3030
defaultPieBg: string;
3131
usaMap: Record<string, unknown>;
32+
defaultDatasourceBoxplot: unknown[];
3233
};

‎client/packages/lowcoder-comps/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import { BarChartCompWithDefault } from "comps/barChartComp/barChartComp";
2323
import { LineChartCompWithDefault } from "comps/lineChartComp/lineChartComp";
2424
import { PieChartCompWithDefault } from "comps/pieChartComp/pieChartComp";
2525
import { ScatterChartCompWithDefault } from "comps/scatterChartComp/scatterChartComp";
26+
import { BoxplotChartCompWithDefault } from "comps/boxplotChartComp/boxplotChartComp";
2627

2728
export default {
2829
chart: ChartCompWithDefault,
@@ -31,6 +32,7 @@ export default {
3132
lineChart: LineChartCompWithDefault,
3233
pieChart: PieChartCompWithDefault,
3334
scatterChart: ScatterChartCompWithDefault,
35+
boxplotChart: BoxplotChartCompWithDefault,
3436
chartsGeoMap: ChartsGeoMapComp,
3537
funnelChart: FunnelChartCompWithDefault,
3638
gaugeChart: GaugeChartCompWithDefault,

0 commit comments

Comments
 (0)
Please sign in to comment.