@classic-homes/charts-svelte 0.5.0 → 0.6.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.
@@ -29,6 +29,7 @@
29
29
  seriesLabelLookup,
30
30
  toPercentStack,
31
31
  withPointLabels,
32
+ withGhostSeries,
32
33
  withReferenceMarks,
33
34
  withZoomInset,
34
35
  } from '@classic-homes/charts-core';
@@ -221,7 +222,7 @@
221
222
  formatters.axis(yAxisConfig)
222
223
  ),
223
224
  series: withForecastLines(
224
- withReferenceMarks(series, referenceLines, referenceBands, {
225
+ withReferenceMarks(withGhostSeries(series, data.series), referenceLines, referenceBands, {
225
226
  valueAxis: 'y',
226
227
  format: formatters.format,
227
228
  color: chartColors.reference,
@@ -19,6 +19,7 @@
19
19
  forecastTagLookup,
20
20
  withForecastBars,
21
21
  formatValue,
22
+ ghostStackId,
22
23
  generateBarChartDescription,
23
24
  getChartColors,
24
25
  gridInset,
@@ -31,6 +32,8 @@
31
32
  toPercentStack,
32
33
  withPointLabels,
33
34
  withEndLabelInset,
35
+ withGhostSeries,
36
+ withoutGhostSeries,
34
37
  withReferenceMarks,
35
38
  withZoomInset,
36
39
  } from '@classic-homes/charts-core';
@@ -146,39 +149,48 @@
146
149
  const zoomAxis = isHorizontal ? 'y' : 'x';
147
150
  const legendBottom = builtInLegend && (legendConfig?.position ?? 'bottom') === 'bottom';
148
151
 
149
- const stackIds = data.series.map((s) => s.stack ?? (stacked ? 'Total' : undefined));
150
- const series = data.series.map((source, index) => ({
151
- name: source.name,
152
- type: 'bar',
153
- data: withForecastBars(
154
- withPointLabels(
155
- pairWithCategories(data.categories, plotted[index].data, categoryAxisType),
156
- source.labels,
157
- formatters.label,
158
- { position: labelPosition }
152
+ // Ghost series stack in a parallel group, beside the real stacks
153
+ const stackIds = data.series.map((s) =>
154
+ ghostStackId(s.stack ?? (stacked ? 'Total' : undefined), s)
155
+ );
156
+ const series = withGhostSeries(
157
+ data.series.map((source, index) => ({
158
+ name: source.name,
159
+ type: 'bar',
160
+ data: withForecastBars(
161
+ withPointLabels(
162
+ pairWithCategories(data.categories, plotted[index].data, categoryAxisType),
163
+ source.labels,
164
+ formatters.label,
165
+ { position: labelPosition }
166
+ ),
167
+ source,
168
+ source.color || chartColors.series[index % chartColors.series.length]
159
169
  ),
160
- source,
161
- source.color || chartColors.series[index % chartColors.series.length]
162
- ),
163
- stack: stackIds[index],
164
- itemStyle: {
165
- ...(source.color && { color: source.color }),
166
- borderRadius: barCornerRadius(isHorizontal, stackIds, index),
167
- },
168
- label: buildDataLabel({ show: true, position: labelPosition }, undefined, {
169
- show: showValues,
170
- formatter: formatters.label,
171
- getLabel,
172
- }),
173
- emphasis: {
174
- focus: 'series',
175
- },
176
- }));
170
+ stack: stackIds[index],
171
+ itemStyle: {
172
+ ...(source.color && { color: source.color }),
173
+ borderRadius: barCornerRadius(isHorizontal, stackIds, index),
174
+ },
175
+ label: buildDataLabel({ show: true, position: labelPosition }, undefined, {
176
+ show: showValues,
177
+ formatter: formatters.label,
178
+ getLabel,
179
+ }),
180
+ emphasis: {
181
+ focus: 'series',
182
+ },
183
+ })),
184
+ data.series
185
+ );
186
+ // Ghost series count toward neither totals nor end labels
187
+ const solid = withoutGhostSeries(data.series);
188
+ const solidPlotted = plotted.filter((_, index) => data.series[index].variant !== 'ghost');
177
189
 
178
190
  // Totals use the value format, not the percent one: they show the raw sum
179
191
  const format = resolveValueFormatter(valueFormat, locale);
180
192
  const totalSeries = showTotal
181
- ? buildStackTotalSeries(data.categories, data.series, {
193
+ ? buildStackTotalSeries(data.categories, solid, {
182
194
  stacked,
183
195
  position: labelPosition,
184
196
  format,
@@ -190,9 +202,11 @@
190
202
  const endLabels = !isHorizontal
191
203
  ? []
192
204
  : totalSeries.length
193
- ? [...stackTotals(data.series, stacked).values()].flat().map((t) => formatValue(t, format))
205
+ ? [...stackTotals(solid, stacked).values()].flat().map((t) => formatValue(t, format))
194
206
  : showValues
195
- ? plotted.flatMap((s) => s.data.map((value) => formatValue(value, formatters.format)))
207
+ ? solidPlotted.flatMap((s) =>
208
+ s.data.map((value) => formatValue(value, formatters.format))
209
+ )
196
210
  : [];
197
211
 
198
212
  return mergeChartOption(
@@ -28,6 +28,7 @@
28
28
  seriesLabelLookup,
29
29
  toPercentStack,
30
30
  withPointLabels,
31
+ withGhostSeries,
31
32
  withReferenceMarks,
32
33
  withZoomInset,
33
34
  } from '@classic-homes/charts-core';
@@ -212,7 +213,7 @@
212
213
  formatters.axis(yAxisConfig)
213
214
  ),
214
215
  series: withForecastLines(
215
- withReferenceMarks(series, referenceLines, referenceBands, {
216
+ withReferenceMarks(withGhostSeries(series, data.series), referenceLines, referenceBands, {
216
217
  valueAxis: 'y',
217
218
  format: formatters.format,
218
219
  color: chartColors.reference,
@@ -1,4 +1,4 @@
1
1
  export { ChartContainer, ChartSkeleton, ChartError, ChartEmpty, ChartLegend, type ChartContainerProps, type ChartLegendProps, type ChartSkeletonProps, type ChartErrorProps, type ChartEmptyProps, ChartDataTable, type ChartDataTableProps, } from './base/index.js';
2
2
  export { LineChart, BarChart, PieChart, AreaChart, ScatterChart, DonutChart, } from './core/index.js';
3
3
  export { RadarChart, FunnelChart, GaugeChart, HeatmapChart, TreemapChart, CandlestickChart, SankeyChart, } from './extended/index.js';
4
- export { ComboChart, DotPlotChart, BulletChart, HistogramChart, BoxPlotChart, WaterfallChart, Sparkline, StatCard, StatGroup, type StatGroupProps, } from './report/index.js';
4
+ export { ComboChart, DotPlotChart, BulletChart, HistogramChart, BoxPlotChart, WaterfallChart, Sparkline, StatCard, DeltaText, StatGroup, type StatGroupProps, } from './report/index.js';
@@ -5,4 +5,4 @@ export { LineChart, BarChart, PieChart, AreaChart, ScatterChart, DonutChart, } f
5
5
  // Extended charts
6
6
  export { RadarChart, FunnelChart, GaugeChart, HeatmapChart, TreemapChart, CandlestickChart, SankeyChart, } from './extended/index.js';
7
7
  // Report charts and stats
8
- export { ComboChart, DotPlotChart, BulletChart, HistogramChart, BoxPlotChart, WaterfallChart, Sparkline, StatCard, StatGroup, } from './report/index.js';
8
+ export { ComboChart, DotPlotChart, BulletChart, HistogramChart, BoxPlotChart, WaterfallChart, Sparkline, StatCard, DeltaText, StatGroup, } from './report/index.js';
@@ -26,6 +26,7 @@
26
26
  valueAxisConfig,
27
27
  valueLabelFormatter,
28
28
  withPointLabels,
29
+ withGhostSeries,
29
30
  withReferenceMarks,
30
31
  withZoomInset,
31
32
  } from '@classic-homes/charts-core';
@@ -243,12 +244,17 @@
243
244
  ]
244
245
  : []),
245
246
  ],
246
- series: withReferenceMarks(series, referenceLines, referenceBands, {
247
- valueAxis: 'y',
248
- format: formatters.format,
249
- color: chartColors.reference,
250
- bandColor: chartColors.referenceBand,
251
- }),
247
+ series: withReferenceMarks(
248
+ withGhostSeries(series, data.series),
249
+ referenceLines,
250
+ referenceBands,
251
+ {
252
+ valueAxis: 'y',
253
+ format: formatters.format,
254
+ color: chartColors.reference,
255
+ bandColor: chartColors.referenceBand,
256
+ }
257
+ ),
252
258
  } as EChartsOption,
253
259
  optionOverride
254
260
  );
@@ -0,0 +1,72 @@
1
+ <script lang="ts">
2
+ import type { DeltaTextProps } from '@classic-homes/charts-core';
3
+ import { computeDelta, formatValue, resolveValueFormatter } from '@classic-homes/charts-core';
4
+ import { cn } from '../../utils.js';
5
+
6
+ let {
7
+ current,
8
+ previous,
9
+ display = 'percent',
10
+ goodWhen = 'up',
11
+ valueFormat,
12
+ locale = 'en-US',
13
+ label,
14
+ class: className,
15
+ }: DeltaTextProps = $props();
16
+
17
+ const format = $derived(resolveValueFormatter(valueFormat, locale));
18
+
19
+ const change = $derived(computeDelta(current, previous));
20
+
21
+ const percentFormat = $derived(
22
+ new Intl.NumberFormat(locale, {
23
+ style: 'percent',
24
+ maximumFractionDigits: 1,
25
+ signDisplay: 'exceptZero',
26
+ })
27
+ );
28
+
29
+ const changeText = $derived.by(() => {
30
+ if (!change) return '';
31
+ const sign = change.change > 0 ? '+' : change.change < 0 ? '−' : '';
32
+ const absolute = `${sign}${formatValue(Math.abs(change.change), format)}`;
33
+ const percent = change.ratio === null ? null : percentFormat.format(change.ratio);
34
+ switch (display) {
35
+ case 'absolute':
36
+ return absolute;
37
+ case 'both':
38
+ return percent ? `${absolute} (${percent})` : absolute;
39
+ default:
40
+ // A change from zero has no percentage
41
+ return percent ?? absolute;
42
+ }
43
+ });
44
+
45
+ const changeTone = $derived.by(() => {
46
+ if (!change || change.direction === 'flat' || goodWhen === 'none') {
47
+ return 'text-muted-foreground';
48
+ }
49
+ return goodWhen === change.direction ? 'text-success' : 'text-destructive';
50
+ });
51
+
52
+ const labelText = $derived(
53
+ label ?? (typeof previous === 'number' ? `vs ${formatValue(previous, format)}` : '')
54
+ );
55
+ </script>
56
+
57
+ {#if change}
58
+ <span
59
+ data-testid="delta-text"
60
+ class={cn('inline-flex flex-wrap items-baseline gap-x-1 text-xs', className)}
61
+ >
62
+ <span class={cn('font-medium tabular-nums', changeTone)}>
63
+ <span aria-hidden="true">
64
+ {change.direction === 'up' ? '▲' : change.direction === 'down' ? '▼' : ''}
65
+ </span>
66
+ {changeText}
67
+ </span>
68
+ {#if labelText}
69
+ <span class="text-muted-foreground">{labelText}</span>
70
+ {/if}
71
+ </span>
72
+ {/if}
@@ -0,0 +1,4 @@
1
+ import type { DeltaTextProps } from '@classic-homes/charts-core';
2
+ declare const DeltaText: import("svelte").Component<DeltaTextProps, {}, "">;
3
+ type DeltaText = ReturnType<typeof DeltaText>;
4
+ export default DeltaText;
@@ -1,7 +1,8 @@
1
1
  <script lang="ts">
2
2
  import type { StatCardProps } from '@classic-homes/charts-core';
3
- import { computeDelta, formatValue, resolveValueFormatter } from '@classic-homes/charts-core';
3
+ import { formatValue, resolveValueFormatter } from '@classic-homes/charts-core';
4
4
  import { cn } from '../../utils.js';
5
+ import DeltaText from './DeltaText.svelte';
5
6
  import Sparkline from './Sparkline.svelte';
6
7
 
7
8
  let {
@@ -27,47 +28,7 @@
27
28
  typeof value === 'string' ? value : formatValue(value ?? null, format, emptyValue)
28
29
  );
29
30
 
30
- const change = $derived(
31
- delta
32
- ? computeDelta(delta.current ?? (typeof value === 'number' ? value : null), delta.previous)
33
- : null
34
- );
35
-
36
- const percentFormat = $derived(
37
- new Intl.NumberFormat(locale, {
38
- style: 'percent',
39
- maximumFractionDigits: 1,
40
- signDisplay: 'exceptZero',
41
- })
42
- );
43
-
44
- const changeText = $derived.by(() => {
45
- if (!change || !delta) return '';
46
- const sign = change.change > 0 ? '+' : change.change < 0 ? '−' : '';
47
- const absolute = `${sign}${formatValue(Math.abs(change.change), format)}`;
48
- const percent = change.ratio === null ? null : percentFormat.format(change.ratio);
49
- switch (delta.display ?? 'percent') {
50
- case 'absolute':
51
- return absolute;
52
- case 'both':
53
- return percent ? `${absolute} (${percent})` : absolute;
54
- default:
55
- // A change from zero has no percentage
56
- return percent ?? absolute;
57
- }
58
- });
59
-
60
- const changeTone = $derived.by(() => {
61
- if (!change || change.direction === 'flat' || delta?.goodWhen === 'none') {
62
- return 'text-muted-foreground';
63
- }
64
- const good = (delta?.goodWhen ?? 'up') === change.direction;
65
- return good ? 'text-success' : 'text-destructive';
66
- });
67
-
68
- const previousText = $derived(
69
- delta && typeof delta.previous === 'number' ? formatValue(delta.previous, format) : ''
70
- );
31
+ const deltaCurrent = $derived(delta?.current ?? (typeof value === 'number' ? value : null));
71
32
 
72
33
  const valueTone = $derived(
73
34
  {
@@ -114,18 +75,17 @@
114
75
  <div class="mt-1 flex items-end justify-between gap-3">
115
76
  <div class="min-w-0">
116
77
  <div class={cn('text-2xl font-semibold tabular-nums', valueTone)}>{display}</div>
117
- {#if changeText}
118
- <div class="mt-1 flex flex-wrap items-baseline gap-x-1 text-xs">
119
- <span class={cn('font-medium tabular-nums', changeTone)}>
120
- <span aria-hidden="true">
121
- {change?.direction === 'up' ? '▲' : change?.direction === 'down' ? '▼' : ''}
122
- </span>
123
- {changeText}
124
- </span>
125
- <span class="text-muted-foreground">
126
- {delta?.label ?? (previousText ? `vs ${previousText}` : '')}
127
- </span>
128
- </div>
78
+ {#if delta}
79
+ <DeltaText
80
+ class="mt-1 flex"
81
+ current={deltaCurrent}
82
+ previous={delta.previous}
83
+ display={delta.display}
84
+ goodWhen={delta.goodWhen}
85
+ {valueFormat}
86
+ {locale}
87
+ label={delta.label}
88
+ />
129
89
  {/if}
130
90
  {#if hint}
131
91
  <p class="mt-1 text-xs text-muted-foreground">{hint}</p>
@@ -6,6 +6,7 @@ export { default as BoxPlotChart } from './BoxPlotChart.svelte';
6
6
  export { default as WaterfallChart } from './WaterfallChart.svelte';
7
7
  export { default as Sparkline } from './Sparkline.svelte';
8
8
  export { default as StatCard } from './StatCard.svelte';
9
+ export { default as DeltaText } from './DeltaText.svelte';
9
10
  export { default as StatGroup } from './StatGroup.svelte';
10
11
  import type { ComponentProps } from 'svelte';
11
12
  import type StatGroup from './StatGroup.svelte';
@@ -6,4 +6,5 @@ export { default as BoxPlotChart } from './BoxPlotChart.svelte';
6
6
  export { default as WaterfallChart } from './WaterfallChart.svelte';
7
7
  export { default as Sparkline } from './Sparkline.svelte';
8
8
  export { default as StatCard } from './StatCard.svelte';
9
+ export { default as DeltaText } from './DeltaText.svelte';
9
10
  export { default as StatGroup } from './StatGroup.svelte';
@@ -3,8 +3,8 @@
3
3
  *
4
4
  * Svelte ECharts components for the Classic Theme design system
5
5
  */
6
- export { ChartContainer, ChartSkeleton, ChartError, ChartEmpty, ChartLegend, type ChartContainerProps, type ChartLegendProps, type ChartSkeletonProps, type ChartErrorProps, type ChartEmptyProps, ChartDataTable, type ChartDataTableProps, LineChart, BarChart, PieChart, AreaChart, ScatterChart, DonutChart, RadarChart, FunnelChart, GaugeChart, HeatmapChart, TreemapChart, CandlestickChart, SankeyChart, ComboChart, DotPlotChart, BulletChart, HistogramChart, BoxPlotChart, WaterfallChart, Sparkline, StatCard, StatGroup, type StatGroupProps, } from './components/index.js';
6
+ export { ChartContainer, ChartSkeleton, ChartError, ChartEmpty, ChartLegend, type ChartContainerProps, type ChartLegendProps, type ChartSkeletonProps, type ChartErrorProps, type ChartEmptyProps, ChartDataTable, type ChartDataTableProps, LineChart, BarChart, PieChart, AreaChart, ScatterChart, DonutChart, RadarChart, FunnelChart, GaugeChart, HeatmapChart, TreemapChart, CandlestickChart, SankeyChart, ComboChart, DotPlotChart, BulletChart, HistogramChart, BoxPlotChart, WaterfallChart, Sparkline, StatCard, DeltaText, StatGroup, type StatGroupProps, } from './components/index.js';
7
7
  export { useChart, exportChartImage, registerEchartsModules, type ExportImageOptions, useChartTheme, useChartResize, registerClassicThemes, useReducedMotion, useChartLegend, type CustomLegendProps, type UseChartLegendOptions, type UseChartLegendReturn, type UseChartOptions, type UseChartReturn, } from './composables/index.js';
8
8
  export { cn } from './utils.js';
9
9
  export { fromRecords, toSliceData, resolveValueFormatter, formatValue, computeDelta, binValues, boxplotStats, waterfallLayout, toPercentStack, tableToCsv, downloadCsv, chartFilename, type FromRecordsOptions, type ToSliceDataOptions, type Delta, } from '@classic-homes/charts-core';
10
- export type { BaseChartProps, LegendChartProps, LabelChartProps, AxisChartProps, AxisConfig, ChartFormatter, ChartLegendConfig, ChartLegendPosition, LabelFormatterParams, TooltipFormatterParams, LegendItem, LegendRenderProps, LineChartProps, BarChartProps, PieChartProps, DonutChartProps, AreaChartProps, ScatterChartProps, RadarChartProps, FunnelChartProps, GaugeChartProps, HeatmapChartProps, TreemapChartProps, CandlestickChartProps, SankeyChartProps, LineChartData, LineSeriesData, BarChartData, BarSeriesData, PieSliceData, AreaChartData, AreaSeriesData, ScatterChartData, ScatterSeriesData, RadarChartData, RadarSeriesData, RadarIndicator, FunnelSliceData, HeatmapData, TreemapNode, TreemapLevel, CandlestickData, CandlestickItem, SankeyData, SankeyNode, SankeyLink, BaseEventParams, DataPointEventParams, SliceEventParams, HeatmapEventParams, TreemapEventParams, CandlestickEventParams, SankeyEventParams, ChartEventHandlers, ChartExportConfig, ReferenceChartProps, ZoomChartProps, RowHeightProps, ComboChartProps, DotPlotChartProps, BulletChartProps, HistogramChartProps, BoxPlotChartProps, WaterfallChartProps, SparklineProps, StatCardProps, StatDelta, CategoryChartData, CategorySeriesData, ComboChartData, ComboSeriesData, DotPlotChartData, DotPlotSeriesData, BulletItem, HistogramBin, HistogramBucket, BoxPlotGroup, BoxPlotStats, WaterfallStep, ValueFormat, ValueFormatPreset, ReferenceLine, ReferenceBand, ChartDataTable as ChartDataTableData, } from '@classic-homes/charts-core';
10
+ export type { BaseChartProps, LegendChartProps, LabelChartProps, AxisChartProps, AxisConfig, ChartFormatter, ChartLegendConfig, ChartLegendPosition, LabelFormatterParams, TooltipFormatterParams, LegendItem, LegendRenderProps, LineChartProps, BarChartProps, PieChartProps, DonutChartProps, AreaChartProps, ScatterChartProps, RadarChartProps, FunnelChartProps, GaugeChartProps, HeatmapChartProps, TreemapChartProps, CandlestickChartProps, SankeyChartProps, LineChartData, LineSeriesData, BarChartData, BarSeriesData, PieSliceData, AreaChartData, AreaSeriesData, ScatterChartData, ScatterSeriesData, RadarChartData, RadarSeriesData, RadarIndicator, FunnelSliceData, HeatmapData, TreemapNode, TreemapLevel, CandlestickData, CandlestickItem, SankeyData, SankeyNode, SankeyLink, BaseEventParams, DataPointEventParams, SliceEventParams, HeatmapEventParams, TreemapEventParams, CandlestickEventParams, SankeyEventParams, ChartEventHandlers, ChartExportConfig, ReferenceChartProps, ZoomChartProps, RowHeightProps, ComboChartProps, DotPlotChartProps, BulletChartProps, HistogramChartProps, BoxPlotChartProps, WaterfallChartProps, SparklineProps, StatCardProps, StatDelta, DeltaTextProps, CategoryChartData, CategorySeriesData, ComboChartData, ComboSeriesData, DotPlotChartData, DotPlotSeriesData, BulletItem, HistogramBin, HistogramBucket, BoxPlotGroup, BoxPlotStats, WaterfallStep, ValueFormat, ValueFormatPreset, ReferenceLine, ReferenceBand, ChartDataTable as ChartDataTableData, } from '@classic-homes/charts-core';
package/dist/lib/index.js CHANGED
@@ -12,7 +12,7 @@ LineChart, BarChart, PieChart, AreaChart, ScatterChart, DonutChart,
12
12
  // Extended charts
13
13
  RadarChart, FunnelChart, GaugeChart, HeatmapChart, TreemapChart, CandlestickChart, SankeyChart,
14
14
  // Report charts and stats
15
- ComboChart, DotPlotChart, BulletChart, HistogramChart, BoxPlotChart, WaterfallChart, Sparkline, StatCard, StatGroup, } from './components/index.js';
15
+ ComboChart, DotPlotChart, BulletChart, HistogramChart, BoxPlotChart, WaterfallChart, Sparkline, StatCard, DeltaText, StatGroup, } from './components/index.js';
16
16
  // Composables
17
17
  export { useChart, exportChartImage, registerEchartsModules, useChartTheme, useChartResize, registerClassicThemes, useReducedMotion, useChartLegend, } from './composables/index.js';
18
18
  // Utils
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@classic-homes/charts-svelte",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "Svelte ECharts components for Classic Theme",
5
5
  "type": "module",
6
6
  "svelte": "./dist/lib/index.js",