@cfasim-ui/charts 0.7.8 → 0.8.1

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 (67) hide show
  1. package/dist/BarChart/BarChart.d.ts +9 -26
  2. package/dist/ChartMenu/ChartMenu.d.ts +3 -2
  3. package/dist/ChartTooltip/ChartTooltip.d.ts +9 -12
  4. package/dist/ChoroplethMap/ChoroplethMap.d.ts +42 -190
  5. package/dist/ChoroplethMap/ChoroplethTooltip.d.ts +20 -27
  6. package/dist/ChoroplethMap/canvasLayer.d.ts +23 -6
  7. package/dist/ChoroplethMap/cityLayout.d.ts +3 -2
  8. package/dist/ChoroplethMap/mixedGeo.d.ts +74 -0
  9. package/dist/ChoroplethMap/mixedGeo.test.d.ts +1 -0
  10. package/dist/DataTable/DataTable.d.ts +3 -2
  11. package/dist/LineChart/LineChart.d.ts +9 -26
  12. package/dist/_shared/ChartAnnotations.d.ts +3 -2
  13. package/dist/_shared/ChartZoomControls.d.ts +3 -2
  14. package/dist/_shared/index.d.ts +2 -1
  15. package/dist/_shared/mapTheme.d.ts +159 -0
  16. package/dist/_shared/mapTheme.test.d.ts +1 -0
  17. package/dist/index.css +1 -1
  18. package/dist/index.d.ts +1 -0
  19. package/dist/index.js +1821 -1462
  20. package/docs/BarChart.md +776 -0
  21. package/docs/ChoroplethMap.md +1394 -0
  22. package/docs/DataTable.md +386 -0
  23. package/docs/LineChart.md +1267 -0
  24. package/docs/index.json +56 -0
  25. package/package.json +25 -21
  26. package/src/BarChart/BarChart.md +743 -0
  27. package/src/BarChart/BarChart.vue +1901 -0
  28. package/src/ChartMenu/ChartMenu.vue +220 -0
  29. package/src/ChartMenu/download.ts +120 -0
  30. package/src/ChartTooltip/ChartTooltip.vue +97 -0
  31. package/src/ChoroplethMap/ChoroplethMap.md +1354 -0
  32. package/src/ChoroplethMap/ChoroplethMap.vue +3778 -0
  33. package/src/ChoroplethMap/ChoroplethTooltip.vue +103 -0
  34. package/src/ChoroplethMap/canvasLayer.ts +373 -0
  35. package/src/ChoroplethMap/cityLayout.ts +262 -0
  36. package/src/ChoroplethMap/hsaMapping.ts +4116 -0
  37. package/src/ChoroplethMap/mixedGeo.ts +201 -0
  38. package/src/DataTable/DataTable.md +372 -0
  39. package/src/DataTable/DataTable.vue +406 -0
  40. package/src/LineChart/LineChart.md +1225 -0
  41. package/src/LineChart/LineChart.vue +1555 -0
  42. package/src/_shared/ChartAnnotations.vue +420 -0
  43. package/src/_shared/ChartZoomControls.vue +138 -0
  44. package/src/_shared/annotations.ts +106 -0
  45. package/src/_shared/axes.ts +69 -0
  46. package/src/_shared/chartProps.ts +201 -0
  47. package/src/_shared/computeTicks.ts +42 -0
  48. package/src/_shared/contrast.ts +100 -0
  49. package/src/_shared/dateAxis.ts +501 -0
  50. package/src/_shared/index.ts +78 -0
  51. package/src/_shared/mapTheme.ts +551 -0
  52. package/src/_shared/scale.ts +86 -0
  53. package/src/_shared/seriesCsv.ts +68 -0
  54. package/src/_shared/touch.ts +8 -0
  55. package/src/_shared/useChartFoundation.ts +175 -0
  56. package/src/_shared/useChartFullscreen.ts +254 -0
  57. package/src/_shared/useChartMenu.ts +111 -0
  58. package/src/_shared/useChartPadding.ts +235 -0
  59. package/src/_shared/useChartSize.ts +58 -0
  60. package/src/_shared/useChartTooltip.ts +205 -0
  61. package/src/env.d.ts +4 -0
  62. package/src/hsa-mapping.ts +1 -0
  63. package/src/index.ts +41 -0
  64. package/src/tooltip-position.ts +55 -0
  65. package/src/us-cities/data.ts +1371 -0
  66. package/src/us-cities/index.ts +122 -0
  67. package/src/us-cities.ts +7 -0
@@ -0,0 +1,220 @@
1
+ <script setup lang="ts">
2
+ import {
3
+ DropdownMenuRoot,
4
+ DropdownMenuTrigger,
5
+ DropdownMenuPortal,
6
+ DropdownMenuContent,
7
+ DropdownMenuItem,
8
+ } from "reka-ui";
9
+
10
+ export interface ChartMenuItem {
11
+ label: string;
12
+ action: () => void;
13
+ /** Sets aria-pressed on the menu item — use for toggle-style items. */
14
+ ariaPressed?: boolean;
15
+ }
16
+
17
+ const props = withDefaults(
18
+ defineProps<{
19
+ items: ChartMenuItem[];
20
+ /** Force the dropdown style even when only one item is provided. */
21
+ forceDropdown?: boolean;
22
+ /**
23
+ * When the chart is expanded, the menu trigger is replaced by a
24
+ * persistent close (✕) button that emits `close`.
25
+ */
26
+ isFullscreen?: boolean;
27
+ }>(),
28
+ { forceDropdown: false, isFullscreen: false },
29
+ );
30
+
31
+ const emit = defineEmits<{ (e: "close"): void }>();
32
+
33
+ const useDropdown = () => props.forceDropdown || props.items.length > 1;
34
+ </script>
35
+
36
+ <template>
37
+ <div
38
+ class="chart-menu-trigger-area"
39
+ :class="{ 'chart-menu-trigger-area--expanded': isFullscreen }"
40
+ >
41
+ <!-- Expanded: a close button replaces the menu trigger -->
42
+ <button
43
+ v-if="isFullscreen"
44
+ class="chart-menu-button chart-close-button"
45
+ aria-label="Collapse"
46
+ @click="emit('close')"
47
+ >
48
+ <svg
49
+ width="16"
50
+ height="16"
51
+ viewBox="0 0 16 16"
52
+ fill="none"
53
+ stroke="currentColor"
54
+ stroke-width="1.75"
55
+ stroke-linecap="round"
56
+ aria-hidden="true"
57
+ >
58
+ <path d="M4 4l8 8M12 4l-8 8" />
59
+ </svg>
60
+ </button>
61
+ <!-- Single item: plain button -->
62
+ <button
63
+ v-else-if="!useDropdown()"
64
+ class="chart-menu-button chart-menu-single"
65
+ :aria-label="items[0].label"
66
+ @click="items[0].action"
67
+ >
68
+ <svg
69
+ width="14"
70
+ height="14"
71
+ viewBox="0 0 14 14"
72
+ fill="none"
73
+ stroke="currentColor"
74
+ stroke-width="1.5"
75
+ stroke-linecap="round"
76
+ stroke-linejoin="round"
77
+ aria-hidden="true"
78
+ >
79
+ <path d="M7 1v8M3 6l4 4 4-4M2 13h10" />
80
+ </svg>
81
+ </button>
82
+ <!-- Multiple items: dropdown menu -->
83
+ <DropdownMenuRoot v-else :modal="false">
84
+ <DropdownMenuTrigger class="chart-menu-button" aria-label="Chart options">
85
+ <svg
86
+ width="16"
87
+ height="16"
88
+ viewBox="0 0 16 16"
89
+ fill="currentColor"
90
+ aria-hidden="true"
91
+ >
92
+ <circle cx="3" cy="8" r="1.5" />
93
+ <circle cx="8" cy="8" r="1.5" />
94
+ <circle cx="13" cy="8" r="1.5" />
95
+ </svg>
96
+ </DropdownMenuTrigger>
97
+ <DropdownMenuPortal>
98
+ <DropdownMenuContent
99
+ class="chart-menu-content"
100
+ :side-offset="4"
101
+ align="end"
102
+ >
103
+ <DropdownMenuItem
104
+ v-for="item in items"
105
+ :key="item.label"
106
+ class="chart-menu-item"
107
+ :aria-pressed="item.ariaPressed"
108
+ @select="item.action"
109
+ >
110
+ {{ item.label }}
111
+ </DropdownMenuItem>
112
+ </DropdownMenuContent>
113
+ </DropdownMenuPortal>
114
+ </DropdownMenuRoot>
115
+ </div>
116
+ </template>
117
+
118
+ <style scoped>
119
+ .chart-menu-trigger-area {
120
+ position: absolute;
121
+ top: 0.5em;
122
+ right: 0.5em;
123
+ z-index: 1;
124
+ }
125
+
126
+ /* In fullscreen the button anchors to the viewport corner (the wrapper's
127
+ content padding doesn't move an absolutely-positioned child), so inset it
128
+ further for modal-style breathing room. */
129
+ .chart-menu-trigger-area--expanded {
130
+ top: 1.25em;
131
+ right: 1.25em;
132
+ }
133
+
134
+ .chart-menu-button {
135
+ display: flex;
136
+ align-items: center;
137
+ justify-content: center;
138
+ width: 28px;
139
+ height: 28px;
140
+ border: 1px solid var(--color-border);
141
+ border-radius: 0.25em;
142
+ background: var(--color-bg-0, #fff);
143
+ color: var(--color-text-secondary);
144
+ cursor: pointer;
145
+ opacity: 0;
146
+ transition: opacity 0.15s;
147
+ }
148
+
149
+ .chart-menu-button[data-state="open"] {
150
+ opacity: 1;
151
+ }
152
+
153
+ /* The close button is always visible while the chart is expanded. */
154
+ .chart-close-button {
155
+ opacity: 1;
156
+ }
157
+
158
+ .chart-menu-button:hover {
159
+ background: var(--color-bg-1, rgba(0, 0, 0, 0.05));
160
+ color: var(--color-text);
161
+ }
162
+ </style>
163
+
164
+ <style>
165
+ /* Reveal the menu button when the user hovers/focuses anywhere in the
166
+ chart. These reference the parent wrapper classes, so they can't be
167
+ scoped to ChartMenu. (In fullscreen the menu button is swapped for the
168
+ always-visible close button, so no rule is needed there.) */
169
+ .line-chart-wrapper:hover .chart-menu-button,
170
+ .bar-chart-wrapper:hover .chart-menu-button,
171
+ .choropleth-wrapper:hover .chart-menu-button,
172
+ .line-chart-wrapper:focus-within .chart-menu-button,
173
+ .bar-chart-wrapper:focus-within .chart-menu-button,
174
+ .choropleth-wrapper:focus-within .chart-menu-button {
175
+ opacity: 1;
176
+ }
177
+
178
+ /* Visually-hidden live region used by each chart wrapper to announce
179
+ expansion. Lives here so it ships once with the shared menu. */
180
+ .chart-sr-only {
181
+ position: absolute;
182
+ width: 1px;
183
+ height: 1px;
184
+ padding: 0;
185
+ margin: -1px;
186
+ overflow: hidden;
187
+ clip: rect(0, 0, 0, 0);
188
+ white-space: nowrap;
189
+ border: 0;
190
+ }
191
+
192
+ .chart-menu-content {
193
+ z-index: 100;
194
+ background: var(--color-bg-0);
195
+ border: 1px solid var(--color-border);
196
+ border-radius: 0.25em;
197
+ padding: 0.25em;
198
+ box-shadow:
199
+ 0 4px 6px -1px rgba(0, 0, 0, 0.1),
200
+ 0 2px 4px -2px rgba(0, 0, 0, 0.1);
201
+ min-width: 140px;
202
+ }
203
+
204
+ .chart-menu-item {
205
+ display: flex;
206
+ align-items: center;
207
+ padding: 0.375em 0.5em;
208
+ border-radius: 0.25em;
209
+ font-size: var(--font-size-sm);
210
+ cursor: pointer;
211
+ user-select: none;
212
+ outline: none;
213
+ white-space: nowrap;
214
+ }
215
+
216
+ .chart-menu-item[data-highlighted] {
217
+ background: var(--color-primary);
218
+ color: white;
219
+ }
220
+ </style>
@@ -0,0 +1,120 @@
1
+ export function downloadBlob(blob: Blob, name: string) {
2
+ const url = URL.createObjectURL(blob);
3
+ const a = document.createElement("a");
4
+ a.href = url;
5
+ a.download = name;
6
+ a.click();
7
+ URL.revokeObjectURL(url);
8
+ }
9
+
10
+ /** Inherited CSS properties propagated onto the cloned SVG root so text and
11
+ * `currentColor` strokes render with the same color/typography as the
12
+ * live chart when the SVG is opened standalone or rasterized to PNG. */
13
+ const ROOT_INHERITED_PROPS = [
14
+ "color",
15
+ "font-family",
16
+ "font-size",
17
+ "font-weight",
18
+ ] as const;
19
+
20
+ /** Presentation attributes that may contain `var(--…)` references. The
21
+ * fallback inside `var(--name, #fff)` is the only thing that renders when
22
+ * the custom property is undefined outside the page context — so we
23
+ * resolve them against the document's CSS custom properties before
24
+ * serializing. */
25
+ const VAR_RESOLVABLE_ATTRS = ["fill", "stroke"] as const;
26
+
27
+ /** Resolve a single `var(--name)` or `var(--name, fallback)` expression.
28
+ * Looks the name up on `document.documentElement` (where theme tokens are
29
+ * declared); falls back to the in-expression fallback, then the original
30
+ * expression if neither resolves. */
31
+ function resolveVarExpression(expr: string): string {
32
+ const match = expr.match(
33
+ /^\s*var\(\s*(--[\w-]+)\s*(?:,\s*([^)]*?)\s*)?\)\s*$/,
34
+ );
35
+ if (!match) return expr;
36
+ const [, name, fallback] = match;
37
+ const value = window
38
+ .getComputedStyle(document.documentElement)
39
+ .getPropertyValue(name)
40
+ .trim();
41
+ if (value) return value;
42
+ if (fallback) return fallback.trim();
43
+ return expr;
44
+ }
45
+
46
+ /** Clone `svg` and inline the styles that don't survive a standalone render:
47
+ * inherited `color`/`font-*` on the root (so `currentColor` and unset
48
+ * font-family resolve), and any `var(--…)` references in fill/stroke
49
+ * attributes resolved against the document. */
50
+ export function prepareSvgForExport(svg: SVGSVGElement): SVGSVGElement {
51
+ const clone = svg.cloneNode(true) as SVGSVGElement;
52
+ clone.setAttribute("xmlns", "http://www.w3.org/2000/svg");
53
+
54
+ const rootComputed = window.getComputedStyle(svg);
55
+ const rootStyleParts: string[] = [];
56
+ for (const prop of ROOT_INHERITED_PROPS) {
57
+ const value = rootComputed.getPropertyValue(prop);
58
+ if (value) rootStyleParts.push(`${prop}: ${value}`);
59
+ }
60
+ const existingRootStyle = clone.getAttribute("style") ?? "";
61
+ clone.setAttribute(
62
+ "style",
63
+ [existingRootStyle, ...rootStyleParts].filter(Boolean).join("; "),
64
+ );
65
+
66
+ for (const target of clone.querySelectorAll<SVGElement>("*")) {
67
+ for (const attr of VAR_RESOLVABLE_ATTRS) {
68
+ const raw = target.getAttribute(attr);
69
+ if (!raw || !raw.includes("var(")) continue;
70
+ target.setAttribute(attr, resolveVarExpression(raw));
71
+ }
72
+ }
73
+
74
+ return clone;
75
+ }
76
+
77
+ export function saveSvg(svg: SVGSVGElement, filename: string) {
78
+ const clone = prepareSvgForExport(svg);
79
+ const xml = new XMLSerializer().serializeToString(clone);
80
+ downloadBlob(new Blob([xml], { type: "image/svg+xml" }), `${filename}.svg`);
81
+ }
82
+
83
+ export function savePng(svg: SVGSVGElement, filename: string) {
84
+ const clone = prepareSvgForExport(svg);
85
+ const xml = new XMLSerializer().serializeToString(clone);
86
+ const svgBlob = new Blob([xml], { type: "image/svg+xml;charset=utf-8" });
87
+ const url = URL.createObjectURL(svgBlob);
88
+ const img = new Image();
89
+ const w = svg.width.baseVal.value || svg.clientWidth;
90
+ const h = svg.height.baseVal.value || svg.clientHeight;
91
+ const scale = 2;
92
+ img.onload = () => {
93
+ const canvas = document.createElement("canvas");
94
+ canvas.width = w * scale;
95
+ canvas.height = h * scale;
96
+ const ctx = canvas.getContext("2d")!;
97
+ ctx.scale(scale, scale);
98
+ ctx.drawImage(img, 0, 0, w, h);
99
+ canvas.toBlob((blob) => {
100
+ if (blob) downloadBlob(blob, `${filename}.png`);
101
+ });
102
+ URL.revokeObjectURL(url);
103
+ };
104
+ img.src = url;
105
+ }
106
+
107
+ /**
108
+ * Downloads a rendering canvas's current contents as a PNG — no SVG
109
+ * round-trip. The canvas backing store is already sized at
110
+ * devicePixelRatio, so the export is naturally high-resolution.
111
+ */
112
+ export function saveCanvasPng(canvas: HTMLCanvasElement, filename: string) {
113
+ canvas.toBlob((blob) => {
114
+ if (blob) downloadBlob(blob, `${filename}.png`);
115
+ }, "image/png");
116
+ }
117
+
118
+ export function downloadCsv(csv: string, filename: string) {
119
+ downloadBlob(new Blob([csv], { type: "text/csv" }), `${filename}.csv`);
120
+ }
@@ -0,0 +1,97 @@
1
+ <script setup lang="ts">
2
+ import {
3
+ PopoverRoot,
4
+ PopoverAnchor,
5
+ PopoverPortal,
6
+ PopoverContent,
7
+ } from "reka-ui";
8
+
9
+ const props = withDefaults(
10
+ defineProps<{
11
+ /** Pixel x-coordinate relative to the positioned parent container */
12
+ x: number;
13
+ /** Pixel y-coordinate relative to the positioned parent container */
14
+ y: number;
15
+ /** Whether the tooltip is visible */
16
+ open: boolean;
17
+ /** Interaction mode. Default: 'hover' */
18
+ mode?: "hover" | "click";
19
+ /** Offset from anchor in pixels. Default: 8 */
20
+ sideOffset?: number;
21
+ }>(),
22
+ {
23
+ mode: "hover",
24
+ sideOffset: 16,
25
+ },
26
+ );
27
+
28
+ defineEmits<{
29
+ /** Fired in click mode when user dismisses the tooltip */
30
+ (e: "close"): void;
31
+ }>();
32
+ </script>
33
+
34
+ <template>
35
+ <!-- Hover mode: lightweight CSS-positioned div -->
36
+ <div
37
+ v-if="mode === 'hover' && open"
38
+ class="chart-tooltip-content"
39
+ :style="{
40
+ position: 'absolute',
41
+ left: `${x + sideOffset}px`,
42
+ top: `${y}px`,
43
+ transform: 'translateY(-50%)',
44
+ }"
45
+ >
46
+ <slot />
47
+ </div>
48
+
49
+ <!-- Click mode: reka-ui Popover for dismiss-on-outside-click -->
50
+ <template v-else-if="mode === 'click'">
51
+ <PopoverRoot :open="open">
52
+ <PopoverAnchor as-child>
53
+ <div
54
+ class="chart-tooltip-anchor"
55
+ :style="{ left: `${x}px`, top: `${y}px` }"
56
+ />
57
+ </PopoverAnchor>
58
+ <PopoverPortal>
59
+ <PopoverContent
60
+ v-if="open"
61
+ class="chart-tooltip-content"
62
+ side="right"
63
+ :side-offset="sideOffset"
64
+ update-position-strategy="always"
65
+ @pointer-down-outside="$emit('close')"
66
+ @escape-key-down="$emit('close')"
67
+ >
68
+ <slot />
69
+ </PopoverContent>
70
+ </PopoverPortal>
71
+ </PopoverRoot>
72
+ </template>
73
+ </template>
74
+
75
+ <style scoped>
76
+ .chart-tooltip-anchor {
77
+ position: absolute;
78
+ width: 1px;
79
+ height: 1px;
80
+ pointer-events: none;
81
+ }
82
+ </style>
83
+
84
+ <style>
85
+ .chart-tooltip-content {
86
+ z-index: 100;
87
+ background: var(--color-bg-0, #fff);
88
+ border: 1px solid var(--color-border, #e5e7eb);
89
+ border-radius: 0.375em;
90
+ padding: 0.5em 0.75em;
91
+ font-size: var(--font-size-sm, 0.875rem);
92
+ box-shadow:
93
+ 0 4px 6px -1px rgba(0, 0, 0, 0.1),
94
+ 0 2px 4px -2px rgba(0, 0, 0, 0.1);
95
+ pointer-events: none;
96
+ }
97
+ </style>