@pixerate/editor-svelte 0.119.0 → 0.120.0

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/README.md CHANGED
@@ -89,7 +89,7 @@ Tables are on by default in `createRichTextPreset` (`table: false` turns them of
89
89
  | `@pixerate/editor-svelte/parameters` | `ParametersPanel` (a whole schema with groups, presets, undo / reset / randomize and keyframe diamonds), `createParameters` (call `destroy()` when done), `useParameterCommands`, and the field controls `ParameterFieldControl`, `ParameterSlider`, `ParameterToggle`, `ParameterSelect`, `ParameterColor`, `ParameterVector`, `ParameterImage`, `ParameterText`, `KeyframeDiamond` over `@pixerate/editor/parameters` (see [the parameters guide](../../docs/parameters.md)) |
90
90
  | `@pixerate/editor-svelte/color` | `ColorPicker` (interactive hex/rgb/hsl/oklch picker with alpha, swatches, eyedropper, recent history), `DualColorPicker` (dual light/dark mode color picker with theme preview and switch), `createColorScheme` / `resolvedColor` (the active scheme from the system, a root attribute or an override) |
91
91
  | `@pixerate/editor-svelte/gradient` | `GradientEditor` (interactive stops bar, midpoints, presets, angle dial, type selector, OKLab/OKLCH/sRGB/linear-sRGB interpolation, CSS output), `FillPicker` (controlled solid-or-gradient fill picker), `createGradientController` |
92
- | `@pixerate/editor-svelte/canvas` | Runes for `@xyflow/svelte`: `createCanvasGraph`, `createCanvasClipboard`, `createCanvasDocking`, `createCanvasShortcuts`, `createCanvasInteractions`, `createCanvasExplosion`, `createCanvasNodeSync`, `createCanvasMultiDrag`, `createStreamingCanvas`, `AgentPresenceLayer`, `FloatingHorizontalScrollbar`, plus layout helpers |
92
+ | `@pixerate/editor-svelte/canvas` | Runes for `@xyflow/svelte`: `createCanvasGraph`, `createCanvasClipboard`, `createCanvasDocking`, `createCanvasShortcuts`, `createCanvasInteractions`, `createCanvasExplosion`, `createCanvasExplosionState`, `createCanvasNodeSync`, `createCanvasMultiDrag`, `createStreamingCanvas`, `AgentPresenceLayer`, `FloatingHorizontalScrollbar`, plus layout helpers |
93
93
 
94
94
  Spreadsheet persistence: `createReactiveSpreadsheet` exposes `document` (and passes it to `onDocumentChange`) with the defined names its formulas use in `document.names`, so saving that document and passing it back as `document` restores workbook, sheet-scoped and LAMBDA names. For several sheets, save `controller.getWorkbook()` instead.
95
95
 
@@ -1,6 +1,7 @@
1
1
  <script lang="ts">
2
- import { chartColorInputValue, chartSeriesColor, formatChartNumber, type ChartController } from '@pixerate/editor/chart';
2
+ import { chartColorInputValue, chartSeriesColor, chartTreeFromSeries, formatChartNumber, type ChartController } from '@pixerate/editor/chart';
3
3
  import ChartCommitInput from './ChartCommitInput.svelte';
4
+ import ChartTreeGrid from './ChartTreeGrid.svelte';
4
5
  import { chartStateOf } from './createChartEditor.svelte.js';
5
6
  import { chartButtonClass, chartCellClass, chartIconButtonClass, chartInputClass } from './styles.js';
6
7
 
@@ -18,6 +19,7 @@
18
19
  const view = chartStateOf(() => controller);
19
20
  const spec = $derived(view.current.spec);
20
21
  const selection = $derived(view.current.selection);
22
+ const canGroup = $derived(spec.type === 'treemap' || spec.type === 'sunburst');
21
23
 
22
24
  const cellText = (value: number | null | undefined) => (value === null || value === undefined ? '' : String(value));
23
25
 
@@ -30,6 +32,9 @@
30
32
  };
31
33
  </script>
32
34
 
35
+ {#if spec.tree.length}
36
+ <ChartTreeGrid {controller} {readOnly} class={className} />
37
+ {:else}
33
38
  <div class="overflow-auto rounded-md border border-border {className}" data-chart-grid="">
34
39
  <table class="w-full border-collapse text-xs">
35
40
  <thead class="bg-muted/50">
@@ -121,6 +126,15 @@
121
126
  {#if !readOnly}
122
127
  <div class="p-1">
123
128
  <button type="button" class="{chartButtonClass} h-6 border-dashed" data-action="add-category" onclick={() => controller.addCategory()}>+ Add category</button>
129
+ {#if canGroup}
130
+ <button
131
+ type="button"
132
+ class="{chartButtonClass} ml-1 h-6"
133
+ data-action="tree-from-table"
134
+ title="Edit the categories as groups of items"
135
+ onclick={() => controller.setTree(chartTreeFromSeries(spec.categories, spec.series.find((s) => !s.hidden)?.values ?? []))}>Group items</button>
136
+ {/if}
124
137
  </div>
125
138
  {/if}
126
139
  </div>
140
+ {/if}
@@ -18,6 +18,8 @@
18
18
  CHART_LABEL_POSITIONS,
19
19
  CHART_LAYOUT_PRESETS,
20
20
  CHART_LEGEND_POSITIONS,
21
+ CHART_LIKERT_NEUTRALS,
22
+ CHART_LIKERT_SORTS,
21
23
  CHART_MARKERS,
22
24
  CHART_NUMBER_STYLES,
23
25
  CHART_SERIES_TYPES,
@@ -36,6 +38,7 @@
36
38
  defaultChartErrorBarValue,
37
39
  formatChartAmountList,
38
40
  isCartesianChart,
41
+ suggestChartReferences,
39
42
  parseChartAmountList,
40
43
  type ChartAxis,
41
44
  type ChartController,
@@ -66,6 +69,7 @@
66
69
  const view = chartStateOf(() => controller);
67
70
  const spec = $derived(view.current.spec);
68
71
  const selectedSeries = $derived(view.current.selectedSeries);
72
+ const selection = $derived(view.current.selection);
69
73
  const show = (section: ChartInspectorSection) => !sections || sections.includes(section);
70
74
  const cartesian = $derived(isCartesianChart(spec.type));
71
75
  const family = $derived(chartFamily(spec));
@@ -80,6 +84,10 @@
80
84
  const horizontal = $derived(spec.orientation === 'horizontal');
81
85
  // Log and reversed axes and error bars are drawn by the bar / line / area / scatter layouts.
82
86
  const scales = $derived(family === 'cartesian' || family === 'xy');
87
+ // Reference lines and bands, callouts and colour by category are drawn by the same layouts.
88
+ const annotated = $derived(scales);
89
+ const oneSeries = $derived(cartesian && spec.series.filter((s) => !s.hidden).length === 1);
90
+ const calloutIndex = $derived(selection?.kind === 'point' && selection.seriesId === selectedSeries?.id ? selection.index : null);
83
91
  const cap = (s: string) => s.charAt(0).toUpperCase() + s.slice(1);
84
92
  const stackingLabels: Record<string, string> = { none: 'None', stacked: 'Stacked', percent: '100%' };
85
93
 
@@ -332,6 +340,12 @@
332
340
  <ChartCommitInput id="{idPrefix}-subtitle" value={spec.subtitle} data-option="subtitle" oncommit={(subtitle) => void controller.setSubtitle(subtitle)} />
333
341
  </div>
334
342
  </div>
343
+ <div class="flex items-center gap-2">
344
+ <label for="{idPrefix}-note" class="{chartLabelClass} w-20 shrink-0">Note</label>
345
+ <div class="min-w-0 flex-1">
346
+ <ChartCommitInput id="{idPrefix}-note" value={spec.note} placeholder="Source or footnote" data-option="note" oncommit={(note) => void controller.setNote(note)} />
347
+ </div>
348
+ </div>
335
349
  </div>
336
350
  {/if}
337
351
 
@@ -409,6 +423,62 @@
409
423
  </div>
410
424
  </div>
411
425
  {/if}
426
+ {#if spec.type === 'sunburst'}
427
+ <div class="flex items-center gap-2">
428
+ <label for="{idPrefix}-sunburst-label" class="{chartLabelClass} w-20 shrink-0">Caption</label>
429
+ <div class="min-w-0 flex-1">
430
+ <ChartCommitInput id="{idPrefix}-sunburst-label" value={spec.sunburstLabel} placeholder="Under the total" data-option="sunburstLabel" oncommit={(sunburstLabel) => controller.update({ sunburstLabel }).ok} />
431
+ </div>
432
+ </div>
433
+ {/if}
434
+ {#if spec.type === 'likert'}
435
+ <div class="flex items-center gap-2">
436
+ <label for="{idPrefix}-likert-neutral" class="{chartLabelClass} w-20 shrink-0">Neutral</label>
437
+ <select id="{idPrefix}-likert-neutral" class={chartInputClass} value={spec.likertNeutral} data-option="likertNeutral" onchange={(e) => controller.update({ likertNeutral: e.currentTarget.value as (typeof CHART_LIKERT_NEUTRALS)[number] })}>
438
+ {#each CHART_LIKERT_NEUTRALS as n (n)}
439
+ <option value={n}>{n === 'split' ? 'Across the centre' : 'In its own column'}</option>
440
+ {/each}
441
+ </select>
442
+ </div>
443
+ <div class="flex items-center gap-2">
444
+ <label for="{idPrefix}-likert-sort" class="{chartLabelClass} w-20 shrink-0">Order</label>
445
+ <select id="{idPrefix}-likert-sort" class={chartInputClass} value={spec.likertSort} data-option="likertSort" onchange={(e) => controller.update({ likertSort: e.currentTarget.value as (typeof CHART_LIKERT_SORTS)[number] })}>
446
+ {#each CHART_LIKERT_SORTS as o (o)}
447
+ <option value={o}>{o === 'none' ? 'As entered' : o === 'positive' ? 'Most agreement first' : 'Most disagreement first'}</option>
448
+ {/each}
449
+ </select>
450
+ </div>
451
+ {/if}
452
+ {#if spec.type === 'parliament'}
453
+ <div class="flex items-center gap-2">
454
+ <label for="{idPrefix}-parliament-arc" class="{chartLabelClass} w-20 shrink-0">Sweep</label>
455
+ <input
456
+ id="{idPrefix}-parliament-arc"
457
+ type="range"
458
+ min="90"
459
+ max="360"
460
+ step="10"
461
+ value={spec.parliamentArc}
462
+ data-option="parliamentArc"
463
+ class="w-full accent-primary"
464
+ onpointerdown={() => controller.beginGesture('parliamentArc')}
465
+ onpointerup={() => controller.endGesture()}
466
+ oninput={(e) => controller.update({ parliamentArc: Number(e.currentTarget.value) })}
467
+ />
468
+ </div>
469
+ <div class="flex items-center gap-2">
470
+ <label for="{idPrefix}-parliament-majority" class="{chartLabelClass} w-20 shrink-0">Majority</label>
471
+ <div class="min-w-0 flex-1">
472
+ <ChartCommitInput id="{idPrefix}-parliament-majority" value={spec.parliamentMajority === null ? '' : String(spec.parliamentMajority)} placeholder="Half plus one" inputmode="numeric" data-option="parliamentMajority" oncommit={(text) => controller.update({ parliamentMajority: text.trim() === '' ? null : Number(text) }).ok} />
473
+ </div>
474
+ </div>
475
+ <div class="flex items-center gap-2">
476
+ <label for="{idPrefix}-parliament-label" class="{chartLabelClass} w-20 shrink-0">Caption</label>
477
+ <div class="min-w-0 flex-1">
478
+ <ChartCommitInput id="{idPrefix}-parliament-label" value={spec.parliamentLabel} placeholder="seats" data-option="parliamentLabel" oncommit={(parliamentLabel) => controller.update({ parliamentLabel }).ok} />
479
+ </div>
480
+ </div>
481
+ {/if}
412
482
  {#if spec.type === 'radar'}
413
483
  <div class="flex items-center gap-2">
414
484
  <label for="{idPrefix}-radar-ticks" class="{chartLabelClass} w-20 shrink-0">Rings</label>
@@ -450,6 +520,9 @@
450
520
  {#if hasLines}
451
521
  {@render toggle('Points', 'points', spec.points, (points) => controller.update({ points }))}
452
522
  {/if}
523
+ {#if oneSeries}
524
+ {@render toggle('Colour by category', 'varyColors', spec.varyColors, (varyColors) => controller.update({ varyColors }))}
525
+ {/if}
453
526
  {@render toggle('Data labels', 'dataLabels', spec.dataLabels, (dataLabels) => controller.update({ dataLabels }))}
454
527
  </div>
455
528
  </div>
@@ -483,6 +556,43 @@
483
556
  {/if}
484
557
  {/if}
485
558
 
559
+ {#if show('annotations') && annotated}
560
+ <div class={chartSectionClass} data-section="annotations">
561
+ <h4 class="text-xs font-semibold text-foreground">Annotations</h4>
562
+ <div class="flex items-center gap-2">
563
+ <label for="{idPrefix}-highlight" class="{chartLabelClass} w-20 shrink-0">Highlight</label>
564
+ <div class="min-w-0 flex-1">
565
+ <ChartCommitInput
566
+ id="{idPrefix}-highlight"
567
+ value={spec.highlight.join(', ')}
568
+ placeholder="Categories or series"
569
+ data-option="highlight"
570
+ oncommit={(text) => controller.setHighlight(text.split(',').map((n) => n.trim()).filter(Boolean)).ok}
571
+ />
572
+ </div>
573
+ </div>
574
+ {#each spec.referenceLines as line, i (i)}
575
+ <div class="flex items-center gap-1" data-reference-line={i}>
576
+ <ChartCommitInput value={String(line.value)} aria-label="Line value" inputmode="decimal" data-option="referenceLine.{i}.value" class="{chartInputClass} w-16 shrink-0" oncommit={(text) => controller.updateReferenceLine(i, { value: Number(text) }).ok} />
577
+ <ChartCommitInput value={line.label ?? ''} aria-label="Line label" placeholder="Label" data-option="referenceLine.{i}.label" class="{chartInputClass} flex-1" oncommit={(text) => controller.updateReferenceLine(i, { label: text || null }).ok} />
578
+ <button type="button" class={chartButtonClass} aria-label="Delete reference line" data-action="reference-line-delete" onclick={() => controller.removeReferenceLine(i)}>×</button>
579
+ </div>
580
+ {/each}
581
+ {#each spec.referenceBands as band, i (i)}
582
+ <div class="flex items-center gap-1" data-reference-band={i}>
583
+ <ChartCommitInput value={String(band.from)} aria-label="Band from" inputmode="decimal" data-option="referenceBand.{i}.from" class="{chartInputClass} w-14 shrink-0" oncommit={(text) => controller.updateReferenceBand(i, { from: Number(text) }).ok} />
584
+ <ChartCommitInput value={String(band.to)} aria-label="Band to" inputmode="decimal" data-option="referenceBand.{i}.to" class="{chartInputClass} w-14 shrink-0" oncommit={(text) => controller.updateReferenceBand(i, { to: Number(text) }).ok} />
585
+ <ChartCommitInput value={band.label ?? ''} aria-label="Band label" placeholder="Label" data-option="referenceBand.{i}.label" class="{chartInputClass} flex-1" oncommit={(text) => controller.updateReferenceBand(i, { label: text || null }).ok} />
586
+ <button type="button" class={chartButtonClass} aria-label="Delete reference band" data-action="reference-band-delete" onclick={() => controller.removeReferenceBand(i)}>×</button>
587
+ </div>
588
+ {/each}
589
+ <div class="flex gap-1">
590
+ <button type="button" class={chartButtonClass} data-option="referenceLine.add" onclick={() => controller.addReferenceLine(suggestChartReferences(spec).line)}>Add line</button>
591
+ <button type="button" class={chartButtonClass} data-option="referenceBand.add" onclick={() => controller.addReferenceBand(suggestChartReferences(spec).band)}>Add band</button>
592
+ </div>
593
+ </div>
594
+ {/if}
595
+
486
596
  {#if show('series') && selectedSeries}
487
597
  {@const series = selectedSeries}
488
598
  {@const drawnAs = chartSeriesType(spec, series)}
@@ -546,6 +656,21 @@
546
656
  </select>
547
657
  </div>
548
658
  {/if}
659
+ {#if annotated && calloutIndex !== null}
660
+ {@const at = calloutIndex}
661
+ <div class="flex items-center gap-2">
662
+ <label for="{idPrefix}-series-callout" class="{chartLabelClass} w-20 shrink-0">Callout</label>
663
+ <div class="min-w-0 flex-1">
664
+ <ChartCommitInput
665
+ id="{idPrefix}-series-callout"
666
+ value={series.callouts?.[at] ?? ''}
667
+ placeholder="Label for {spec.categories[at] ?? 'this point'}"
668
+ data-option="series.callout"
669
+ oncommit={(text) => controller.setCallout(series.id, at, text || null).ok}
670
+ />
671
+ </div>
672
+ </div>
673
+ {/if}
549
674
  {#if scales}
550
675
  {@render errorBarFields(series, 'errorBars')}
551
676
  {/if}
@@ -0,0 +1,93 @@
1
+ <script lang="ts">
2
+ import {
3
+ addChartTreeRow,
4
+ chartTreeColumnLabels,
5
+ chartTreeDepth,
6
+ chartTreeToRows,
7
+ formatChartNumber,
8
+ removeChartTreeRow,
9
+ setChartTreeRowName,
10
+ setChartTreeRowValue,
11
+ type ChartController
12
+ } from '@pixerate/editor/chart';
13
+ import ChartCommitInput from './ChartCommitInput.svelte';
14
+ import { chartStateOf } from './createChartEditor.svelte.js';
15
+ import { chartButtonClass, chartCellClass, chartIconButtonClass } from './styles.js';
16
+
17
+ interface Props {
18
+ controller: ChartController;
19
+ /** Hide the add / delete buttons. */
20
+ readOnly?: boolean;
21
+ class?: string;
22
+ }
23
+
24
+ let { controller, readOnly = false, class: className = '' }: Props = $props();
25
+
26
+ const view = chartStateOf(() => controller);
27
+ const spec = $derived(view.current.spec);
28
+ // `+ Level` raises the column count; it stays until the grid is unmounted, so a new column can be filled in.
29
+ let columns = $state(1);
30
+ const rows = $derived(chartTreeToRows(spec.tree));
31
+ const depth = $derived(Math.max(columns, chartTreeDepth(spec.tree)));
32
+ const labels = $derived(chartTreeColumnLabels(depth));
33
+
34
+ const commit = (next: Parameters<ChartController['setTreeRows']>[0]) => controller.setTreeRows(next).ok;
35
+ </script>
36
+
37
+ <!-- Grouped data as a table: the names from the group down to each item, then its value. -->
38
+ <div class="overflow-auto rounded-md border border-border {className}" data-chart-grid="" data-chart-tree-grid="">
39
+ <table class="w-full border-collapse text-xs">
40
+ <thead class="bg-muted/50">
41
+ <tr>
42
+ {#each labels as label (label)}
43
+ <th class="border-b border-r border-border px-2 py-1 text-left font-medium text-muted-foreground">{label}</th>
44
+ {/each}
45
+ <th class="border-b border-r border-border px-2 py-1 text-right font-medium text-muted-foreground">Value</th>
46
+ {#if !readOnly}<th class="w-8 border-b border-border"></th>{/if}
47
+ </tr>
48
+ </thead>
49
+ <tbody>
50
+ {#each rows as row, r (r)}
51
+ <tr data-tree-row={r}>
52
+ {#each labels as label, k (k)}
53
+ <td class="border-b border-r border-border p-0">
54
+ <ChartCommitInput
55
+ value={row.path[k] ?? ''}
56
+ aria-label="{label}, row {r + 1}"
57
+ data-tree-cell="{r}:{k}"
58
+ readonly={readOnly}
59
+ oncommit={(name) => commit(setChartTreeRowName(rows, r, k, name))}
60
+ class="{chartCellClass} text-left"
61
+ />
62
+ </td>
63
+ {/each}
64
+ <td class="border-b border-r border-border p-0">
65
+ <ChartCommitInput
66
+ value={String(row.value)}
67
+ inputmode="decimal"
68
+ aria-label="Value of {row.path.join(' / ')}"
69
+ title={formatChartNumber(row.value, spec.yAxis.format)}
70
+ data-tree-value={r}
71
+ readonly={readOnly}
72
+ oncommit={(text) => commit(setChartTreeRowValue(rows, r, text))}
73
+ class={chartCellClass}
74
+ />
75
+ </td>
76
+ {#if !readOnly}
77
+ <td class="border-b border-border px-1">
78
+ <button type="button" class={chartIconButtonClass} title="Delete item" aria-label="Delete {row.path.join(' / ')}" data-action="tree-remove-row" onclick={() => commit(removeChartTreeRow(rows, r))}>×</button>
79
+ </td>
80
+ {/if}
81
+ </tr>
82
+ {/each}
83
+ </tbody>
84
+ </table>
85
+ {#if !readOnly}
86
+ <div class="flex flex-wrap gap-1 p-1">
87
+ <button type="button" class="{chartButtonClass} h-6 border-dashed" data-action="tree-add-row" onclick={() => commit(addChartTreeRow(rows))}>+ Add item</button>
88
+ <button type="button" class="{chartButtonClass} h-6 border-dashed" data-action="tree-add-level" disabled={depth >= 6} onclick={() => (columns = depth + 1)}>+ Level</button>
89
+ <span class="flex-1"></span>
90
+ <button type="button" class="{chartButtonClass} h-6" data-action="tree-ungroup" title="Keep the group totals as plain data" onclick={() => controller.setTree([])}>Ungroup</button>
91
+ </div>
92
+ {/if}
93
+ </div>
@@ -0,0 +1,10 @@
1
+ import { type ChartController } from '@pixerate/editor/chart';
2
+ interface Props {
3
+ controller: ChartController;
4
+ /** Hide the add / delete buttons. */
5
+ readOnly?: boolean;
6
+ class?: string;
7
+ }
8
+ declare const ChartTreeGrid: import("svelte").Component<Props, {}, "">;
9
+ type ChartTreeGrid = ReturnType<typeof ChartTreeGrid>;
10
+ export default ChartTreeGrid;
@@ -8,6 +8,7 @@ import type ChartSparklineComponent from "./ChartSparkline.svelte";
8
8
  export { default as ChartEditor } from "./ChartEditor.svelte";
9
9
  export { default as ChartCanvas } from "./ChartCanvas.svelte";
10
10
  export { default as ChartDataGrid } from "./ChartDataGrid.svelte";
11
+ export { default as ChartTreeGrid } from "./ChartTreeGrid.svelte";
11
12
  export { default as ChartInspector } from "./ChartInspector.svelte";
12
13
  export { default as ChartCommitInput } from "./ChartCommitInput.svelte";
13
14
  export { default as ChartMarks } from "./ChartMarks.svelte";
@@ -1,6 +1,7 @@
1
1
  export { default as ChartEditor } from "./ChartEditor.svelte";
2
2
  export { default as ChartCanvas } from "./ChartCanvas.svelte";
3
3
  export { default as ChartDataGrid } from "./ChartDataGrid.svelte";
4
+ export { default as ChartTreeGrid } from "./ChartTreeGrid.svelte";
4
5
  export { default as ChartInspector } from "./ChartInspector.svelte";
5
6
  export { default as ChartCommitInput } from "./ChartCommitInput.svelte";
6
7
  export { default as ChartMarks } from "./ChartMarks.svelte";
@@ -1 +1 @@
1
- export type ChartInspectorSection = 'type' | 'style' | 'titles' | 'options' | 'legend' | 'axes' | 'series';
1
+ export type ChartInspectorSection = 'type' | 'style' | 'titles' | 'options' | 'legend' | 'axes' | 'annotations' | 'series';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pixerate/editor-svelte",
3
- "version": "0.119.0",
3
+ "version": "0.120.0",
4
4
  "description": "Svelte bindings and prompt/rich-text components for @pixerate/editor",
5
5
  "author": "Pixerate",
6
6
  "license": "MIT",
@@ -168,7 +168,7 @@
168
168
  "@tiptap/extension-bubble-menu": "^2.27.3",
169
169
  "@tiptap/extension-placeholder": "^2.27.3",
170
170
  "@tiptap/starter-kit": "^2.27.3",
171
- "@pixerate/editor": "0.162.0"
171
+ "@pixerate/editor": "0.163.0"
172
172
  },
173
173
  "peerDependencies": {
174
174
  "@dagrejs/dagre": "^1.1.4",