@sentropic/design-system-svelte 0.34.22 → 0.34.24
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/AreaChart.svelte +160 -1
- package/dist/AreaChart.svelte.d.ts +16 -0
- package/dist/AreaChart.svelte.d.ts.map +1 -1
- package/dist/BarChart.svelte +191 -1
- package/dist/BarChart.svelte.d.ts +18 -0
- package/dist/BarChart.svelte.d.ts.map +1 -1
- package/dist/DonutChart.svelte +52 -2
- package/dist/DonutChart.svelte.d.ts +9 -0
- package/dist/DonutChart.svelte.d.ts.map +1 -1
- package/dist/LineChart.svelte +162 -1
- package/dist/LineChart.svelte.d.ts +17 -0
- package/dist/LineChart.svelte.d.ts.map +1 -1
- package/dist/StackedBarChart.svelte +44 -0
- package/dist/StackedBarChart.svelte.d.ts +9 -0
- package/dist/StackedBarChart.svelte.d.ts.map +1 -1
- package/dist/chartAnnotations.d.ts +132 -0
- package/dist/chartAnnotations.d.ts.map +1 -0
- package/dist/chartAnnotations.js +125 -0
- package/dist/chartDataLabels.d.ts +35 -0
- package/dist/chartDataLabels.d.ts.map +1 -0
- package/dist/chartDataLabels.js +31 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/LineChart.svelte
CHANGED
|
@@ -53,6 +53,13 @@
|
|
|
53
53
|
|
|
54
54
|
<script lang="ts">
|
|
55
55
|
import ChartDataList from "./ChartDataList.svelte";
|
|
56
|
+
import {
|
|
57
|
+
resolveAnnotations,
|
|
58
|
+
annotationDataListItems,
|
|
59
|
+
polygonPoints,
|
|
60
|
+
type ChartAnnotation
|
|
61
|
+
} from "./chartAnnotations.js";
|
|
62
|
+
import { formatDataLabel, normalizeDataLabels, type DataLabelsProp } from "./chartDataLabels.js";
|
|
56
63
|
|
|
57
64
|
type LineChartProps = {
|
|
58
65
|
data: LineChartDatum[];
|
|
@@ -70,6 +77,21 @@
|
|
|
70
77
|
goalLine?: ChartGoalLine;
|
|
71
78
|
/** Least-squares trend line over the data points. */
|
|
72
79
|
trend?: boolean;
|
|
80
|
+
/**
|
|
81
|
+
* Annotation overlay in DATA space (points, labels, axis lines, regions,
|
|
82
|
+
* polygons). Resolved to pixels via the chart's scales and drawn in a
|
|
83
|
+
* dedicated `<g class="st-lineChart__annotations">` — regions behind the
|
|
84
|
+
* series, every other kind above it. Additive: absent ⇒ unchanged.
|
|
85
|
+
*/
|
|
86
|
+
annotations?: ChartAnnotation[];
|
|
87
|
+
/**
|
|
88
|
+
* Per-point value labels. `false`/absent (default) → none. `true` → each
|
|
89
|
+
* point's value with the chart's numeric formatter. Object → `format(value)`
|
|
90
|
+
* and/or a `position` override. Default position is `top` (above the point).
|
|
91
|
+
* Labels are `aria-hidden` — the values already live in the accessible
|
|
92
|
+
* ChartDataList.
|
|
93
|
+
*/
|
|
94
|
+
dataLabels?: DataLabelsProp;
|
|
73
95
|
/**
|
|
74
96
|
* Fixed value-axis (y) domain `[min, max]`. When provided (and finite,
|
|
75
97
|
* min<max) the y scale uses it instead of the data-derived range — letting
|
|
@@ -104,6 +126,8 @@
|
|
|
104
126
|
bands,
|
|
105
127
|
goalLine,
|
|
106
128
|
trend = false,
|
|
129
|
+
annotations,
|
|
130
|
+
dataLabels,
|
|
107
131
|
domain,
|
|
108
132
|
scale = "linear",
|
|
109
133
|
invertAxis = false,
|
|
@@ -441,6 +465,53 @@
|
|
|
441
465
|
};
|
|
442
466
|
});
|
|
443
467
|
|
|
468
|
+
// --- Annotation overlay ---------------------------------------------------
|
|
469
|
+
// Data-space annotations resolved to absolute pixels via the chart scales.
|
|
470
|
+
// `xScale` honours the ordinal/numeric x domain (a category matches by
|
|
471
|
+
// equality, a numeric value must sit inside the domain); `yScale` reuses the
|
|
472
|
+
// value fraction. Out-of-domain coordinates yield `null` → the resolver drops
|
|
473
|
+
// them, so an annotation never escapes the plot.
|
|
474
|
+
const annotationXScale = $derived((v: number | string): number | null => {
|
|
475
|
+
if (xDomain.kind === "numeric") {
|
|
476
|
+
if (typeof v !== "number" || !Number.isFinite(v)) return null;
|
|
477
|
+
if (v < xDomain.min || v > xDomain.max) return null;
|
|
478
|
+
return scaleLinear(v, xDomain.min, xDomain.max, 0, plotWidth);
|
|
479
|
+
}
|
|
480
|
+
const i = data.findIndex((d) => d.x === v);
|
|
481
|
+
if (i < 0) return null;
|
|
482
|
+
const denom = Math.max(data.length - 1, 1);
|
|
483
|
+
return data.length === 1 ? plotWidth / 2 : (i / denom) * plotWidth;
|
|
484
|
+
});
|
|
485
|
+
const annotationYScale = $derived((v: number): number | null => {
|
|
486
|
+
if (!Number.isFinite(v)) return null;
|
|
487
|
+
return plotHeight * (1 - valueFraction(v));
|
|
488
|
+
});
|
|
489
|
+
const resolvedAnnotations = $derived(
|
|
490
|
+
resolveAnnotations(annotations, {
|
|
491
|
+
xScale: annotationXScale,
|
|
492
|
+
yScale: annotationYScale,
|
|
493
|
+
plotLeft: MARGIN.left,
|
|
494
|
+
plotTop: MARGIN.top,
|
|
495
|
+
plotWidth,
|
|
496
|
+
plotHeight
|
|
497
|
+
})
|
|
498
|
+
);
|
|
499
|
+
const annotationRegions = $derived(resolvedAnnotations.filter((a) => a.kind === "region"));
|
|
500
|
+
const annotationAbove = $derived(resolvedAnnotations.filter((a) => a.kind !== "region"));
|
|
501
|
+
|
|
502
|
+
// --- Data labels ----------------------------------------------------------
|
|
503
|
+
// One value label per point. Default `top`: just above the dot. `center` sits
|
|
504
|
+
// on the dot. aria-hidden (values are in the ChartDataList already).
|
|
505
|
+
const dataLabelOpts = $derived(normalizeDataLabels(dataLabels));
|
|
506
|
+
const dataLabelItems = $derived.by(() => {
|
|
507
|
+
if (!dataLabelOpts.enabled) return [] as { key: number; x: number; y: number; text: string; baseline: string }[];
|
|
508
|
+
return points.map((p) => {
|
|
509
|
+
const text = formatDataLabel(p.datum.y, dataLabelOpts, formatTick);
|
|
510
|
+
const center = dataLabelOpts.position === "center" || dataLabelOpts.position === "inside";
|
|
511
|
+
return { key: p.index, x: p.x, y: center ? p.y : p.y - 8, text, baseline: center ? "middle" : "auto" };
|
|
512
|
+
});
|
|
513
|
+
});
|
|
514
|
+
|
|
444
515
|
// --- Forecast segments ------------------------------------------------------
|
|
445
516
|
// A datum with `forecast: true` renders as a forecast point: its dot takes
|
|
446
517
|
// the forecast tone and every segment touching a forecast point is dashed,
|
|
@@ -466,7 +537,8 @@
|
|
|
466
537
|
|
|
467
538
|
const dataValueItems = $derived([
|
|
468
539
|
...data.map((d, i) => (forecastFlags[i] ? `${d.x}: ${d.y} (prévision)` : `${d.x}: ${d.y}`)),
|
|
469
|
-
...overlayDataListItems(referenceLines, bands, goal, trendModel)
|
|
540
|
+
...overlayDataListItems(referenceLines, bands, goal, trendModel),
|
|
541
|
+
...annotationDataListItems(annotations)
|
|
470
542
|
]);
|
|
471
543
|
|
|
472
544
|
function buildLinearPath(pts: { x: number; y: number }[]): string {
|
|
@@ -651,6 +723,20 @@
|
|
|
651
723
|
<line class="st-lineChart__trend" x1={trendLine.x1} y1={trendLine.y1} x2={trendLine.x2} y2={trendLine.y2} />
|
|
652
724
|
{/if}
|
|
653
725
|
|
|
726
|
+
<!-- Annotation regions sit BEHIND the series (filled bands). -->
|
|
727
|
+
{#if annotationRegions.length > 0}
|
|
728
|
+
<g class="st-lineChart__annotations st-lineChart__annotations--behind">
|
|
729
|
+
{#each annotationRegions as a (a.key)}
|
|
730
|
+
{#if a.kind === "region"}
|
|
731
|
+
<rect class="st-lineChart__annotationRegion" x={a.x} y={a.y} width={a.width} height={a.height} />
|
|
732
|
+
{#if a.label}
|
|
733
|
+
<text class="st-lineChart__annotationLabel" x={a.x + 4} y={a.y + 11}>{a.label}</text>
|
|
734
|
+
{/if}
|
|
735
|
+
{/if}
|
|
736
|
+
{/each}
|
|
737
|
+
</g>
|
|
738
|
+
{/if}
|
|
739
|
+
|
|
654
740
|
{#if area && areaPath}
|
|
655
741
|
<path class="st-lineChart__area" d={areaPath} />
|
|
656
742
|
{/if}
|
|
@@ -694,6 +780,46 @@
|
|
|
694
780
|
{goalGeom.label ?? `Objectif ${goalGeom.value}`}
|
|
695
781
|
</text>
|
|
696
782
|
{/if}
|
|
783
|
+
|
|
784
|
+
<!-- Annotations ABOVE the series: lines, shapes, points, labels. -->
|
|
785
|
+
{#if annotationAbove.length > 0}
|
|
786
|
+
<g class="st-lineChart__annotations st-lineChart__annotations--above">
|
|
787
|
+
{#each annotationAbove as a (a.key)}
|
|
788
|
+
{#if a.kind === "line"}
|
|
789
|
+
<line class="st-lineChart__annotationLine" x1={a.x1} y1={a.y1} x2={a.x2} y2={a.y2} />
|
|
790
|
+
{#if a.label}
|
|
791
|
+
<text
|
|
792
|
+
class="st-lineChart__annotationLabel"
|
|
793
|
+
x={a.axis === "x" ? a.x1 + 4 : MARGIN.left + plotWidth - 4}
|
|
794
|
+
y={a.axis === "x" ? MARGIN.top + 11 : a.y1 - 4}
|
|
795
|
+
text-anchor={a.axis === "x" ? "start" : "end"}
|
|
796
|
+
>{a.label}</text>
|
|
797
|
+
{/if}
|
|
798
|
+
{:else if a.kind === "shape"}
|
|
799
|
+
<polygon class="st-lineChart__annotationShape" points={polygonPoints(a.points)} />
|
|
800
|
+
{#if a.label}
|
|
801
|
+
<text class="st-lineChart__annotationLabel" x={a.labelX} y={a.labelY} text-anchor="middle">{a.label}</text>
|
|
802
|
+
{/if}
|
|
803
|
+
{:else if a.kind === "point"}
|
|
804
|
+
<circle class="st-lineChart__annotationPoint" cx={a.x} cy={a.y} r="4.5" />
|
|
805
|
+
{#if a.label}
|
|
806
|
+
<text class="st-lineChart__annotationLabel" x={a.x} y={a.y - 8} text-anchor="middle">{a.label}</text>
|
|
807
|
+
{/if}
|
|
808
|
+
{:else}
|
|
809
|
+
<text class="st-lineChart__annotationText" x={a.x} y={a.y} text-anchor={a.anchor}>{a.text}</text>
|
|
810
|
+
{/if}
|
|
811
|
+
{/each}
|
|
812
|
+
</g>
|
|
813
|
+
{/if}
|
|
814
|
+
|
|
815
|
+
<!-- Data labels — one value per point, drawn on top. aria-hidden. -->
|
|
816
|
+
{#if dataLabelItems.length > 0}
|
|
817
|
+
<g class="st-lineChart__dataLabels" aria-hidden="true">
|
|
818
|
+
{#each dataLabelItems as d (d.key)}
|
|
819
|
+
<text class="st-lineChart__dataLabel" x={d.x} y={d.y} text-anchor="middle" dominant-baseline={d.baseline}>{d.text}</text>
|
|
820
|
+
{/each}
|
|
821
|
+
</g>
|
|
822
|
+
{/if}
|
|
697
823
|
</svg>
|
|
698
824
|
</div>
|
|
699
825
|
|
|
@@ -867,4 +993,39 @@
|
|
|
867
993
|
font-size: 0.6875rem;
|
|
868
994
|
font-weight: 600;
|
|
869
995
|
}
|
|
996
|
+
|
|
997
|
+
/* --- Annotation layer ----------------------------------------------------
|
|
998
|
+
Regions render BEHIND the series; lines/shapes/points/labels render ABOVE. */
|
|
999
|
+
.st-lineChart__annotationRegion {
|
|
1000
|
+
fill: color-mix(in srgb, var(--st-semantic-feedback-info) 12%, transparent);
|
|
1001
|
+
stroke: none;
|
|
1002
|
+
}
|
|
1003
|
+
.st-lineChart__annotationLine {
|
|
1004
|
+
stroke: var(--st-semantic-feedback-info);
|
|
1005
|
+
stroke-width: 1.5;
|
|
1006
|
+
stroke-dasharray: 4 3;
|
|
1007
|
+
}
|
|
1008
|
+
.st-lineChart__annotationShape {
|
|
1009
|
+
fill: color-mix(in srgb, var(--st-semantic-feedback-info) 14%, transparent);
|
|
1010
|
+
stroke: var(--st-semantic-feedback-info);
|
|
1011
|
+
stroke-width: 1.5;
|
|
1012
|
+
}
|
|
1013
|
+
.st-lineChart__annotationPoint {
|
|
1014
|
+
fill: var(--st-semantic-feedback-info);
|
|
1015
|
+
stroke: var(--st-semantic-surface-default);
|
|
1016
|
+
stroke-width: 1.5;
|
|
1017
|
+
}
|
|
1018
|
+
.st-lineChart__annotationLabel,
|
|
1019
|
+
.st-lineChart__annotationText {
|
|
1020
|
+
fill: var(--st-semantic-text-primary);
|
|
1021
|
+
font-size: 0.625rem;
|
|
1022
|
+
font-weight: 600;
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
/* Data labels — per-point value, drawn on top. Token-only colour. */
|
|
1026
|
+
.st-lineChart__dataLabel {
|
|
1027
|
+
fill: var(--st-semantic-text-primary);
|
|
1028
|
+
font-size: 0.6875rem;
|
|
1029
|
+
font-weight: 600;
|
|
1030
|
+
}
|
|
870
1031
|
</style>
|
|
@@ -34,6 +34,8 @@ export type ChartGoalLine = {
|
|
|
34
34
|
};
|
|
35
35
|
/** Value-axis scale type. `log` requires strictly positive values. */
|
|
36
36
|
export type ChartScale = "linear" | "log";
|
|
37
|
+
import { type ChartAnnotation } from "./chartAnnotations.js";
|
|
38
|
+
import { type DataLabelsProp } from "./chartDataLabels.js";
|
|
37
39
|
type LineChartProps = {
|
|
38
40
|
data: LineChartDatum[];
|
|
39
41
|
width?: number;
|
|
@@ -50,6 +52,21 @@ type LineChartProps = {
|
|
|
50
52
|
goalLine?: ChartGoalLine;
|
|
51
53
|
/** Least-squares trend line over the data points. */
|
|
52
54
|
trend?: boolean;
|
|
55
|
+
/**
|
|
56
|
+
* Annotation overlay in DATA space (points, labels, axis lines, regions,
|
|
57
|
+
* polygons). Resolved to pixels via the chart's scales and drawn in a
|
|
58
|
+
* dedicated `<g class="st-lineChart__annotations">` — regions behind the
|
|
59
|
+
* series, every other kind above it. Additive: absent ⇒ unchanged.
|
|
60
|
+
*/
|
|
61
|
+
annotations?: ChartAnnotation[];
|
|
62
|
+
/**
|
|
63
|
+
* Per-point value labels. `false`/absent (default) → none. `true` → each
|
|
64
|
+
* point's value with the chart's numeric formatter. Object → `format(value)`
|
|
65
|
+
* and/or a `position` override. Default position is `top` (above the point).
|
|
66
|
+
* Labels are `aria-hidden` — the values already live in the accessible
|
|
67
|
+
* ChartDataList.
|
|
68
|
+
*/
|
|
69
|
+
dataLabels?: DataLabelsProp;
|
|
53
70
|
/**
|
|
54
71
|
* Fixed value-axis (y) domain `[min, max]`. When provided (and finite,
|
|
55
72
|
* min<max) the y scale uses it instead of the data-derived range — letting
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"LineChart.svelte.d.ts","sourceRoot":"","sources":["../src/lib/LineChart.svelte.ts"],"names":[],"mappings":"AAGE,MAAM,MAAM,aAAa,GACrB,WAAW,GACX,WAAW,GACX,WAAW,GACX,WAAW,GACX,WAAW,GACX,WAAW,GACX,WAAW,GACX,WAAW,CAAC;AAEhB,MAAM,MAAM,cAAc,GAAG;IAC3B,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IACnB,CAAC,EAAE,MAAM,CAAC;IACV;;;;;;OAMG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB,CAAC;AAEF;;;GAGG;AACH,MAAM,MAAM,gBAAgB,GAAG,SAAS,GAAG,SAAS,GAAG,SAAS,GAAG,OAAO,GAAG,MAAM,CAAC;AAEpF,MAAM,MAAM,kBAAkB,GAAG;IAC/B,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,gBAAgB,CAAC;IACxB,IAAI,CAAC,EAAE,GAAG,GAAG,GAAG,CAAC;CAClB,CAAC;AAEF,MAAM,MAAM,SAAS,GAAG;IACtB,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,gBAAgB,CAAC;CACzB,CAAC;AAEF,MAAM,MAAM,aAAa,GAAG;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF,sEAAsE;AACtE,MAAM,MAAM,UAAU,GAAG,QAAQ,GAAG,KAAK,CAAC;
|
|
1
|
+
{"version":3,"file":"LineChart.svelte.d.ts","sourceRoot":"","sources":["../src/lib/LineChart.svelte.ts"],"names":[],"mappings":"AAGE,MAAM,MAAM,aAAa,GACrB,WAAW,GACX,WAAW,GACX,WAAW,GACX,WAAW,GACX,WAAW,GACX,WAAW,GACX,WAAW,GACX,WAAW,CAAC;AAEhB,MAAM,MAAM,cAAc,GAAG;IAC3B,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IACnB,CAAC,EAAE,MAAM,CAAC;IACV;;;;;;OAMG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB,CAAC;AAEF;;;GAGG;AACH,MAAM,MAAM,gBAAgB,GAAG,SAAS,GAAG,SAAS,GAAG,SAAS,GAAG,OAAO,GAAG,MAAM,CAAC;AAEpF,MAAM,MAAM,kBAAkB,GAAG;IAC/B,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,gBAAgB,CAAC;IACxB,IAAI,CAAC,EAAE,GAAG,GAAG,GAAG,CAAC;CAClB,CAAC;AAEF,MAAM,MAAM,SAAS,GAAG;IACtB,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,gBAAgB,CAAC;CACzB,CAAC;AAEF,MAAM,MAAM,aAAa,GAAG;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF,sEAAsE;AACtE,MAAM,MAAM,UAAU,GAAG,QAAQ,GAAG,KAAK,CAAC;AAI5C,OAAO,EAIH,KAAK,eAAe,EACrB,MAAM,uBAAuB,CAAC;AACjC,OAAO,EAAwC,KAAK,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAG/F,KAAK,cAAc,GAAG;IACpB,IAAI,EAAE,cAAc,EAAE,CAAC;IACvB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,aAAa,CAAC;IACrB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,qEAAqE;IACrE,cAAc,CAAC,EAAE,kBAAkB,EAAE,CAAC;IACtC,oDAAoD;IACpD,KAAK,CAAC,EAAE,SAAS,EAAE,CAAC;IACpB,qDAAqD;IACrD,QAAQ,CAAC,EAAE,aAAa,CAAC;IACzB,qDAAqD;IACrD,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB;;;;;OAKG;IACH,WAAW,CAAC,EAAE,eAAe,EAAE,CAAC;IAChC;;;;;;OAMG;IACH,UAAU,CAAC,EAAE,cAAc,CAAC;IAC5B;;;;OAIG;IACH,MAAM,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC1B;;;;OAIG;IACH,KAAK,CAAC,EAAE,UAAU,CAAC;IACnB,iDAAiD;IACjD,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB;;;OAGG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AAqpBJ,QAAA,MAAM,SAAS,oDAAwC,CAAC;AACxD,KAAK,SAAS,GAAG,UAAU,CAAC,OAAO,SAAS,CAAC,CAAC;AAC9C,eAAe,SAAS,CAAC"}
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
|
|
18
18
|
<script lang="ts">
|
|
19
19
|
import ChartDataList from "./ChartDataList.svelte";
|
|
20
|
+
import { formatDataLabel, normalizeDataLabels, type DataLabelsProp } from "./chartDataLabels.js";
|
|
20
21
|
|
|
21
22
|
type StackedBarChartProps = {
|
|
22
23
|
data: StackedBarDatum[];
|
|
@@ -24,6 +25,14 @@
|
|
|
24
25
|
height?: number;
|
|
25
26
|
label: string;
|
|
26
27
|
showLegend?: boolean;
|
|
28
|
+
/**
|
|
29
|
+
* Per-segment value labels. `false`/absent (default) → none. `true` → each
|
|
30
|
+
* segment's value with the chart's numeric formatter. Object → `format(value)`
|
|
31
|
+
* and/or a `position` override (default `center` of the segment). Segments too
|
|
32
|
+
* short to host a legible label are skipped. Labels are `aria-hidden` — the
|
|
33
|
+
* values already live in the accessible ChartDataList.
|
|
34
|
+
*/
|
|
35
|
+
dataLabels?: DataLabelsProp;
|
|
27
36
|
class?: string;
|
|
28
37
|
};
|
|
29
38
|
|
|
@@ -33,12 +42,16 @@
|
|
|
33
42
|
height = 260,
|
|
34
43
|
label,
|
|
35
44
|
showLegend = true,
|
|
45
|
+
dataLabels,
|
|
36
46
|
class: className
|
|
37
47
|
}: StackedBarChartProps = $props();
|
|
38
48
|
|
|
39
49
|
const MARGIN = { top: 14, right: 16, bottom: 34, left: 44 };
|
|
40
50
|
const TONES = ["category1","category2","category3","category4","category5","category6","category7","category8"] as const;
|
|
41
51
|
|
|
52
|
+
// A segment must be at least this tall (px) to host a legible label.
|
|
53
|
+
const DATA_LABEL_MIN_SEG_PX = 14;
|
|
54
|
+
|
|
42
55
|
function niceTicks(min: number, max: number, target = 5): number[] {
|
|
43
56
|
if (!Number.isFinite(min) || !Number.isFinite(max) || min === max) return [Number.isFinite(max) ? max : 0];
|
|
44
57
|
const range = max - min;
|
|
@@ -104,6 +117,26 @@
|
|
|
104
117
|
data.flatMap((bar) => bar.segments.map((seg) => `${bar.label}, ${seg.label}: ${seg.value}`))
|
|
105
118
|
);
|
|
106
119
|
|
|
120
|
+
// --- Data labels ----------------------------------------------------------
|
|
121
|
+
// One value label centred in each segment (default `center`). Segments shorter
|
|
122
|
+
// than DATA_LABEL_MIN_SEG_PX are skipped so labels stay legible. aria-hidden
|
|
123
|
+
// (values are in the ChartDataList already).
|
|
124
|
+
const dataLabelOpts = $derived(normalizeDataLabels(dataLabels));
|
|
125
|
+
const dataLabelItems = $derived(
|
|
126
|
+
dataLabelOpts.enabled
|
|
127
|
+
? bars.flatMap((bar) =>
|
|
128
|
+
bar.segs
|
|
129
|
+
.filter((s) => s.height >= DATA_LABEL_MIN_SEG_PX)
|
|
130
|
+
.map((s) => ({
|
|
131
|
+
key: `${bar.label}-${s.seg.label}`,
|
|
132
|
+
x: s.cx,
|
|
133
|
+
y: s.cy,
|
|
134
|
+
text: formatDataLabel(s.seg.value, dataLabelOpts, fmt)
|
|
135
|
+
}))
|
|
136
|
+
)
|
|
137
|
+
: []
|
|
138
|
+
);
|
|
139
|
+
|
|
107
140
|
function handleVisualPointerMove(event: PointerEvent) {
|
|
108
141
|
const target = event.target;
|
|
109
142
|
if (!(target instanceof Element)) {
|
|
@@ -148,6 +181,15 @@
|
|
|
148
181
|
/>
|
|
149
182
|
{/each}
|
|
150
183
|
{/each}
|
|
184
|
+
|
|
185
|
+
<!-- Data labels — one value per segment, drawn on top. aria-hidden. -->
|
|
186
|
+
{#if dataLabelItems.length > 0}
|
|
187
|
+
<g class="st-stackedBar__dataLabels" aria-hidden="true">
|
|
188
|
+
{#each dataLabelItems as d (d.key)}
|
|
189
|
+
<text class="st-stackedBar__dataLabel" x={d.x} y={d.y} text-anchor="middle" dominant-baseline="central">{d.text}</text>
|
|
190
|
+
{/each}
|
|
191
|
+
</g>
|
|
192
|
+
{/if}
|
|
151
193
|
</svg>
|
|
152
194
|
</div>
|
|
153
195
|
|
|
@@ -189,6 +231,8 @@
|
|
|
189
231
|
.st-stackedBar__seg--category6 { fill: var(--st-semantic-data-category6); }
|
|
190
232
|
.st-stackedBar__seg--category7 { fill: var(--st-semantic-data-category7); }
|
|
191
233
|
.st-stackedBar__seg--category8 { fill: var(--st-semantic-data-category8); }
|
|
234
|
+
/* Data labels — per-segment value, centred. Token-only colour. */
|
|
235
|
+
.st-stackedBar__dataLabel { fill: var(--st-semantic-text-inverse); font-size: 0.6875rem; font-weight: 600; }
|
|
192
236
|
.st-stackedBar__tooltip {
|
|
193
237
|
background: var(--st-semantic-surface-inverse); border-radius: var(--st-radius-sm, 0.25rem);
|
|
194
238
|
color: var(--st-semantic-text-inverse); display: inline-flex; flex-direction: column; font-size: 0.75rem;
|
|
@@ -8,12 +8,21 @@ export type StackedBarDatum = {
|
|
|
8
8
|
label: string;
|
|
9
9
|
segments: StackedBarSegment[];
|
|
10
10
|
};
|
|
11
|
+
import { type DataLabelsProp } from "./chartDataLabels.js";
|
|
11
12
|
type StackedBarChartProps = {
|
|
12
13
|
data: StackedBarDatum[];
|
|
13
14
|
width?: number;
|
|
14
15
|
height?: number;
|
|
15
16
|
label: string;
|
|
16
17
|
showLegend?: boolean;
|
|
18
|
+
/**
|
|
19
|
+
* Per-segment value labels. `false`/absent (default) → none. `true` → each
|
|
20
|
+
* segment's value with the chart's numeric formatter. Object → `format(value)`
|
|
21
|
+
* and/or a `position` override (default `center` of the segment). Segments too
|
|
22
|
+
* short to host a legible label are skipped. Labels are `aria-hidden` — the
|
|
23
|
+
* values already live in the accessible ChartDataList.
|
|
24
|
+
*/
|
|
25
|
+
dataLabels?: DataLabelsProp;
|
|
17
26
|
class?: string;
|
|
18
27
|
};
|
|
19
28
|
declare const StackedBarChart: import("svelte").Component<StackedBarChartProps, {}, "">;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"StackedBarChart.svelte.d.ts","sourceRoot":"","sources":["../src/lib/StackedBarChart.svelte.ts"],"names":[],"mappings":"AAGE,MAAM,MAAM,cAAc,GACtB,WAAW,GAAG,WAAW,GAAG,WAAW,GAAG,WAAW,GACrD,WAAW,GAAG,WAAW,GAAG,WAAW,GAAG,WAAW,CAAC;AAE1D,MAAM,MAAM,iBAAiB,GAAG;IAC9B,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,cAAc,CAAC;CACvB,CAAC;AAEF,MAAM,MAAM,eAAe,GAAG;IAC5B,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,iBAAiB,EAAE,CAAC;CAC/B,CAAC;
|
|
1
|
+
{"version":3,"file":"StackedBarChart.svelte.d.ts","sourceRoot":"","sources":["../src/lib/StackedBarChart.svelte.ts"],"names":[],"mappings":"AAGE,MAAM,MAAM,cAAc,GACtB,WAAW,GAAG,WAAW,GAAG,WAAW,GAAG,WAAW,GACrD,WAAW,GAAG,WAAW,GAAG,WAAW,GAAG,WAAW,CAAC;AAE1D,MAAM,MAAM,iBAAiB,GAAG;IAC9B,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,cAAc,CAAC;CACvB,CAAC;AAEF,MAAM,MAAM,eAAe,GAAG;IAC5B,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,iBAAiB,EAAE,CAAC;CAC/B,CAAC;AAIJ,OAAO,EAAwC,KAAK,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAG/F,KAAK,oBAAoB,GAAG;IAC1B,IAAI,EAAE,eAAe,EAAE,CAAC;IACxB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB;;;;;;OAMG;IACH,UAAU,CAAC,EAAE,cAAc,CAAC;IAC5B,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AAmLJ,QAAA,MAAM,eAAe,0DAAwC,CAAC;AAC9D,KAAK,eAAe,GAAG,UAAU,CAAC,OAAO,eAAe,CAAC,CAAC;AAC1D,eAAe,eAAe,CAAC"}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Marker glyph for a `point` annotation. Defaults to `circle`.
|
|
3
|
+
*/
|
|
4
|
+
export type ChartAnnotationMarker = "circle" | "square" | "diamond";
|
|
5
|
+
/**
|
|
6
|
+
* A single chart annotation in DATA space. Discriminated on `kind`:
|
|
7
|
+
* - `point` — a marked coordinate `(x, y)` with an optional label.
|
|
8
|
+
* - `label` — free text anchored at `(x, y)`.
|
|
9
|
+
* - `line` — a full-width/height guide on one axis at `value` (vertical when
|
|
10
|
+
* `axis: "x"`, horizontal when `axis: "y"`).
|
|
11
|
+
* - `region` — a filled band on one axis between `from` and `to`.
|
|
12
|
+
* - `shape` — a closed polygon through `points` (data-space coordinates).
|
|
13
|
+
*
|
|
14
|
+
* `x` may be a string for categorical (ordinal) x axes — it then matches a
|
|
15
|
+
* category by equality; `y` and value-axis numbers are always numeric.
|
|
16
|
+
*/
|
|
17
|
+
export type ChartAnnotation = {
|
|
18
|
+
kind: "point";
|
|
19
|
+
x: number | string;
|
|
20
|
+
y: number;
|
|
21
|
+
label?: string;
|
|
22
|
+
marker?: ChartAnnotationMarker;
|
|
23
|
+
} | {
|
|
24
|
+
kind: "label";
|
|
25
|
+
x: number | string;
|
|
26
|
+
y: number;
|
|
27
|
+
text: string;
|
|
28
|
+
anchor?: "start" | "middle" | "end";
|
|
29
|
+
} | {
|
|
30
|
+
kind: "line";
|
|
31
|
+
axis: "x" | "y";
|
|
32
|
+
value: number;
|
|
33
|
+
label?: string;
|
|
34
|
+
} | {
|
|
35
|
+
kind: "region";
|
|
36
|
+
axis: "x" | "y";
|
|
37
|
+
from: number;
|
|
38
|
+
to: number;
|
|
39
|
+
label?: string;
|
|
40
|
+
} | {
|
|
41
|
+
kind: "shape";
|
|
42
|
+
points: {
|
|
43
|
+
x: number | string;
|
|
44
|
+
y: number;
|
|
45
|
+
}[];
|
|
46
|
+
label?: string;
|
|
47
|
+
};
|
|
48
|
+
/**
|
|
49
|
+
* Scale context for {@link resolveAnnotations}. `xScale` / `yScale` map a data
|
|
50
|
+
* value to a pixel offset RELATIVE to the plot (0..plotWidth / 0..plotHeight),
|
|
51
|
+
* returning `null` when the value is non-finite, out of domain, or — for a
|
|
52
|
+
* categorical x axis — does not match any category. `plotLeft` / `plotTop` are
|
|
53
|
+
* the plot's pixel origin (chart margins); `plotWidth` / `plotHeight` its size.
|
|
54
|
+
*/
|
|
55
|
+
export type AnnotationScaleContext = {
|
|
56
|
+
xScale: (value: number | string) => number | null;
|
|
57
|
+
yScale: (value: number) => number | null;
|
|
58
|
+
plotLeft: number;
|
|
59
|
+
plotTop: number;
|
|
60
|
+
plotWidth: number;
|
|
61
|
+
plotHeight: number;
|
|
62
|
+
};
|
|
63
|
+
/** Resolved annotation primitive in ABSOLUTE pixel space (plot origin folded in). */
|
|
64
|
+
export type ResolvedAnnotation = {
|
|
65
|
+
kind: "region";
|
|
66
|
+
key: number;
|
|
67
|
+
axis: "x" | "y";
|
|
68
|
+
x: number;
|
|
69
|
+
y: number;
|
|
70
|
+
width: number;
|
|
71
|
+
height: number;
|
|
72
|
+
label?: string;
|
|
73
|
+
} | {
|
|
74
|
+
kind: "line";
|
|
75
|
+
key: number;
|
|
76
|
+
axis: "x" | "y";
|
|
77
|
+
x1: number;
|
|
78
|
+
y1: number;
|
|
79
|
+
x2: number;
|
|
80
|
+
y2: number;
|
|
81
|
+
label?: string;
|
|
82
|
+
} | {
|
|
83
|
+
kind: "shape";
|
|
84
|
+
key: number;
|
|
85
|
+
points: {
|
|
86
|
+
x: number;
|
|
87
|
+
y: number;
|
|
88
|
+
}[];
|
|
89
|
+
label?: string;
|
|
90
|
+
labelX: number;
|
|
91
|
+
labelY: number;
|
|
92
|
+
} | {
|
|
93
|
+
kind: "point";
|
|
94
|
+
key: number;
|
|
95
|
+
x: number;
|
|
96
|
+
y: number;
|
|
97
|
+
label?: string;
|
|
98
|
+
marker: ChartAnnotationMarker;
|
|
99
|
+
} | {
|
|
100
|
+
kind: "label";
|
|
101
|
+
key: number;
|
|
102
|
+
x: number;
|
|
103
|
+
y: number;
|
|
104
|
+
text: string;
|
|
105
|
+
anchor: "start" | "middle" | "end";
|
|
106
|
+
};
|
|
107
|
+
/**
|
|
108
|
+
* Resolves DATA-space annotations to ABSOLUTE-pixel primitives. Pure: given the
|
|
109
|
+
* same inputs it always returns the same output, with no DOM/framework access.
|
|
110
|
+
*
|
|
111
|
+
* Region/line on `axis: "x"` are positioned via `xScale` (vertical guide / band
|
|
112
|
+
* spanning the full plot height); on `axis: "y"` via `yScale` (horizontal guide
|
|
113
|
+
* / band spanning the full plot width). Points, labels and shape vertices are
|
|
114
|
+
* positioned via both scales. Any annotation whose required coordinates fall
|
|
115
|
+
* outside the domain (scale returns `null`) or are non-finite is DROPPED, so an
|
|
116
|
+
* annotation never escapes the plot. The original index is preserved as `key`
|
|
117
|
+
* for stable rendering keys + a11y ordering.
|
|
118
|
+
*/
|
|
119
|
+
export declare function resolveAnnotations(annotations: ReadonlyArray<ChartAnnotation> | undefined, ctx: AnnotationScaleContext): ResolvedAnnotation[];
|
|
120
|
+
/**
|
|
121
|
+
* Screen-reader descriptions of the annotations, appended after the data values
|
|
122
|
+
* (and any analytical overlays) in the ChartDataList. Each item is
|
|
123
|
+
* `Annotation: <label/text>`; annotations without a label/text are skipped
|
|
124
|
+
* (nothing meaningful to announce). Empty when no annotation is present.
|
|
125
|
+
*/
|
|
126
|
+
export declare function annotationDataListItems(annotations: ReadonlyArray<ChartAnnotation> | undefined): string[];
|
|
127
|
+
/** SVG polygon `points` attribute from resolved pixel vertices. */
|
|
128
|
+
export declare function polygonPoints(points: ReadonlyArray<{
|
|
129
|
+
x: number;
|
|
130
|
+
y: number;
|
|
131
|
+
}>): string;
|
|
132
|
+
//# sourceMappingURL=chartAnnotations.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"chartAnnotations.d.ts","sourceRoot":"","sources":["../src/lib/chartAnnotations.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
// --- Chart annotation layer (shared, framework-agnostic) -------------------
|
|
2
|
+
//
|
|
3
|
+
// A declarative overlay of annotations expressed in DATA space (x / y values),
|
|
4
|
+
// resolved here to framework-agnostic PIXEL primitives. The model is the
|
|
5
|
+
// `ChartAnnotation` discriminated union provided by dataviz-core; the resolver
|
|
6
|
+
// is pure and identical across react / vue / svelte so the three renderers stay
|
|
7
|
+
// in strict parity. Purely additive: an absent/empty `annotations` prop yields
|
|
8
|
+
// no primitives and no a11y items.
|
|
9
|
+
const finite = (v) => typeof v === "number" && Number.isFinite(v);
|
|
10
|
+
/**
|
|
11
|
+
* Resolves DATA-space annotations to ABSOLUTE-pixel primitives. Pure: given the
|
|
12
|
+
* same inputs it always returns the same output, with no DOM/framework access.
|
|
13
|
+
*
|
|
14
|
+
* Region/line on `axis: "x"` are positioned via `xScale` (vertical guide / band
|
|
15
|
+
* spanning the full plot height); on `axis: "y"` via `yScale` (horizontal guide
|
|
16
|
+
* / band spanning the full plot width). Points, labels and shape vertices are
|
|
17
|
+
* positioned via both scales. Any annotation whose required coordinates fall
|
|
18
|
+
* outside the domain (scale returns `null`) or are non-finite is DROPPED, so an
|
|
19
|
+
* annotation never escapes the plot. The original index is preserved as `key`
|
|
20
|
+
* for stable rendering keys + a11y ordering.
|
|
21
|
+
*/
|
|
22
|
+
export function resolveAnnotations(annotations, ctx) {
|
|
23
|
+
if (!annotations || annotations.length === 0)
|
|
24
|
+
return [];
|
|
25
|
+
const { xScale, yScale, plotLeft, plotTop, plotWidth, plotHeight } = ctx;
|
|
26
|
+
const out = [];
|
|
27
|
+
annotations.forEach((a, key) => {
|
|
28
|
+
switch (a.kind) {
|
|
29
|
+
case "region": {
|
|
30
|
+
if (!finite(a.from) || !finite(a.to))
|
|
31
|
+
return;
|
|
32
|
+
if (a.axis === "x") {
|
|
33
|
+
const p1 = xScale(a.from);
|
|
34
|
+
const p2 = xScale(a.to);
|
|
35
|
+
if (p1 === null || p2 === null)
|
|
36
|
+
return;
|
|
37
|
+
const x = plotLeft + Math.min(p1, p2);
|
|
38
|
+
out.push({ kind: "region", key, axis: "x", x, y: plotTop, width: Math.max(Math.abs(p2 - p1), 0.5), height: plotHeight, label: a.label });
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
const p1 = yScale(a.from);
|
|
42
|
+
const p2 = yScale(a.to);
|
|
43
|
+
if (p1 === null || p2 === null)
|
|
44
|
+
return;
|
|
45
|
+
const y = plotTop + Math.min(p1, p2);
|
|
46
|
+
out.push({ kind: "region", key, axis: "y", x: plotLeft, y, width: plotWidth, height: Math.max(Math.abs(p2 - p1), 0.5), label: a.label });
|
|
47
|
+
}
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
case "line": {
|
|
51
|
+
if (!finite(a.value))
|
|
52
|
+
return;
|
|
53
|
+
if (a.axis === "x") {
|
|
54
|
+
const p = xScale(a.value);
|
|
55
|
+
if (p === null)
|
|
56
|
+
return;
|
|
57
|
+
const x = plotLeft + p;
|
|
58
|
+
out.push({ kind: "line", key, axis: "x", x1: x, y1: plotTop, x2: x, y2: plotTop + plotHeight, label: a.label });
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
const p = yScale(a.value);
|
|
62
|
+
if (p === null)
|
|
63
|
+
return;
|
|
64
|
+
const y = plotTop + p;
|
|
65
|
+
out.push({ kind: "line", key, axis: "y", x1: plotLeft, y1: y, x2: plotLeft + plotWidth, y2: y, label: a.label });
|
|
66
|
+
}
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
case "shape": {
|
|
70
|
+
if (!a.points || a.points.length < 2)
|
|
71
|
+
return;
|
|
72
|
+
const pts = [];
|
|
73
|
+
for (const p of a.points) {
|
|
74
|
+
const px = xScale(p.x);
|
|
75
|
+
const py = yScale(p.y);
|
|
76
|
+
if (px === null || py === null)
|
|
77
|
+
return; // a missing vertex invalidates the polygon
|
|
78
|
+
pts.push({ x: plotLeft + px, y: plotTop + py });
|
|
79
|
+
}
|
|
80
|
+
const labelX = pts.reduce((s, p) => s + p.x, 0) / pts.length;
|
|
81
|
+
const labelY = pts.reduce((s, p) => s + p.y, 0) / pts.length;
|
|
82
|
+
out.push({ kind: "shape", key, points: pts, label: a.label, labelX, labelY });
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
case "point": {
|
|
86
|
+
const px = xScale(a.x);
|
|
87
|
+
const py = yScale(a.y);
|
|
88
|
+
if (px === null || py === null)
|
|
89
|
+
return;
|
|
90
|
+
out.push({ kind: "point", key, x: plotLeft + px, y: plotTop + py, label: a.label, marker: a.marker ?? "circle" });
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
case "label": {
|
|
94
|
+
const px = xScale(a.x);
|
|
95
|
+
const py = yScale(a.y);
|
|
96
|
+
if (px === null || py === null)
|
|
97
|
+
return;
|
|
98
|
+
out.push({ kind: "label", key, x: plotLeft + px, y: plotTop + py, text: a.text, anchor: a.anchor ?? "middle" });
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
return out;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Screen-reader descriptions of the annotations, appended after the data values
|
|
107
|
+
* (and any analytical overlays) in the ChartDataList. Each item is
|
|
108
|
+
* `Annotation: <label/text>`; annotations without a label/text are skipped
|
|
109
|
+
* (nothing meaningful to announce). Empty when no annotation is present.
|
|
110
|
+
*/
|
|
111
|
+
export function annotationDataListItems(annotations) {
|
|
112
|
+
if (!annotations || annotations.length === 0)
|
|
113
|
+
return [];
|
|
114
|
+
const items = [];
|
|
115
|
+
for (const a of annotations) {
|
|
116
|
+
const text = a.kind === "label" ? a.text : a.label;
|
|
117
|
+
if (text)
|
|
118
|
+
items.push(`Annotation: ${text}`);
|
|
119
|
+
}
|
|
120
|
+
return items;
|
|
121
|
+
}
|
|
122
|
+
/** SVG polygon `points` attribute from resolved pixel vertices. */
|
|
123
|
+
export function polygonPoints(points) {
|
|
124
|
+
return points.map((p) => `${p.x.toFixed(2)},${p.y.toFixed(2)}`).join(" ");
|
|
125
|
+
}
|