flexdesk 0.17.7 → 0.17.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +18 -0
- package/css/flexdesk.css +9 -2
- package/dist/charts.js +1 -1
- package/dist/{chunk-RMTRWWEO.js → chunk-EIYNF4BD.js} +66 -25
- package/dist/chunk-EIYNF4BD.js.map +7 -0
- package/dist/flexdesk.css +9 -2
- package/dist/tiles.js +1 -1
- package/docs/components/charts.md +66 -0
- package/package.json +1 -1
- package/src/charts/plot_grid.js +16 -4
- package/src/charts/plot_popout_window.js +2 -1
- package/src/charts/series_transforms.js +114 -20
- package/dist/chunk-RMTRWWEO.js.map +0 -7
|
@@ -4,6 +4,72 @@
|
|
|
4
4
|
|
|
5
5
|
Plotly-backed charts: the wrapper, the chart types, subplot layouts, downsampling, the plot grid, the plot-config editor and the two plot windows. Plotly itself is not in the bundle; call `setPlotlySource()` with the path to your copy. The plot window's `streamEvents` option is described with the [other application-neutral options](../concepts/domain-neutrality.md#no-application-globals-the-gallery-picker-and-plot-window-events-0170).
|
|
6
6
|
|
|
7
|
+
## A series transform knows its series, and takes a choice parameter (0.17.8)
|
|
8
|
+
|
|
9
|
+
Two additions to [`seriesTransforms`](#derived-series-seriestransforms-0170).
|
|
10
|
+
A transform written for `run(y, params)` with number parameters works exactly
|
|
11
|
+
as before.
|
|
12
|
+
|
|
13
|
+
**The series it runs on.** `run` gets a third argument:
|
|
14
|
+
|
|
15
|
+
```js
|
|
16
|
+
run(y, params, { series, subplot, results, resolveSeries })
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
`series` is the subplot's series entry the trace was drawn from (the same
|
|
20
|
+
object, with its `variable` and whatever else you keep on it, such as a
|
|
21
|
+
`scenario`), `subplot` the subplot config, `results` the results the figure was
|
|
22
|
+
drawn from, and `resolveSeries` the `(series, results, subplot) => { y, x? }`
|
|
23
|
+
the figure was drawn with (yours, or `resolveSeriesData`). `y` is still the
|
|
24
|
+
drawn values, after the subplot's NaN handling and whatever your
|
|
25
|
+
`resolveSeries` did to them (a rebase, say). So a transform can draw the same
|
|
26
|
+
series from somewhere else, resolved the same way, instead of guessing which
|
|
27
|
+
series `y` is:
|
|
28
|
+
|
|
29
|
+
```js
|
|
30
|
+
delta: {
|
|
31
|
+
label: 'Scenario delta',
|
|
32
|
+
params: { baseline: { label: 'Compare with', type: 'choice',
|
|
33
|
+
options: () => scenarios.map((s) => ({ value: s.id, label: s.name })) } },
|
|
34
|
+
outputs: { difference: { label: 'Difference', suffix: ' (Δ)', lineStyle: 'dashed' } },
|
|
35
|
+
run: (y, { baseline }, { series, subplot, resolveSeries }) => {
|
|
36
|
+
const base = resolveSeries(series, resultsOf(baseline), subplot)?.y;
|
|
37
|
+
return base ? { difference: y.map((v, i) => v - base[i]) } : {};
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
`renderPlotGrid`, and so `PlotPopoutWindow`, run transforms on the same
|
|
43
|
+
traces as in 0.17.7, with the same derived traces: every line or scatter trace
|
|
44
|
+
of the figure, overlays included, never a derived trace. A trace drawn from one
|
|
45
|
+
of the subplot's series gets that series as `context.series`; any other trace,
|
|
46
|
+
such as an overlay, gets `context.series` null (with `subplot`, `results` and
|
|
47
|
+
`resolveSeries` still given), so a transform that needs a series, such as a
|
|
48
|
+
scenario delta, skips it itself (`if (!series) return {};`).
|
|
49
|
+
`deriveSeriesTraces(traces, subplot, seriesTransforms,
|
|
50
|
+
{ onError, seriesOf, results, resolveSeries })` takes the same as options:
|
|
51
|
+
`seriesOf(trace)` names the series a trace was drawn from (null for none); it
|
|
52
|
+
only fills `context.series` and does not choose which traces are run on.
|
|
53
|
+
Without these options `series`, `results` and `resolveSeries` are null.
|
|
54
|
+
|
|
55
|
+
**A choice parameter.** A parameter declared `{ type: 'choice', options }` is
|
|
56
|
+
a select instead of a number field. `options` is an array of
|
|
57
|
+
`{ value, label }` (a bare value, a `disabled` option and a `{ group, options }`
|
|
58
|
+
entry work as in the other FlexDesk selects), or a function
|
|
59
|
+
`(subplot) => options`, called when the controls are drawn and also whenever
|
|
60
|
+
the parameter needs its default (nothing saved, no `default`), which is on
|
|
61
|
+
each redraw of the figure. It is drawn by
|
|
62
|
+
`renderSeriesTransformControls` with the same select the settings view and
|
|
63
|
+
the flow kit use (choice_controls' `selectBox`), so a value comes back as the
|
|
64
|
+
option's own value (a number stays a number) and a saved value the list no
|
|
65
|
+
longer offers stays chosen, shown as `<value> (not found)`. The pick is saved
|
|
66
|
+
under `subplot.transforms.<id>.params` and passed to `run` like a number
|
|
67
|
+
parameter. Without a `default` and with nothing saved, the parameter is its
|
|
68
|
+
first enabled option (`{ group, options }` entries flattened in order, as the
|
|
69
|
+
select draws them), in the select and in `run`.
|
|
70
|
+
|
|
71
|
+
`tests/series_transforms.test.mjs` §7 to §10 pin all of it.
|
|
72
|
+
|
|
7
73
|
## A data series with no saved plot config opens (0.17.5)
|
|
8
74
|
|
|
9
75
|
`openDataSeriesPlotWindow` (and `new DataSeriesPlotWindow`, the module's
|
package/package.json
CHANGED
package/src/charts/plot_grid.js
CHANGED
|
@@ -84,6 +84,10 @@ function resolveSeriesData(series, results) {
|
|
|
84
84
|
* @param {Object} [options.overlays] - `{ [series.id]: { years, values } }`, drawn for
|
|
85
85
|
* every series with an `overlay` description.
|
|
86
86
|
* @param {Object} [options.seriesTransforms] - `{ [id]: transform }` (series_transforms.js).
|
|
87
|
+
* Each runs on every line/scatter trace (overlays included, derived traces
|
|
88
|
+
* not) as `run(y, params, { series, subplot, results, resolveSeries })`,
|
|
89
|
+
* `series` being the subplot's series entry the trace was drawn from, or
|
|
90
|
+
* null for an overlay.
|
|
87
91
|
* @param {Function} [options.onTransformError] - `({ transformId, traceName, error })`.
|
|
88
92
|
* @param {Function} [options.derivedTraces] - `(traces, subplot, { results, index })
|
|
89
93
|
* => trace[] | Promise<trace[]>`: extra traces appended to a subplot's figure.
|
|
@@ -197,6 +201,8 @@ function renderPlotGrid(container, plotConfig, results, options = {}) {
|
|
|
197
201
|
/** The subplot's own series (overlays and derived traces aside), and how many are hidden. */
|
|
198
202
|
let ownCount = 0;
|
|
199
203
|
let hiddenCount = 0;
|
|
204
|
+
/** Each trace drawn from one of the subplot's series, to that series (a transform's context.series). */
|
|
205
|
+
const seriesByTrace = new Map();
|
|
200
206
|
|
|
201
207
|
const useX = sp.xAxis && sp.xAxis.useTime === false && sp.xAxis.variable;
|
|
202
208
|
const xData = useX
|
|
@@ -237,7 +243,7 @@ function renderPlotGrid(container, plotConfig, results, options = {}) {
|
|
|
237
243
|
const y = Array.isArray(resolved.y)
|
|
238
244
|
? applyNanHandling(resolved.y, sp.nanHandling ?? 'gap', x)
|
|
239
245
|
: resolved.y;
|
|
240
|
-
|
|
246
|
+
const trace = buildTrace({
|
|
241
247
|
chartType: sp.chartType || 'line',
|
|
242
248
|
x,
|
|
243
249
|
y,
|
|
@@ -249,7 +255,9 @@ function renderPlotGrid(container, plotConfig, results, options = {}) {
|
|
|
249
255
|
yAxisId: yRef,
|
|
250
256
|
showMarkers: sp.showDataPoints ?? false,
|
|
251
257
|
stackGroup: s.stackGroup ?? null,
|
|
252
|
-
})
|
|
258
|
+
});
|
|
259
|
+
traces.push(trace);
|
|
260
|
+
seriesByTrace.set(trace, s);
|
|
253
261
|
|
|
254
262
|
const overlay = s.overlay ? opts.overlays?.[s.id] : null;
|
|
255
263
|
if (!overlay?.years?.length) continue;
|
|
@@ -280,8 +288,12 @@ function renderPlotGrid(container, plotConfig, results, options = {}) {
|
|
|
280
288
|
}
|
|
281
289
|
});
|
|
282
290
|
|
|
283
|
-
traces.push(...await deriveSeriesTraces(traces, sp, opts.seriesTransforms,
|
|
284
|
-
|
|
291
|
+
traces.push(...await deriveSeriesTraces(traces, sp, opts.seriesTransforms, {
|
|
292
|
+
onError: opts.onTransformError ?? undefined,
|
|
293
|
+
seriesOf: (trace) => seriesByTrace.get(trace) ?? null,
|
|
294
|
+
results: data,
|
|
295
|
+
resolveSeries: opts.resolveSeries,
|
|
296
|
+
}));
|
|
285
297
|
if (opts.derivedTraces) {
|
|
286
298
|
const extra = await opts.derivedTraces(traces, sp, { results: data, index });
|
|
287
299
|
if (Array.isArray(extra)) traces.push(...extra);
|
|
@@ -50,7 +50,8 @@ export class PlotPopoutWindow {
|
|
|
50
50
|
* renders the Config tab. What it returns is disposed (`dispose()`) when the
|
|
51
51
|
* window closes, so an editor drawn there (renderPlotConfigEditor) goes with it.
|
|
52
52
|
* @param {Object} [options.seriesTransforms] - `{ [id]: transform }` (series_transforms.js);
|
|
53
|
-
* absent => no derived series are drawn
|
|
53
|
+
* absent => no derived series are drawn. Each runs as
|
|
54
|
+
* `run(y, params, { series, subplot, results, resolveSeries })` (renderPlotGrid).
|
|
54
55
|
* @param {string} [options.refreshEvent='data:changed'] - Bus event after which the
|
|
55
56
|
* window re-reads `getResults()` and redraws.
|
|
56
57
|
* @param {{delta?: string|null, complete?: string|null}} [options.streamEvents] - Bus
|
|
@@ -15,11 +15,44 @@
|
|
|
15
15
|
* },
|
|
16
16
|
* };
|
|
17
17
|
*
|
|
18
|
-
* `run(y, params)` resolves `{ [output]: number[] }`. It is called
|
|
19
|
-
* eligible trace (a 2-D line/scatter trace) for each transform with at
|
|
20
|
-
* output switched on, and every switched-on output it returns is
|
|
21
|
-
* extra trace in the source trace's colour, named
|
|
22
|
-
* the output's `lineStyle` (solid | dashed |
|
|
18
|
+
* `run(y, params, context)` resolves `{ [output]: number[] }`. It is called
|
|
19
|
+
* ONCE per eligible trace (a 2-D line/scatter trace) for each transform with at
|
|
20
|
+
* least one output switched on, and every switched-on output it returns is
|
|
21
|
+
* drawn as an extra trace in the source trace's colour, named
|
|
22
|
+
* `<source name><suffix>`, with the output's `lineStyle` (solid | dashed |
|
|
23
|
+
* dotted) and `lineWidth`.
|
|
24
|
+
*
|
|
25
|
+
* `y` is the drawn values (after the subplot's NaN handling and whatever the
|
|
26
|
+
* embedder's `resolveSeries` did, such as a rebase). `context` (0.17.8) says
|
|
27
|
+
* which series they are:
|
|
28
|
+
*
|
|
29
|
+
* { series, // the subplot's series entry the trace was drawn from
|
|
30
|
+
* // (its variable, scenario, ...); null when unknown
|
|
31
|
+
* subplot, // the subplot config
|
|
32
|
+
* results, // the results the figure was drawn from, or null
|
|
33
|
+
* resolveSeries } // the `(series, results, subplot) => { y, x? }` the
|
|
34
|
+
* // figure was drawn with, or null
|
|
35
|
+
*
|
|
36
|
+
* so a transform can look the same series up elsewhere (a baseline scenario)
|
|
37
|
+
* and resolve it the same way. renderPlotGrid (and so PlotPopoutWindow) runs
|
|
38
|
+
* transforms on the same traces as before 0.17.8 (overlays included, derived
|
|
39
|
+
* traces never); a trace drawn from one of the subplot's series gets that
|
|
40
|
+
* series, any other (an overlay) gets `series: null`. A transform written as
|
|
41
|
+
* `run(y, params)` ignores the third argument and works as before.
|
|
42
|
+
*
|
|
43
|
+
* A parameter is a number field by default:
|
|
44
|
+
*
|
|
45
|
+
* window: { label: 'Window', default: 5, min: 1, step: 1 }
|
|
46
|
+
*
|
|
47
|
+
* or, declared `type: 'choice'` (0.17.8), a select over `options`: an array of
|
|
48
|
+
* `{ value, label }`, or a function `(subplot) => [{ value, label }]` called
|
|
49
|
+
* when the controls are drawn (and when the parameter needs its default):
|
|
50
|
+
*
|
|
51
|
+
* baseline: { label: 'Compare with', type: 'choice',
|
|
52
|
+
* options: () => scenarios.map((s) => ({ value: s.id, label: s.name })) }
|
|
53
|
+
*
|
|
54
|
+
* The chosen value is saved and passed to `run` like a number. Without a
|
|
55
|
+
* `default` and with nothing saved, a choice is its first enabled option.
|
|
23
56
|
*
|
|
24
57
|
* What is switched on lives in the subplot, under `subplot.transforms`:
|
|
25
58
|
*
|
|
@@ -30,14 +63,38 @@
|
|
|
30
63
|
*/
|
|
31
64
|
|
|
32
65
|
import { buildTrace } from './chart_types.js';
|
|
66
|
+
import { flatChoices, selectBox } from '../ui/components/choice_controls.js';
|
|
67
|
+
|
|
68
|
+
/** Whether a parameter spec is a choice (a select) rather than a number. */
|
|
69
|
+
const isChoice = (spec) => spec?.type === 'choice';
|
|
70
|
+
|
|
71
|
+
/** A choice parameter's options for `subplot`: its array, or what its function returns. */
|
|
72
|
+
function choiceOptions(spec, subplot) {
|
|
73
|
+
const options = typeof spec.options === 'function' ? spec.options(subplot) : spec.options;
|
|
74
|
+
return Array.isArray(options) ? options : [];
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* A choice parameter's value when none is declared or saved: the option
|
|
79
|
+
* selectBox selects for it, its first enabled one, `{ group, options }`
|
|
80
|
+
* entries flattened in order (the first of all when every one is disabled).
|
|
81
|
+
*/
|
|
82
|
+
function firstChoice(spec, subplot) {
|
|
83
|
+
const flat = flatChoices(choiceOptions(spec, subplot));
|
|
84
|
+
return (flat.find((o) => !o.disabled) ?? flat[0])?.value;
|
|
85
|
+
}
|
|
33
86
|
|
|
34
87
|
/** A transform's parameter values: its declared defaults under the subplot's own. */
|
|
35
|
-
function resolveParams(transform, state) {
|
|
88
|
+
function resolveParams(transform, state, subplot) {
|
|
89
|
+
const saved = state?.params ?? {};
|
|
36
90
|
const params = {};
|
|
37
91
|
for (const [name, spec] of Object.entries(transform.params ?? {})) {
|
|
38
92
|
params[name] = spec?.default;
|
|
93
|
+
if (isChoice(spec) && spec.default === undefined && saved[name] === undefined) {
|
|
94
|
+
params[name] = firstChoice(spec, subplot);
|
|
95
|
+
}
|
|
39
96
|
}
|
|
40
|
-
return { ...params, ...
|
|
97
|
+
return { ...params, ...saved };
|
|
41
98
|
}
|
|
42
99
|
|
|
43
100
|
/** The output names of `transform` that `state` switches on, in declared order. */
|
|
@@ -60,23 +117,34 @@ function isEligible(trace) {
|
|
|
60
117
|
* @param {Object} [options]
|
|
61
118
|
* @param {Function} [options.onError] - `({ transformId, traceName, error })`,
|
|
62
119
|
* called for each skipped run.
|
|
120
|
+
* @param {Function} [options.seriesOf] - `(trace) => series | null`: the series
|
|
121
|
+
* config a trace was drawn from, passed as `context.series` (null for a
|
|
122
|
+
* trace it names none for, and for every trace without it). It does not
|
|
123
|
+
* choose the sources: every eligible trace is run on, as before.
|
|
124
|
+
* @param {Object} [options.results] - The results the figure was drawn from
|
|
125
|
+
* (`context.results`).
|
|
126
|
+
* @param {Function} [options.resolveSeries] - The resolver the figure was drawn
|
|
127
|
+
* with (`context.resolveSeries`).
|
|
63
128
|
* @returns {Promise<Object[]>} The extra traces, in transform then trace order.
|
|
64
129
|
*/
|
|
65
|
-
async function deriveSeriesTraces(traces, subplot, seriesTransforms,
|
|
130
|
+
async function deriveSeriesTraces(traces, subplot, seriesTransforms,
|
|
131
|
+
{ onError, seriesOf, results, resolveSeries } = {}) {
|
|
66
132
|
const derived = [];
|
|
67
133
|
if (!seriesTransforms) return derived;
|
|
68
|
-
const sources = traces.filter(isEligible)
|
|
134
|
+
const sources = traces.filter(isEligible)
|
|
135
|
+
.map((trace) => ({ trace, series: typeof seriesOf === 'function' ? (seriesOf(trace) ?? null) : null }));
|
|
69
136
|
|
|
70
137
|
for (const [transformId, transform] of Object.entries(seriesTransforms)) {
|
|
71
138
|
const state = subplot?.transforms?.[transformId];
|
|
72
139
|
const outputs = enabledOutputs(transform, state);
|
|
73
140
|
if (!outputs.length || typeof transform?.run !== 'function') continue;
|
|
74
|
-
const params = resolveParams(transform, state);
|
|
141
|
+
const params = resolveParams(transform, state, subplot);
|
|
75
142
|
|
|
76
|
-
for (const source of sources) {
|
|
143
|
+
for (const { trace: source, series } of sources) {
|
|
144
|
+
const context = { series, subplot, results: results ?? null, resolveSeries: resolveSeries ?? null };
|
|
77
145
|
let result;
|
|
78
146
|
try {
|
|
79
|
-
result = await transform.run(source.y, params);
|
|
147
|
+
result = await transform.run(source.y, params, context);
|
|
80
148
|
} catch (error) {
|
|
81
149
|
onError?.({ transformId, traceName: source.name, error });
|
|
82
150
|
continue;
|
|
@@ -112,7 +180,8 @@ async function deriveSeriesTraces(traces, subplot, seriesTransforms, { onError }
|
|
|
112
180
|
|
|
113
181
|
/**
|
|
114
182
|
* The controls for `seriesTransforms` in a subplot's config: per transform, a
|
|
115
|
-
* toggle for each output and a number field for
|
|
183
|
+
* toggle for each output and, for each parameter, a number field or, for a
|
|
184
|
+
* `type: 'choice'` parameter, a select (choice_controls' selectBox). They edit
|
|
116
185
|
* `subplot.transforms` in place and call `onChange()` after every edit. With no
|
|
117
186
|
* transforms (null or `{}`) nothing is rendered.
|
|
118
187
|
*
|
|
@@ -173,22 +242,47 @@ function renderSeriesTransformControls(container, subplot, seriesTransforms, { o
|
|
|
173
242
|
row.className = 'twm-series-transforms__row';
|
|
174
243
|
const text = document.createElement('span');
|
|
175
244
|
text.textContent = spec?.label ?? name;
|
|
245
|
+
const writeParam = (value) => {
|
|
246
|
+
const state = stateOf(transformId);
|
|
247
|
+
const params = { ...(state.params ?? {}) };
|
|
248
|
+
if (value === undefined) delete params[name];
|
|
249
|
+
else params[name] = value;
|
|
250
|
+
state.params = params;
|
|
251
|
+
onChange?.();
|
|
252
|
+
};
|
|
253
|
+
const saved = subplot.transforms?.[transformId]?.params?.[name];
|
|
254
|
+
|
|
255
|
+
if (isChoice(spec)) {
|
|
256
|
+
const current = saved ?? spec.default ?? firstChoice(spec, subplot);
|
|
257
|
+
const choice = selectBox(choiceOptions(spec, subplot), current, {
|
|
258
|
+
label: spec?.label ?? name,
|
|
259
|
+
valueAttr: 'value',
|
|
260
|
+
className: 'twm-series-transforms__param twm-series-transforms__select',
|
|
261
|
+
});
|
|
262
|
+
choice.box.addEventListener('change', () => {
|
|
263
|
+
const value = choice.valueOf();
|
|
264
|
+
// Redrawn with the chosen value, as settings do: a stale
|
|
265
|
+
// '(not found)' entry goes once an offered option is picked.
|
|
266
|
+
choice.setOptions(choiceOptions(spec, subplot), value);
|
|
267
|
+
writeParam(value);
|
|
268
|
+
});
|
|
269
|
+
paramInputs.push(choice.box);
|
|
270
|
+
row.append(text, choice.box);
|
|
271
|
+
group.appendChild(row);
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
274
|
+
|
|
176
275
|
const input = document.createElement('input');
|
|
177
276
|
input.type = 'number';
|
|
178
277
|
input.className = 'twm-series-transforms__param';
|
|
179
278
|
for (const attr of ['min', 'max', 'step']) {
|
|
180
279
|
if (spec?.[attr] != null) input[attr] = String(spec[attr]);
|
|
181
280
|
}
|
|
182
|
-
const current =
|
|
281
|
+
const current = saved ?? spec?.default;
|
|
183
282
|
input.value = current == null ? '' : String(current);
|
|
184
283
|
input.addEventListener('change', () => {
|
|
185
284
|
const value = parseFloat(input.value);
|
|
186
|
-
|
|
187
|
-
const params = { ...(state.params ?? {}) };
|
|
188
|
-
if (Number.isFinite(value)) params[name] = value;
|
|
189
|
-
else delete params[name];
|
|
190
|
-
state.params = params;
|
|
191
|
-
onChange?.();
|
|
285
|
+
writeParam(Number.isFinite(value) ? value : undefined);
|
|
192
286
|
});
|
|
193
287
|
paramInputs.push(input);
|
|
194
288
|
row.append(text, input);
|