@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.
- package/dist/chart/ChartCanvas.svelte +32 -98
- package/dist/chart/ChartCanvas.svelte.d.ts +7 -1
- package/dist/chart/ChartCanvas.svelte.d.ts.map +1 -1
- package/dist/chart/ChartInspector.svelte +185 -28
- package/dist/chart/ChartInspector.svelte.d.ts +1 -1
- package/dist/chart/ChartInspector.svelte.d.ts.map +1 -1
- package/dist/chart/ChartMarks.svelte +102 -0
- package/dist/chart/ChartMarks.svelte.d.ts +11 -0
- package/dist/chart/ChartMarks.svelte.d.ts.map +1 -0
- package/dist/chart/ChartPresenceLayer.svelte +30 -0
- package/dist/chart/ChartPresenceLayer.svelte.d.ts +9 -0
- package/dist/chart/ChartPresenceLayer.svelte.d.ts.map +1 -0
- package/dist/chart/ChartSceneSvg.svelte +17 -0
- package/dist/chart/ChartSceneSvg.svelte.d.ts +11 -0
- package/dist/chart/ChartSceneSvg.svelte.d.ts.map +1 -0
- package/dist/chart/ChartSparkline.svelte +27 -0
- package/dist/chart/ChartSparkline.svelte.d.ts +20 -0
- package/dist/chart/ChartSparkline.svelte.d.ts.map +1 -0
- package/dist/chart/ChartStreamingView.svelte +92 -0
- package/dist/chart/ChartStreamingView.svelte.d.ts +25 -0
- package/dist/chart/ChartStreamingView.svelte.d.ts.map +1 -0
- package/dist/chart/createChartEditor.svelte.d.ts +25 -1
- package/dist/chart/createChartEditor.svelte.d.ts.map +1 -1
- package/dist/chart/createChartEditor.svelte.js +25 -1
- package/dist/chart/index.d.ts +9 -0
- package/dist/chart/index.d.ts.map +1 -1
- package/dist/chart/index.js +5 -0
- package/dist/chart-node/ChartNodeView.svelte +62 -0
- package/dist/chart-node/ChartNodeView.svelte.d.ts +14 -0
- package/dist/chart-node/ChartNodeView.svelte.d.ts.map +1 -0
- package/dist/chart-node/createChartNodeView.svelte.d.ts +21 -0
- package/dist/chart-node/createChartNodeView.svelte.d.ts.map +1 -0
- package/dist/chart-node/createChartNodeView.svelte.js +28 -0
- package/dist/chart-node/index.d.ts +6 -0
- package/dist/chart-node/index.d.ts.map +1 -0
- package/dist/chart-node/index.js +2 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- 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
|
|
68
|
-
let
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
{
|
|
134
|
-
|
|
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,
|
|
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
|
|
56
|
-
const
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
|
|
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(
|
|
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
|
|
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', `${
|
|
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="{
|
|
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
|
-
|
|
89
|
-
|
|
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}-
|
|
94
|
-
<ChartCommitInput id="{idPrefix}-
|
|
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}-
|
|
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}-
|
|
139
|
+
id="{idPrefix}-{axis}-format"
|
|
102
140
|
class={chartInputClass}
|
|
103
141
|
value={value.format.style ?? 'number'}
|
|
104
|
-
data-option="
|
|
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="
|
|
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}-
|
|
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}-
|
|
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') &&
|
|
251
|
-
{
|
|
252
|
-
|
|
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;
|
|
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"}
|