nuxt-charts 3.0.0-beta.3 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/README.md +46 -1
  2. package/dist/module.json +1 -1
  3. package/dist/module.mjs +0 -1
  4. package/dist/runtime/assets/components.css +1 -1
  5. package/dist/runtime/assets/theme.css +1 -1
  6. package/dist/runtime/components/AreaChart.vue +7 -1
  7. package/dist/runtime/components/BarChart.vue +7 -1
  8. package/dist/runtime/components/BubbleChart.vue +43 -17
  9. package/dist/runtime/components/CandlestickChart.vue +60 -18
  10. package/dist/runtime/components/DonutChart.vue +28 -9
  11. package/dist/runtime/components/FunnelChart.vue +31 -8
  12. package/dist/runtime/components/LineChart.vue +7 -1
  13. package/dist/runtime/components/RadarChart.vue +25 -4
  14. package/dist/runtime/components/RadialBarChart.vue +48 -8
  15. package/dist/runtime/components/SankeyChart.vue +32 -9
  16. package/dist/runtime/components/StatusTrackerChart.vue +33 -7
  17. package/dist/runtime/components/internal/CandlestickTooltip.vue +6 -4
  18. package/dist/runtime/components/internal/CandlestickTooltip.vue.d.ts +1 -0
  19. package/dist/runtime/components/internal/CartesianFrame.vue +37 -9
  20. package/dist/runtime/components/internal/ChartAccessibility.vue +20 -0
  21. package/dist/runtime/components/internal/{ChartSkeleton.d.vue.ts → ChartAccessibility.vue.d.ts} +13 -10
  22. package/dist/runtime/components/internal/ChartEmptyState.vue +12 -0
  23. package/dist/runtime/components/internal/{CandlestickTooltip.d.vue.ts → ChartEmptyState.vue.d.ts} +2 -12
  24. package/dist/runtime/components/internal/ChartLegend.vue.d.ts +1 -1
  25. package/dist/runtime/components/internal/ChartTooltip.vue +2 -1
  26. package/dist/runtime/components/internal/CubeBarShape.vue.d.ts +1 -1
  27. package/dist/runtime/components/internal/FunnelClassicShape.vue +2 -1
  28. package/dist/runtime/components/internal/FunnelLayeredShape.vue +2 -1
  29. package/dist/runtime/components/internal/FunnelTooltip.vue +2 -1
  30. package/dist/runtime/components/internal/tooltipContent.d.ts +1 -1
  31. package/dist/runtime/components/internal/tooltipContent.js +6 -2
  32. package/dist/runtime/types/charts.d.ts +44 -34
  33. package/dist/runtime/types/shared.d.ts +5 -11
  34. package/dist/runtime/utils/categories.d.ts +1 -1
  35. package/dist/runtime/utils/categories.js +2 -2
  36. package/dist/runtime/utils/data.d.ts +19 -0
  37. package/dist/runtime/utils/data.js +50 -0
  38. package/dist/runtime/utils/format.d.ts +5 -0
  39. package/dist/runtime/utils/format.js +12 -0
  40. package/dist/runtime/utils/index.d.ts +2 -0
  41. package/dist/runtime/utils/index.js +2 -0
  42. package/dist/runtime/utils/theme.js +3 -0
  43. package/package.json +2 -2
  44. package/dist/runtime/components/AreaChart.d.vue.ts +0 -23
  45. package/dist/runtime/components/BarChart.d.vue.ts +0 -23
  46. package/dist/runtime/components/BubbleChart.d.vue.ts +0 -23
  47. package/dist/runtime/components/CandlestickChart.d.vue.ts +0 -19
  48. package/dist/runtime/components/DonutChart.d.vue.ts +0 -21
  49. package/dist/runtime/components/FunnelChart.d.vue.ts +0 -26
  50. package/dist/runtime/components/LineChart.d.vue.ts +0 -23
  51. package/dist/runtime/components/RadarChart.d.vue.ts +0 -19
  52. package/dist/runtime/components/RadialBarChart.d.vue.ts +0 -19
  53. package/dist/runtime/components/SankeyChart.d.vue.ts +0 -36
  54. package/dist/runtime/components/StatusTrackerChart.d.vue.ts +0 -19
  55. package/dist/runtime/components/internal/CartesianFrame.d.vue.ts +0 -57
  56. package/dist/runtime/components/internal/ChartLegend.d.vue.ts +0 -34
  57. package/dist/runtime/components/internal/ChartTooltip.d.vue.ts +0 -3
  58. package/dist/runtime/components/internal/CubeBarShape.d.vue.ts +0 -36
  59. package/dist/runtime/components/internal/FunnelClassicShape.d.vue.ts +0 -81
  60. package/dist/runtime/components/internal/FunnelLayeredShape.d.vue.ts +0 -63
  61. package/dist/runtime/components/internal/FunnelTooltip.d.vue.ts +0 -16
  62. package/dist/runtime/components/internal/VariantBarShape.d.vue.ts +0 -36
@@ -8,6 +8,8 @@ import {
8
8
  import ChartContainer from "./internal/ChartContainer";
9
9
  import ChartSkeleton from "./internal/ChartSkeleton.vue";
10
10
  import ChartLegend from "./internal/ChartLegend.vue";
11
+ import ChartEmptyState from "./internal/ChartEmptyState.vue";
12
+ import ChartAccessibility from "./internal/ChartAccessibility.vue";
11
13
  import FunnelClassicShape from "./internal/FunnelClassicShape.vue";
12
14
  import FunnelTooltip from "./internal/FunnelTooltip.vue";
13
15
  import FunnelLayeredShape, {
@@ -16,8 +18,11 @@ import { categoriesToSeries } from "../utils/categories";
16
18
  import { legendPositionToLegendProps, resolveLegendWrapperStyle } from "../utils/legend";
17
19
  import { toCssProperties } from "../utils/style";
18
20
  import { themeToVars } from "../utils/theme";
21
+ import { DEFAULT_MAX_DATA_POINTS, normalizeNamedValues, sampleData, warnDataLimit } from "../utils/data";
19
22
  const props = defineProps({
20
23
  data: { type: Array, required: true },
24
+ nameKey: { type: null, required: false },
25
+ valueKey: { type: null, required: false },
21
26
  categories: { type: Object, required: true },
22
27
  height: { type: Number, required: true },
23
28
  variant: { type: String, required: false },
@@ -33,9 +38,14 @@ const props = defineProps({
33
38
  legendStyle: { type: [String, Object], required: false },
34
39
  hideTooltip: { type: Boolean, required: false },
35
40
  tooltipTitleFormatter: { type: Function, required: false },
36
- tooltip: { type: Object, required: false },
37
41
  theme: { type: Object, required: false },
38
42
  duration: { type: Number, required: false },
43
+ ariaLabel: { type: String, required: false },
44
+ ariaDescription: { type: String, required: false },
45
+ accessibleDataTable: { type: Boolean, required: false, default: true },
46
+ maxDataPoints: { type: Number, required: false },
47
+ emptyLabel: { type: String, required: false },
48
+ error: { type: String, required: false },
39
49
  legendVariant: { type: String, required: false },
40
50
  tooltipVariant: { type: String, required: false },
41
51
  tooltipRoundness: { type: String, required: false },
@@ -46,14 +56,19 @@ defineSlots();
46
56
  const themeVars = computed(() => themeToVars(props.theme));
47
57
  const stages = computed(() => {
48
58
  const cats = categoriesToSeries(props.categories);
49
- return props.data.map((value, i) => ({
50
- name: cats[i]?.name ?? String(i),
59
+ const normalized = normalizeNamedValues(props.data, cats.map((cat) => String(cat.name)), props.nameKey, props.valueKey);
60
+ const limit = props.maxDataPoints ?? DEFAULT_MAX_DATA_POINTS;
61
+ warnDataLimit("FunnelChart", normalized.length, limit);
62
+ return sampleData(normalized, limit).map(({ row, value, sourceIndex, name }) => ({
63
+ ...row && typeof row === "object" ? row : {},
64
+ name,
51
65
  value,
52
- formattedValue: props.valueFormatter?.(value, i),
53
- fill: cats[i]?.color ?? `var(--chart-color-${i})`,
54
- color: cats[i]?.color ?? `var(--chart-color-${i})`
66
+ formattedValue: props.valueFormatter?.(value, sourceIndex),
67
+ fill: cats[sourceIndex]?.color ?? `var(--vc-series-${sourceIndex % 8})`,
68
+ color: cats[sourceIndex]?.color ?? `var(--vc-series-${sourceIndex % 8})`
55
69
  }));
56
70
  });
71
+ const accessibleRows = computed(() => stages.value.map((stage) => ({ label: stage.name, values: [{ label: "Value", value: stage.formattedValue ?? stage.value }] })));
57
72
  const isLayered = computed(() => props.variant === "layered");
58
73
  const showValueLabel = computed(() => props.showValueLabel ?? true);
59
74
  const NAMED_LABEL_SIZES = { sm: 10, md: 12, lg: 14 };
@@ -112,7 +127,7 @@ function updateTooltipPosition(event) {
112
127
  };
113
128
  }
114
129
  function showTooltip(event, datum) {
115
- activeDatum.value = datum;
130
+ activeDatum.value = props.tooltipTitleFormatter ? { ...datum, name: String(props.tooltipTitleFormatter(datum)) } : datum;
116
131
  updateTooltipPosition(event);
117
132
  }
118
133
  function clearTooltip() {
@@ -125,14 +140,18 @@ function clearTooltip() {
125
140
  v-if="isLayered"
126
141
  ref="chartContainer"
127
142
  class="vc-funnel-layered vue-chrts"
143
+ role="group"
144
+ :aria-label="ariaLabel ?? 'Funnel chart'"
128
145
  :style="{ height: `${height}px`, ...themeVars }"
129
146
  >
147
+ <ChartAccessibility :label="ariaLabel ?? 'Funnel chart'" :description="ariaDescription" :rows="accessibleRows" :show-table="accessibleDataTable !== false" />
130
148
  <ChartSkeleton
131
149
  v-if="loading"
132
150
  :height="height"
133
151
  shape="bars"
134
152
  :label="loadingLabel ?? 'Loading'"
135
153
  />
154
+ <ChartEmptyState v-else-if="error || stages.length === 0" :height="height" :message="error || emptyLabel" />
136
155
  <ChartContainer v-else width="100%" height="100%">
137
156
  <VccsFunnelChart :margin="{ top: 16, right: 0, bottom: 8, left: 0 }">
138
157
  <Funnel
@@ -142,6 +161,7 @@ function clearTooltip() {
142
161
  last-shape-type="rectangle"
143
162
  stroke="none"
144
163
  :is-animation-active="duration !== void 0 && duration !== 0"
164
+ :transition="{ duration: (duration ?? 800) / 1e3, ease: 'easeOut' }"
145
165
  >
146
166
  <template #shape="shapeProps">
147
167
  <!--
@@ -180,13 +200,15 @@ function clearTooltip() {
180
200
  />
181
201
  </div>
182
202
 
183
- <div v-else ref="chartContainer" class="vc-funnel vue-chrts" :style="themeVars">
203
+ <div v-else ref="chartContainer" class="vc-funnel vue-chrts" role="group" :aria-label="ariaLabel ?? 'Funnel chart'" :style="themeVars">
204
+ <ChartAccessibility :label="ariaLabel ?? 'Funnel chart'" :description="ariaDescription" :rows="accessibleRows" :show-table="accessibleDataTable !== false" />
184
205
  <ChartSkeleton
185
206
  v-if="loading"
186
207
  :height="height"
187
208
  shape="bars"
188
209
  :label="loadingLabel ?? 'Loading'"
189
210
  />
211
+ <ChartEmptyState v-else-if="error || stages.length === 0" :height="height" :message="error || emptyLabel" />
190
212
  <ChartContainer v-else width="100%" :height="height">
191
213
  <VccsFunnelChart>
192
214
  <Funnel
@@ -196,6 +218,7 @@ function clearTooltip() {
196
218
  :last-shape-type="lastShapeType ?? 'triangle'"
197
219
  stroke="none"
198
220
  :is-animation-active="duration !== void 0 && duration !== 0"
221
+ :transition="{ duration: (duration ?? 800) / 1e3, ease: 'easeOut' }"
199
222
  >
200
223
  <template #shape="shapeProps">
201
224
  <slot
@@ -52,7 +52,6 @@ const props = defineProps({
52
52
  xAxisConfig: { type: Object, required: false },
53
53
  yAxisConfig: { type: Object, required: false },
54
54
  yAxes: { type: Object, required: false },
55
- tooltip: { type: Object, required: false },
56
55
  referenceLines: { type: Array, required: false },
57
56
  syncId: { type: String, required: false },
58
57
  minMaxTicksOnly: { type: Boolean, required: false },
@@ -61,6 +60,12 @@ const props = defineProps({
61
60
  crosshairConfig: { type: Object, required: false },
62
61
  theme: { type: Object, required: false },
63
62
  backgroundPattern: { type: String, required: false },
63
+ ariaLabel: { type: String, required: false },
64
+ ariaDescription: { type: String, required: false },
65
+ accessibleDataTable: { type: Boolean, required: false, default: true },
66
+ maxDataPoints: { type: Number, required: false },
67
+ emptyLabel: { type: String, required: false },
68
+ error: { type: String, required: false },
64
69
  legendVariant: { type: String, required: false },
65
70
  tooltipVariant: { type: String, required: false },
66
71
  tooltipRoundness: { type: String, required: false },
@@ -134,6 +139,7 @@ defineSlots();
134
139
  :dot="showDots || s.dot"
135
140
  :hide="s.hidden"
136
141
  :is-animation-active="duration !== void 0 && duration !== 0"
142
+ :transition="{ duration: (duration ?? 800) / 1e3, ease: 'easeOut' }"
137
143
  >
138
144
  <template v-if="showDots" #dot="dotProps">
139
145
  <ChartDot
@@ -12,6 +12,8 @@ import {
12
12
  import ChartContainer from "./internal/ChartContainer";
13
13
  import ChartSkeleton from "./internal/ChartSkeleton.vue";
14
14
  import ChartLegend from "./internal/ChartLegend.vue";
15
+ import ChartEmptyState from "./internal/ChartEmptyState.vue";
16
+ import ChartAccessibility from "./internal/ChartAccessibility.vue";
15
17
  import ChartDot from "./internal/ChartDot";
16
18
  import { tooltipContentFor } from "./internal/tooltipContent";
17
19
  import PolarCenterSync from "./internal/PolarCenterSync";
@@ -20,6 +22,7 @@ import { categoriesToSeries } from "../utils/categories";
20
22
  import { legendPositionToLegendProps, resolveLegendWrapperStyle } from "../utils/legend";
21
23
  import { toCssProperties } from "../utils/style";
22
24
  import { themeToVars } from "../utils/theme";
25
+ import { DEFAULT_MAX_DATA_POINTS, normalizeNumericRows, sampleData, warnDataLimit } from "../utils/data";
23
26
  const props = defineProps({
24
27
  data: { type: Array, required: true },
25
28
  height: { type: Number, required: true },
@@ -41,9 +44,14 @@ const props = defineProps({
41
44
  outerRadius: { type: [Number, String], required: false },
42
45
  angleAxisTickSize: { type: Number, required: false },
43
46
  margin: { type: Object, required: false },
44
- tooltip: { type: Object, required: false },
45
47
  theme: { type: Object, required: false },
46
48
  duration: { type: Number, required: false },
49
+ ariaLabel: { type: String, required: false },
50
+ ariaDescription: { type: String, required: false },
51
+ accessibleDataTable: { type: Boolean, required: false, default: true },
52
+ maxDataPoints: { type: Number, required: false },
53
+ emptyLabel: { type: String, required: false },
54
+ error: { type: String, required: false },
47
55
  legendVariant: { type: String, required: false },
48
56
  tooltipVariant: { type: String, required: false },
49
57
  tooltipRoundness: { type: String, required: false },
@@ -52,6 +60,16 @@ const props = defineProps({
52
60
  });
53
61
  const gradientScope = useId();
54
62
  const series = computed(() => categoriesToSeries(props.categories).filter((s) => !s.hidden));
63
+ const chartData = computed(() => {
64
+ const normalized = normalizeNumericRows(props.data, series.value.map((item) => item.dataKey));
65
+ const limit = props.maxDataPoints ?? DEFAULT_MAX_DATA_POINTS;
66
+ warnDataLimit("RadarChart", normalized.length, limit);
67
+ return sampleData(normalized, limit);
68
+ });
69
+ const accessibleRows = computed(() => chartData.value.map((row, index) => ({
70
+ label: String(row[props.dataKey] ?? index + 1),
71
+ values: series.value.map((item) => ({ label: String(item.name), value: row[item.dataKey] }))
72
+ })));
55
73
  const polygonFillOpacity = computed(
56
74
  () => props.variant === "lines" ? 0 : isGradient.value ? 1 : props.fillOpacity ?? 0.6
57
75
  );
@@ -63,7 +81,7 @@ function polygonFill(dataKey, color) {
63
81
  }
64
82
  const showDots = computed(() => props.dotVariant !== void 0);
65
83
  const tooltipContent = computed(
66
- () => tooltipContentFor(props.tooltipVariant, props.tooltipRoundness)
84
+ () => tooltipContentFor(props.tooltipVariant, props.tooltipRoundness, props.tooltipTitleFormatter)
67
85
  );
68
86
  const angleKey = computed(() => String(props.dataKey));
69
87
  const legend = computed(() => legendPositionToLegendProps(props.legendPosition));
@@ -89,16 +107,18 @@ const resolvedOuterRadius = computed(() => {
89
107
  </script>
90
108
 
91
109
  <template>
92
- <div class="vue-chrts" :style="themeVars">
110
+ <div class="vue-chrts" role="group" :aria-label="ariaLabel ?? 'Radar chart'" :style="themeVars">
111
+ <ChartAccessibility :label="ariaLabel ?? 'Radar chart'" :description="ariaDescription" :rows="accessibleRows" :show-table="accessibleDataTable !== false" />
93
112
  <ChartSkeleton
94
113
  v-if="loading"
95
114
  :height="height ?? 200"
96
115
  shape="ring"
97
116
  :label="loadingLabel ?? 'Loading'"
98
117
  />
118
+ <ChartEmptyState v-else-if="error || chartData.length === 0" :height="height" :message="error || emptyLabel" />
99
119
  <ChartContainer v-else width="100%" :height="height">
100
120
  <VccsRadarChart
101
- :data="data"
121
+ :data="chartData"
102
122
  :outer-radius="resolvedOuterRadius"
103
123
  :cx="center?.cx"
104
124
  :cy="center?.cy"
@@ -141,6 +161,7 @@ const resolvedOuterRadius = computed(() => {
141
161
  :fill-opacity="polygonFillOpacity"
142
162
  :dot="showDots"
143
163
  :is-animation-active="duration !== void 0 && duration !== 0"
164
+ :transition="{ duration: (duration ?? 800) / 1e3, ease: 'easeOut' }"
144
165
  >
145
166
  <template v-if="showDots" #dot="dotProps">
146
167
  <ChartDot
@@ -9,14 +9,19 @@ import {
9
9
  import ChartContainer from "./internal/ChartContainer";
10
10
  import ChartSkeleton from "./internal/ChartSkeleton.vue";
11
11
  import ChartLegend from "./internal/ChartLegend.vue";
12
+ import ChartEmptyState from "./internal/ChartEmptyState.vue";
13
+ import ChartAccessibility from "./internal/ChartAccessibility.vue";
12
14
  import { tooltipContentFor } from "./internal/tooltipContent";
13
15
  import PolarCenterSync from "./internal/PolarCenterSync";
14
16
  import { categoriesToSeries } from "../utils/categories";
15
17
  import { legendPositionToLegendProps, resolveLegendWrapperStyle } from "../utils/legend";
16
18
  import { toCssProperties } from "../utils/style";
17
- import { themeToVars } from "../utils/theme";
19
+ import { resolveHoverVisible, themeToVars } from "../utils/theme";
20
+ import { DEFAULT_MAX_DATA_POINTS, normalizeNamedValues, sampleData, warnDataLimit } from "../utils/data";
18
21
  const props = defineProps({
19
22
  data: { type: Array, required: true },
23
+ nameKey: { type: null, required: false },
24
+ valueKey: { type: null, required: false },
20
25
  categories: { type: Object, required: true },
21
26
  height: { type: Number, required: true },
22
27
  variant: { type: String, required: false },
@@ -31,9 +36,14 @@ const props = defineProps({
31
36
  legendStyle: { type: [String, Object], required: false },
32
37
  hideTooltip: { type: Boolean, required: false },
33
38
  tooltipTitleFormatter: { type: Function, required: false },
34
- tooltip: { type: Object, required: false },
35
39
  theme: { type: Object, required: false },
36
40
  duration: { type: Number, required: false },
41
+ ariaLabel: { type: String, required: false },
42
+ ariaDescription: { type: String, required: false },
43
+ accessibleDataTable: { type: Boolean, required: false, default: true },
44
+ maxDataPoints: { type: Number, required: false },
45
+ emptyLabel: { type: String, required: false },
46
+ error: { type: String, required: false },
37
47
  legendVariant: { type: String, required: false },
38
48
  tooltipVariant: { type: String, required: false },
39
49
  tooltipRoundness: { type: String, required: false },
@@ -43,10 +53,14 @@ const props = defineProps({
43
53
  const themeVars = computed(() => themeToVars(props.theme));
44
54
  const bars = computed(() => {
45
55
  const cats = categoriesToSeries(props.categories);
46
- return props.data.map((value, i) => ({
47
- name: cats[i]?.name ?? String(i),
56
+ const normalized = normalizeNamedValues(props.data, cats.map((cat) => String(cat.name)), props.nameKey, props.valueKey);
57
+ const limit = props.maxDataPoints ?? DEFAULT_MAX_DATA_POINTS;
58
+ warnDataLimit("RadialBarChart", normalized.length, limit);
59
+ return sampleData(normalized, limit).map(({ row, value, sourceIndex, name }) => ({
60
+ ...row && typeof row === "object" ? row : {},
61
+ name,
48
62
  value,
49
- fill: cats[i]?.color ?? `var(--chart-color-${i})`
63
+ fill: cats[sourceIndex]?.color ?? `var(--vc-series-${sourceIndex % 8})`
50
64
  }));
51
65
  });
52
66
  const angles = computed(() => {
@@ -57,7 +71,26 @@ const angles = computed(() => {
57
71
  };
58
72
  });
59
73
  const tooltipContent = computed(
60
- () => tooltipContentFor(props.tooltipVariant, props.tooltipRoundness)
74
+ () => tooltipContentFor(props.tooltipVariant, props.tooltipRoundness, props.tooltipTitleFormatter)
75
+ );
76
+ const accessibleRows = computed(() => bars.value.map((bar) => ({ label: bar.name, values: [{ label: "Value", value: bar.value }] })));
77
+ const hiddenTooltipContent = () => null;
78
+ const radiusInPixels = (radius, fallback) => {
79
+ if (typeof radius === "number") return radius;
80
+ const percentage = Number.parseFloat(radius);
81
+ return Number.isFinite(percentage) ? props.height / 2 * percentage / 100 : fallback;
82
+ };
83
+ const hoverStrokeWidth = computed(() => {
84
+ const outer = radiusInPixels(props.outerRadius ?? "100%", props.height / 2);
85
+ const inner = radiusInPixels(props.innerRadius ?? "30%", props.height * 0.15);
86
+ return Math.max((outer - inner) / Math.max(bars.value.length, 1) * 0.9, 1);
87
+ });
88
+ const cursor = computed(
89
+ () => resolveHoverVisible(props.theme) ? {
90
+ fill: "none",
91
+ stroke: "var(--vc-hover-fill)",
92
+ strokeWidth: hoverStrokeWidth.value
93
+ } : false
61
94
  );
62
95
  const legend = computed(() => legendPositionToLegendProps(props.legendPosition));
63
96
  const legendWrapperStyle = computed(
@@ -71,13 +104,15 @@ const onCenter = (next) => {
71
104
  </script>
72
105
 
73
106
  <template>
74
- <div class="vue-chrts" :style="themeVars">
107
+ <div class="vue-chrts" role="group" :aria-label="ariaLabel ?? 'Radial bar chart'" :style="themeVars">
108
+ <ChartAccessibility :label="ariaLabel ?? 'Radial bar chart'" :description="ariaDescription" :rows="accessibleRows" :show-table="accessibleDataTable !== false" />
75
109
  <ChartSkeleton
76
110
  v-if="loading"
77
111
  :height="height ?? 200"
78
112
  shape="ring"
79
113
  :label="loadingLabel ?? 'Loading'"
80
114
  />
115
+ <ChartEmptyState v-else-if="error || bars.length === 0" :height="height" :message="error || emptyLabel" />
81
116
  <ChartContainer v-else width="100%" :height="height">
82
117
  <VccsRadialBarChart
83
118
  :data="bars"
@@ -94,8 +129,13 @@ const onCenter = (next) => {
94
129
  :background="background ?? true"
95
130
  :corner-radius="cornerRadius"
96
131
  :is-animation-active="duration !== void 0 && duration !== 0"
132
+ :transition="{ duration: (duration ?? 400) / 1e3, ease: 'easeOut' }"
133
+ />
134
+ <Tooltip
135
+ :content="hideTooltip ? hiddenTooltipContent : tooltipContent"
136
+ :cursor="cursor"
137
+ :is-animation-active="false"
97
138
  />
98
- <Tooltip v-if="!hideTooltip" :content="tooltipContent" :is-animation-active="false" />
99
139
  <Legend
100
140
  v-if="!hideLegend"
101
141
  :align="legend.align"
@@ -4,7 +4,10 @@ import { Sankey, Tooltip } from "vccs";
4
4
  import ChartContainer from "./internal/ChartContainer";
5
5
  import ChartSkeleton from "./internal/ChartSkeleton.vue";
6
6
  import ChartTooltip from "./internal/ChartTooltip.vue";
7
+ import ChartEmptyState from "./internal/ChartEmptyState.vue";
8
+ import ChartAccessibility from "./internal/ChartAccessibility.vue";
7
9
  import { themeToVars } from "../utils/theme";
10
+ import { DEFAULT_MAX_DATA_POINTS, sampleData, toFiniteNumber, warnDataLimit } from "../utils/data";
8
11
  const props = defineProps({
9
12
  data: { type: Object, required: true },
10
13
  height: { type: Number, required: true },
@@ -22,9 +25,14 @@ const props = defineProps({
22
25
  nodePadding: { type: Number, required: false, default: 10 },
23
26
  iterations: { type: Number, required: false, default: 32 },
24
27
  hideTooltip: { type: Boolean, required: false },
25
- tooltip: { type: Object, required: false },
26
28
  theme: { type: Object, required: false },
27
29
  duration: { type: Number, required: false, default: 800 },
30
+ ariaLabel: { type: String, required: false },
31
+ ariaDescription: { type: String, required: false },
32
+ accessibleDataTable: { type: Boolean, required: false, default: true },
33
+ maxDataPoints: { type: Number, required: false },
34
+ emptyLabel: { type: String, required: false },
35
+ error: { type: String, required: false },
28
36
  legendVariant: { type: String, required: false },
29
37
  tooltipVariant: { type: String, required: false },
30
38
  tooltipRoundness: { type: String, required: false },
@@ -47,11 +55,11 @@ const nodeLabel = (node) => {
47
55
  const input = node;
48
56
  return props.label?.(input) ?? String(input.name ?? input.id ?? "");
49
57
  };
50
- const resolvedNodeColor = (node) => props.nodeColor?.(node) ?? "var(--chart-color-0)";
58
+ const resolvedNodeColor = (node) => props.nodeColor?.(node) ?? "var(--vc-series-0)";
51
59
  const resolvedLinkColor = (link) => {
52
60
  const layoutLink = link;
53
61
  const inputLink = layoutLink.__source === void 0 ? layoutLink : { ...layoutLink, source: layoutLink.__source };
54
- return props.linkColor?.(inputLink) ?? "var(--chart-color-0)";
62
+ return props.linkColor?.(inputLink) ?? "var(--vc-series-0)";
55
63
  };
56
64
  function handleMouseEnter(item, type) {
57
65
  activeColor.value = type === "node" ? resolvedNodeColor(item) : resolvedLinkColor(item);
@@ -66,30 +74,45 @@ const chartData = computed(() => {
66
74
  if (node.id !== void 0) ids.set(node.id, index);
67
75
  });
68
76
  const resolveEndpoint = (endpoint) => ids.has(endpoint) ? ids.get(endpoint) : endpoint;
69
- return {
70
- nodes,
71
- links: props.data.links.map((link) => ({
77
+ const links = props.data.links.flatMap((link) => {
78
+ const value = toFiniteNumber(props.linkValue?.(link) ?? link.value ?? 0);
79
+ if (value === void 0 || value < 0) return [];
80
+ return [{
72
81
  ...link,
73
82
  __source: link.source,
74
83
  source: resolveEndpoint(link.source),
75
84
  target: resolveEndpoint(link.target),
76
- value: props.linkValue?.(link) ?? link.value ?? 0
77
- }))
85
+ value
86
+ }];
87
+ });
88
+ const limit = props.maxDataPoints ?? DEFAULT_MAX_DATA_POINTS;
89
+ warnDataLimit("SankeyChart", links.length, limit);
90
+ return {
91
+ nodes,
92
+ links: sampleData(links, limit)
78
93
  };
79
94
  });
95
+ const accessibleRows = computed(() => chartData.value.links.map((link) => ({
96
+ label: `${String(link.__source)} to ${String(link.target)}`,
97
+ values: [{ label: "Value", value: link.value }]
98
+ })));
80
99
  </script>
81
100
 
82
101
  <template>
83
102
  <div
84
103
  class="vc-sankey vue-chrts"
104
+ role="group"
105
+ :aria-label="ariaLabel ?? 'Sankey chart'"
85
106
  :style="{ position: 'relative', width: '100%', height: `${height}px`, ...themeVars }"
86
107
  >
108
+ <ChartAccessibility :label="ariaLabel ?? 'Sankey chart'" :description="ariaDescription" :rows="accessibleRows" :show-table="accessibleDataTable !== false" />
87
109
  <ChartSkeleton
88
110
  v-if="loading"
89
111
  :height="height"
90
112
  shape="bars"
91
113
  :label="loadingLabel ?? 'Loading'"
92
114
  />
115
+ <ChartEmptyState v-else-if="error || chartData.links.length === 0" :height="height" :message="error || emptyLabel" />
93
116
  <ChartContainer v-else width="100%" height="100%">
94
117
  <Sankey
95
118
  :data="chartData"
@@ -112,7 +135,7 @@ const chartData = computed(() => {
112
135
  :width="nodeProps.width"
113
136
  :height="nodeProps.height"
114
137
  :fill="resolvedNodeColor(nodeProps.payload)"
115
- stroke="var(--vc-surface, #fff)"
138
+ stroke="var(--vc-surface-bg, #fff)"
116
139
  />
117
140
  <text
118
141
  :x="nodeProps.payload.depth === 0 ? nodeProps.x - 8 : nodeProps.x + nodeProps.width + 8"
@@ -1,6 +1,11 @@
1
1
  <script setup>
2
2
  import { computed, onMounted, onUnmounted, ref } from "vue";
3
3
  import { categoriesToSeries } from "../utils/categories";
4
+ import ChartAccessibility from "./internal/ChartAccessibility.vue";
5
+ import ChartEmptyState from "./internal/ChartEmptyState.vue";
6
+ import ChartSkeleton from "./internal/ChartSkeleton.vue";
7
+ import { toFiniteNumber } from "../utils/data";
8
+ import { formatNumber } from "../utils/format";
4
9
  const props = defineProps({
5
10
  data: { type: Array, required: true },
6
11
  categories: { type: Object, required: true },
@@ -21,7 +26,18 @@ const props = defineProps({
21
26
  hideHeader: { type: Boolean, required: false, default: false },
22
27
  hideLegend: { type: Boolean, required: false, default: false },
23
28
  hideTooltip: { type: Boolean, required: false, default: false },
24
- rounded: { type: Boolean, required: false, default: true }
29
+ rounded: { type: Boolean, required: false, default: true },
30
+ ariaLabel: { type: String, required: false },
31
+ ariaDescription: { type: String, required: false },
32
+ accessibleDataTable: { type: Boolean, required: false, default: true },
33
+ maxDataPoints: { type: Number, required: false },
34
+ emptyLabel: { type: String, required: false },
35
+ error: { type: String, required: false },
36
+ legendVariant: { type: String, required: false },
37
+ tooltipVariant: { type: String, required: false },
38
+ tooltipRoundness: { type: String, required: false },
39
+ loading: { type: Boolean, required: false },
40
+ loadingLabel: { type: String, required: false }
25
41
  });
26
42
  const containerRef = ref(null);
27
43
  const containerWidth = ref(0);
@@ -59,9 +75,9 @@ const visibleBars = computed(() => {
59
75
  const dataIndex = start + visibleIndex;
60
76
  const status = props.statusAccessor?.(datum, dataIndex) ?? datum.status;
61
77
  const category = categoryMap.value.get(status);
62
- const value = props.valueAccessor?.(datum, dataIndex) ?? datum.value;
78
+ const value = toFiniteNumber(props.valueAccessor?.(datum, dataIndex) ?? datum.value);
63
79
  const label = props.labelAccessor?.(datum, dataIndex) ?? datum.label ?? category?.name ?? status;
64
- const formattedValue = typeof value === "number" ? props.valueFormatter?.(value, datum, dataIndex) ?? value.toLocaleString() : void 0;
80
+ const formattedValue = typeof value === "number" ? props.valueFormatter?.(value, datum, dataIndex) ?? formatNumber(value) : void 0;
65
81
  return {
66
82
  key: `${dataIndex}-${status}`,
67
83
  status,
@@ -81,10 +97,15 @@ const barStyle = computed(() => ({
81
97
  width: `${Math.max(1, props.barWidth)}px`
82
98
  }));
83
99
  const ariaLabel = computed(() => {
100
+ if (props.ariaLabel) return props.ariaLabel;
84
101
  const prefix = props.title ? `${props.title}: ` : "";
85
102
  const samples = props.data.length === 1 ? "1 status sample" : `${props.data.length} status samples`;
86
103
  return `${prefix}${samples}`;
87
104
  });
105
+ const accessibleRows = computed(() => visibleBars.value.filter((bar) => !bar.isEmpty).map((bar) => ({
106
+ label: bar.label,
107
+ values: [{ label: "Status", value: bar.status }]
108
+ })));
88
109
  function updateContainerWidth() {
89
110
  containerWidth.value = containerRef.value?.offsetWidth ?? 0;
90
111
  }
@@ -101,6 +122,7 @@ onUnmounted(() => {
101
122
 
102
123
  <template>
103
124
  <section class="vc-status-tracker vue-chrts" :aria-label="ariaLabel">
125
+ <ChartAccessibility :label="ariaLabel" :description="ariaDescription" :rows="accessibleRows" :show-table="accessibleDataTable !== false" />
104
126
  <header v-if="!hideHeader && (title || summary !== void 0)" class="vc-status-tracker__header">
105
127
  <h3 v-if="title" class="vc-status-tracker__title">
106
128
  {{ title }}
@@ -110,11 +132,13 @@ onUnmounted(() => {
110
132
  </p>
111
133
  </header>
112
134
 
135
+ <ChartSkeleton v-if="loading" :height="height" shape="bars" :label="loadingLabel ?? 'Loading'" />
136
+ <ChartEmptyState v-else-if="error || data.length === 0" :height="height" :message="error || emptyLabel" />
113
137
  <div
138
+ v-else
114
139
  ref="containerRef"
115
140
  class="vc-status-tracker__track"
116
- role="img"
117
- :aria-label="ariaLabel"
141
+ role="list"
118
142
  :style="trackerStyle"
119
143
  >
120
144
  <span
@@ -129,15 +153,17 @@ onUnmounted(() => {
129
153
  :style="{ ...barStyle, backgroundColor: bar.color }"
130
154
  :title="hideTooltip ? void 0 : bar.title"
131
155
  :aria-label="bar.title"
156
+ :tabindex="bar.isEmpty ? void 0 : 0"
157
+ role="listitem"
132
158
  />
133
159
  </div>
134
160
 
135
- <div v-if="startLabel || endLabel" class="vc-status-tracker__labels">
161
+ <div v-if="!loading && !error && data.length && (startLabel || endLabel)" class="vc-status-tracker__labels">
136
162
  <span>{{ startLabel }}</span>
137
163
  <span>{{ endLabel }}</span>
138
164
  </div>
139
165
 
140
- <ul v-if="!hideLegend && legendItems.length" class="vc-status-tracker__legend">
166
+ <ul v-if="!loading && !error && data.length && !hideLegend && legendItems.length" class="vc-status-tracker__legend">
141
167
  <li v-for="item in legendItems" :key="item.dataKey" class="vc-status-tracker__legend-item">
142
168
  <span class="vc-status-tracker__legend-dot" :style="{ backgroundColor: item.color }" />
143
169
  <span>{{ item.name }}</span>
@@ -1,16 +1,18 @@
1
1
  <script setup>
2
2
  import { computed } from "vue";
3
+ import { formatNumber } from "../../utils/format";
3
4
  const props = defineProps({
4
5
  row: { type: Object, required: true },
5
6
  upColor: { type: String, required: true },
6
7
  downColor: { type: String, required: true },
7
- valueFormatter: { type: Function, required: false }
8
+ valueFormatter: { type: Function, required: false },
9
+ title: { type: [String, Number], required: false }
8
10
  });
9
11
  const directionColor = computed(
10
12
  () => props.row.close >= props.row.open ? props.upColor : props.downColor
11
13
  );
12
14
  function fmt(value) {
13
- return props.valueFormatter ? props.valueFormatter(value) : value.toLocaleString();
15
+ return props.valueFormatter ? props.valueFormatter(value) : formatNumber(value);
14
16
  }
15
17
  const rows = computed(() => {
16
18
  const r = props.row;
@@ -19,14 +21,14 @@ const rows = computed(() => {
19
21
  { name: "High", value: fmt(r.high), color: props.upColor },
20
22
  { name: "Low", value: fmt(r.low), color: props.downColor },
21
23
  { name: "Close", value: fmt(r.close), color: directionColor.value },
22
- ...r.volume != null && !Number.isNaN(r.volume) ? [{ name: "Volume", value: r.volume.toLocaleString(), color: "var(--vc-tooltip-muted, oklch(0.556 0 0))" }] : []
24
+ ...r.volume != null && !Number.isNaN(r.volume) ? [{ name: "Volume", value: formatNumber(r.volume), color: "var(--vc-tooltip-muted, oklch(0.556 0 0))" }] : []
23
25
  ];
24
26
  });
25
27
  </script>
26
28
 
27
29
  <template>
28
30
  <div class="vc-tooltip">
29
- <p class="vc-tooltip__label">{{ row.label }}</p>
31
+ <p class="vc-tooltip__label">{{ title ?? row.label }}</p>
30
32
  <ul class="vc-tooltip__list">
31
33
  <li v-for="item in rows" :key="item.name" class="vc-tooltip__item">
32
34
  <span class="vc-tooltip__dot" :style="{ background: item.color }" />
@@ -11,6 +11,7 @@ type __VLS_Props = {
11
11
  upColor: string;
12
12
  downColor: string;
13
13
  valueFormatter?: (value: number) => string;
14
+ title?: string | number;
14
15
  };
15
16
  declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
16
17
  declare const _default: typeof __VLS_export;