@cfasim-ui/docs 0.4.9 → 0.4.11

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.
@@ -13,10 +13,12 @@ import {
13
13
  useChartFoundation,
14
14
  makeTooltipValueFormatter,
15
15
  ChartAnnotations,
16
+ INLINE_LEGEND_ROW_HEIGHT,
16
17
  type ChartData,
17
18
  type ChartCommonProps,
18
19
  type ChartHoverPayload,
19
20
  type ChartTooltipBaseProps,
21
+ type ChartTooltipValue,
20
22
  } from "../_shared/index.js";
21
23
 
22
24
  export type BarChartData = ChartData;
@@ -29,6 +31,16 @@ export interface BarSeries {
29
31
  opacity?: number;
30
32
  /** Label shown in the inline legend. */
31
33
  legend?: string;
34
+ /**
35
+ * Whether this series appears in the inline legend. Defaults to true.
36
+ * Has no effect when `legend` is unset (no legend entry to begin with).
37
+ */
38
+ showInLegend?: boolean;
39
+ /**
40
+ * Whether this series contributes a value to the tooltip. Defaults to
41
+ * true. The bars are still drawn.
42
+ */
43
+ showInTooltip?: boolean;
32
44
  }
33
45
 
34
46
  interface BarChartProps extends ChartCommonProps {
@@ -104,8 +116,6 @@ defineSlots<{
104
116
  tooltip?(props: ChartTooltipBaseProps & { category: string }): unknown;
105
117
  }>();
106
118
 
107
- const hasInlineLegend = computed(() => allSeries.value.some((s) => s.legend));
108
-
109
119
  const EMPTY_DATA: readonly number[] = [];
110
120
 
111
121
  type ResolvedSeries = {
@@ -113,6 +123,8 @@ type ResolvedSeries = {
113
123
  color?: string;
114
124
  opacity?: number;
115
125
  legend?: string;
126
+ showInLegend?: boolean;
127
+ showInTooltip?: boolean;
116
128
  };
117
129
 
118
130
  function resolveSeries(s: BarSeries): ResolvedSeries {
@@ -121,6 +133,8 @@ function resolveSeries(s: BarSeries): ResolvedSeries {
121
133
  color: s.color,
122
134
  opacity: s.opacity,
123
135
  legend: s.legend,
136
+ showInLegend: s.showInLegend,
137
+ showInTooltip: s.showInTooltip,
124
138
  };
125
139
  }
126
140
 
@@ -462,12 +476,16 @@ interface InlineLegendItem {
462
476
  const inlineLegendItems = computed<InlineLegendItem[]>(() => {
463
477
  const items: InlineLegendItem[] = [];
464
478
  allSeries.value.forEach((s, i) => {
465
- if (!s.legend) return;
479
+ if (!s.legend || s.showInLegend === false) return;
466
480
  items.push({ label: s.legend, color: s.color ?? defaultColor(i) });
467
481
  });
468
482
  return items;
469
483
  });
470
484
 
485
+ const inlineLegendLabels = computed(() =>
486
+ inlineLegendItems.value.map((item) => item.label),
487
+ );
488
+
471
489
  function toCsv(): string {
472
490
  if (typeof props.csv === "function") return props.csv();
473
491
  if (typeof props.csv === "string") return props.csv;
@@ -514,6 +532,7 @@ const {
514
532
  height,
515
533
  padding,
516
534
  legendY,
535
+ inlineLegendLayout,
517
536
  innerW,
518
537
  innerH,
519
538
  bounds,
@@ -525,6 +544,7 @@ const {
525
544
  downloadLinkText,
526
545
  csvHref,
527
546
  menuFilename,
547
+ isFullscreen,
528
548
  } = useChartFoundation({
529
549
  width: () => props.width,
530
550
  height: () => props.height,
@@ -538,7 +558,7 @@ const {
538
558
  filename: () => props.filename,
539
559
  downloadLink: () => props.downloadLink,
540
560
  chartPadding: () => props.chartPadding,
541
- hasInlineLegend: () => hasInlineLegend.value,
561
+ inlineLegendLabels: () => inlineLegendLabels.value,
542
562
  hasTooltipSlot: () => hasTooltipSlot.value,
543
563
  getCsv: toCsv,
544
564
  pointerToIndex,
@@ -554,14 +574,21 @@ const hoveredCategoryLabel = computed(() => {
554
574
  const hoverSlotProps = computed(() => {
555
575
  const idx = hoverIndex.value;
556
576
  if (idx === null) return null;
557
- return {
558
- index: idx,
559
- category: categoryLabels.value[idx] ?? String(idx),
560
- values: allSeries.value.map((s, i) => ({
577
+ const series = allSeries.value;
578
+ const values: ChartTooltipValue[] = [];
579
+ for (let i = 0; i < series.length; i++) {
580
+ const s = series[i];
581
+ if (s.showInTooltip === false) continue;
582
+ values.push({
561
583
  value: Number(s.data[idx] ?? NaN),
562
584
  color: s.color ?? defaultColor(i),
563
585
  seriesIndex: i,
564
- })),
586
+ });
587
+ }
588
+ return {
589
+ index: idx,
590
+ category: categoryLabels.value[idx] ?? String(idx),
591
+ values,
565
592
  data: props.tooltipData?.[idx] ?? null,
566
593
  };
567
594
  });
@@ -586,11 +613,36 @@ const hoverBand = computed(() => {
586
613
  h: slotSize.value,
587
614
  };
588
615
  });
616
+
617
+ /**
618
+ * Legend items joined with their wrapped pixel positions. `x` is the
619
+ * left edge of the indicator; `y` is the center of the row.
620
+ */
621
+ const positionedLegendItems = computed(() => {
622
+ const positions = inlineLegendLayout.value.positions;
623
+ const pad = padding.value.left;
624
+ const baseY = legendY.value;
625
+ return inlineLegendItems.value.map((item, i) => {
626
+ const pos = positions[i];
627
+ return {
628
+ ...item,
629
+ x: pad + pos.x,
630
+ y: baseY + pos.row * INLINE_LEGEND_ROW_HEIGHT,
631
+ };
632
+ });
633
+ });
589
634
  </script>
590
635
 
591
636
  <template>
592
- <div ref="containerRef" class="bar-chart-wrapper">
637
+ <div
638
+ ref="containerRef"
639
+ class="bar-chart-wrapper"
640
+ :class="{ 'is-fullscreen': isFullscreen }"
641
+ >
593
642
  <ChartMenu v-if="menu" :items="menuItems" />
643
+ <div class="chart-sr-only" aria-live="polite">
644
+ {{ isFullscreen ? "Chart expanded to fill window" : "" }}
645
+ </div>
594
646
  <svg ref="svgRef" :width="width" :height="height">
595
647
  <!-- title -->
596
648
  <text
@@ -605,18 +657,18 @@ const hoverBand = computed(() => {
605
657
  {{ title }}
606
658
  </text>
607
659
  <!-- inline legend -->
608
- <g v-if="inlineLegendItems.length > 0">
609
- <template v-for="(item, i) in inlineLegendItems" :key="'ileg' + i">
660
+ <g v-if="positionedLegendItems.length > 0">
661
+ <template v-for="(item, i) in positionedLegendItems" :key="'ileg' + i">
610
662
  <rect
611
- :x="padding.left + i * 120"
612
- :y="legendY - 5"
663
+ :x="item.x"
664
+ :y="item.y - 5"
613
665
  width="12"
614
666
  height="10"
615
667
  :fill="item.color"
616
668
  />
617
669
  <text
618
- :x="padding.left + i * 120 + 18"
619
- :y="legendY + 4"
670
+ :x="item.x + 18"
671
+ :y="item.y + 4"
620
672
  font-size="11"
621
673
  fill="currentColor"
622
674
  >
@@ -837,10 +889,6 @@ const hoverBand = computed(() => {
837
889
  width: 100%;
838
890
  }
839
891
 
840
- .bar-chart-wrapper:hover :deep(.chart-menu-button) {
841
- opacity: 1;
842
- }
843
-
844
892
  .bar-chart-tooltip-label {
845
893
  font-weight: 600;
846
894
  margin-bottom: 0.25em;
@@ -10,6 +10,8 @@ import {
10
10
  export interface ChartMenuItem {
11
11
  label: string;
12
12
  action: () => void;
13
+ /** Sets aria-pressed on the menu item — use for toggle-style items. */
14
+ ariaPressed?: boolean;
13
15
  }
14
16
 
15
17
  const props = withDefaults(
@@ -72,6 +74,7 @@ const useDropdown = () => props.forceDropdown || props.items.length > 1;
72
74
  v-for="item in items"
73
75
  :key="item.label"
74
76
  class="chart-menu-item"
77
+ :aria-pressed="item.ariaPressed"
75
78
  @select="item.action"
76
79
  >
77
80
  {{ item.label }}
@@ -21,6 +21,7 @@ import { fipsToHsa, hsaNames } from "./hsaMapping.js";
21
21
  import ChartMenu from "../ChartMenu/ChartMenu.vue";
22
22
  import type { ChartMenuItem } from "../ChartMenu/ChartMenu.vue";
23
23
  import { saveSvg, savePng } from "../ChartMenu/download.js";
24
+ import { useChartFullscreen } from "../_shared/index.js";
24
25
  import { placeTooltip } from "../tooltip-position.js";
25
26
  import ChoroplethTooltip from "./ChoroplethTooltip.vue";
26
27
 
@@ -1298,9 +1299,12 @@ const gradientCss = computed(() => {
1298
1299
  return `linear-gradient(to right, ${stops})`;
1299
1300
  });
1300
1301
 
1302
+ const fullscreen = useChartFullscreen();
1303
+
1301
1304
  const menuItems = computed<ChartMenuItem[]>(() => {
1302
1305
  const fname = menuFilename();
1303
1306
  return [
1307
+ fullscreen.menuItem.value,
1304
1308
  {
1305
1309
  label: "Save as SVG",
1306
1310
  action: () => {
@@ -1360,8 +1364,17 @@ watch(
1360
1364
  </script>
1361
1365
 
1362
1366
  <template>
1363
- <div ref="containerRef" :class="['choropleth-wrapper', { pannable: pan }]">
1367
+ <div
1368
+ ref="containerRef"
1369
+ :class="[
1370
+ 'choropleth-wrapper',
1371
+ { pannable: pan, 'is-fullscreen': fullscreen.isFullscreen.value },
1372
+ ]"
1373
+ >
1364
1374
  <ChartMenu v-if="menu" :items="menuItems" />
1375
+ <div class="chart-sr-only" aria-live="polite">
1376
+ {{ fullscreen.isFullscreen.value ? "Map expanded to fill window" : "" }}
1377
+ </div>
1365
1378
  <!--
1366
1379
  Title + legend live as an HTML overlay on top of the SVG so they keep
1367
1380
  their intrinsic px sizes regardless of how the browser scales the
@@ -1476,10 +1489,6 @@ watch(
1476
1489
  cursor: grabbing;
1477
1490
  }
1478
1491
 
1479
- .choropleth-wrapper:hover :deep(.chart-menu-button) {
1480
- opacity: 1;
1481
- }
1482
-
1483
1492
  .state-path {
1484
1493
  cursor: pointer;
1485
1494
  }
@@ -98,6 +98,46 @@ When `x` is omitted, `y`/`data` values are plotted at indices 0, 1, 2, etc.
98
98
  </template>
99
99
  </ComponentDemo>
100
100
 
101
+ ### Legend
102
+
103
+ Set `legend` on a series to add an entry to the inline legend strip above the plot. When the items don't all fit on one row at the chart's current width, the legend wraps to additional rows and the plot area shrinks to keep the legend clear of the data. Use `showInLegend: false` to keep a series off the legend (e.g. when its `legend` string is reused as a CSV column header) and `showInTooltip: false` to omit it from the hover tooltip.
104
+
105
+ <ComponentDemo>
106
+ <LineChart
107
+ :series="[
108
+ { data: [0, 12, 28, 45, 60, 55, 40, 25, 12], color: '#0057b7', legend: 'No interventions' },
109
+ { data: [0, 10, 22, 38, 50, 45, 32, 18, 8], color: '#fb7e38', legend: 'School closures' },
110
+ { data: [0, 8, 18, 30, 40, 35, 25, 14, 6], color: '#ef4444', legend: 'Masking + distancing' },
111
+ { data: [0, 6, 14, 24, 32, 28, 20, 10, 4], color: '#10b981', legend: 'Vaccination only' },
112
+ { data: [0, 4, 10, 16, 22, 19, 13, 7, 3], color: '#6366f1', legend: 'Vaccination + masking' },
113
+ { data: [0, 3, 7, 11, 15, 13, 9, 5, 2], color: '#a855f7', legend: 'All interventions combined' },
114
+ ]"
115
+ :height="240"
116
+ x-label="Weeks"
117
+ y-label="Incidence"
118
+ />
119
+
120
+ <template #code>
121
+
122
+ ```vue
123
+ <LineChart
124
+ :series="[
125
+ { data: scenarioA, color: '#0057b7', legend: 'No interventions' },
126
+ { data: scenarioB, color: '#fb7e38', legend: 'School closures' },
127
+ { data: scenarioC, color: '#ef4444', legend: 'Masking + distancing' },
128
+ { data: scenarioD, color: '#10b981', legend: 'Vaccination only' },
129
+ { data: scenarioE, color: '#6366f1', legend: 'Vaccination + masking' },
130
+ { data: scenarioF, color: '#a855f7', legend: 'All interventions combined' },
131
+ ]"
132
+ :height="240"
133
+ x-label="Weeks"
134
+ y-label="Incidence"
135
+ />
136
+ ```
137
+
138
+ </template>
139
+ </ComponentDemo>
140
+
101
141
  ### Tooltip
102
142
 
103
143
  Hover over the chart to see a tooltip with values at each data point. Set `tooltip-trigger="hover"` to enable the built-in tooltip with crosshair and highlight dots. Use the `#tooltip` slot for custom content. Pass `tooltip-value-format` to control how numeric values render (e.g. percentages, currency); it falls back to `y-tick-format` when omitted.
@@ -248,6 +288,46 @@ floor, and `yMin <= 0` is ignored.
248
288
  </template>
249
289
  </ComponentDemo>
250
290
 
291
+ ### Outline
292
+
293
+ Set `outline: true` on a series to draw a page-colored stroke behind the
294
+ line. This is useful when lines overlap, cross dense areas, or sit on a
295
+ busy background — the outline keeps each series visually distinct.
296
+
297
+ <ComponentDemo>
298
+ <LineChart
299
+ :series="[
300
+ { data: [0, 10, 25, 45, 60, 55, 40, 20, 8], color: '#fb7e38', strokeWidth: 3, outline: true },
301
+ { data: [0, 12, 22, 40, 58, 58, 42, 22, 10], color: '#0057b7', strokeWidth: 3, outline: true },
302
+ ]"
303
+ :height="200"
304
+ />
305
+
306
+ <template #code>
307
+
308
+ ```vue
309
+ <LineChart
310
+ :series="[
311
+ {
312
+ data: [0, 10, 25, 45, 60, 55, 40, 20, 8],
313
+ color: '#fb7e38',
314
+ strokeWidth: 3,
315
+ outline: true,
316
+ },
317
+ {
318
+ data: [0, 12, 22, 40, 58, 58, 42, 22, 10],
319
+ color: '#0057b7',
320
+ strokeWidth: 3,
321
+ outline: true,
322
+ },
323
+ ]"
324
+ :height="200"
325
+ />
326
+ ```
327
+
328
+ </template>
329
+ </ComponentDemo>
330
+
251
331
  ### Many trajectories with low opacity
252
332
 
253
333
  <ComponentDemo>
@@ -664,6 +744,13 @@ positioned from the anchor as usual via `offset`.
664
744
  </template>
665
745
  </ComponentDemo>
666
746
 
747
+ ### Chart menu
748
+
749
+ The chart menu (top-right) leads with Expand / Collapse and then Save as SVG,
750
+ Save as PNG, and Download CSV. Expand grows the chart to fill the browser
751
+ window so the plot has more room; press <kbd>Esc</kbd> or pick "Collapse" to
752
+ return to the inline size. Set `menu="false"` to hide the trigger entirely.
753
+
667
754
  ### Custom CSV download
668
755
 
669
756
  By default, the Download CSV menu item exports the chart series as CSV. Use
@@ -789,6 +876,7 @@ interface Series {
789
876
  strokeWidth?: number;
790
877
  opacity?: number;
791
878
  line?: boolean;
879
+ outline?: boolean; // page-colored stroke drawn behind the line
792
880
  dots?: boolean;
793
881
  dotRadius?: number;
794
882
  dotFill?: string;
@@ -13,10 +13,12 @@ import {
13
13
  useChartFoundation,
14
14
  makeTooltipValueFormatter,
15
15
  ChartAnnotations,
16
+ INLINE_LEGEND_ROW_HEIGHT,
16
17
  type ChartData,
17
18
  type ChartCommonProps,
18
19
  type ChartHoverPayload,
19
20
  type ChartTooltipBaseProps,
21
+ type ChartTooltipValue,
20
22
  } from "../_shared/index.js";
21
23
 
22
24
  /**
@@ -48,12 +50,28 @@ export interface Series {
48
50
  lineOpacity?: number;
49
51
  dotOpacity?: number;
50
52
  line?: boolean;
53
+ /**
54
+ * Draw a page-colored stroke behind the line so it stays visually
55
+ * separated from overlapping series and busy backgrounds. Has no
56
+ * effect when `line` is `false`.
57
+ */
58
+ outline?: boolean;
51
59
  dots?: boolean;
52
60
  dotRadius?: number;
53
61
  dotFill?: string;
54
62
  dotStroke?: string;
55
63
  /** Label shown in the inline legend */
56
64
  legend?: string;
65
+ /**
66
+ * Whether this series appears in the inline legend. Defaults to true.
67
+ * Has no effect when `legend` is unset (no legend entry to begin with).
68
+ */
69
+ showInLegend?: boolean;
70
+ /**
71
+ * Whether this series contributes a value to the tooltip and shows a
72
+ * hover dot. Defaults to true. The series line/dots are still drawn.
73
+ */
74
+ showInTooltip?: boolean;
57
75
  }
58
76
 
59
77
  export interface Area {
@@ -167,15 +185,6 @@ defineSlots<{
167
185
  tooltip?(props: ChartTooltipBaseProps & { xLabel?: string }): unknown;
168
186
  }>();
169
187
 
170
- const hasInlineLegend = computed(
171
- () =>
172
- allSeries.value.some((s) => s.legend) ||
173
- props.areaSections?.some(
174
- (s) => s.legend === "inline" && (s.label || s.description),
175
- ) ||
176
- false,
177
- );
178
-
179
188
  /**
180
189
  * Internal series shape where `data` (y-values) is always resolved.
181
190
  * `Series.y` takes precedence over `Series.data` when both are set.
@@ -503,7 +512,7 @@ interface InlineLegendItem {
503
512
  const inlineLegendItems = computed<InlineLegendItem[]>(() => {
504
513
  const items: InlineLegendItem[] = [];
505
514
  for (const s of allSeries.value) {
506
- if (!s.legend) continue;
515
+ if (!s.legend || s.showInLegend === false) continue;
507
516
  items.push({
508
517
  label: s.legend,
509
518
  color: s.color ?? "currentColor",
@@ -533,6 +542,10 @@ const inlineLegendItems = computed<InlineLegendItem[]>(() => {
533
542
  return items;
534
543
  });
535
544
 
545
+ const inlineLegendLabels = computed(() =>
546
+ inlineLegendItems.value.map((item) => item.label),
547
+ );
548
+
536
549
  const totalHeight = computed(
537
550
  () => height.value + sectionLabels.value.extraHeight,
538
551
  );
@@ -675,6 +688,7 @@ const hoverDots = computed(() => {
675
688
  if (targetX === null) return [];
676
689
  const dots: { x: number; y: number; color: string }[] = [];
677
690
  for (const s of allSeries.value) {
691
+ if (s.showInTooltip === false) continue;
678
692
  const nIdx = nearestIndex(s, targetX);
679
693
  if (nIdx === null) continue;
680
694
  const yv = s.data[nIdx];
@@ -704,17 +718,22 @@ const hoverSlotProps = computed(() => {
704
718
  } else {
705
719
  xLabel = formatTick(displayX);
706
720
  }
721
+ const series = allSeries.value;
722
+ const values: ChartTooltipValue[] = [];
723
+ for (let i = 0; i < series.length; i++) {
724
+ const s = series[i];
725
+ if (s.showInTooltip === false) continue;
726
+ const nIdx = nearestIndex(s, targetX);
727
+ values.push({
728
+ value: nIdx !== null ? Number(s.data[nIdx]) : NaN,
729
+ color: s.color ?? "currentColor",
730
+ seriesIndex: i,
731
+ });
732
+ }
707
733
  return {
708
734
  index: idx,
709
735
  xLabel,
710
- values: allSeries.value.map((s, i) => {
711
- const nIdx = nearestIndex(s, targetX);
712
- return {
713
- value: nIdx !== null ? Number(s.data[nIdx]) : NaN,
714
- color: s.color ?? "currentColor",
715
- seriesIndex: i,
716
- };
717
- }),
736
+ values,
718
737
  data: props.tooltipData?.[idx] ?? null,
719
738
  };
720
739
  });
@@ -747,6 +766,7 @@ const {
747
766
  height,
748
767
  padding,
749
768
  legendY,
769
+ inlineLegendLayout,
750
770
  innerW,
751
771
  innerH,
752
772
  bounds,
@@ -758,6 +778,7 @@ const {
758
778
  downloadLinkText,
759
779
  csvHref,
760
780
  menuFilename,
781
+ isFullscreen,
761
782
  } = useChartFoundation({
762
783
  width: () => props.width,
763
784
  height: () => props.height,
@@ -771,17 +792,43 @@ const {
771
792
  filename: () => props.filename,
772
793
  downloadLink: () => props.downloadLink,
773
794
  chartPadding: () => props.chartPadding,
774
- hasInlineLegend: () => hasInlineLegend.value,
795
+ inlineLegendLabels: () => inlineLegendLabels.value,
775
796
  hasTooltipSlot: () => hasTooltipSlot.value,
776
797
  getCsv: toCsv,
777
798
  pointerToIndex: indexFromPointer,
778
799
  onHover: (payload) => emit("hover", payload),
800
+ extraBelowHeight: () => sectionLabels.value.extraHeight,
801
+ });
802
+
803
+ /**
804
+ * Legend items joined with their wrapped pixel positions. `x` is the
805
+ * left edge of the indicator; `y` is the center of the row.
806
+ */
807
+ const positionedLegendItems = computed(() => {
808
+ const positions = inlineLegendLayout.value.positions;
809
+ const pad = padding.value.left;
810
+ const baseY = legendY.value;
811
+ return inlineLegendItems.value.map((item, i) => {
812
+ const pos = positions[i];
813
+ return {
814
+ ...item,
815
+ x: pad + pos.x,
816
+ y: baseY + pos.row * INLINE_LEGEND_ROW_HEIGHT,
817
+ };
818
+ });
779
819
  });
780
820
  </script>
781
821
 
782
822
  <template>
783
- <div ref="containerRef" class="line-chart-wrapper">
823
+ <div
824
+ ref="containerRef"
825
+ class="line-chart-wrapper"
826
+ :class="{ 'is-fullscreen': isFullscreen }"
827
+ >
784
828
  <ChartMenu v-if="menu" :items="menuItems" />
829
+ <div class="chart-sr-only" aria-live="polite">
830
+ {{ isFullscreen ? "Chart expanded to fill window" : "" }}
831
+ </div>
785
832
  <svg ref="svgRef" :width="width" :height="totalHeight">
786
833
  <!-- title -->
787
834
  <text
@@ -796,15 +843,15 @@ const {
796
843
  {{ title }}
797
844
  </text>
798
845
  <!-- inline legend -->
799
- <g v-if="inlineLegendItems.length > 0">
800
- <template v-for="(item, i) in inlineLegendItems" :key="'ileg' + i">
846
+ <g v-if="positionedLegendItems.length > 0">
847
+ <template v-for="(item, i) in positionedLegendItems" :key="'ileg' + i">
801
848
  <!-- series indicator: line -->
802
849
  <line
803
850
  v-if="item.type === 'series'"
804
- :x1="padding.left + i * 120"
805
- :y1="legendY"
806
- :x2="padding.left + i * 120 + 12"
807
- :y2="legendY"
851
+ :x1="item.x"
852
+ :y1="item.y"
853
+ :x2="item.x + 12"
854
+ :y2="item.y"
808
855
  :stroke="item.color"
809
856
  stroke-width="2"
810
857
  :stroke-dasharray="item.dashed ? '4 2' : undefined"
@@ -812,8 +859,8 @@ const {
812
859
  <!-- section indicator: filled circle -->
813
860
  <circle
814
861
  v-else
815
- :cx="padding.left + i * 120 + 4"
816
- :cy="legendY"
862
+ :cx="item.x + 4"
863
+ :cy="item.y"
817
864
  r="4"
818
865
  :fill="item.color"
819
866
  :fill-opacity="item.fillOpacity"
@@ -821,8 +868,8 @@ const {
821
868
  stroke-width="1.5"
822
869
  />
823
870
  <text
824
- :x="padding.left + i * 120 + 18"
825
- :y="legendY + 4"
871
+ :x="item.x + 18"
872
+ :y="item.y + 4"
826
873
  font-size="11"
827
874
  fill="currentColor"
828
875
  >
@@ -936,6 +983,16 @@ const {
936
983
  />
937
984
  <!-- data lines and dots -->
938
985
  <template v-for="(s, i) in allSeries" :key="i">
986
+ <path
987
+ v-if="s.line !== false && s.outline"
988
+ :d="toPath(s)"
989
+ fill="none"
990
+ stroke="var(--color-bg-0, #fff)"
991
+ :stroke-width="(s.strokeWidth ?? 1.5) + 4"
992
+ stroke-linecap="round"
993
+ stroke-linejoin="round"
994
+ data-testid="line-outline"
995
+ />
939
996
  <path
940
997
  v-if="s.line !== false"
941
998
  :d="toPath(s)"
@@ -1147,10 +1204,6 @@ const {
1147
1204
  width: 100%;
1148
1205
  }
1149
1206
 
1150
- .line-chart-wrapper:hover :deep(.chart-menu-button) {
1151
- opacity: 1;
1152
- }
1153
-
1154
1207
  .line-chart-tooltip-label {
1155
1208
  font-weight: 600;
1156
1209
  margin-bottom: 0.25em;
@@ -0,0 +1,51 @@
1
+ /* Shared chart styles for the Expand / fullscreen menu item.
2
+ Bundled into @cfasim-ui/charts/style.css via index.ts so it ships
3
+ once for the whole package instead of repeating in every chart SFC. */
4
+
5
+ .line-chart-wrapper:hover .chart-menu-button,
6
+ .bar-chart-wrapper:hover .chart-menu-button,
7
+ .choropleth-wrapper:hover .chart-menu-button,
8
+ .line-chart-wrapper:focus-within .chart-menu-button,
9
+ .bar-chart-wrapper:focus-within .chart-menu-button,
10
+ .choropleth-wrapper:focus-within .chart-menu-button,
11
+ .line-chart-wrapper.is-fullscreen .chart-menu-button,
12
+ .bar-chart-wrapper.is-fullscreen .chart-menu-button,
13
+ .choropleth-wrapper.is-fullscreen .chart-menu-button {
14
+ opacity: 1;
15
+ }
16
+
17
+ .line-chart-wrapper.is-fullscreen,
18
+ .bar-chart-wrapper.is-fullscreen,
19
+ .choropleth-wrapper.is-fullscreen {
20
+ position: fixed;
21
+ inset: 0;
22
+ z-index: var(--cfasim-z-fullscreen, 1000);
23
+ background: var(--color-bg-0, #fff);
24
+ color: var(--color-text, inherit);
25
+ padding: 2em;
26
+ box-sizing: border-box;
27
+ display: flex;
28
+ flex-direction: column;
29
+ justify-content: center;
30
+ }
31
+
32
+ /* ChoroplethMap doesn't go through useChartFoundation, so its SVG keeps
33
+ its prop-driven width/height. Stretch it to fill the expanded box. */
34
+ .choropleth-wrapper.is-fullscreen svg {
35
+ flex: 1 1 auto;
36
+ min-height: 0;
37
+ height: 100%;
38
+ width: 100%;
39
+ }
40
+
41
+ .chart-sr-only {
42
+ position: absolute;
43
+ width: 1px;
44
+ height: 1px;
45
+ padding: 0;
46
+ margin: -1px;
47
+ overflow: hidden;
48
+ clip: rect(0, 0, 0, 0);
49
+ white-space: nowrap;
50
+ border: 0;
51
+ }
@@ -16,16 +16,18 @@ export {
16
16
  export { useChartSize, type ChartSizeOptions } from "./useChartSize.js";
17
17
  export {
18
18
  useChartPadding,
19
- INLINE_LEGEND_HEIGHT,
19
+ INLINE_LEGEND_ROW_HEIGHT,
20
20
  type ChartPaddingOptions,
21
21
  type ChartPadding,
22
22
  type ChartBounds,
23
+ type PositionedLegendItem,
23
24
  } from "./useChartPadding.js";
24
25
  export {
25
26
  useChartTooltip,
26
27
  type ChartTooltipOptions,
27
28
  } from "./useChartTooltip.js";
28
29
  export { useChartMenu, type ChartMenuOptions } from "./useChartMenu.js";
30
+ export { useChartFullscreen } from "./useChartFullscreen.js";
29
31
  export { seriesToCsv, categoricalToCsv, type CsvSeries } from "./seriesCsv.js";
30
32
  export { default as ChartAnnotations } from "./ChartAnnotations.vue";
31
33
  export type { ChartAnnotation } from "./annotations.js";