@pixerate/editor-svelte 0.42.1 → 0.44.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.
Files changed (40) hide show
  1. package/dist/chart/ChartCanvas.svelte +32 -98
  2. package/dist/chart/ChartCanvas.svelte.d.ts +7 -1
  3. package/dist/chart/ChartCanvas.svelte.d.ts.map +1 -1
  4. package/dist/chart/ChartInspector.svelte +185 -28
  5. package/dist/chart/ChartInspector.svelte.d.ts +1 -1
  6. package/dist/chart/ChartInspector.svelte.d.ts.map +1 -1
  7. package/dist/chart/ChartMarks.svelte +102 -0
  8. package/dist/chart/ChartMarks.svelte.d.ts +11 -0
  9. package/dist/chart/ChartMarks.svelte.d.ts.map +1 -0
  10. package/dist/chart/ChartPresenceLayer.svelte +30 -0
  11. package/dist/chart/ChartPresenceLayer.svelte.d.ts +9 -0
  12. package/dist/chart/ChartPresenceLayer.svelte.d.ts.map +1 -0
  13. package/dist/chart/ChartSceneSvg.svelte +17 -0
  14. package/dist/chart/ChartSceneSvg.svelte.d.ts +11 -0
  15. package/dist/chart/ChartSceneSvg.svelte.d.ts.map +1 -0
  16. package/dist/chart/ChartSparkline.svelte +27 -0
  17. package/dist/chart/ChartSparkline.svelte.d.ts +20 -0
  18. package/dist/chart/ChartSparkline.svelte.d.ts.map +1 -0
  19. package/dist/chart/ChartStreamingView.svelte +92 -0
  20. package/dist/chart/ChartStreamingView.svelte.d.ts +25 -0
  21. package/dist/chart/ChartStreamingView.svelte.d.ts.map +1 -0
  22. package/dist/chart/createChartEditor.svelte.d.ts +25 -1
  23. package/dist/chart/createChartEditor.svelte.d.ts.map +1 -1
  24. package/dist/chart/createChartEditor.svelte.js +25 -1
  25. package/dist/chart/index.d.ts +9 -0
  26. package/dist/chart/index.d.ts.map +1 -1
  27. package/dist/chart/index.js +5 -0
  28. package/dist/chart-node/ChartNodeView.svelte +62 -0
  29. package/dist/chart-node/ChartNodeView.svelte.d.ts +14 -0
  30. package/dist/chart-node/ChartNodeView.svelte.d.ts.map +1 -0
  31. package/dist/chart-node/createChartNodeView.svelte.d.ts +21 -0
  32. package/dist/chart-node/createChartNodeView.svelte.d.ts.map +1 -0
  33. package/dist/chart-node/createChartNodeView.svelte.js +28 -0
  34. package/dist/chart-node/index.d.ts +6 -0
  35. package/dist/chart-node/index.d.ts.map +1 -0
  36. package/dist/chart-node/index.js +2 -0
  37. package/dist/index.d.ts +1 -0
  38. package/dist/index.d.ts.map +1 -1
  39. package/dist/index.js +1 -0
  40. package/package.json +2 -2
@@ -3,19 +3,20 @@
3
3
  import {
4
4
  CSS_VARIABLE_CHART_THEME,
5
5
  chartClientToScene,
6
- chartTextBaseline,
7
- chartTextTransform,
8
6
  describeChartSelection,
9
7
  handleChartKey,
10
8
  layoutChart,
11
9
  startChartPointDrag,
12
10
  type ChartController,
13
- type ChartMark,
14
11
  type ChartPointDrag,
12
+ type ChartPresence,
13
+ type ChartScene,
15
14
  type ChartTextMeasure,
16
15
  type ChartTheme
17
16
  } from '@pixerate/editor/chart';
18
17
  import { chartStateOf } from './createChartEditor.svelte.js';
18
+ import ChartMarks from './ChartMarks.svelte';
19
+ import ChartPresenceLayer from './ChartPresenceLayer.svelte';
19
20
 
20
21
  interface Props {
21
22
  controller: ChartController;
@@ -33,6 +34,10 @@
33
34
  measureText?: ChartTextMeasure;
34
35
  emptyMessage?: string;
35
36
  class?: string;
37
+ /** Agent cursors to draw over the chart (see `ChartDirector`). */
38
+ presence?: ChartPresence;
39
+ /** Receives the scene as drawn (e.g. for `ChartDirector`'s `scene`). */
40
+ sceneRef?: { current: ChartScene | null };
36
41
  }
37
42
 
38
43
  let {
@@ -44,7 +49,9 @@
44
49
  editable = true,
45
50
  measureText,
46
51
  emptyMessage,
47
- class: className = ''
52
+ class: className = '',
53
+ presence,
54
+ sceneRef
48
55
  }: Props = $props();
49
56
 
50
57
  const view = chartStateOf(() => controller);
@@ -64,8 +71,8 @@
64
71
  return () => observer.disconnect();
65
72
  });
66
73
  let drag: ChartPointDrag | null = null;
67
- // Frozen while a point is dragged so the value axis does not rescale under the pointer.
68
- let valueDomain = $state<ChartPointDrag['valueDomain'] | null>(null);
74
+ // Frozen while a point is dragged so the axes do not rescale under the pointer.
75
+ let frozen = $state.raw<{ valueDomain: ChartPointDrag['valueDomain']; valueDomain2: ChartPointDrag['valueDomain2'] | null; xDomain: ChartPointDrag['xDomain'] | null; readout: ChartPointDrag['target'] } | null>(null);
69
76
 
70
77
  const sceneWidth = $derived((responsive && measured) || width);
71
78
  const fullTheme = $derived({ ...CSS_VARIABLE_CHART_THEME, ...theme });
@@ -77,16 +84,20 @@
77
84
  selection: editable ? view.current.selection : null,
78
85
  measureText,
79
86
  emptyMessage,
80
- valueDomain
87
+ valueDomain: frozen?.valueDomain,
88
+ valueDomain2: frozen?.valueDomain2,
89
+ xDomain: frozen?.xDomain,
90
+ readout: frozen?.readout
81
91
  })
82
92
  );
93
+ $effect(() => {
94
+ if (sceneRef) sceneRef.current = scene;
95
+ });
83
96
  const byKey = $derived(new Map(scene.marks.map((m) => [m.key, m])));
84
97
  const label = $derived(view.current.spec.title || 'Chart');
85
98
 
86
99
  onDestroy(() => drag?.end());
87
100
 
88
- const pointerEvents = (mark: ChartMark) => (mark.role === 'selection' || (!mark.target && mark.role !== 'background') ? 'none' : undefined);
89
- const cursor = (mark: ChartMark) => (editable && mark.target && mark.role !== 'background' ? 'pointer' : undefined);
90
101
  const scenePoint = (e: PointerEvent) => chartClientToScene(scene, svg!.getBoundingClientRect(), e.clientX, e.clientY);
91
102
 
92
103
  function pointerdown(e: PointerEvent) {
@@ -96,19 +107,26 @@
96
107
  controller.select(target ?? null);
97
108
  drag = startChartPointDrag(controller, scene, target, scenePoint(e));
98
109
  if (drag) {
99
- valueDomain = drag.valueDomain;
110
+ frozen = { valueDomain: drag.valueDomain, valueDomain2: drag.valueDomain2 ?? null, xDomain: drag.xDomain ?? null, readout: drag.target };
100
111
  svg?.setPointerCapture?.(e.pointerId);
101
112
  }
102
113
  }
103
114
  function pointermove(e: PointerEvent) {
104
- drag?.move(scenePoint(e));
115
+ drag?.move(scenePoint(e), { fine: e.altKey, lockAxis: e.shiftKey, noSnap: e.ctrlKey || e.metaKey });
105
116
  }
106
117
  function endDrag() {
107
118
  drag?.end();
108
119
  drag = null;
109
- valueDomain = null;
120
+ frozen = null;
110
121
  }
111
122
  function keydown(e: KeyboardEvent) {
123
+ if (drag && e.key === 'Escape') {
124
+ drag.cancel();
125
+ drag = null;
126
+ frozen = null;
127
+ e.preventDefault();
128
+ return;
129
+ }
112
130
  if (editable && handleChartKey(controller, scene, e)) e.preventDefault();
113
131
  }
114
132
  </script>
@@ -130,91 +148,7 @@
130
148
  onpointercancel={endDrag}
131
149
  onkeydown={keydown}
132
150
  >
133
- {#each scene.marks as mark (mark.key)}
134
- {#if mark.kind === 'rect'}
135
- <rect
136
- data-role={mark.role}
137
- data-chart-key={mark.target ? mark.key : undefined}
138
- data-selected={mark.selected ? '' : undefined}
139
- x={mark.x}
140
- y={mark.y}
141
- width={mark.width}
142
- height={mark.height}
143
- rx={mark.radius}
144
- opacity={mark.opacity}
145
- pointer-events={pointerEvents(mark)}
146
- cursor={cursor(mark)}
147
- style:fill={mark.fill}
148
- style:stroke={mark.stroke}
149
- style:stroke-width={mark.strokeWidth}
150
- />
151
- {:else if mark.kind === 'path'}
152
- <path
153
- data-role={mark.role}
154
- data-chart-key={mark.target ? mark.key : undefined}
155
- data-selected={mark.selected ? '' : undefined}
156
- d={mark.d}
157
- stroke-linejoin="round"
158
- stroke-linecap="round"
159
- stroke-dasharray={mark.dash}
160
- opacity={mark.opacity}
161
- pointer-events={pointerEvents(mark)}
162
- cursor={cursor(mark)}
163
- style:fill={mark.fill ?? 'none'}
164
- style:stroke={mark.stroke}
165
- style:stroke-width={mark.strokeWidth}
166
- />
167
- {:else if mark.kind === 'circle'}
168
- <circle
169
- data-role={mark.role}
170
- data-chart-key={mark.target ? mark.key : undefined}
171
- data-selected={mark.selected ? '' : undefined}
172
- cx={mark.cx}
173
- cy={mark.cy}
174
- r={mark.r}
175
- opacity={mark.opacity}
176
- pointer-events={pointerEvents(mark)}
177
- cursor={cursor(mark)}
178
- style:fill={mark.fill}
179
- style:stroke={mark.stroke}
180
- style:stroke-width={mark.strokeWidth}
181
- />
182
- {:else if mark.kind === 'line'}
183
- <line
184
- data-role={mark.role}
185
- data-chart-key={mark.target ? mark.key : undefined}
186
- data-selected={mark.selected ? '' : undefined}
187
- x1={mark.x1}
188
- y1={mark.y1}
189
- x2={mark.x2}
190
- y2={mark.y2}
191
- stroke-dasharray={mark.dash}
192
- opacity={mark.opacity}
193
- pointer-events={pointerEvents(mark)}
194
- cursor={cursor(mark)}
195
- style:stroke={mark.stroke}
196
- style:stroke-width={mark.strokeWidth}
197
- />
198
- {:else}
199
- <text
200
- data-role={mark.role}
201
- data-chart-key={mark.target ? mark.key : undefined}
202
- data-selected={mark.selected ? '' : undefined}
203
- x={mark.x}
204
- y={mark.y}
205
- text-anchor={mark.anchor}
206
- dominant-baseline={chartTextBaseline(mark.baseline)}
207
- transform={chartTextTransform(mark)}
208
- opacity={mark.opacity}
209
- pointer-events={pointerEvents(mark)}
210
- cursor={cursor(mark)}
211
- style:fill={mark.fill}
212
- style:font-size="{mark.size}px"
213
- style:font-weight={mark.weight}
214
- style:font-family={scene.theme.fontFamily}
215
- style:user-select="none">{mark.text}</text
216
- >
217
- {/if}
218
- {/each}
151
+ <ChartMarks marks={scene.marks} fontFamily={scene.theme.fontFamily} interactive={editable} />
152
+ {#if presence}<ChartPresenceLayer {presence} />{/if}
219
153
  </svg>
220
154
  </div>
@@ -1,4 +1,4 @@
1
- import { type ChartController, type ChartTextMeasure, type ChartTheme } from '@pixerate/editor/chart';
1
+ import { type ChartController, type ChartPresence, type ChartScene, type ChartTextMeasure, type ChartTheme } from '@pixerate/editor/chart';
2
2
  interface Props {
3
3
  controller: ChartController;
4
4
  /** Scene width; with `responsive` this is only the fallback before the container is measured (default 640). */
@@ -15,6 +15,12 @@ interface Props {
15
15
  measureText?: ChartTextMeasure;
16
16
  emptyMessage?: string;
17
17
  class?: string;
18
+ /** Agent cursors to draw over the chart (see `ChartDirector`). */
19
+ presence?: ChartPresence;
20
+ /** Receives the scene as drawn (e.g. for `ChartDirector`'s `scene`). */
21
+ sceneRef?: {
22
+ current: ChartScene | null;
23
+ };
18
24
  }
19
25
  declare const ChartCanvas: import("svelte").Component<Props, {}, "">;
20
26
  type ChartCanvas = ReturnType<typeof ChartCanvas>;
@@ -1 +1 @@
1
- {"version":3,"file":"ChartCanvas.svelte.d.ts","sourceRoot":"","sources":["../../src/chart/ChartCanvas.svelte.ts"],"names":[],"mappings":"AAIA,OAAO,EASH,KAAK,eAAe,EAGpB,KAAK,gBAAgB,EACrB,KAAK,UAAU,EAChB,MAAM,wBAAwB,CAAC;AAIhC,UAAU,KAAK;IACb,UAAU,EAAE,eAAe,CAAC;IAC5B,+GAA+G;IAC/G,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,4CAA4C;IAC5C,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,qDAAqD;IACrD,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,8DAA8D;IAC9D,KAAK,CAAC,EAAE,OAAO,CAAC,UAAU,CAAC,CAAC;IAC5B,gFAAgF;IAChF,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,oDAAoD;IACpD,WAAW,CAAC,EAAE,gBAAgB,CAAC;IAC/B,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AA0GH,QAAA,MAAM,WAAW,2CAAwC,CAAC;AAC1D,KAAK,WAAW,GAAG,UAAU,CAAC,OAAO,WAAW,CAAC,CAAC;AAClD,eAAe,WAAW,CAAC"}
1
+ {"version":3,"file":"ChartCanvas.svelte.d.ts","sourceRoot":"","sources":["../../src/chart/ChartCanvas.svelte.ts"],"names":[],"mappings":"AAIA,OAAO,EAOH,KAAK,eAAe,EAEpB,KAAK,aAAa,EAClB,KAAK,UAAU,EACf,KAAK,gBAAgB,EACrB,KAAK,UAAU,EAChB,MAAM,wBAAwB,CAAC;AAMhC,UAAU,KAAK;IACb,UAAU,EAAE,eAAe,CAAC;IAC5B,+GAA+G;IAC/G,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,4CAA4C;IAC5C,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,qDAAqD;IACrD,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,8DAA8D;IAC9D,KAAK,CAAC,EAAE,OAAO,CAAC,UAAU,CAAC,CAAC;IAC5B,gFAAgF;IAChF,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,oDAAoD;IACpD,WAAW,CAAC,EAAE,gBAAgB,CAAC;IAC/B,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,kEAAkE;IAClE,QAAQ,CAAC,EAAE,aAAa,CAAC;IACzB,wEAAwE;IACxE,QAAQ,CAAC,EAAE;QAAE,OAAO,EAAE,UAAU,GAAG,IAAI,CAAA;KAAE,CAAC;CAC3C;AA8GH,QAAA,MAAM,WAAW,2CAAwC,CAAC;AAC1D,KAAK,WAAW,GAAG,UAAU,CAAC,OAAO,WAAW,CAAC,CAAC;AAClD,eAAe,WAAW,CAAC"}
@@ -3,26 +3,38 @@
3
3
  // between server and client renders.
4
4
  let nextId = 0;
5
5
 
6
- export type ChartInspectorSection = 'type' | 'titles' | 'options' | 'legend' | 'axes' | 'series';
6
+ export type ChartInspectorSection = 'type' | 'style' | 'titles' | 'options' | 'legend' | 'axes' | 'series';
7
7
  </script>
8
8
 
9
9
  <script lang="ts">
10
10
  import {
11
+ CHART_COLOR_SCHEMES,
11
12
  CHART_CURVES,
13
+ CHART_LABEL_POSITIONS,
14
+ CHART_LAYOUT_PRESETS,
12
15
  CHART_LEGEND_POSITIONS,
16
+ CHART_MARKERS,
13
17
  CHART_NUMBER_STYLES,
14
18
  CHART_SERIES_TYPES,
15
19
  CHART_STACKINGS,
20
+ CHART_TRENDLINE_TYPES,
16
21
  CHART_TYPE_LABELS,
17
22
  CHART_TYPES,
23
+ checkChartColors,
18
24
  chartColorInputValue,
25
+ chartFamily,
19
26
  chartSeriesColor,
20
27
  chartSeriesType,
28
+ chartSupportsTrendlines,
29
+ chartTrendlineName,
21
30
  isCartesianChart,
22
31
  type ChartAxis,
23
32
  type ChartController,
33
+ type ChartLabelPosition,
34
+ type ChartMarker,
24
35
  type ChartNumberFormat,
25
- type ChartSeriesType
36
+ type ChartSeriesType,
37
+ type ChartTrendlineType
26
38
  } from '@pixerate/editor/chart';
27
39
  import ChartCommitInput from './ChartCommitInput.svelte';
28
40
  import { chartStateOf } from './createChartEditor.svelte.js';
@@ -44,23 +56,36 @@
44
56
  const selectedSeries = $derived(view.current.selectedSeries);
45
57
  const show = (section: ChartInspectorSection) => !sections || sections.includes(section);
46
58
  const cartesian = $derived(isCartesianChart(spec.type));
59
+ const family = $derived(chartFamily(spec));
60
+ const hasAxes = $derived(family === 'cartesian' || family === 'xy' || family === 'waterfall' || family === 'histogram' || family === 'box');
47
61
  const types = $derived(spec.series.map((s) => chartSeriesType(spec, s)));
48
62
  const hasBars = $derived(cartesian && types.includes('bar'));
49
63
  const hasLines = $derived(cartesian && types.some((t) => t === 'line' || t === 'area'));
64
+ const usesSecondary = $derived(family === 'cartesian' && spec.series.some((s) => s.axis === 'secondary' && !s.hidden));
65
+ const colorCheck = $derived(checkChartColors(spec.series.filter((s) => !s.hidden).map((s) => chartSeriesColor(spec, spec.series.indexOf(s)))));
66
+ const currentScheme = $derived(CHART_COLOR_SCHEMES.find((c) => c.palette.join() === spec.palette.join())?.id ?? '');
50
67
  const seriesIndex = $derived(selectedSeries ? spec.series.findIndex((s) => s.id === selectedSeries.id) : -1);
51
68
  const horizontal = $derived(spec.orientation === 'horizontal');
52
69
  const cap = (s: string) => s.charAt(0).toUpperCase() + s.slice(1);
53
70
  const stackingLabels: Record<string, string> = { none: 'None', stacked: 'Stacked', percent: '100%' };
54
71
 
55
- const setAxis = (axis: 'x' | 'y', patch: Partial<ChartAxis>) => controller.setAxis(axis, patch);
56
- const setFormat = (patch: Partial<ChartNumberFormat>) => setAxis('y', { format: { ...spec.yAxis.format, ...patch } });
57
- const commitBound = (key: 'min' | 'max') => (text: string) => setAxis('y', { [key]: text.trim() === '' ? null : Number(text) }).ok;
58
- function commitDecimals(text: string) {
59
- const format = { ...spec.yAxis.format };
72
+ const keyOf = (axis: 'x' | 'y' | 'y2') => (axis === 'y2' ? 'yAxis2' : `${axis}Axis`);
73
+ const setAxis = (axis: 'x' | 'y' | 'y2', patch: Partial<ChartAxis>) => controller.setAxis(axis, patch);
74
+ const setFormat = (axis: 'x' | 'y' | 'y2', value: ChartAxis, patch: Partial<ChartNumberFormat>) => setAxis(axis, { format: { ...value.format, ...patch } });
75
+ const commitNumber = (axis: 'x' | 'y' | 'y2', key: 'min' | 'max' | 'step') => (text: string) => setAxis(axis, { [key]: text.trim() === '' ? null : Number(text) }).ok;
76
+ function commitDecimals(axis: 'x' | 'y' | 'y2', value: ChartAxis, text: string) {
77
+ const format = { ...value.format };
60
78
  if (text.trim() === '') delete format.decimals;
61
79
  else format.decimals = Number(text);
62
- return setAxis('y', { format }).ok;
80
+ return setAxis(axis, { format }).ok;
63
81
  }
82
+ function commitCode(axis: 'x' | 'y' | 'y2', value: ChartAxis, code: string) {
83
+ const format = { ...value.format };
84
+ if (code.trim()) format.code = code.trim();
85
+ else delete format.code;
86
+ return setAxis(axis, { format }).ok;
87
+ }
88
+ const side = (axis: 'x' | 'y' | 'y2') => (axis === 'x' ? (horizontal ? 'left' : 'bottom') : axis === 'y2' ? (horizontal ? 'top' : 'right') : horizontal ? 'bottom' : 'left');
64
89
  </script>
65
90
 
66
91
  {#snippet toggle(label: string, name: string, checked: boolean, onchange: (checked: boolean) => void)}
@@ -70,39 +95,52 @@
70
95
  </label>
71
96
  {/snippet}
72
97
 
73
- {#snippet axisSection(axis: 'x' | 'y', value: ChartAxis)}
98
+ {#snippet axisSection(axis: 'x' | 'y' | 'y2', value: ChartAxis, numeric: boolean)}
99
+ {@const key = keyOf(axis)}
100
+ {@const valueAxis = axis !== 'x' || numeric}
74
101
  <div class={chartSectionClass} data-section="axis-{axis}">
75
102
  <div class="flex items-center justify-between">
76
103
  <h4 class="text-xs font-semibold text-foreground">
77
- {axis === 'x' ? 'Category axis' : 'Value axis'}
78
- <span class="ml-1 font-normal text-muted-foreground">({axis === 'x' ? (horizontal ? 'left' : 'bottom') : horizontal ? 'bottom' : 'left'})</span>
104
+ {axis === 'x' ? (numeric ? 'X axis' : 'Category axis') : axis === 'y' ? 'Value axis' : 'Secondary axis'}
105
+ <span class="ml-1 font-normal text-muted-foreground">({side(axis)})</span>
79
106
  </h4>
80
- {@render toggle('Show', `${axis}Axis.show`, value.show, (v) => setAxis(axis, { show: v }))}
107
+ {@render toggle('Show', `${key}.show`, value.show, (v) => setAxis(axis, { show: v }))}
81
108
  </div>
82
109
  <div class="flex items-center gap-2">
83
110
  <label for="{idPrefix}-{axis}-title" class="{chartLabelClass} w-20 shrink-0">Title</label>
84
111
  <div class="min-w-0 flex-1">
85
- <ChartCommitInput id="{idPrefix}-{axis}-title" value={value.title} data-option="{axis}Axis.title" oncommit={(title) => void setAxis(axis, { title })} />
112
+ <ChartCommitInput id="{idPrefix}-{axis}-title" value={value.title} data-option="{key}.title" oncommit={(title) => void setAxis(axis, { title })} />
86
113
  </div>
87
114
  </div>
88
- {@render toggle('Grid lines', `${axis}Axis.gridlines`, value.gridlines, (v) => setAxis(axis, { gridlines: v }))}
89
- {#if axis === 'y'}
115
+ <div class="flex flex-wrap gap-x-4 gap-y-1">
116
+ {@render toggle('Grid lines', `${key}.gridlines`, value.gridlines, (v) => setAxis(axis, { gridlines: v }))}
117
+ {#if axis === 'x' && !numeric}
118
+ {@render toggle('Dates', 'xAxis.kind', value.kind === 'date', (dates) => setAxis('x', { kind: dates ? 'date' : undefined } as Partial<ChartAxis>))}
119
+ {/if}
120
+ </div>
121
+ {#if valueAxis}
122
+ <div class="flex items-center gap-2">
123
+ <label for="{idPrefix}-{axis}-step" class="{chartLabelClass} w-20 shrink-0">Step</label>
124
+ <div class="min-w-0 flex-1">
125
+ <ChartCommitInput id="{idPrefix}-{axis}-step" value={value.step === null ? '' : String(value.step)} placeholder="Auto" inputmode="decimal" data-option="{key}.step" oncommit={commitNumber(axis, 'step')} />
126
+ </div>
127
+ </div>
90
128
  <div class="flex items-center gap-2">
91
129
  <span class="{chartLabelClass} w-20 shrink-0">Min / max</span>
92
130
  <div class="flex min-w-0 flex-1 gap-1">
93
- <ChartCommitInput id="{idPrefix}-y-min" value={value.min === null ? '' : String(value.min)} placeholder="Auto" inputmode="decimal" data-option="yAxis.min" oncommit={commitBound('min')} />
94
- <ChartCommitInput id="{idPrefix}-y-max" value={value.max === null ? '' : String(value.max)} placeholder="Auto" inputmode="decimal" data-option="yAxis.max" oncommit={commitBound('max')} />
131
+ <ChartCommitInput id="{idPrefix}-{axis}-min" value={value.min === null ? '' : String(value.min)} placeholder="Auto" inputmode="decimal" data-option="{key}.min" oncommit={commitNumber(axis, 'min')} />
132
+ <ChartCommitInput id="{idPrefix}-{axis}-max" value={value.max === null ? '' : String(value.max)} placeholder="Auto" inputmode="decimal" data-option="{key}.max" oncommit={commitNumber(axis, 'max')} />
95
133
  </div>
96
134
  </div>
97
135
  <div class="flex items-center gap-2">
98
- <label for="{idPrefix}-y-format" class="{chartLabelClass} w-20 shrink-0">Format</label>
136
+ <label for="{idPrefix}-{axis}-format" class="{chartLabelClass} w-20 shrink-0">Format</label>
99
137
  <div class="flex min-w-0 flex-1 gap-1">
100
138
  <select
101
- id="{idPrefix}-y-format"
139
+ id="{idPrefix}-{axis}-format"
102
140
  class={chartInputClass}
103
141
  value={value.format.style ?? 'number'}
104
- data-option="yAxis.format.style"
105
- onchange={(e) => setFormat({ style: e.currentTarget.value as ChartNumberFormat['style'] })}
142
+ data-option="{key}.format.style"
143
+ onchange={(e) => setFormat(axis, value, { style: e.currentTarget.value as ChartNumberFormat['style'] })}
106
144
  >
107
145
  {#each CHART_NUMBER_STYLES as style (style)}
108
146
  <option value={style}>{cap(style)}</option>
@@ -114,20 +152,26 @@
114
152
  aria-label="Decimals"
115
153
  title="Decimals"
116
154
  inputmode="numeric"
117
- data-option="yAxis.format.decimals"
155
+ data-option="{key}.format.decimals"
118
156
  class="{chartInputClass} w-14 shrink-0"
119
- oncommit={commitDecimals}
157
+ oncommit={(text) => commitDecimals(axis, value, text)}
120
158
  />
121
159
  </div>
122
160
  </div>
123
161
  {#if value.format.style === 'currency'}
124
162
  <div class="flex items-center gap-2">
125
- <label for="{idPrefix}-y-currency" class="{chartLabelClass} w-20 shrink-0">Currency</label>
163
+ <label for="{idPrefix}-{axis}-currency" class="{chartLabelClass} w-20 shrink-0">Currency</label>
126
164
  <div class="min-w-0 flex-1">
127
- <ChartCommitInput id="{idPrefix}-y-currency" value={value.format.currency ?? 'USD'} data-option="yAxis.format.currency" oncommit={(c) => void setFormat({ currency: c.toUpperCase() })} />
165
+ <ChartCommitInput id="{idPrefix}-{axis}-currency" value={value.format.currency ?? 'USD'} data-option="{key}.format.currency" oncommit={(c) => void setFormat(axis, value, { currency: c.toUpperCase() })} />
128
166
  </div>
129
167
  </div>
130
168
  {/if}
169
+ <div class="flex items-center gap-2">
170
+ <label for="{idPrefix}-{axis}-code" class="{chartLabelClass} w-20 shrink-0">Code</label>
171
+ <div class="min-w-0 flex-1">
172
+ <ChartCommitInput id="{idPrefix}-{axis}-code" value={value.format.code ?? ''} placeholder={'e.g. #,##0.0,,"M"'} data-option="{key}.format.code" oncommit={(code) => commitCode(axis, value, code)} />
173
+ </div>
174
+ </div>
131
175
  {/if}
132
176
  </div>
133
177
  {/snippet}
@@ -145,6 +189,33 @@
145
189
  </div>
146
190
  {/if}
147
191
 
192
+ {#if show('style')}
193
+ <div class={chartSectionClass} data-section="style">
194
+ <div class="flex items-center gap-2">
195
+ <label for="{idPrefix}-layout" class="{chartLabelClass} w-20 shrink-0">Layout</label>
196
+ <select id="{idPrefix}-layout" class={chartInputClass} value="" data-option="preset.layout" onchange={(e) => e.currentTarget.value && controller.applyPreset({ layout: e.currentTarget.value })}>
197
+ <option value="">Quick layout…</option>
198
+ {#each CHART_LAYOUT_PRESETS as p (p.id)}
199
+ <option value={p.id} title={p.description}>{p.label}</option>
200
+ {/each}
201
+ </select>
202
+ </div>
203
+ <div class="flex items-center gap-2">
204
+ <label for="{idPrefix}-colors" class="{chartLabelClass} w-20 shrink-0">Colours</label>
205
+ <select id="{idPrefix}-colors" class={chartInputClass} value={currentScheme} data-option="preset.colors" onchange={(e) => e.currentTarget.value && controller.applyPreset({ colors: e.currentTarget.value })}>
206
+ <option value="">Custom</option>
207
+ {#each CHART_COLOR_SCHEMES as c (c.id)}
208
+ <option value={c.id} title={c.description}>{c.label}</option>
209
+ {/each}
210
+ </select>
211
+ </div>
212
+ {#if !colorCheck.ok}
213
+ <p class="text-[11px] leading-snug text-amber-600 dark:text-amber-400" data-chart-color-warning="">{colorCheck.issues[0]}</p>
214
+ {/if}
215
+ <button type="button" class={chartButtonClass} data-action="transpose" onclick={() => controller.transpose()}>Switch rows and columns</button>
216
+ </div>
217
+ {/if}
218
+
148
219
  {#if show('titles')}
149
220
  <div class={chartSectionClass} data-section="titles">
150
221
  <div class="flex items-center gap-2">
@@ -191,6 +262,32 @@
191
262
  {/each}
192
263
  </div>
193
264
  </div>
265
+ <div class="flex items-center gap-2">
266
+ <label for="{idPrefix}-gap" class="{chartLabelClass} w-20 shrink-0">Spacing</label>
267
+ <input
268
+ id="{idPrefix}-gap"
269
+ type="range"
270
+ min="0"
271
+ max="0.9"
272
+ step="0.02"
273
+ value={spec.gapWidth}
274
+ data-option="gapWidth"
275
+ class="w-full accent-primary"
276
+ onpointerdown={() => controller.beginGesture('gapWidth')}
277
+ onpointerup={() => controller.endGesture()}
278
+ oninput={(e) => controller.update({ gapWidth: Number(e.currentTarget.value) })}
279
+ />
280
+ </div>
281
+ {/if}
282
+ {#if spec.dataLabels && hasBars}
283
+ <div class="flex items-center gap-2">
284
+ <label for="{idPrefix}-label-position" class="{chartLabelClass} w-20 shrink-0">Labels</label>
285
+ <select id="{idPrefix}-label-position" class={chartInputClass} value={spec.labelPosition} data-option="labelPosition" onchange={(e) => controller.update({ labelPosition: e.currentTarget.value as ChartLabelPosition })}>
286
+ {#each CHART_LABEL_POSITIONS as p (p)}
287
+ <option value={p}>{cap(p)}</option>
288
+ {/each}
289
+ </select>
290
+ </div>
194
291
  {/if}
195
292
  {#if hasLines}
196
293
  <div class="flex items-center gap-2">
@@ -202,6 +299,14 @@
202
299
  </select>
203
300
  </div>
204
301
  {/if}
302
+ {#if spec.type === 'histogram'}
303
+ <div class="flex items-center gap-2">
304
+ <label for="{idPrefix}-bins" class="{chartLabelClass} w-20 shrink-0">Bins</label>
305
+ <div class="min-w-0 flex-1">
306
+ <ChartCommitInput id="{idPrefix}-bins" value={spec.bins === null ? '' : String(spec.bins)} placeholder="Auto" inputmode="numeric" data-option="bins" oncommit={(text) => controller.update({ bins: text.trim() === '' ? null : Number(text) }).ok} />
307
+ </div>
308
+ </div>
309
+ {/if}
205
310
  {#if spec.type === 'donut'}
206
311
  <div class="flex items-center gap-2">
207
312
  <label for="{idPrefix}-hole" class="{chartLabelClass} w-20 shrink-0">Hole</label>
@@ -221,6 +326,9 @@
221
326
  </div>
222
327
  {/if}
223
328
  <div class="flex flex-wrap gap-x-4 gap-y-1">
329
+ {#if spec.type === 'waterfall'}
330
+ {@render toggle('Total bar', 'showTotal', spec.showTotal, (showTotal) => controller.update({ showTotal }))}
331
+ {/if}
224
332
  {#if hasLines}
225
333
  {@render toggle('Points', 'points', spec.points, (points) => controller.update({ points }))}
226
334
  {/if}
@@ -247,13 +355,19 @@
247
355
  </div>
248
356
  {/if}
249
357
 
250
- {#if show('axes') && cartesian}
251
- {@render axisSection('x', spec.xAxis)}
252
- {@render axisSection('y', spec.yAxis)}
358
+ {#if show('axes') && hasAxes}
359
+ {#if family !== 'box' && family !== 'histogram'}
360
+ {@render axisSection('x', spec.xAxis, family === 'xy')}
361
+ {/if}
362
+ {@render axisSection('y', spec.yAxis, false)}
363
+ {#if usesSecondary}
364
+ {@render axisSection('y2', spec.yAxis2, false)}
365
+ {/if}
253
366
  {/if}
254
367
 
255
368
  {#if show('series') && selectedSeries}
256
369
  {@const series = selectedSeries}
370
+ {@const drawnAs = chartSeriesType(spec, series)}
257
371
  <div class={chartSectionClass} data-section="series" data-series-id={series.id}>
258
372
  <h4 class="text-xs font-semibold text-foreground">Series</h4>
259
373
  <div class="flex items-center gap-2">
@@ -297,8 +411,51 @@
297
411
  </select>
298
412
  </div>
299
413
  {/if}
414
+ {#if (cartesian || family === 'xy') && (drawnAs === 'line' || drawnAs === 'area' || drawnAs === 'scatter')}
415
+ <div class="flex items-center gap-2">
416
+ <label for="{idPrefix}-series-marker" class="{chartLabelClass} w-20 shrink-0">Marker</label>
417
+ <select
418
+ id="{idPrefix}-series-marker"
419
+ class={chartInputClass}
420
+ value={series.marker ?? ''}
421
+ data-option="series.marker"
422
+ onchange={(e) => controller.updateSeries(series.id, { marker: (e.currentTarget.value || null) as ChartMarker | null })}
423
+ >
424
+ <option value="">Default</option>
425
+ {#each CHART_MARKERS as m (m)}
426
+ <option value={m}>{cap(m)}</option>
427
+ {/each}
428
+ </select>
429
+ </div>
430
+ {/if}
431
+ {#if chartSupportsTrendlines(spec.type, spec.stacking)}
432
+ <div class="space-y-1" data-section="trendlines">
433
+ {#each series.trendlines ?? [] as t, i (i)}
434
+ <div class="flex items-center gap-1" data-trendline={i}>
435
+ <span class="flex-1 truncate text-muted-foreground">{chartTrendlineName(t, series.name)}</span>
436
+ {@render toggle('R²', `trendline.${i}.showR2`, !!t.showR2, (showR2) => controller.updateTrendline(series.id, i, { showR2 }))}
437
+ <button type="button" class={chartButtonClass} aria-label="Delete trendline" data-action="trendline-delete" onclick={() => controller.removeTrendline(series.id, i)}>×</button>
438
+ </div>
439
+ {/each}
440
+ <select
441
+ class={chartInputClass}
442
+ value=""
443
+ aria-label="Add trendline"
444
+ data-option="trendline.add"
445
+ onchange={(e) => e.currentTarget.value && controller.addTrendline(series.id, { type: e.currentTarget.value as ChartTrendlineType })}
446
+ >
447
+ <option value="">Add trendline…</option>
448
+ {#each CHART_TRENDLINE_TYPES as t (t)}
449
+ <option value={t}>{cap(t.replace('-', ' '))}</option>
450
+ {/each}
451
+ </select>
452
+ </div>
453
+ {/if}
300
454
  <div class="flex flex-wrap items-center gap-1">
301
455
  {@render toggle('Hidden', 'series.hidden', !!series.hidden, (hidden) => controller.updateSeries(series.id, { hidden }))}
456
+ {#if family === 'cartesian'}
457
+ {@render toggle('Secondary axis', 'series.axis', series.axis === 'secondary', (secondary) => controller.updateSeries(series.id, { axis: secondary ? 'secondary' : null }))}
458
+ {/if}
302
459
  <span class="flex-1"></span>
303
460
  <button type="button" class={chartButtonClass} disabled={seriesIndex <= 0} aria-label="Move series earlier" data-action="series-up" onclick={() => controller.moveSeries(series.id, seriesIndex - 1)}>↑</button>
304
461
  <button
@@ -1,4 +1,4 @@
1
- export type ChartInspectorSection = 'type' | 'titles' | 'options' | 'legend' | 'axes' | 'series';
1
+ export type ChartInspectorSection = 'type' | 'style' | 'titles' | 'options' | 'legend' | 'axes' | 'series';
2
2
  import { type ChartController } from '@pixerate/editor/chart';
3
3
  interface Props {
4
4
  controller: ChartController;
@@ -1 +1 @@
1
- {"version":3,"file":"ChartInspector.svelte.d.ts","sourceRoot":"","sources":["../../src/chart/ChartInspector.svelte.ts"],"names":[],"mappings":"AAOE,MAAM,MAAM,qBAAqB,GAAG,MAAM,GAAG,QAAQ,GAAG,SAAS,GAAG,QAAQ,GAAG,MAAM,GAAG,QAAQ,CAAC;AAGnG,OAAO,EAaH,KAAK,eAAe,EAGrB,MAAM,wBAAwB,CAAC;AAMhC,UAAU,KAAK;IACb,UAAU,EAAE,eAAe,CAAC;IAC5B,qEAAqE;IACrE,QAAQ,CAAC,EAAE,SAAS,qBAAqB,EAAE,CAAC;IAC5C,8CAA8C;IAC9C,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AA2OH,QAAA,MAAM,cAAc,2CAAwC,CAAC;AAC7D,KAAK,cAAc,GAAG,UAAU,CAAC,OAAO,cAAc,CAAC,CAAC;AACxD,eAAe,cAAc,CAAC"}
1
+ {"version":3,"file":"ChartInspector.svelte.d.ts","sourceRoot":"","sources":["../../src/chart/ChartInspector.svelte.ts"],"names":[],"mappings":"AAOE,MAAM,MAAM,qBAAqB,GAAG,MAAM,GAAG,OAAO,GAAG,QAAQ,GAAG,SAAS,GAAG,QAAQ,GAAG,MAAM,GAAG,QAAQ,CAAC;AAG7G,OAAO,EAsBH,KAAK,eAAe,EAMrB,MAAM,wBAAwB,CAAC;AAMhC,UAAU,KAAK;IACb,UAAU,EAAE,eAAe,CAAC;IAC5B,qEAAqE;IACrE,QAAQ,CAAC,EAAE,SAAS,qBAAqB,EAAE,CAAC;IAC5C,8CAA8C;IAC9C,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAoWH,QAAA,MAAM,cAAc,2CAAwC,CAAC;AAC7D,KAAK,cAAc,GAAG,UAAU,CAAC,OAAO,cAAc,CAAC,CAAC;AACxD,eAAe,cAAc,CAAC"}