@cfasim-ui/charts 0.1.9 → 0.2.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cfasim-ui/charts",
3
- "version": "0.1.9",
3
+ "version": "0.2.0",
4
4
  "type": "module",
5
5
  "description": "Chart visualization components for cfasim-ui",
6
6
  "license": "Apache-2.0",
@@ -13,11 +13,21 @@
13
13
  "access": "public"
14
14
  },
15
15
  "files": [
16
- "src"
16
+ "dist"
17
17
  ],
18
+ "main": "./dist/index.js",
19
+ "types": "./dist/index.d.ts",
18
20
  "exports": {
19
- ".": "./src/index.ts"
21
+ ".": {
22
+ "development": "./src/index.ts",
23
+ "types": "./dist/index.d.ts",
24
+ "import": "./dist/index.js"
25
+ },
26
+ "./style.css": "./dist/style.css"
20
27
  },
28
+ "sideEffects": [
29
+ "*.css"
30
+ ],
21
31
  "dependencies": {
22
32
  "d3-geo": "^3.1.1",
23
33
  "d3-selection": "^3.0.0",
@@ -25,7 +35,7 @@
25
35
  "reka-ui": "^2.9.2",
26
36
  "topojson-client": "^3.1.0",
27
37
  "us-atlas": "^3.0.1",
28
- "@cfasim-ui/shared": "0.1.9"
38
+ "@cfasim-ui/shared": "0.2.0"
29
39
  },
30
40
  "peerDependencies": {
31
41
  "vue": "^3.5.0"
@@ -37,8 +47,12 @@
37
47
  "@types/topojson-client": "^3.1.5",
38
48
  "@types/topojson-specification": "^1.0.5",
39
49
  "@vitejs/plugin-vue": "^6.0.5",
50
+ "vite-plugin-dts": "^4.5.4",
40
51
  "@vue/test-utils": "^2.4.6",
41
52
  "happy-dom": "^20.8.9",
42
53
  "vitest": "^4.1.0"
54
+ },
55
+ "scripts": {
56
+ "build": "vite build"
43
57
  }
44
58
  }
@@ -1,140 +0,0 @@
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
- }
14
-
15
- defineProps<{
16
- items: ChartMenuItem[];
17
- }>();
18
- </script>
19
-
20
- <template>
21
- <div class="chart-menu-trigger-area">
22
- <!-- Single item: plain button -->
23
- <button
24
- v-if="items.length === 1"
25
- class="chart-menu-button chart-menu-single"
26
- :aria-label="items[0].label"
27
- @click="items[0].action"
28
- >
29
- <svg
30
- width="14"
31
- height="14"
32
- viewBox="0 0 14 14"
33
- fill="none"
34
- stroke="currentColor"
35
- stroke-width="1.5"
36
- stroke-linecap="round"
37
- stroke-linejoin="round"
38
- aria-hidden="true"
39
- >
40
- <path d="M7 1v8M3 6l4 4 4-4M2 13h10" />
41
- </svg>
42
- </button>
43
- <!-- Multiple items: dropdown menu -->
44
- <DropdownMenuRoot v-else>
45
- <DropdownMenuTrigger class="chart-menu-button" aria-label="Chart options">
46
- <svg
47
- width="16"
48
- height="16"
49
- viewBox="0 0 16 16"
50
- fill="currentColor"
51
- aria-hidden="true"
52
- >
53
- <circle cx="3" cy="8" r="1.5" />
54
- <circle cx="8" cy="8" r="1.5" />
55
- <circle cx="13" cy="8" r="1.5" />
56
- </svg>
57
- </DropdownMenuTrigger>
58
- <DropdownMenuPortal>
59
- <DropdownMenuContent
60
- class="chart-menu-content"
61
- :side-offset="4"
62
- align="end"
63
- >
64
- <DropdownMenuItem
65
- v-for="item in items"
66
- :key="item.label"
67
- class="chart-menu-item"
68
- @select="item.action"
69
- >
70
- {{ item.label }}
71
- </DropdownMenuItem>
72
- </DropdownMenuContent>
73
- </DropdownMenuPortal>
74
- </DropdownMenuRoot>
75
- </div>
76
- </template>
77
-
78
- <style scoped>
79
- .chart-menu-trigger-area {
80
- position: absolute;
81
- top: 0;
82
- right: 0;
83
- z-index: 1;
84
- }
85
-
86
- .chart-menu-button {
87
- display: flex;
88
- align-items: center;
89
- justify-content: center;
90
- width: 28px;
91
- height: 28px;
92
- border: 1px solid var(--color-border);
93
- border-radius: 0.25em;
94
- background: var(--color-bg-0, #fff);
95
- color: var(--color-text-secondary);
96
- cursor: pointer;
97
- opacity: 0;
98
- transition: opacity 0.15s;
99
- }
100
-
101
- .chart-menu-button[data-state="open"] {
102
- opacity: 1;
103
- }
104
-
105
- .chart-menu-button:hover {
106
- background: var(--color-bg-1, rgba(0, 0, 0, 0.05));
107
- color: var(--color-text);
108
- }
109
- </style>
110
-
111
- <style>
112
- .chart-menu-content {
113
- z-index: 100;
114
- background: var(--color-bg-0);
115
- border: 1px solid var(--color-border);
116
- border-radius: 0.25em;
117
- padding: 0.25em;
118
- box-shadow:
119
- 0 4px 6px -1px rgba(0, 0, 0, 0.1),
120
- 0 2px 4px -2px rgba(0, 0, 0, 0.1);
121
- min-width: 140px;
122
- }
123
-
124
- .chart-menu-item {
125
- display: flex;
126
- align-items: center;
127
- padding: 0.375em 0.5em;
128
- border-radius: 0.25em;
129
- font-size: var(--font-size-sm);
130
- cursor: pointer;
131
- user-select: none;
132
- outline: none;
133
- white-space: nowrap;
134
- }
135
-
136
- .chart-menu-item[data-highlighted] {
137
- background: var(--color-primary);
138
- color: white;
139
- }
140
- </style>
@@ -1,44 +0,0 @@
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
- export function saveSvg(svg: SVGSVGElement, filename: string) {
11
- const clone = svg.cloneNode(true) as SVGSVGElement;
12
- clone.setAttribute("xmlns", "http://www.w3.org/2000/svg");
13
- const xml = new XMLSerializer().serializeToString(clone);
14
- downloadBlob(new Blob([xml], { type: "image/svg+xml" }), `${filename}.svg`);
15
- }
16
-
17
- export function savePng(svg: SVGSVGElement, filename: string) {
18
- const clone = svg.cloneNode(true) as SVGSVGElement;
19
- clone.setAttribute("xmlns", "http://www.w3.org/2000/svg");
20
- const xml = new XMLSerializer().serializeToString(clone);
21
- const svgBlob = new Blob([xml], { type: "image/svg+xml;charset=utf-8" });
22
- const url = URL.createObjectURL(svgBlob);
23
- const img = new Image();
24
- const w = svg.width.baseVal.value || svg.clientWidth;
25
- const h = svg.height.baseVal.value || svg.clientHeight;
26
- const scale = 2;
27
- img.onload = () => {
28
- const canvas = document.createElement("canvas");
29
- canvas.width = w * scale;
30
- canvas.height = h * scale;
31
- const ctx = canvas.getContext("2d")!;
32
- ctx.scale(scale, scale);
33
- ctx.drawImage(img, 0, 0, w, h);
34
- canvas.toBlob((blob) => {
35
- if (blob) downloadBlob(blob, `${filename}.png`);
36
- });
37
- URL.revokeObjectURL(url);
38
- };
39
- img.src = url;
40
- }
41
-
42
- export function downloadCsv(csv: string, filename: string) {
43
- downloadBlob(new Blob([csv], { type: "text/csv" }), `${filename}.csv`);
44
- }
@@ -1,330 +0,0 @@
1
- # ChoroplethMap
2
-
3
- A US choropleth map using D3's Albers USA projection, which repositions Alaska and Hawaii to the bottom left. Supports state-level, county-level, and HSA-level (Health Service Areas) rendering via the `geoType` prop. Includes built-in GeoJSON for all US states, territories, counties, and 949 HSAs.
4
-
5
- ## Examples
6
-
7
- ### Basic with state data
8
-
9
- <ComponentDemo>
10
- <ChoroplethMap
11
- :data="[
12
- { id: '06', value: 100 },
13
- { id: '36', value: 80 },
14
- { id: '48', value: 90 },
15
- { id: '12', value: 70 },
16
- { id: '17', value: 60 },
17
- { id: '37', value: 50 },
18
- { id: '42', value: 55 },
19
- { id: '39', value: 45 },
20
- { id: '13', value: 40 },
21
- { id: '26', value: 35 },
22
- ]"
23
- title="Cases by State"
24
- :legend-title="'Cases'"
25
- :height="400"
26
- />
27
-
28
- <template #code>
29
-
30
- ```vue
31
- <ChoroplethMap
32
- :data="[
33
- { id: '06', value: 100 },
34
- { id: '36', value: 80 },
35
- { id: '48', value: 90 },
36
- { id: '12', value: 70 },
37
- { id: '17', value: 60 },
38
- ]"
39
- title="Cases by State"
40
- :legend-title="'Cases'"
41
- :height="400"
42
- />
43
- ```
44
-
45
- </template>
46
- </ComponentDemo>
47
-
48
- ### Custom color scale
49
-
50
- <ComponentDemo>
51
- <ChoroplethMap
52
- :data="[
53
- { id: 'California', value: 100 },
54
- { id: 'Texas', value: 85 },
55
- { id: 'Florida', value: 70 },
56
- { id: 'New York', value: 90 },
57
- { id: 'Pennsylvania', value: 50 },
58
- { id: 'Illinois', value: 60 },
59
- { id: 'Ohio', value: 40 },
60
- { id: 'Georgia', value: 55 },
61
- { id: 'North Carolina', value: 45 },
62
- { id: 'Michigan', value: 35 },
63
- ]"
64
- :color-scale="{ min: '#fff5f0', max: '#a50f15' }"
65
- :legend-title="'Severity'"
66
- :height="400"
67
- />
68
-
69
- <template #code>
70
-
71
- ```vue
72
- <ChoroplethMap
73
- :data="[
74
- { id: 'California', value: 100 },
75
- { id: 'Texas', value: 85 },
76
- { id: 'Florida', value: 70 },
77
- { id: 'New York', value: 90 },
78
- ]"
79
- :color-scale="{ min: '#fff5f0', max: '#a50f15' }"
80
- :legend-title="'Severity'"
81
- :height="400"
82
- />
83
- ```
84
-
85
- </template>
86
- </ComponentDemo>
87
-
88
- ### Threshold color scale
89
-
90
- Use an array of `ThresholdStop` objects instead of a linear scale. Each stop defines a `min` threshold — values at or above that threshold get the stop's color. The highest matching stop wins.
91
-
92
- <ComponentDemo>
93
- <ChoroplethMap
94
- :data="[
95
- { id: 'California', value: 80 },
96
- { id: 'Texas', value: 45 },
97
- { id: 'Florida', value: 60 },
98
- { id: 'New York', value: 25 },
99
- { id: 'Pennsylvania', value: 8 },
100
- { id: 'Illinois', value: 55 },
101
- { id: 'Ohio', value: 30 },
102
- { id: 'Georgia', value: 70 },
103
- { id: 'North Carolina', value: 15 },
104
- { id: 'Michigan', value: 3 },
105
- ]"
106
- :color-scale="[
107
- { min: 0, color: '#fee5d9', label: 'Low' },
108
- { min: 10, color: '#fcae91', label: 'Some' },
109
- { min: 30, color: '#fb6a4a', label: 'Moderate' },
110
- { min: 60, color: '#cb181d', label: 'High' },
111
- ]"
112
- title="Risk Level"
113
- :legend-title="'Risk'"
114
- :height="400"
115
- />
116
-
117
- <template #code>
118
-
119
- ```vue
120
- <ChoroplethMap
121
- :data="stateData"
122
- :color-scale="[
123
- { min: 0, color: '#fee5d9', label: 'Low' },
124
- { min: 10, color: '#fcae91', label: 'Some' },
125
- { min: 30, color: '#fb6a4a', label: 'Moderate' },
126
- { min: 60, color: '#cb181d', label: 'High' },
127
- ]"
128
- title="Risk Level"
129
- :legend-title="'Risk'"
130
- :height="400"
131
- />
132
- ```
133
-
134
- </template>
135
- </ComponentDemo>
136
-
137
- ### Categorical color scale
138
-
139
- Use an array of `CategoricalStop` objects to map string values to colors. Each stop defines a `value` to match and a `color` to apply.
140
-
141
- <ComponentDemo>
142
- <ChoroplethMap
143
- :data="[
144
- { id: 'California', value: 'high' },
145
- { id: 'Texas', value: 'medium' },
146
- { id: 'Florida', value: 'high' },
147
- { id: 'New York', value: 'low' },
148
- { id: 'Pennsylvania', value: 'low' },
149
- { id: 'Illinois', value: 'medium' },
150
- { id: 'Ohio', value: 'low' },
151
- { id: 'Georgia', value: 'high' },
152
- { id: 'North Carolina', value: 'medium' },
153
- { id: 'Michigan', value: 'low' },
154
- ]"
155
- :color-scale="[
156
- { value: 'low', color: '#fee5d9' },
157
- { value: 'medium', color: '#fb6a4a' },
158
- { value: 'high', color: '#cb181d' },
159
- ]"
160
- title="Risk Category"
161
- :legend-title="'Risk'"
162
- :height="400"
163
- />
164
-
165
- <template #code>
166
-
167
- ```vue
168
- <ChoroplethMap
169
- :data="stateData"
170
- :color-scale="[
171
- { value: 'low', color: '#fee5d9' },
172
- { value: 'medium', color: '#fb6a4a' },
173
- { value: 'high', color: '#cb181d' },
174
- ]"
175
- title="Risk Category"
176
- :legend-title="'Risk'"
177
- :height="400"
178
- />
179
- ```
180
-
181
- </template>
182
- </ComponentDemo>
183
-
184
- ### County-level map with pan and zoom
185
-
186
- Set `geoType="counties"` to render county-level data using 5-digit FIPS codes. State borders are drawn on top for context. Use `pan` and `zoom` props to enable interactive navigation — useful for dense county data.
187
-
188
- <ComponentDemo>
189
- <ChoroplethMap
190
- geo-type="counties"
191
- :pan="true"
192
- :zoom="true"
193
- :data="[
194
- { id: '06037', value: 100 },
195
- { id: '06073', value: 80 },
196
- { id: '06059', value: 70 },
197
- { id: '36061', value: 90 },
198
- { id: '36047', value: 75 },
199
- { id: '17031', value: 85 },
200
- { id: '48201', value: 65 },
201
- { id: '04013', value: 60 },
202
- { id: '12086', value: 55 },
203
- { id: '53033', value: 50 },
204
- ]"
205
- title="Cases by County"
206
- :legend-title="'Cases'"
207
- :height="400"
208
- />
209
-
210
- <template #code>
211
-
212
- ```vue
213
- <ChoroplethMap
214
- geo-type="counties"
215
- pan
216
- zoom
217
- :data="[
218
- { id: '06037', value: 100 },
219
- { id: '36061', value: 90 },
220
- { id: '17031', value: 85 },
221
- { id: '48201', value: 65 },
222
- { id: '04013', value: 60 },
223
- ]"
224
- title="Cases by County"
225
- :legend-title="'Cases'"
226
- :height="400"
227
- />
228
- ```
229
-
230
- </template>
231
- </ComponentDemo>
232
-
233
- ### HSA-level map
234
-
235
- Set `geoType="hsas"` to render Health Service Area boundaries. HSAs are dissolved from county boundaries using a built-in FIPS-to-HSA mapping. Use 6-digit HSA codes as IDs. State borders are overlaid for context.
236
-
237
- <ComponentDemo>
238
- <ChoroplethMap
239
- geo-type="hsas"
240
- :pan="true"
241
- :zoom="true"
242
- :data="[
243
- { id: '010259', value: 100 },
244
- { id: '060766', value: 90 },
245
- { id: '120159', value: 85 },
246
- { id: '090121', value: 70 },
247
- { id: '110061', value: 60 },
248
- { id: '040765', value: 55 },
249
- { id: '080731', value: 50 },
250
- { id: '050527', value: 45 },
251
- { id: '100075', value: 40 },
252
- { id: '020820', value: 35 },
253
- ]"
254
- title="Cases by HSA"
255
- :legend-title="'Cases'"
256
- :height="400"
257
- />
258
-
259
- <template #code>
260
-
261
- ```vue
262
- <ChoroplethMap
263
- geo-type="hsas"
264
- pan
265
- zoom
266
- :data="[
267
- { id: '010259', value: 100 },
268
- { id: '060766', value: 90 },
269
- { id: '120159', value: 85 },
270
- { id: '090121', value: 70 },
271
- { id: '110061', value: 60 },
272
- ]"
273
- title="Cases by HSA"
274
- :legend-title="'Cases'"
275
- :height="400"
276
- />
277
- ```
278
-
279
- </template>
280
- </ComponentDemo>
281
-
282
- <!--@include: ./_api/choropleth-map.md-->
283
-
284
- ### StateData
285
-
286
- ```ts
287
- interface StateData {
288
- /** FIPS code (e.g. "06" for California, "06037" for LA County) or name */
289
- id: string;
290
- value: number | string;
291
- }
292
- ```
293
-
294
- ### ChoroplethColorScale
295
-
296
- ```ts
297
- interface ChoroplethColorScale {
298
- /** Minimum color (CSS color string). Default: "#e5f0fa" */
299
- min?: string;
300
- /** Maximum color (CSS color string). Default: "#08519c" */
301
- max?: string;
302
- }
303
- ```
304
-
305
- ### ThresholdStop
306
-
307
- Pass an array of `ThresholdStop` as `colorScale` for discrete color buckets instead of a linear gradient. The highest matching `min` wins.
308
-
309
- ```ts
310
- interface ThresholdStop {
311
- /** Lower bound (inclusive). Values at or above this get this color. */
312
- min: number;
313
- color: string;
314
- /** Optional label for the legend (defaults to the min value) */
315
- label?: string;
316
- }
317
- ```
318
-
319
- ### CategoricalStop
320
-
321
- Pass an array of `CategoricalStop` as `colorScale` to map string values to colors. States whose `value` matches a stop's `value` get that color; unmatched values get `noDataColor`.
322
-
323
- ```ts
324
- interface CategoricalStop {
325
- /** The categorical value to match */
326
- value: string;
327
- /** CSS color string */
328
- color: string;
329
- }
330
- ```