@selvajs/ui 6.6.0-beta.19 → 6.6.0-beta.20
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/dist/components/compute/AppLayout.svelte +24 -21
- package/dist/components/compute/AppLayout.svelte.d.ts +11 -5
- package/dist/components/compute/ComputeApp.svelte +3 -0
- package/dist/components/compute/SolveMessages.svelte +19 -74
- package/dist/components/compute/SolveMessages.svelte.d.ts +5 -0
- package/dist/components/preview/ChartOutput.svelte +41 -22
- package/dist/components/primitives/SolvingIndicator.svelte +242 -41
- package/dist/components/primitives/SolvingIndicator.svelte.d.ts +13 -4
- package/dist/components/viewer/Viewer.svelte +3 -9
- package/dist/components/viewer/Viewer.svelte.d.ts +2 -2
- package/dist/compute/liveProgress.svelte.d.ts +16 -0
- package/dist/compute/liveProgress.svelte.js +90 -0
- package/dist/features/chart/plotly-compat.d.ts +15 -0
- package/dist/features/chart/plotly-compat.js +124 -0
- package/dist/i18n/messages.d.ts +4 -0
- package/dist/i18n/messages.js +4 -0
- package/package.json +6 -6
- package/src/lib/components/compute/AppLayout.svelte +24 -21
- package/src/lib/components/compute/ComputeApp.svelte +3 -0
- package/src/lib/components/compute/SolveMessages.svelte +19 -74
- package/src/lib/components/preview/ChartOutput.svelte +41 -22
- package/src/lib/components/primitives/SolvingIndicator.svelte +242 -41
- package/src/lib/components/viewer/Viewer.svelte +3 -9
- package/src/lib/compute/liveProgress.svelte.ts +103 -0
- package/src/lib/features/chart/plotly-compat.ts +141 -0
- package/src/lib/i18n/messages.ts +8 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import type { Snippet } from 'svelte';
|
|
3
3
|
import type { UISchema, SupportedTypes, ParameterPreset } from '@selvajs/schemas';
|
|
4
|
-
import type { SolveSettlement } from '@selvajs/solve/client';
|
|
4
|
+
import type { LiveSolveState, SolveSettlement } from '@selvajs/solve/client';
|
|
5
5
|
import type { ActionButton } from '../../types/actionButton';
|
|
6
6
|
import type { PresetLabels } from '../../types/presetLabels';
|
|
7
7
|
import { ChevronUp, Download, Ellipsis, Upload } from '@lucide/svelte';
|
|
@@ -35,13 +35,19 @@
|
|
|
35
35
|
meshes?: any[];
|
|
36
36
|
isSolving: boolean;
|
|
37
37
|
/**
|
|
38
|
-
* `createSolvingIndicator(...).show`, not `isSolving`:
|
|
39
|
-
*
|
|
38
|
+
* `createSolvingIndicator(...).show`, not `isSolving`: grows the action row's solve status,
|
|
39
|
+
* which would otherwise flash on every sub-second solve.
|
|
40
40
|
*/
|
|
41
41
|
showSolvingIndicator?: boolean;
|
|
42
|
-
/** `createSolvingIndicator(...).slow`: the
|
|
42
|
+
/** `createSolvingIndicator(...).slow`: the solve status grows into a card with progress. */
|
|
43
43
|
solveIsSlow?: boolean;
|
|
44
|
-
/**
|
|
44
|
+
/** The session's live channel: step, count and bar for that card. */
|
|
45
|
+
live?: LiveSolveState;
|
|
46
|
+
/** Abort in that card. The host's `SolveMessages` should then take `progressShownByHost`. */
|
|
47
|
+
onabort?: () => void;
|
|
48
|
+
/** As passed to `createSolvingIndicator`. */
|
|
49
|
+
freezesDuringSolve?: boolean;
|
|
50
|
+
/** @deprecated Ignored: the solving status always sits in the action row. */
|
|
45
51
|
solvingIndicatorPlacement?: 'floating' | 'inline';
|
|
46
52
|
/** Rendered above the action row, for a host that keeps the solve's messages in the flow. */
|
|
47
53
|
solveMessages?: Snippet;
|
|
@@ -75,7 +81,9 @@
|
|
|
75
81
|
isSolving,
|
|
76
82
|
showSolvingIndicator = false,
|
|
77
83
|
solveIsSlow = false,
|
|
78
|
-
|
|
84
|
+
live,
|
|
85
|
+
onabort,
|
|
86
|
+
freezesDuringSolve = false,
|
|
79
87
|
solveMessages,
|
|
80
88
|
hasPendingChanges = false,
|
|
81
89
|
hasNeverSolved = false,
|
|
@@ -100,8 +108,6 @@
|
|
|
100
108
|
let presets = $state<ParameterPresetManager | null>(null);
|
|
101
109
|
const hasMenu = $derived(showSaveButton || showLoadButton || panelActions.length > 0);
|
|
102
110
|
const resultIsStale = $derived(hasPendingChanges || hasNeverSolved);
|
|
103
|
-
// With a Calculate button the button itself says it is solving.
|
|
104
|
-
const showSolvingChip = $derived(showSolvingIndicator && schema.instanceSolve !== false);
|
|
105
111
|
|
|
106
112
|
const shownTabs = $derived(
|
|
107
113
|
schema.layout.type === 'tabbed'
|
|
@@ -283,13 +289,18 @@
|
|
|
283
289
|
{oncalculate}
|
|
284
290
|
trailing={hasMenu ? actionsMenu : undefined}
|
|
285
291
|
/>
|
|
286
|
-
{:else
|
|
292
|
+
{:else}
|
|
287
293
|
<div class="mt-3 pb-3 pt-2 px-2 gap-2 flex items-center border-t border-border">
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
294
|
+
<SolvingIndicator
|
|
295
|
+
{isSolving}
|
|
296
|
+
show={showSolvingIndicator}
|
|
297
|
+
slow={solveIsSlow}
|
|
298
|
+
{live}
|
|
299
|
+
{onabort}
|
|
300
|
+
{freezesDuringSolve}
|
|
301
|
+
/>
|
|
291
302
|
{#if hasMenu}
|
|
292
|
-
|
|
303
|
+
{@render actionsMenu()}
|
|
293
304
|
{/if}
|
|
294
305
|
</div>
|
|
295
306
|
{/if}
|
|
@@ -340,8 +351,6 @@
|
|
|
340
351
|
<Viewer
|
|
341
352
|
{meshes}
|
|
342
353
|
bind:isFullscreen={isViewerFullscreen}
|
|
343
|
-
isSolving={showSolvingIndicator}
|
|
344
|
-
isSlow={solveIsSlow}
|
|
345
354
|
drawerOpen={drawer === 'full'}
|
|
346
355
|
{logoUrl}
|
|
347
356
|
{onViewerReady}
|
|
@@ -486,8 +495,6 @@
|
|
|
486
495
|
<Viewer
|
|
487
496
|
{meshes}
|
|
488
497
|
bind:isFullscreen={isViewerFullscreen}
|
|
489
|
-
isSolving={showSolvingIndicator}
|
|
490
|
-
isSlow={solveIsSlow}
|
|
491
498
|
{logoUrl}
|
|
492
499
|
{onViewerReady}
|
|
493
500
|
viewerConfig={{
|
|
@@ -548,10 +555,6 @@
|
|
|
548
555
|
{/if}
|
|
549
556
|
</div>
|
|
550
557
|
{/if}
|
|
551
|
-
|
|
552
|
-
{#if solvingIndicatorPlacement === 'floating'}
|
|
553
|
-
<SolvingIndicator show={showSolvingChip} />
|
|
554
|
-
{/if}
|
|
555
558
|
</main>
|
|
556
559
|
|
|
557
560
|
<!-- The save, load and validation dialogs; the footer's menu opens them. -->
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Snippet } from 'svelte';
|
|
2
2
|
import type { UISchema, SupportedTypes, ParameterPreset } from '@selvajs/schemas';
|
|
3
|
-
import type { SolveSettlement } from '@selvajs/solve/client';
|
|
3
|
+
import type { LiveSolveState, SolveSettlement } from '@selvajs/solve/client';
|
|
4
4
|
import type { ActionButton } from '../../types/actionButton';
|
|
5
5
|
import type { PresetLabels } from '../../types/presetLabels';
|
|
6
6
|
import type { ThreeViewer } from '@selvajs/visualization/render';
|
|
@@ -15,13 +15,19 @@ interface Props {
|
|
|
15
15
|
meshes?: any[];
|
|
16
16
|
isSolving: boolean;
|
|
17
17
|
/**
|
|
18
|
-
* `createSolvingIndicator(...).show`, not `isSolving`:
|
|
19
|
-
*
|
|
18
|
+
* `createSolvingIndicator(...).show`, not `isSolving`: grows the action row's solve status,
|
|
19
|
+
* which would otherwise flash on every sub-second solve.
|
|
20
20
|
*/
|
|
21
21
|
showSolvingIndicator?: boolean;
|
|
22
|
-
/** `createSolvingIndicator(...).slow`: the
|
|
22
|
+
/** `createSolvingIndicator(...).slow`: the solve status grows into a card with progress. */
|
|
23
23
|
solveIsSlow?: boolean;
|
|
24
|
-
/**
|
|
24
|
+
/** The session's live channel: step, count and bar for that card. */
|
|
25
|
+
live?: LiveSolveState;
|
|
26
|
+
/** Abort in that card. The host's `SolveMessages` should then take `progressShownByHost`. */
|
|
27
|
+
onabort?: () => void;
|
|
28
|
+
/** As passed to `createSolvingIndicator`. */
|
|
29
|
+
freezesDuringSolve?: boolean;
|
|
30
|
+
/** @deprecated Ignored: the solving status always sits in the action row. */
|
|
25
31
|
solvingIndicatorPlacement?: 'floating' | 'inline';
|
|
26
32
|
/** Rendered above the action row, for a host that keeps the solve's messages in the flow. */
|
|
27
33
|
solveMessages?: Snippet;
|
|
@@ -288,6 +288,8 @@
|
|
|
288
288
|
isSolving={session.isSolving}
|
|
289
289
|
showSolvingIndicator={solvingIndicator.show}
|
|
290
290
|
solveIsSlow={solvingIndicator.slow}
|
|
291
|
+
live={session.live}
|
|
292
|
+
onabort={() => session.abort()}
|
|
291
293
|
hasPendingChanges={session.hasPendingChanges}
|
|
292
294
|
hasNeverSolved={session.hasNeverSolved}
|
|
293
295
|
bind:isViewerFullscreen
|
|
@@ -318,4 +320,5 @@
|
|
|
318
320
|
onabort={() => session.abort()}
|
|
319
321
|
onconfirm={() => session.acknowledge()}
|
|
320
322
|
ondiscard={() => session.discard()}
|
|
323
|
+
progressShownByHost={schema?.instanceSolve !== false}
|
|
321
324
|
/>
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
import IssueList, { type Issue } from '../primitives/IssueList.svelte';
|
|
21
21
|
import SeverityIcon from '../primitives/SeverityIcon.svelte';
|
|
22
22
|
import { getLocaleContext } from '../../i18n/localeContext.svelte.js';
|
|
23
|
+
import { createLiveProgress } from '../../compute/liveProgress.svelte.js';
|
|
23
24
|
|
|
24
25
|
interface Props {
|
|
25
26
|
/** What the running (or most recent) solve has said over the live channel. */
|
|
@@ -38,6 +39,11 @@
|
|
|
38
39
|
freezesDuringSolve?: boolean;
|
|
39
40
|
/** `floating` overlays the bottom centre; `inline` sits in the flow below the host's content. */
|
|
40
41
|
placement?: 'floating' | 'inline';
|
|
42
|
+
/**
|
|
43
|
+
* The host shows progress and Abort itself (`AppLayout`'s solve status), so the live view
|
|
44
|
+
* keeps only the messages that arrive mid-solve.
|
|
45
|
+
*/
|
|
46
|
+
progressShownByHost?: boolean;
|
|
41
47
|
}
|
|
42
48
|
|
|
43
49
|
let {
|
|
@@ -48,7 +54,8 @@
|
|
|
48
54
|
onconfirm,
|
|
49
55
|
ondiscard,
|
|
50
56
|
freezesDuringSolve = false,
|
|
51
|
-
placement = 'floating'
|
|
57
|
+
placement = 'floating',
|
|
58
|
+
progressShownByHost = false
|
|
52
59
|
}: Props = $props();
|
|
53
60
|
|
|
54
61
|
const locale = getLocaleContext();
|
|
@@ -56,50 +63,10 @@
|
|
|
56
63
|
|
|
57
64
|
const REMARK_LINGER_MS = 6000;
|
|
58
65
|
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
// bar runs linearly to where the step will end, over that time. Steps are weighted by time, so
|
|
64
|
-
// linear gives one steady speed across the solve; any easing curve visibly speeds up and
|
|
65
|
-
// crawls within each step. A slower run waits at the step's end; a faster one snaps ahead.
|
|
66
|
-
let bars = $state.raw<Record<string, { width: number; ms: number }>>({});
|
|
67
|
-
// Any live event re-runs the effect below. Only a source with a new report may re-snap;
|
|
68
|
-
// the rest keep easing where they are.
|
|
69
|
-
let seen = new Map<string, unknown>();
|
|
70
|
-
let targets: Record<string, { width: number; ms: number }> = {};
|
|
71
|
-
$effect(() => {
|
|
72
|
-
const snapped: Record<string, { width: number; ms: number }> = {};
|
|
73
|
-
const eased: Record<string, { width: number; ms: number }> = {};
|
|
74
|
-
const current = untrack(() => bars);
|
|
75
|
-
// eslint-disable-next-line svelte/prefer-svelte-reactivity -- bookkeeping for this effect; reactive reads here would re-trigger it
|
|
76
|
-
const nextSeen = new Map<string, unknown>();
|
|
77
|
-
for (const p of progress) {
|
|
78
|
-
if (p.fraction === undefined) continue;
|
|
79
|
-
const key = p.source ?? '';
|
|
80
|
-
nextSeen.set(key, p);
|
|
81
|
-
if (seen.get(key) === p && current[key]) {
|
|
82
|
-
snapped[key] = current[key];
|
|
83
|
-
// This run's cleanup cancelled the frame that would have started the ease.
|
|
84
|
-
if (targets[key] && current[key] !== targets[key]) eased[key] = targets[key];
|
|
85
|
-
continue;
|
|
86
|
-
}
|
|
87
|
-
snapped[key] = { width: p.fraction, ms: 150 };
|
|
88
|
-
if (p.nextFraction !== undefined && p.stepMs !== undefined) {
|
|
89
|
-
eased[key] = { width: p.nextFraction, ms: p.stepMs };
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
seen = nextSeen;
|
|
93
|
-
targets = eased;
|
|
94
|
-
bars = snapped;
|
|
95
|
-
if (Object.keys(eased).length === 0) return;
|
|
96
|
-
// Two frames: the snapped width must be painted before the eased one is set, or the
|
|
97
|
-
// browser folds both into one style change and there is nothing to transition from.
|
|
98
|
-
let frame = requestAnimationFrame(() => {
|
|
99
|
-
frame = requestAnimationFrame(() => (bars = { ...snapped, ...eased }));
|
|
100
|
-
});
|
|
101
|
-
return () => cancelAnimationFrame(frame);
|
|
102
|
-
});
|
|
66
|
+
const liveProgress = createLiveProgress(() => live);
|
|
67
|
+
const progress = $derived(liveProgress.progress);
|
|
68
|
+
const bars = $derived(liveProgress.bars);
|
|
69
|
+
const headline = $derived(liveProgress.headline);
|
|
103
70
|
// Grasshopper's own complaints stay in the footer log; this panel shows what the author wrote.
|
|
104
71
|
const authored = $derived(diagnostics.filter((d) => d.isGate));
|
|
105
72
|
|
|
@@ -207,37 +174,11 @@
|
|
|
207
174
|
}))
|
|
208
175
|
);
|
|
209
176
|
|
|
210
|
-
// Steps a few milliseconds long would swap the headline faster than it can be read. Each one
|
|
211
|
-
// stays up for at least this long; steps that end sooner are skipped in the label but still
|
|
212
|
-
// count in the bar. The label and its step count move together so they never disagree.
|
|
213
|
-
const HEADLINE_MIN_MS = 200;
|
|
214
|
-
let headline = $state.raw<(typeof progress)[number] | undefined>(undefined);
|
|
215
|
-
let headlineAt = 0;
|
|
216
|
-
$effect(() => {
|
|
217
|
-
const next = progress[0];
|
|
218
|
-
const shown = untrack(() => headline);
|
|
219
|
-
if (next?.label === shown?.label && next?.done === shown?.done) {
|
|
220
|
-
headline = next;
|
|
221
|
-
return;
|
|
222
|
-
}
|
|
223
|
-
const show = () => {
|
|
224
|
-
headline = next;
|
|
225
|
-
headlineAt = performance.now();
|
|
226
|
-
};
|
|
227
|
-
const wait = HEADLINE_MIN_MS - (performance.now() - headlineAt);
|
|
228
|
-
if (!shown || wait <= 0) {
|
|
229
|
-
show();
|
|
230
|
-
return;
|
|
231
|
-
}
|
|
232
|
-
const timer = setTimeout(show, wait);
|
|
233
|
-
return () => clearTimeout(timer);
|
|
234
|
-
});
|
|
235
|
-
|
|
236
177
|
const visible = $derived(
|
|
237
178
|
view === 'review' || view === 'blocked'
|
|
238
179
|
? true
|
|
239
180
|
: view === 'live'
|
|
240
|
-
? revealed && (rows.length > 0 || progress.length > 0)
|
|
181
|
+
? revealed && (rows.length > 0 || (!progressShownByHost && progress.length > 0))
|
|
241
182
|
: rows.length > 0 && lingering
|
|
242
183
|
);
|
|
243
184
|
|
|
@@ -297,7 +238,7 @@
|
|
|
297
238
|
data-testid="solve-messages"
|
|
298
239
|
data-view={view}
|
|
299
240
|
>
|
|
300
|
-
{#if view === 'live'}
|
|
241
|
+
{#if view === 'live' && !progressShownByHost}
|
|
301
242
|
{@const head = headline ?? progress[0]}
|
|
302
243
|
<div>
|
|
303
244
|
<div>
|
|
@@ -402,7 +343,11 @@
|
|
|
402
343
|
{/if}
|
|
403
344
|
|
|
404
345
|
{#if rows.length > 0}
|
|
405
|
-
<div
|
|
346
|
+
<div
|
|
347
|
+
class="max-h-40 overflow-y-auto {view === 'live' && !progressShownByHost
|
|
348
|
+
? 'mt-3 pt-3 border-t'
|
|
349
|
+
: ''}"
|
|
350
|
+
>
|
|
406
351
|
<IssueList {issues} variant="plain" />
|
|
407
352
|
</div>
|
|
408
353
|
{/if}
|
|
@@ -17,6 +17,11 @@ interface Props {
|
|
|
17
17
|
freezesDuringSolve?: boolean;
|
|
18
18
|
/** `floating` overlays the bottom centre; `inline` sits in the flow below the host's content. */
|
|
19
19
|
placement?: 'floating' | 'inline';
|
|
20
|
+
/**
|
|
21
|
+
* The host shows progress and Abort itself (`AppLayout`'s solve status), so the live view
|
|
22
|
+
* keeps only the messages that arrive mid-solve.
|
|
23
|
+
*/
|
|
24
|
+
progressShownByHost?: boolean;
|
|
20
25
|
}
|
|
21
26
|
declare const SolveMessages: import("svelte").Component<Props, {}, "">;
|
|
22
27
|
type SolveMessages = ReturnType<typeof SolveMessages>;
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
import { loadScript } from '../../utils/loadScript.js';
|
|
7
7
|
import { getLocaleContext } from '../../i18n/localeContext.svelte.js';
|
|
8
8
|
import { formatMessage } from '../../i18n/messages.js';
|
|
9
|
+
import { upgradePlotlyFigure, type PlotlyFigure } from '../../features/chart/plotly-compat.js';
|
|
9
10
|
|
|
10
11
|
interface Props {
|
|
11
12
|
item: OutputChartLayoutItem;
|
|
@@ -17,19 +18,18 @@
|
|
|
17
18
|
const locale = getLocaleContext();
|
|
18
19
|
const t = $derived(locale.messages);
|
|
19
20
|
|
|
20
|
-
const MAP_TYPE_SET = new Set([
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
};
|
|
21
|
+
const MAP_TYPE_SET = new Set([
|
|
22
|
+
'scattermap',
|
|
23
|
+
'choroplethmap',
|
|
24
|
+
'densitymap',
|
|
25
|
+
'choropleth',
|
|
26
|
+
'scattergeo'
|
|
27
|
+
]);
|
|
28
28
|
|
|
29
29
|
const figData = $derived.by(() => {
|
|
30
30
|
if (!value) return null;
|
|
31
31
|
try {
|
|
32
|
-
return JSON.parse(value) as PlotlyFigure;
|
|
32
|
+
return upgradePlotlyFigure(JSON.parse(value) as PlotlyFigure);
|
|
33
33
|
} catch {
|
|
34
34
|
return null;
|
|
35
35
|
}
|
|
@@ -47,9 +47,9 @@
|
|
|
47
47
|
if (window.Plotly) return window.Plotly;
|
|
48
48
|
|
|
49
49
|
// Pinned by hash: the plugin's web UI runs this on the origin that holds its bridge secret.
|
|
50
|
-
await loadScript('https://cdn.plot.ly/plotly-
|
|
50
|
+
await loadScript('https://cdn.plot.ly/plotly-4.1.2.min.js', {
|
|
51
51
|
crossOrigin: 'anonymous',
|
|
52
|
-
integrity: 'sha384-
|
|
52
|
+
integrity: 'sha384-hHv6D3tTUUBpp4tVv6t2Dc3oGuRhphmWOTKUR8jWyN0Pnon1g3iebKCNDNmKmxo8'
|
|
53
53
|
});
|
|
54
54
|
// @ts-expect-error — Plotly is loaded dynamically onto window
|
|
55
55
|
return window.Plotly;
|
|
@@ -113,8 +113,13 @@
|
|
|
113
113
|
return a === 255 ? `rgb(${r}, ${g}, ${b})` : `rgba(${r}, ${g}, ${b}, ${(a / 255).toFixed(3)})`;
|
|
114
114
|
}
|
|
115
115
|
|
|
116
|
-
function getThemeColors(): {
|
|
117
|
-
|
|
116
|
+
function getThemeColors(): {
|
|
117
|
+
textColor: string;
|
|
118
|
+
gridColor: string;
|
|
119
|
+
fontFamily: string;
|
|
120
|
+
colorway: string[];
|
|
121
|
+
} {
|
|
122
|
+
if (!containerEl) return { textColor: '#333', gridColor: '#eee', fontFamily: '', colorway: [] };
|
|
118
123
|
const style = getComputedStyle(containerEl);
|
|
119
124
|
const textColor = toRgb(style.getPropertyValue('color').trim() || '#333');
|
|
120
125
|
const gridColor = toRgb(
|
|
@@ -122,7 +127,12 @@
|
|
|
122
127
|
style.getPropertyValue('--color-border')?.trim() ||
|
|
123
128
|
'rgba(128,128,128,0.2)'
|
|
124
129
|
);
|
|
125
|
-
|
|
130
|
+
// Figures that leave colours unset take Selva's chart palette rather than Plotly's.
|
|
131
|
+
const colorway = [1, 2, 3, 4, 5]
|
|
132
|
+
.map((i) => style.getPropertyValue(`--chart-${i}`).trim())
|
|
133
|
+
.filter(Boolean)
|
|
134
|
+
.map(toRgb);
|
|
135
|
+
return { textColor, gridColor, fontFamily: style.fontFamily, colorway };
|
|
126
136
|
}
|
|
127
137
|
|
|
128
138
|
async function renderChart() {
|
|
@@ -131,7 +141,7 @@
|
|
|
131
141
|
loading = true;
|
|
132
142
|
try {
|
|
133
143
|
const Plotly = await loadPlotly();
|
|
134
|
-
const { textColor, gridColor } = getThemeColors();
|
|
144
|
+
const { textColor, gridColor, fontFamily, colorway } = getThemeColors();
|
|
135
145
|
const isMapFigure = figData.data?.some((t) => MAP_TYPE_SET.has(t.type as string));
|
|
136
146
|
|
|
137
147
|
const {
|
|
@@ -151,7 +161,8 @@
|
|
|
151
161
|
...layoutRest,
|
|
152
162
|
autosize: true,
|
|
153
163
|
margin: { t: 24, r: 8, b: 40, l: 60, pad: 0 },
|
|
154
|
-
font: { color: textColor },
|
|
164
|
+
font: { color: textColor, ...(fontFamily ? { family: fontFamily } : {}) },
|
|
165
|
+
...(colorway.length && !layoutRest.colorway ? { colorway } : {}),
|
|
155
166
|
paper_bgcolor: layoutRest.paper_bgcolor ?? 'transparent',
|
|
156
167
|
plot_bgcolor: layoutRest.plot_bgcolor ?? 'transparent',
|
|
157
168
|
// Plotly's axis tick/label color doesn't inherit the top-level `font.color`, so this
|
|
@@ -166,12 +177,20 @@
|
|
|
166
177
|
: {})
|
|
167
178
|
};
|
|
168
179
|
|
|
169
|
-
Plotly.react(containerEl,
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
180
|
+
Plotly.react(containerEl, {
|
|
181
|
+
data: figData.data,
|
|
182
|
+
layout,
|
|
183
|
+
// Animated figures (e.g. Canopy's Animate) carry their frames here.
|
|
184
|
+
frames: figData.frames ?? [],
|
|
185
|
+
config: {
|
|
186
|
+
responsive: true,
|
|
187
|
+
displayModeBar: 'hover',
|
|
188
|
+
displaylogo: false,
|
|
189
|
+
// Plotly 4's "Share chart" button uploads the figure to Plotly's cloud.
|
|
190
|
+
showSendToCloud: false,
|
|
191
|
+
modeBarButtonsToRemove: ['sendDataToCloud', 'lasso2d', 'select2d'],
|
|
192
|
+
...figData.config
|
|
193
|
+
}
|
|
175
194
|
});
|
|
176
195
|
} catch (e) {
|
|
177
196
|
error = e instanceof Error ? e.message : t.chartRenderFailed;
|