animot-presenter 0.5.11 → 0.5.13
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/AnimotPresenter.svelte +27 -2
- package/dist/cdn/animot-presenter.css +1 -1
- package/dist/cdn/animot-presenter.esm.js +5481 -5223
- package/dist/cdn/animot-presenter.min.js +9 -9
- package/dist/renderers/ChartRenderer.svelte +254 -232
- package/dist/renderers/ChartRenderer.svelte.d.ts +3 -0
- package/dist/types.d.ts +24 -0
- package/dist/utils/chart.d.ts +15 -0
- package/dist/utils/chart.js +94 -0
- package/dist/utils/decorations.js +104 -38
- package/package.json +84 -84
|
@@ -1,135 +1,167 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import type { ChartElement, ChartSeries, ChartDataPoint } from '../types';
|
|
2
3
|
import { onMount } from 'svelte';
|
|
3
|
-
import
|
|
4
|
+
import { normalizeSeries, resolveColor, formatValue, computeYRange } from '../utils/chart';
|
|
4
5
|
|
|
5
|
-
interface Props {
|
|
6
|
-
|
|
6
|
+
interface Props {
|
|
7
|
+
element: ChartElement;
|
|
8
|
+
slideId: string;
|
|
9
|
+
/** Optional snapshot of the same chart on the OUTGOING slide. When set,
|
|
10
|
+
* the renderer tweens data values from prior state to current. */
|
|
11
|
+
previousElement?: ChartElement | null;
|
|
12
|
+
}
|
|
13
|
+
let { element, slideId, previousElement = null }: Props = $props();
|
|
7
14
|
|
|
8
15
|
let animationProgress = $state(1);
|
|
16
|
+
let morphProgress = $state(1);
|
|
9
17
|
let animationFrame: number | null = null;
|
|
10
18
|
|
|
11
|
-
function
|
|
12
|
-
const startTime = performance.now();
|
|
13
|
-
const duration = element.animationDuration;
|
|
19
|
+
function ease(t: number) { return 1 - Math.pow(1 - t, 3); }
|
|
14
20
|
|
|
21
|
+
function animate(setter: (v: number) => void, duration: number, delay = 0) {
|
|
22
|
+
if (animationFrame) cancelAnimationFrame(animationFrame);
|
|
23
|
+
const start = performance.now() + delay;
|
|
24
|
+
setter(0);
|
|
15
25
|
function tick() {
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
if (progress < 1) {
|
|
23
|
-
animationFrame = requestAnimationFrame(tick);
|
|
24
|
-
}
|
|
26
|
+
const now = performance.now();
|
|
27
|
+
if (now < start) { animationFrame = requestAnimationFrame(tick); return; }
|
|
28
|
+
const p = Math.min((now - start) / duration, 1);
|
|
29
|
+
setter(ease(p));
|
|
30
|
+
if (p < 1) animationFrame = requestAnimationFrame(tick);
|
|
25
31
|
}
|
|
32
|
+
animationFrame = requestAnimationFrame(tick);
|
|
33
|
+
}
|
|
26
34
|
|
|
27
|
-
|
|
28
|
-
if (
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
}
|
|
35
|
+
function runAnimation() {
|
|
36
|
+
if (previousElement) {
|
|
37
|
+
animationProgress = 1;
|
|
38
|
+
animate((v) => (morphProgress = v), element.animationDuration);
|
|
39
|
+
} else {
|
|
40
|
+
morphProgress = 1;
|
|
41
|
+
animate((v) => (animationProgress = v), element.animationDuration, 200);
|
|
42
|
+
}
|
|
35
43
|
}
|
|
36
44
|
|
|
37
|
-
// Trigger animation on mount
|
|
38
45
|
onMount(() => {
|
|
39
|
-
|
|
40
|
-
return () => {
|
|
41
|
-
if (animationFrame) cancelAnimationFrame(animationFrame);
|
|
42
|
-
};
|
|
46
|
+
runAnimation();
|
|
47
|
+
return () => { if (animationFrame) cancelAnimationFrame(animationFrame); };
|
|
43
48
|
});
|
|
44
49
|
|
|
45
|
-
|
|
46
|
-
$effect(() => {
|
|
47
|
-
if (slideId) {
|
|
48
|
-
animateChart();
|
|
49
|
-
}
|
|
50
|
-
});
|
|
50
|
+
$effect(() => { if (slideId) runAnimation(); });
|
|
51
51
|
|
|
52
|
-
|
|
53
|
-
const
|
|
52
|
+
const seriesList = $derived(normalizeSeries(element));
|
|
53
|
+
const prevSeriesList = $derived(previousElement ? normalizeSeries(previousElement) : null);
|
|
54
|
+
const isMultiSeries = $derived((element.series?.length ?? 0) > 1);
|
|
55
|
+
const barLayout = $derived(element.barLayout ?? 'grouped');
|
|
56
|
+
const stagger = $derived(element.revealStagger ?? 0);
|
|
54
57
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
58
|
+
function effectiveValue(sIdx: number, pIdx: number): number {
|
|
59
|
+
const target = seriesList[sIdx]?.data[pIdx]?.value ?? 0;
|
|
60
|
+
if (prevSeriesList) {
|
|
61
|
+
const prevSeries =
|
|
62
|
+
prevSeriesList.find((s) => s.id === seriesList[sIdx]?.id) ?? prevSeriesList[sIdx];
|
|
63
|
+
const targetLabel = seriesList[sIdx]?.data[pIdx]?.label;
|
|
64
|
+
const prevPoint =
|
|
65
|
+
prevSeries?.data.find((p) => p.label === targetLabel) ?? prevSeries?.data[pIdx];
|
|
66
|
+
const prev = prevPoint?.value ?? target;
|
|
67
|
+
return prev + (target - prev) * morphProgress;
|
|
68
|
+
}
|
|
69
|
+
const totalPoints = seriesList[0]?.data.length ?? 1;
|
|
70
|
+
if (stagger > 0 && element.animationDuration > 0) {
|
|
71
|
+
const slice = stagger / element.animationDuration;
|
|
72
|
+
const localStart = pIdx * slice;
|
|
73
|
+
const localEnd = Math.min(1, localStart + (1 - slice * (totalPoints - 1)));
|
|
74
|
+
const span = Math.max(0.0001, localEnd - localStart);
|
|
75
|
+
const local = Math.min(1, Math.max(0, (animationProgress - localStart) / span));
|
|
76
|
+
return target * local;
|
|
77
|
+
}
|
|
78
|
+
return target * animationProgress;
|
|
68
79
|
}
|
|
69
80
|
|
|
70
|
-
|
|
71
|
-
const
|
|
72
|
-
|
|
73
|
-
|
|
81
|
+
const yRange = $derived(computeYRange(element));
|
|
82
|
+
const yTop = 10;
|
|
83
|
+
const yBottom = 90;
|
|
84
|
+
function yFor(value: number): number {
|
|
85
|
+
const { min, max } = yRange;
|
|
86
|
+
const frac = (value - min) / (max - min);
|
|
87
|
+
return yBottom - frac * (yBottom - yTop);
|
|
88
|
+
}
|
|
89
|
+
const zeroY = $derived(yFor(0));
|
|
74
90
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
91
|
+
const labelCount = $derived(seriesList[0]?.data.length ?? 0);
|
|
92
|
+
const slotWidth = $derived(labelCount > 0 ? 80 / labelCount : 80);
|
|
93
|
+
const slotPad = $derived(slotWidth * 0.15);
|
|
94
|
+
function barSlotX(pIdx: number): number { return 10 + pIdx * slotWidth + slotPad; }
|
|
95
|
+
function barSlotInnerWidth(): number { return slotWidth - slotPad * 2; }
|
|
80
96
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
97
|
+
const pieSeries = $derived(seriesList[0]);
|
|
98
|
+
const pieSegments = $derived.by(() => {
|
|
99
|
+
const data = pieSeries?.data ?? [];
|
|
100
|
+
const animated = data.map((_, i) => effectiveValue(0, i));
|
|
101
|
+
const total = animated.reduce((s, v) => s + Math.max(0, v), 0) || 1;
|
|
102
|
+
let cur = -90;
|
|
103
|
+
return data.map((d, i) => {
|
|
104
|
+
const v = Math.max(0, animated[i]);
|
|
105
|
+
const pct = v / total;
|
|
106
|
+
const start = cur;
|
|
107
|
+
cur += pct * 360;
|
|
108
|
+
return { point: d, index: i, startAngle: start, endAngle: cur, percentage: pct };
|
|
88
109
|
});
|
|
89
110
|
});
|
|
90
111
|
|
|
91
|
-
// SVG arc path for pie
|
|
92
112
|
function describeArc(cx: number, cy: number, r: number, startAngle: number, endAngle: number): string {
|
|
113
|
+
if (endAngle - startAngle <= 0.01) return '';
|
|
93
114
|
const start = polarToCartesian(cx, cy, r, endAngle);
|
|
94
115
|
const end = polarToCartesian(cx, cy, r, startAngle);
|
|
95
116
|
const largeArcFlag = endAngle - startAngle <= 180 ? 0 : 1;
|
|
96
|
-
|
|
97
|
-
return [
|
|
98
|
-
'M', cx, cy,
|
|
99
|
-
'L', start.x, start.y,
|
|
100
|
-
'A', r, r, 0, largeArcFlag, 0, end.x, end.y,
|
|
101
|
-
'Z'
|
|
102
|
-
].join(' ');
|
|
117
|
+
return `M ${cx} ${cy} L ${start.x} ${start.y} A ${r} ${r} 0 ${largeArcFlag} 0 ${end.x} ${end.y} Z`;
|
|
103
118
|
}
|
|
104
|
-
|
|
105
|
-
// SVG arc path for donut
|
|
106
119
|
function describeDonutArc(cx: number, cy: number, outerR: number, innerR: number, startAngle: number, endAngle: number): string {
|
|
120
|
+
if (endAngle - startAngle <= 0.01) return '';
|
|
107
121
|
const outerStart = polarToCartesian(cx, cy, outerR, endAngle);
|
|
108
122
|
const outerEnd = polarToCartesian(cx, cy, outerR, startAngle);
|
|
109
123
|
const innerStart = polarToCartesian(cx, cy, innerR, endAngle);
|
|
110
124
|
const innerEnd = polarToCartesian(cx, cy, innerR, startAngle);
|
|
111
125
|
const largeArcFlag = endAngle - startAngle <= 180 ? 0 : 1;
|
|
112
|
-
|
|
113
|
-
return [
|
|
114
|
-
'M', outerStart.x, outerStart.y,
|
|
115
|
-
'A', outerR, outerR, 0, largeArcFlag, 0, outerEnd.x, outerEnd.y,
|
|
116
|
-
'L', innerEnd.x, innerEnd.y,
|
|
117
|
-
'A', innerR, innerR, 0, largeArcFlag, 1, innerStart.x, innerStart.y,
|
|
118
|
-
'Z'
|
|
119
|
-
].join(' ');
|
|
126
|
+
return `M ${outerStart.x} ${outerStart.y} A ${outerR} ${outerR} 0 ${largeArcFlag} 0 ${outerEnd.x} ${outerEnd.y} L ${innerEnd.x} ${innerEnd.y} A ${innerR} ${innerR} 0 ${largeArcFlag} 1 ${innerStart.x} ${innerStart.y} Z`;
|
|
120
127
|
}
|
|
121
|
-
|
|
122
128
|
function polarToCartesian(cx: number, cy: number, r: number, angle: number) {
|
|
123
129
|
const rad = (angle * Math.PI) / 180;
|
|
124
|
-
return {
|
|
125
|
-
x: cx + r * Math.cos(rad),
|
|
126
|
-
y: cy + r * Math.sin(rad)
|
|
127
|
-
};
|
|
130
|
+
return { x: cx + r * Math.cos(rad), y: cy + r * Math.sin(rad) };
|
|
128
131
|
}
|
|
132
|
+
|
|
133
|
+
const stackedBars = $derived.by(() => {
|
|
134
|
+
if (!isMultiSeries || barLayout !== 'stacked' || element.chartType !== 'bar') return [];
|
|
135
|
+
const labels = seriesList[0]?.data ?? [];
|
|
136
|
+
return labels.map((basePoint, pIdx) => {
|
|
137
|
+
let posOffset = 0;
|
|
138
|
+
let negOffset = 0;
|
|
139
|
+
const segments = seriesList.map((series, sIdx) => {
|
|
140
|
+
const v = effectiveValue(sIdx, pIdx);
|
|
141
|
+
const isPos = v >= 0;
|
|
142
|
+
const fromOffset = isPos ? posOffset : negOffset;
|
|
143
|
+
const toOffset = fromOffset + v;
|
|
144
|
+
if (isPos) posOffset = toOffset; else negOffset = toOffset;
|
|
145
|
+
const yStart = yFor(fromOffset);
|
|
146
|
+
const yEnd = yFor(toOffset);
|
|
147
|
+
return { sIdx, series, value: v, y: Math.min(yStart, yEnd), h: Math.abs(yStart - yEnd) };
|
|
148
|
+
});
|
|
149
|
+
return { basePoint, pIdx, segments };
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
const yTicks = $derived.by(() => {
|
|
154
|
+
if (!element.yAxis?.showLabels) return [];
|
|
155
|
+
const { min, max } = yRange;
|
|
156
|
+
const steps = 4;
|
|
157
|
+
return Array.from({ length: steps + 1 }, (_, i) => {
|
|
158
|
+
const v = min + (i / steps) * (max - min);
|
|
159
|
+
return { value: v, y: yFor(v) };
|
|
160
|
+
});
|
|
161
|
+
});
|
|
129
162
|
</script>
|
|
130
163
|
|
|
131
|
-
<div
|
|
132
|
-
class="chart"
|
|
164
|
+
<div class="animot-chart"
|
|
133
165
|
style:background-color={element.backgroundColor}
|
|
134
166
|
style:padding="{element.padding}px"
|
|
135
167
|
style:border-radius="{element.borderRadius}px"
|
|
@@ -137,180 +169,180 @@
|
|
|
137
169
|
style:font-size="{element.fontSize}px"
|
|
138
170
|
>
|
|
139
171
|
{#if element.title}
|
|
140
|
-
<div class="chart-title">{element.title}</div>
|
|
172
|
+
<div class="animot-chart-title">{element.title}</div>
|
|
141
173
|
{/if}
|
|
142
174
|
|
|
143
|
-
<div class="chart-content">
|
|
175
|
+
<div class="animot-chart-content">
|
|
144
176
|
{#if element.chartType === 'bar'}
|
|
145
|
-
<svg viewBox="0 0 100 100" preserveAspectRatio="xMidYMid meet" class="bar-chart">
|
|
177
|
+
<svg viewBox="0 0 100 100" preserveAspectRatio="xMidYMid meet" class="animot-bar-chart">
|
|
146
178
|
{#if element.showGrid}
|
|
147
|
-
{#each [0, 25, 50, 75, 100] as
|
|
148
|
-
<line x1="0" y1={
|
|
179
|
+
{#each [0, 25, 50, 75, 100] as gy}
|
|
180
|
+
<line x1="0" y1={gy} x2="100" y2={gy} stroke="currentColor" stroke-opacity="0.1" stroke-width="0.5" />
|
|
149
181
|
{/each}
|
|
150
182
|
{/if}
|
|
151
|
-
{#
|
|
152
|
-
{
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
y="98"
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
183
|
+
{#if yRange.min < 0}
|
|
184
|
+
<line x1="0" y1={zeroY} x2="100" y2={zeroY} stroke="currentColor" stroke-opacity="0.3" stroke-width="0.5" />
|
|
185
|
+
{/if}
|
|
186
|
+
|
|
187
|
+
{#if isMultiSeries && barLayout === 'stacked'}
|
|
188
|
+
{#each stackedBars as bar}
|
|
189
|
+
{@const slotX = barSlotX(bar.pIdx)}
|
|
190
|
+
{@const innerW = barSlotInnerWidth()}
|
|
191
|
+
{#each bar.segments as seg}
|
|
192
|
+
{#if seg.h > 0.01}
|
|
193
|
+
<rect x={slotX} y={seg.y} width={innerW} height={seg.h}
|
|
194
|
+
fill={resolveColor(element, seg.sIdx, bar.pIdx, seg.series.data[bar.pIdx], seg.series)} />
|
|
195
|
+
{/if}
|
|
196
|
+
{/each}
|
|
197
|
+
{#if element.showLabels}
|
|
198
|
+
<text x={slotX + innerW / 2} y="98" text-anchor="middle" font-size="4" fill="currentColor">{bar.basePoint.label}</text>
|
|
199
|
+
{/if}
|
|
200
|
+
{/each}
|
|
201
|
+
{:else}
|
|
202
|
+
{#each seriesList[0].data as basePoint, pIdx}
|
|
203
|
+
{@const slotX = barSlotX(pIdx)}
|
|
204
|
+
{@const innerW = barSlotInnerWidth()}
|
|
205
|
+
{@const seriesCount = seriesList.length}
|
|
206
|
+
{@const groupGap = innerW * 0.05}
|
|
207
|
+
{@const barW = (innerW - groupGap * (seriesCount - 1)) / seriesCount}
|
|
208
|
+
{#each seriesList as series, sIdx}
|
|
209
|
+
{@const v = effectiveValue(sIdx, pIdx)}
|
|
210
|
+
{@const yStart = zeroY}
|
|
211
|
+
{@const yEnd = yFor(v)}
|
|
212
|
+
{@const barY = Math.min(yStart, yEnd)}
|
|
213
|
+
{@const barH = Math.abs(yStart - yEnd)}
|
|
214
|
+
<rect x={slotX + sIdx * (barW + groupGap)} y={barY} width={barW} height={barH}
|
|
215
|
+
fill={resolveColor(element, sIdx, pIdx, series.data[pIdx], series)} rx="0.5" />
|
|
216
|
+
{#if element.showValues}
|
|
217
|
+
<text x={slotX + sIdx * (barW + groupGap) + barW / 2}
|
|
218
|
+
y={v >= 0 ? barY - 1 : barY + barH + 3.5}
|
|
219
|
+
text-anchor="middle" font-size="3" fill="currentColor">{formatValue(v, element.valueFormat)}</text>
|
|
220
|
+
{/if}
|
|
221
|
+
{/each}
|
|
222
|
+
{#if element.showLabels}
|
|
223
|
+
<text x={slotX + innerW / 2} y="98" text-anchor="middle" font-size="4" fill="currentColor">{basePoint.label}</text>
|
|
224
|
+
{/if}
|
|
225
|
+
{/each}
|
|
226
|
+
{/if}
|
|
227
|
+
|
|
228
|
+
{#each yTicks as t}
|
|
229
|
+
<text x="1" y={t.y + 1} font-size="3" fill="currentColor" opacity="0.7">{formatValue(t.value, element.valueFormat)}</text>
|
|
181
230
|
{/each}
|
|
182
231
|
</svg>
|
|
232
|
+
|
|
183
233
|
{:else if element.chartType === 'line' || element.chartType === 'area'}
|
|
184
|
-
<svg viewBox="0 0 100 100" preserveAspectRatio="xMidYMid meet" class="line-chart">
|
|
234
|
+
<svg viewBox="0 0 100 100" preserveAspectRatio="xMidYMid meet" class="animot-line-chart">
|
|
185
235
|
{#if element.showGrid}
|
|
186
|
-
{#each [0, 25, 50, 75, 100] as
|
|
187
|
-
<line x1="0" y1={
|
|
188
|
-
{/each}
|
|
189
|
-
{#each element.data as _, i}
|
|
190
|
-
{@const x = 10 + (i / (element.data.length - 1)) * 80}
|
|
191
|
-
<line x1={x} y1="10" x2={x} y2="90" stroke="currentColor" stroke-opacity="0.1" stroke-width="0.5" />
|
|
236
|
+
{#each [0, 25, 50, 75, 100] as gy}
|
|
237
|
+
<line x1="0" y1={gy} x2="100" y2={gy} stroke="currentColor" stroke-opacity="0.1" stroke-width="0.5" />
|
|
192
238
|
{/each}
|
|
193
239
|
{/if}
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
<polygon
|
|
197
|
-
points="{linePoints} {10 + 80},90 10,90"
|
|
198
|
-
fill={element.colors[0]}
|
|
199
|
-
fill-opacity="0.3"
|
|
200
|
-
/>
|
|
240
|
+
{#if yRange.min < 0}
|
|
241
|
+
<line x1="10" y1={zeroY} x2="90" y2={zeroY} stroke="currentColor" stroke-opacity="0.3" stroke-width="0.5" />
|
|
201
242
|
{/if}
|
|
202
243
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
244
|
+
{#each seriesList as series, sIdx}
|
|
245
|
+
{@const pts = series.data.map((_, i) => {
|
|
246
|
+
const x = 10 + (i / Math.max(series.data.length - 1, 1)) * 80;
|
|
247
|
+
const y = yFor(effectiveValue(sIdx, i));
|
|
248
|
+
return `${x},${y}`;
|
|
249
|
+
}).join(' ')}
|
|
250
|
+
{@const seriesColor = resolveColor(element, sIdx, 0, undefined, series)}
|
|
251
|
+
{#if element.chartType === 'area'}
|
|
252
|
+
{@const areaPath = `${pts} 90,${zeroY} 10,${zeroY}`}
|
|
253
|
+
<polygon points={areaPath} fill={seriesColor} fill-opacity={isMultiSeries ? 0.18 : 0.3} />
|
|
254
|
+
{/if}
|
|
255
|
+
<polyline points={pts} fill="none" stroke={seriesColor} stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
|
|
256
|
+
{#each series.data as point, pIdx}
|
|
257
|
+
{@const x = 10 + (pIdx / Math.max(series.data.length - 1, 1)) * 80}
|
|
258
|
+
{@const y = yFor(effectiveValue(sIdx, pIdx))}
|
|
259
|
+
<circle cx={x} cy={y} r="1.4" fill={resolveColor(element, sIdx, pIdx, point, series)} />
|
|
260
|
+
{#if element.showValues}
|
|
261
|
+
<text x={x} y={y - 2.5} text-anchor="middle" font-size="3" fill="currentColor">{formatValue(effectiveValue(sIdx, pIdx), element.valueFormat)}</text>
|
|
262
|
+
{/if}
|
|
263
|
+
{/each}
|
|
264
|
+
{/each}
|
|
211
265
|
|
|
212
|
-
{#
|
|
213
|
-
{
|
|
214
|
-
|
|
215
|
-
<circle cx={x} cy={y} r="2" fill={getColor(i, point.color) || element.colors[0]} />
|
|
216
|
-
{#if element.showLabels}
|
|
266
|
+
{#if element.showLabels}
|
|
267
|
+
{#each seriesList[0]?.data ?? [] as point, pIdx}
|
|
268
|
+
{@const x = 10 + (pIdx / Math.max((seriesList[0]?.data.length ?? 1) - 1, 1)) * 80}
|
|
217
269
|
<text x={x} y="98" text-anchor="middle" font-size="4" fill="currentColor">{point.label}</text>
|
|
218
|
-
{/
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
270
|
+
{/each}
|
|
271
|
+
{/if}
|
|
272
|
+
|
|
273
|
+
{#each yTicks as t}
|
|
274
|
+
<text x="1" y={t.y + 1} font-size="3" fill="currentColor" opacity="0.7">{formatValue(t.value, element.valueFormat)}</text>
|
|
222
275
|
{/each}
|
|
223
276
|
</svg>
|
|
277
|
+
|
|
224
278
|
{:else if element.chartType === 'pie'}
|
|
225
|
-
<svg viewBox="0 0 100 100" class="pie-chart">
|
|
226
|
-
{#each pieSegments as
|
|
227
|
-
{#if
|
|
228
|
-
{@const midAngle = (
|
|
279
|
+
<svg viewBox="0 0 100 100" class="animot-pie-chart">
|
|
280
|
+
{#each pieSegments as seg}
|
|
281
|
+
{#if seg.endAngle - seg.startAngle > 0.1}
|
|
282
|
+
{@const midAngle = (seg.startAngle + seg.endAngle) / 2}
|
|
229
283
|
{@const valuePos = polarToCartesian(50, 50, 28, midAngle)}
|
|
230
284
|
{@const labelPos = polarToCartesian(50, 50, 48, midAngle)}
|
|
231
285
|
{@const textAnchor = midAngle > -90 && midAngle < 90 ? 'start' : 'end'}
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
fill={segment.color}
|
|
235
|
-
/>
|
|
286
|
+
{@const fill = resolveColor(element, 0, seg.index, seg.point, pieSeries)}
|
|
287
|
+
<path d={describeArc(50, 50, 40, seg.startAngle, seg.endAngle)} {fill} />
|
|
236
288
|
{#if element.showValues}
|
|
237
|
-
<text
|
|
238
|
-
x={valuePos.x}
|
|
239
|
-
y={valuePos.y}
|
|
240
|
-
text-anchor="middle"
|
|
241
|
-
dominant-baseline="middle"
|
|
242
|
-
font-size="4"
|
|
243
|
-
fill="white"
|
|
244
|
-
font-weight="600"
|
|
245
|
-
>{Math.round(segment.percentage * 100)}%</text>
|
|
289
|
+
<text x={valuePos.x} y={valuePos.y} text-anchor="middle" dominant-baseline="middle" font-size="4" fill="white" font-weight="600">{Math.round(seg.percentage * 100)}%</text>
|
|
246
290
|
{/if}
|
|
247
291
|
{#if element.showLabels}
|
|
248
|
-
<text
|
|
249
|
-
x={labelPos.x}
|
|
250
|
-
y={labelPos.y}
|
|
251
|
-
text-anchor={textAnchor}
|
|
252
|
-
dominant-baseline="middle"
|
|
253
|
-
font-size="3.5"
|
|
254
|
-
fill="currentColor"
|
|
255
|
-
>{element.data[i].label}</text>
|
|
292
|
+
<text x={labelPos.x} y={labelPos.y} text-anchor={textAnchor} dominant-baseline="middle" font-size="3.5" fill="currentColor">{seg.point.label}</text>
|
|
256
293
|
{/if}
|
|
257
294
|
{/if}
|
|
258
295
|
{/each}
|
|
259
|
-
{#if element.showLegend}
|
|
260
|
-
{#each
|
|
261
|
-
<rect x="85" y={10 + i * 8} width="4" height="4" fill={
|
|
296
|
+
{#if element.showLegend && pieSeries}
|
|
297
|
+
{#each pieSeries.data as point, i}
|
|
298
|
+
<rect x="85" y={10 + i * 8} width="4" height="4" fill={resolveColor(element, 0, i, point, pieSeries)} />
|
|
262
299
|
<text x="91" y={14 + i * 8} font-size="4" fill="currentColor">{point.label}</text>
|
|
263
300
|
{/each}
|
|
264
301
|
{/if}
|
|
265
302
|
</svg>
|
|
303
|
+
|
|
266
304
|
{:else if element.chartType === 'donut'}
|
|
267
|
-
<svg viewBox="0 0 100 100" class="donut-chart">
|
|
268
|
-
{#each pieSegments as
|
|
269
|
-
{#if
|
|
270
|
-
{@const midAngle = (
|
|
305
|
+
<svg viewBox="0 0 100 100" class="animot-donut-chart">
|
|
306
|
+
{#each pieSegments as seg}
|
|
307
|
+
{#if seg.endAngle - seg.startAngle > 0.1}
|
|
308
|
+
{@const midAngle = (seg.startAngle + seg.endAngle) / 2}
|
|
271
309
|
{@const valuePos = polarToCartesian(50, 50, 32.5, midAngle)}
|
|
272
310
|
{@const labelPos = polarToCartesian(50, 50, 48, midAngle)}
|
|
273
311
|
{@const textAnchor = midAngle > -90 && midAngle < 90 ? 'start' : 'end'}
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
fill={segment.color}
|
|
277
|
-
/>
|
|
312
|
+
{@const fill = resolveColor(element, 0, seg.index, seg.point, pieSeries)}
|
|
313
|
+
<path d={describeDonutArc(50, 50, 40, 25, seg.startAngle, seg.endAngle)} {fill} />
|
|
278
314
|
{#if element.showValues}
|
|
279
|
-
<text
|
|
280
|
-
x={valuePos.x}
|
|
281
|
-
y={valuePos.y}
|
|
282
|
-
text-anchor="middle"
|
|
283
|
-
dominant-baseline="middle"
|
|
284
|
-
font-size="3.5"
|
|
285
|
-
fill="white"
|
|
286
|
-
font-weight="600"
|
|
287
|
-
>{Math.round(segment.percentage * 100)}%</text>
|
|
315
|
+
<text x={valuePos.x} y={valuePos.y} text-anchor="middle" dominant-baseline="middle" font-size="3.5" fill="white" font-weight="600">{Math.round(seg.percentage * 100)}%</text>
|
|
288
316
|
{/if}
|
|
289
317
|
{#if element.showLabels}
|
|
290
|
-
<text
|
|
291
|
-
x={labelPos.x}
|
|
292
|
-
y={labelPos.y}
|
|
293
|
-
text-anchor={textAnchor}
|
|
294
|
-
dominant-baseline="middle"
|
|
295
|
-
font-size="3.5"
|
|
296
|
-
fill="currentColor"
|
|
297
|
-
>{element.data[i].label}</text>
|
|
318
|
+
<text x={labelPos.x} y={labelPos.y} text-anchor={textAnchor} dominant-baseline="middle" font-size="3.5" fill="currentColor">{seg.point.label}</text>
|
|
298
319
|
{/if}
|
|
299
320
|
{/if}
|
|
300
321
|
{/each}
|
|
301
|
-
{#if element.showLegend}
|
|
302
|
-
{#each
|
|
303
|
-
<rect x="85" y={10 + i * 8} width="4" height="4" fill={
|
|
322
|
+
{#if element.showLegend && pieSeries}
|
|
323
|
+
{#each pieSeries.data as point, i}
|
|
324
|
+
<rect x="85" y={10 + i * 8} width="4" height="4" fill={resolveColor(element, 0, i, point, pieSeries)} />
|
|
304
325
|
<text x="91" y={14 + i * 8} font-size="4" fill="currentColor">{point.label}</text>
|
|
305
326
|
{/each}
|
|
306
327
|
{/if}
|
|
307
328
|
</svg>
|
|
308
329
|
{/if}
|
|
330
|
+
|
|
331
|
+
{#if element.showLegend && isMultiSeries && (element.chartType === 'bar' || element.chartType === 'line' || element.chartType === 'area')}
|
|
332
|
+
<div class="animot-series-legend">
|
|
333
|
+
{#each seriesList as series, sIdx}
|
|
334
|
+
<span class="animot-legend-item">
|
|
335
|
+
<span class="animot-swatch" style:background={resolveColor(element, sIdx, 0, undefined, series)}></span>
|
|
336
|
+
{series.name || `Series ${sIdx + 1}`}
|
|
337
|
+
</span>
|
|
338
|
+
{/each}
|
|
339
|
+
</div>
|
|
340
|
+
{/if}
|
|
309
341
|
</div>
|
|
310
342
|
</div>
|
|
311
343
|
|
|
312
344
|
<style>
|
|
313
|
-
.chart {
|
|
345
|
+
.animot-chart {
|
|
314
346
|
width: 100%;
|
|
315
347
|
height: 100%;
|
|
316
348
|
display: flex;
|
|
@@ -318,24 +350,14 @@
|
|
|
318
350
|
box-sizing: border-box;
|
|
319
351
|
overflow: hidden;
|
|
320
352
|
}
|
|
321
|
-
|
|
322
|
-
.chart-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
.chart-content {
|
|
329
|
-
flex: 1;
|
|
330
|
-
min-height: 0;
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
.chart-content svg {
|
|
334
|
-
width: 100%;
|
|
335
|
-
height: 100%;
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
.bar-chart, .line-chart {
|
|
339
|
-
overflow: visible;
|
|
353
|
+
.animot-chart-title { font-weight: 600; text-align: center; margin-bottom: 8px; }
|
|
354
|
+
.animot-chart-content { flex: 1; min-height: 0; display: flex; flex-direction: column; }
|
|
355
|
+
.animot-chart-content svg { width: 100%; flex: 1; min-height: 0; }
|
|
356
|
+
.animot-bar-chart, .animot-line-chart { overflow: visible; }
|
|
357
|
+
.animot-series-legend {
|
|
358
|
+
display: flex; flex-wrap: wrap; gap: 12px;
|
|
359
|
+
justify-content: center; padding: 6px 0 0; font-size: 0.85em;
|
|
340
360
|
}
|
|
361
|
+
.animot-legend-item { display: inline-flex; align-items: center; gap: 5px; }
|
|
362
|
+
.animot-swatch { width: 10px; height: 10px; border-radius: 2px; display: inline-block; }
|
|
341
363
|
</style>
|
|
@@ -2,6 +2,9 @@ import type { ChartElement } from '../types';
|
|
|
2
2
|
interface Props {
|
|
3
3
|
element: ChartElement;
|
|
4
4
|
slideId: string;
|
|
5
|
+
/** Optional snapshot of the same chart on the OUTGOING slide. When set,
|
|
6
|
+
* the renderer tweens data values from prior state to current. */
|
|
7
|
+
previousElement?: ChartElement | null;
|
|
5
8
|
}
|
|
6
9
|
declare const ChartRenderer: import("svelte").Component<Props, {}, "">;
|
|
7
10
|
type ChartRenderer = ReturnType<typeof ChartRenderer>;
|
package/dist/types.d.ts
CHANGED
|
@@ -303,10 +303,29 @@ export interface ChartDataPoint {
|
|
|
303
303
|
value: number;
|
|
304
304
|
color?: string;
|
|
305
305
|
}
|
|
306
|
+
export interface ChartSeries {
|
|
307
|
+
id: string;
|
|
308
|
+
name: string;
|
|
309
|
+
data: ChartDataPoint[];
|
|
310
|
+
color?: string;
|
|
311
|
+
}
|
|
312
|
+
export type ChartBarLayout = 'grouped' | 'stacked';
|
|
313
|
+
export interface ChartValueFormat {
|
|
314
|
+
prefix?: string;
|
|
315
|
+
suffix?: string;
|
|
316
|
+
decimals?: number;
|
|
317
|
+
abbreviate?: boolean;
|
|
318
|
+
}
|
|
319
|
+
export interface ChartYAxis {
|
|
320
|
+
min?: number;
|
|
321
|
+
max?: number;
|
|
322
|
+
showLabels?: boolean;
|
|
323
|
+
}
|
|
306
324
|
export interface ChartElement extends BaseElement {
|
|
307
325
|
type: 'chart';
|
|
308
326
|
chartType: ChartType;
|
|
309
327
|
data: ChartDataPoint[];
|
|
328
|
+
series?: ChartSeries[];
|
|
310
329
|
title: string;
|
|
311
330
|
showLabels: boolean;
|
|
312
331
|
showValues: boolean;
|
|
@@ -314,11 +333,16 @@ export interface ChartElement extends BaseElement {
|
|
|
314
333
|
showLegend: boolean;
|
|
315
334
|
animationDuration: number;
|
|
316
335
|
colors: string[];
|
|
336
|
+
useKitPalette?: boolean;
|
|
317
337
|
backgroundColor: string;
|
|
318
338
|
textColor: string;
|
|
319
339
|
fontSize: number;
|
|
320
340
|
padding: number;
|
|
321
341
|
borderRadius: number;
|
|
342
|
+
barLayout?: ChartBarLayout;
|
|
343
|
+
revealStagger?: number;
|
|
344
|
+
valueFormat?: ChartValueFormat;
|
|
345
|
+
yAxis?: ChartYAxis;
|
|
322
346
|
}
|
|
323
347
|
export interface IconElement extends BaseElement {
|
|
324
348
|
type: 'icon';
|