Skip to content

Commit bea3c9a

Browse files
authored
JSON schema: added support for multiple traces in a plot.
2 parents ff3da2e + 459a9a3 commit bea3c9a

File tree

4 files changed

+72
-87
lines changed

4 files changed

+72
-87
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
"url": "git+https://github.com/opencor/webapp.git"
2424
},
2525
"type": "module",
26-
"version": "0.20251210.1",
26+
"version": "0.20251211.0",
2727
"scripts": {
2828
"archive:web": "bun src/renderer/scripts/archive.web.js",
2929
"build": "electron-vite build",

src/renderer/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@
3939
},
4040
"./style.css": "./dist/opencor.css"
4141
},
42-
"version": "0.20251210.1",
42+
"version": "0.20251211.0",
4343
"scripts": {
4444
"build": "vite build",
4545
"build:lib": "vite build --config vite.lib.config.ts && cp index.d.ts dist/index.d.ts",

src/renderer/src/components/views/SimulationExperimentView.vue

Lines changed: 24 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@
4949
</ScrollPanel>
5050
</SplitterPanel>
5151
<SplitterPanel :size="75">
52-
<GraphPanelWidget :key="interactiveModeEnabled ? 'hidden-graph-panel' : 'visible-graph-panel'" :plots="standardPlots" />
52+
<GraphPanelWidget :key="interactiveModeEnabled ? 'hidden-graph-panel' : 'visible-graph-panel'" :data="standardData" />
5353
</SplitterPanel>
5454
</Splitter>
5555
</SplitterPanel>
@@ -88,7 +88,7 @@
8888
v-for="(_plot, index) in (uiJson as any).output.plots"
8989
:key="`plot_${index}`"
9090
:style="{ height: `calc((100% - 0.5 * ${(uiJson as any).output.plots.length - 1}rem) / ${(uiJson as any).output.plots.length})` }"
91-
:plots="interactivePlots[index] || [{ x: { data: [] }, y: { data: [] } }]"
91+
:data="interactiveData[index] || { xValues: [], yValues: [] }"
9292
/>
9393
</div>
9494
</div>
@@ -110,7 +110,7 @@ import * as vueCommon from '../../common/vueCommon';
110110
import * as locApi from '../../libopencor/locApi';
111111
import * as locSedApi from '../../libopencor/locSedApi';
112112
113-
import type { IGraphPanelPlot } from '../widgets/GraphPanelWidget.vue';
113+
import type { IGraphPanelData, IGraphPanelPlotAdditionalTrace } from '../widgets/GraphPanelWidget.vue';
114114
115115
const props = defineProps<{
116116
file: locApi.File;
@@ -174,12 +174,10 @@ const standardInstanceTask = standardInstance.task(0);
174174
const standardParameters = vue.ref<string[]>([]);
175175
const standardXParameter = vue.ref(standardInstanceTask.voiName());
176176
const standardYParameter = vue.ref(standardInstanceTask.stateName(0));
177-
const standardPlots = vue.ref<IGraphPanelPlot[]>([
178-
{
179-
x: { data: [] },
180-
y: { data: [] }
181-
}
182-
]);
177+
const standardData = vue.ref<IGraphPanelData>({
178+
xValues: [],
179+
yValues: []
180+
});
183181
const standardConsoleContents = vue.ref<string>(`<b>${props.file.path()}</b>`);
184182
185183
populateParameters(standardParameters, standardInstanceTask);
@@ -206,16 +204,10 @@ const xInfo = vue.computed(() => locCommon.simulationDataInfo(standardInstanceTa
206204
const yInfo = vue.computed(() => locCommon.simulationDataInfo(standardInstanceTask, standardYParameter.value));
207205
208206
function updatePlot() {
209-
standardPlots.value = [
210-
{
211-
x: {
212-
data: locCommon.simulationData(standardInstanceTask, xInfo.value)
213-
},
214-
y: {
215-
data: locCommon.simulationData(standardInstanceTask, yInfo.value)
216-
}
217-
}
218-
];
207+
standardData.value = {
208+
xValues: locCommon.simulationData(standardInstanceTask, xInfo.value),
209+
yValues: locCommon.simulationData(standardInstanceTask, yInfo.value)
210+
};
219211
}
220212
221213
// Interactive mode.
@@ -225,14 +217,7 @@ const interactiveInstance = interactiveDocument.instantiate();
225217
const interactiveInstanceTask = interactiveInstance.task(0);
226218
const interactiveMath = mathjs.create(mathjs.all ?? {}, {});
227219
const interactiveModel = interactiveDocument.model(0);
228-
const interactivePlots = vue.ref<IGraphPanelPlot[][]>([
229-
[
230-
{
231-
x: { data: [] },
232-
y: { data: [] }
233-
}
234-
]
235-
]);
220+
const interactiveData = vue.ref<IGraphPanelData[]>([]);
236221
const interactiveUiJsonIssues = vue.ref<locApi.IIssue[]>(
237222
interactiveModeAvailable.value ? locApi.uiJsonIssues(props.uiJson) : []
238223
);
@@ -318,7 +303,6 @@ function updateInteractiveSimulation() {
318303
const componentVariableNames = parameter.name.split('/');
319304
320305
interactiveModel.addChange(
321-
// @ts-expect-error (we trust that we have a valid component and variable name)
322306
componentVariableNames[0],
323307
componentVariableNames[1],
324308
String(evaluateValue(parameter.value))
@@ -340,42 +324,26 @@ function updateInteractiveSimulation() {
340324
const parser = interactiveMath.parser();
341325
342326
props.uiJson.output.data.forEach((data: locApi.IUiJsonOutputData) => {
343-
// @ts-expect-error (we trust that we have some valid information)
344327
parser.set(data.id, locCommon.simulationData(interactiveInstanceTask, interactiveIdToInfo[data.id]));
345328
});
346329
347-
interactivePlots.value = props.uiJson.output.plots.map((plot: locApi.IUiJsonOutputPlot) => {
348-
// Default trace.
349-
350-
let res = [
351-
{
352-
x: {
353-
data: parser.evaluate(plot.xValue),
354-
axisTitle: plot.xAxisTitle
355-
},
356-
y: {
357-
data: parser.evaluate(plot.yValue),
358-
axisTitle: plot.yAxisTitle
359-
}
360-
}
361-
];
362-
363-
// Additional traces.
330+
interactiveData.value = props.uiJson.output.plots.map((plot: locApi.IUiJsonOutputPlot) => {
331+
let additionalTraces: IGraphPanelPlotAdditionalTrace[] = [];
364332
365333
plot.additionalTraces?.forEach((additionalTrace: locApi.IUiJsonOutputPlotAdditionalTrace) => {
366-
res.push({
367-
x: {
368-
data: parser.evaluate(additionalTrace.xValue),
369-
axisTitle: plot.xAxisTitle
370-
},
371-
y: {
372-
data: parser.evaluate(additionalTrace.yValue),
373-
axisTitle: plot.yAxisTitle
374-
}
334+
additionalTraces.push({
335+
xValues: parser.evaluate(additionalTrace.xValue),
336+
yValues: parser.evaluate(additionalTrace.yValue)
375337
});
376338
});
377339
378-
return res;
340+
return {
341+
xAxisTitle: plot.xAxisTitle,
342+
xValues: parser.evaluate(plot.xValue),
343+
yAxisTitle: plot.yAxisTitle,
344+
yValues: parser.evaluate(plot.yValue),
345+
additionalTraces: additionalTraces
346+
};
379347
});
380348
}
381349

src/renderer/src/components/widgets/GraphPanelWidget.vue

Lines changed: 46 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -11,19 +11,27 @@ import * as vue from 'vue';
1111
1212
import * as vueCommon from '../../common/vueCommon';
1313
14-
interface IGraphPanelPlotData {
15-
axisTitle?: string;
16-
data: number[];
14+
interface IPlotlyTrace {
15+
x: number[];
16+
y: number[];
1717
}
1818
19-
export interface IGraphPanelPlot {
20-
x: IGraphPanelPlotData;
21-
y: IGraphPanelPlotData;
19+
export interface IGraphPanelPlotAdditionalTrace {
20+
xValues: number[];
21+
yValues: number[];
22+
}
23+
24+
export interface IGraphPanelData {
25+
xAxisTitle?: string;
26+
xValues: number[];
27+
yAxisTitle?: string;
28+
yValues: number[];
29+
additionalTraces?: IGraphPanelPlotAdditionalTrace[];
2230
}
2331
2432
const props = withDefaults(
2533
defineProps<{
26-
plots: IGraphPanelPlot[];
34+
data: IGraphPanelData;
2735
showMarker?: boolean;
2836
}>(),
2937
{
@@ -33,6 +41,7 @@ const props = withDefaults(
3341
3442
const mainDiv = vue.ref<InstanceType<typeof Element> | null>(null);
3543
const isVisible = vue.ref(false);
44+
const rightMargin = vue.ref(-1);
3645
const theme = vueCommon.useTheme();
3746
3847
interface IAxisThemeData {
@@ -86,39 +95,31 @@ function themeData(): IThemeData {
8695
}
8796
8897
vue.watch(
89-
() => [props.plots, theme.useLightMode()],
98+
() => [props.data, theme.useLightMode()],
9099
() => {
91100
// Wait for the DOM to update before proceeding.
92101
93102
vue.nextTick(() => {
94-
// Retrieve the axes titles if there is only one plot.
103+
// Retrieve the axes' titles.
104+
105+
const xAxisTitle = props.data.xAxisTitle;
106+
const yAxisTitle = props.data.yAxisTitle;
107+
108+
// Retrieve all the traces, i.e. the default trace and any additional traces.
95109
96-
const xAxisTitle = props.plots.length === 1 ? props.plots[0]?.x.axisTitle : '';
97-
const yAxisTitle = props.plots.length === 1 ? props.plots[0]?.y.axisTitle : '';
110+
let traces: IPlotlyTrace[] = [{ x: props.data.xValues, y: props.data.yValues }];
111+
112+
for (const additionalTrace of props.data.additionalTraces || []) {
113+
traces.push({ x: additionalTrace.xValues, y: additionalTrace.yValues });
114+
}
98115
99116
// Update the plots.
100117
101118
const axisTitleFontSize = 10;
102119
103120
Plotly.react(
104121
mainDiv.value,
105-
props.plots.map((plot) => ({
106-
x: plot.x.data,
107-
y: plot.y.data
108-
// type: 'scattergl'
109-
//---OPENCOR---
110-
// Ideally, we would render using WebGL, but... Web browsers impose a limit on the number of active WebGL
111-
// contexts that can be used (8 to 16, apparently). So, depending on how the OpenCOR component is used, we may
112-
// reach that limit and get the following warning as a result:
113-
// Too many active WebGL contexts. Oldest context will be lost.
114-
// and nothing gets rendered. Apparently, plotly.js added support for virtual-webgl in version 2.28.0 (see
115-
// https://github.com/plotly/plotly.js/releases/tag/v2.28.0), but to do what they say in
116-
// https://github.com/plotly/plotly.js/pull/6784#issue-1991790973 still results in the same behaviour as
117-
// above. So, it looks like we have no choice but to disable WebGL rendering. The downside is that 1) it
118-
// doesn't look as good and 2) it is not as fast to render when there are a lot of data points. However, to
119-
// use a virtual WebGL would mean that all WebGL-based components would also be using virtual WebGL, which
120-
// might not be desirable.
121-
})),
122+
traces,
122123
{
123124
// Note: the various keys can be found at https://plotly.com/javascript/reference/.
124125
@@ -127,7 +128,7 @@ vue.watch(
127128
t: 0,
128129
l: 0,
129130
b: 35,
130-
r: 0,
131+
r: rightMargin.value,
131132
pad: 0
132133
},
133134
showlegend: false,
@@ -164,6 +165,22 @@ vue.watch(
164165
showTips: false
165166
}
166167
)
168+
.then(() => {
169+
// If needed, determine and set the right margin to accommodate the last X-axis tick label.
170+
171+
if (rightMargin.value === -1) {
172+
const xAxisElements = mainDiv.value?.querySelectorAll('.xtick');
173+
174+
if (xAxisElements !== undefined && xAxisElements.length > 0) {
175+
rightMargin.value =
176+
5 + 0.5 * (xAxisElements[xAxisElements.length - 1]?.getBoundingClientRect()?.width || 0);
177+
178+
Plotly.relayout(mainDiv.value, {
179+
'margin.r': rightMargin.value
180+
});
181+
}
182+
}
183+
})
167184
.then(() => {
168185
if (isVisible.value) {
169186
return;

0 commit comments

Comments
 (0)