Skip to content

Commit 132106c

Browse files
committed
fix(custom-chart-web): use deepmerge for data
1 parent 710aa2a commit 132106c

File tree

3 files changed

+165
-22
lines changed

3 files changed

+165
-22
lines changed

packages/pluggableWidgets/custom-chart-web/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
"@mendix/widget-plugin-mobx-kit": "workspace:*",
5050
"@mendix/widget-plugin-platform": "workspace:*",
5151
"classnames": "^2.5.1",
52+
"deepmerge": "^4.3.1",
5253
"plotly.js-dist-min": "^3.0.0"
5354
},
5455
"devDependencies": {

packages/pluggableWidgets/custom-chart-web/src/utils/utils.spec.ts

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,81 @@ describe("parseData", () => {
8080
const sampleData = JSON.stringify([{ y: [5] }]);
8181
expect(parseData(staticData, attributeData, sampleData)).toEqual([{ type: "line", x: [1], y: [5] }]);
8282
});
83+
84+
describe("deep merge behavior", () => {
85+
it("deeply merges nested marker objects", () => {
86+
const staticData = JSON.stringify([
87+
{ type: "bar", marker: { color: "red", size: 10, line: { width: 2 } } }
88+
]);
89+
const attributeData = JSON.stringify([{ marker: { symbol: "circle", line: { color: "blue" } } }]);
90+
expect(parseData(staticData, attributeData)).toEqual([
91+
{
92+
type: "bar",
93+
marker: {
94+
color: "red",
95+
size: 10,
96+
symbol: "circle",
97+
line: { width: 2, color: "blue" }
98+
}
99+
}
100+
]);
101+
});
102+
103+
it("deeply merges multiple traces with nested objects", () => {
104+
const staticData = JSON.stringify([
105+
{ type: "scatter", marker: { color: "red" }, line: { width: 2 } },
106+
{ type: "bar", marker: { size: 10 } }
107+
]);
108+
const attributeData = JSON.stringify([
109+
{ marker: { symbol: "diamond" }, line: { dash: "dot" } },
110+
{ marker: { color: "blue" } }
111+
]);
112+
expect(parseData(staticData, attributeData)).toEqual([
113+
{
114+
type: "scatter",
115+
marker: { color: "red", symbol: "diamond" },
116+
line: { width: 2, dash: "dot" }
117+
},
118+
{
119+
type: "bar",
120+
marker: { size: 10, color: "blue" }
121+
}
122+
]);
123+
});
124+
125+
it("attribute arrays replace static arrays (not concatenate)", () => {
126+
const staticData = JSON.stringify([{ x: [1, 2, 3], y: [4, 5, 6] }]);
127+
const attributeData = JSON.stringify([{ x: [10, 20] }]);
128+
expect(parseData(staticData, attributeData)).toEqual([{ x: [10, 20], y: [4, 5, 6] }]);
129+
});
130+
131+
it("deeply merges font and other nested layout-like properties in traces", () => {
132+
const staticData = JSON.stringify([
133+
{
134+
type: "scatter",
135+
textfont: { family: "Arial", size: 12 },
136+
hoverlabel: { bgcolor: "white", font: { size: 10 } }
137+
}
138+
]);
139+
const attributeData = JSON.stringify([
140+
{
141+
textfont: { color: "black" },
142+
hoverlabel: { bordercolor: "gray", font: { family: "Helvetica" } }
143+
}
144+
]);
145+
expect(parseData(staticData, attributeData)).toEqual([
146+
{
147+
type: "scatter",
148+
textfont: { family: "Arial", size: 12, color: "black" },
149+
hoverlabel: {
150+
bgcolor: "white",
151+
bordercolor: "gray",
152+
font: { size: 10, family: "Helvetica" }
153+
}
154+
}
155+
]);
156+
});
157+
});
83158
});
84159

85160
describe("parseLayout", () => {
@@ -102,6 +177,73 @@ describe("parseLayout", () => {
102177
const sampleLayout = JSON.stringify({ title: "Sample" });
103178
expect(parseLayout(undefined, attributeLayout, sampleLayout)).toEqual({ title: "Attr" });
104179
});
180+
181+
describe("deep merge behavior", () => {
182+
it("deeply merges nested font objects", () => {
183+
const staticLayout = JSON.stringify({
184+
title: { text: "Chart Title", font: { family: "Arial", size: 16 } }
185+
});
186+
const attributeLayout = JSON.stringify({
187+
title: { font: { color: "blue", weight: "bold" } }
188+
});
189+
expect(parseLayout(staticLayout, attributeLayout)).toEqual({
190+
title: {
191+
text: "Chart Title",
192+
font: { family: "Arial", size: 16, color: "blue", weight: "bold" }
193+
}
194+
});
195+
});
196+
197+
it("deeply merges xaxis and yaxis configurations", () => {
198+
const staticLayout = JSON.stringify({
199+
xaxis: { title: "X Axis", tickfont: { size: 12 }, gridcolor: "lightgray" },
200+
yaxis: { title: "Y Axis", showgrid: true }
201+
});
202+
const attributeLayout = JSON.stringify({
203+
xaxis: { tickfont: { color: "black" }, range: [0, 100] },
204+
yaxis: { gridcolor: "gray" }
205+
});
206+
expect(parseLayout(staticLayout, attributeLayout)).toEqual({
207+
xaxis: {
208+
title: "X Axis",
209+
tickfont: { size: 12, color: "black" },
210+
gridcolor: "lightgray",
211+
range: [0, 100]
212+
},
213+
yaxis: { title: "Y Axis", showgrid: true, gridcolor: "gray" }
214+
});
215+
});
216+
217+
it("deeply merges legend configuration", () => {
218+
const staticLayout = JSON.stringify({
219+
legend: { x: 0.5, y: 1, font: { size: 10 }, bgcolor: "white" }
220+
});
221+
const attributeLayout = JSON.stringify({
222+
legend: { orientation: "h", font: { family: "Helvetica" } }
223+
});
224+
expect(parseLayout(staticLayout, attributeLayout)).toEqual({
225+
legend: {
226+
x: 0.5,
227+
y: 1,
228+
font: { size: 10, family: "Helvetica" },
229+
bgcolor: "white",
230+
orientation: "h"
231+
}
232+
});
233+
});
234+
235+
it("attribute arrays replace static arrays in layout", () => {
236+
const staticLayout = JSON.stringify({
237+
annotations: [{ text: "Note 1" }, { text: "Note 2" }]
238+
});
239+
const attributeLayout = JSON.stringify({
240+
annotations: [{ text: "New Note" }]
241+
});
242+
expect(parseLayout(staticLayout, attributeLayout)).toEqual({
243+
annotations: [{ text: "New Note" }]
244+
});
245+
});
246+
});
105247
});
106248

107249
describe("parseConfig", () => {
Lines changed: 22 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
11
import { EditorStoreState } from "@mendix/shared-charts/main";
2+
import deepmerge from "deepmerge";
23
import { Config, Data, Layout } from "plotly.js-dist-min";
34
import { ChartProps } from "../components/PlotlyChart";
45

6+
// Custom merge options: arrays are replaced (not concatenated) to match Plotly expectations
7+
const mergeOptions: deepmerge.Options = {
8+
arrayMerge: (_target, source) => source
9+
};
10+
511
export function parseData(staticData?: string, attributeData?: string, sampleData?: string): Data[] {
612
try {
713
const staticTraces: Data[] = staticData ? JSON.parse(staticData) : [];
@@ -14,7 +20,9 @@ export function parseData(staticData?: string, attributeData?: string, sampleDat
1420
const result: Data[] = [];
1521

1622
for (let i = 0; i < maxLen; i++) {
17-
result.push({ ...staticTraces[i], ...dynamicTraces[i] } as Data);
23+
const staticTrace = (staticTraces[i] ?? {}) as Record<string, unknown>;
24+
const dynamicTrace = (dynamicTraces[i] ?? {}) as Record<string, unknown>;
25+
result.push(deepmerge(staticTrace, dynamicTrace, mergeOptions) as Data);
1826
}
1927

2028
return result;
@@ -25,19 +33,16 @@ export function parseData(staticData?: string, attributeData?: string, sampleDat
2533
}
2634

2735
export function parseLayout(staticLayout?: string, attributeLayout?: string, sampleLayout?: string): Partial<Layout> {
28-
let finalLayout: Partial<Layout> = {};
29-
3036
try {
31-
const layoutAttribute = attributeLayout ? JSON.parse(attributeLayout) : {};
32-
finalLayout = { ...finalLayout, ...(staticLayout ? JSON.parse(staticLayout) : {}), ...layoutAttribute };
37+
const staticObj = staticLayout ? JSON.parse(staticLayout) : {};
38+
const attrObj = attributeLayout ? JSON.parse(attributeLayout) : {};
39+
const dynamicObj = Object.keys(attrObj).length > 0 ? attrObj : sampleLayout ? JSON.parse(sampleLayout) : {};
3340

34-
if (Object.keys(layoutAttribute).length === 0) {
35-
finalLayout = { ...finalLayout, ...(sampleLayout ? JSON.parse(sampleLayout) : {}) };
36-
}
41+
return deepmerge(staticObj, dynamicObj, mergeOptions);
3742
} catch (error) {
3843
console.error("Error parsing chart layout:", error);
44+
return {};
3945
}
40-
return finalLayout;
4146
}
4247

4348
export function parseConfig(configOptions?: string): Partial<Config> {
@@ -56,16 +61,10 @@ export function parseConfig(configOptions?: string): Partial<Config> {
5661
export function mergeChartProps(chartProps: ChartProps, editorState: EditorStoreState): ChartProps {
5762
return {
5863
...chartProps,
59-
config: {
60-
...chartProps.config,
61-
...parseConfig(editorState.config)
62-
},
63-
layout: {
64-
...chartProps.layout,
65-
...parseLayout(editorState.layout)
66-
},
64+
config: deepmerge(chartProps.config, parseConfig(editorState.config), mergeOptions),
65+
layout: deepmerge(chartProps.layout, parseLayout(editorState.layout), mergeOptions),
6766
data: chartProps.data.map((trace, index) => {
68-
let stateTrace: Data = {};
67+
let stateTrace: Data | null = null;
6968
try {
7069
if (!editorState.data || !editorState.data[index]) {
7170
return trace;
@@ -75,10 +74,11 @@ export function mergeChartProps(chartProps: ChartProps, editorState: EditorStore
7574
console.warn(`Editor props for trace(${index}) is not a valid JSON:${editorState.data[index]}`);
7675
console.warn("Please make sure the props is a valid JSON string.");
7776
}
78-
return {
79-
...trace,
80-
...stateTrace
81-
} as Data;
77+
// deepmerge can't handle null, so return trace unchanged if stateTrace is null/undefined
78+
if (stateTrace == null || typeof stateTrace !== "object") {
79+
return trace;
80+
}
81+
return deepmerge(trace as object, stateTrace as object, mergeOptions) as Data;
8282
})
8383
};
8484
}

0 commit comments

Comments
 (0)