@cfasim-ui/charts 0.1.1 → 0.1.3

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.
@@ -0,0 +1,468 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { mount } from "@vue/test-utils";
3
+ import ChoroplethMap from "./ChoroplethMap.vue";
4
+
5
+ describe("ChoroplethMap", () => {
6
+ it("renders SVG with state paths", () => {
7
+ const wrapper = mount(ChoroplethMap, {
8
+ props: { width: 600, height: 400 },
9
+ });
10
+ const svg = wrapper.find("svg");
11
+ expect(svg.exists()).toBe(true);
12
+ const paths = wrapper.findAll(".state-path");
13
+ // us-atlas states-10m has 56 geometries (50 states + DC + territories)
14
+ expect(paths.length).toBeGreaterThanOrEqual(50);
15
+ });
16
+
17
+ it("renders without data (all states default gray)", () => {
18
+ const wrapper = mount(ChoroplethMap, {
19
+ props: { width: 600, height: 400 },
20
+ });
21
+ const paths = wrapper.findAll(".state-path");
22
+ for (const path of paths) {
23
+ expect(path.attributes("fill")).toBe("#ddd");
24
+ }
25
+ });
26
+
27
+ it("colors states based on data by FIPS id", () => {
28
+ const wrapper = mount(ChoroplethMap, {
29
+ props: {
30
+ width: 600,
31
+ height: 400,
32
+ data: [
33
+ { id: "06", value: 100 }, // California
34
+ { id: "36", value: 0 }, // New York
35
+ ],
36
+ },
37
+ });
38
+ const california = wrapper
39
+ .findAll(".state-path")
40
+ .find((p) => p.find("title").text().includes("California"));
41
+ expect(california).toBeDefined();
42
+ // California has max value so should get the max color
43
+ expect(california!.attributes("fill")).not.toBe("#ddd");
44
+ });
45
+
46
+ it("colors states based on data by name", () => {
47
+ const wrapper = mount(ChoroplethMap, {
48
+ props: {
49
+ width: 600,
50
+ height: 400,
51
+ data: [{ id: "California", value: 50 }],
52
+ },
53
+ });
54
+ const california = wrapper
55
+ .findAll(".state-path")
56
+ .find((p) => p.find("title").text().includes("California"));
57
+ expect(california).toBeDefined();
58
+ expect(california!.attributes("fill")).not.toBe("#ddd");
59
+ });
60
+
61
+ it("renders title when provided", () => {
62
+ const wrapper = mount(ChoroplethMap, {
63
+ props: { width: 600, height: 400, title: "US Cases" },
64
+ });
65
+ const title = wrapper.find("svg > text");
66
+ expect(title.text()).toBe("US Cases");
67
+ });
68
+
69
+ it("applies custom color scale", () => {
70
+ const wrapper = mount(ChoroplethMap, {
71
+ props: {
72
+ width: 600,
73
+ height: 400,
74
+ data: [
75
+ { id: "06", value: 0 },
76
+ { id: "36", value: 100 },
77
+ ],
78
+ colorScale: { min: "#ffffff", max: "#ff0000" },
79
+ },
80
+ });
81
+ const ny = wrapper
82
+ .findAll(".state-path")
83
+ .find((p) => p.find("title").text().includes("New York"));
84
+ expect(ny!.attributes("fill")).toBe("rgb(255,0,0)");
85
+ });
86
+
87
+ it("shows state name and value in tooltip title", () => {
88
+ const wrapper = mount(ChoroplethMap, {
89
+ props: {
90
+ width: 600,
91
+ height: 400,
92
+ data: [{ id: "06", value: 42 }],
93
+ },
94
+ });
95
+ const california = wrapper
96
+ .findAll(".state-path")
97
+ .find((p) => p.find("title").text().includes("California"));
98
+ expect(california!.find("title").text()).toContain("42");
99
+ });
100
+
101
+ it("applies threshold color scale", () => {
102
+ const wrapper = mount(ChoroplethMap, {
103
+ props: {
104
+ width: 600,
105
+ height: 400,
106
+ data: [
107
+ { id: "06", value: 80 },
108
+ { id: "36", value: 30 },
109
+ { id: "48", value: 5 },
110
+ ],
111
+ colorScale: [
112
+ { min: 0, color: "#green1" },
113
+ { min: 10, color: "#yellow1" },
114
+ { min: 50, color: "#red1" },
115
+ ],
116
+ },
117
+ });
118
+ const california = wrapper
119
+ .findAll(".state-path")
120
+ .find((p) => p.find("title").text().includes("California"));
121
+ const ny = wrapper
122
+ .findAll(".state-path")
123
+ .find((p) => p.find("title").text().includes("New York"));
124
+ const tx = wrapper
125
+ .findAll(".state-path")
126
+ .find((p) => p.find("title").text().includes("Texas"));
127
+ expect(california!.attributes("fill")).toBe("#red1");
128
+ expect(ny!.attributes("fill")).toBe("#yellow1");
129
+ expect(tx!.attributes("fill")).toBe("#green1");
130
+ });
131
+
132
+ it("threshold scale returns default gray when below all stops", () => {
133
+ const wrapper = mount(ChoroplethMap, {
134
+ props: {
135
+ width: 600,
136
+ height: 400,
137
+ data: [{ id: "06", value: 5 }],
138
+ colorScale: [{ min: 100, color: "#red" }],
139
+ },
140
+ });
141
+ const california = wrapper
142
+ .findAll(".state-path")
143
+ .find((p) => p.find("title").text().includes("California"));
144
+ expect(california!.attributes("fill")).toBe("#ddd");
145
+ });
146
+
147
+ it("threshold stops can be provided in any order", () => {
148
+ const wrapper = mount(ChoroplethMap, {
149
+ props: {
150
+ width: 600,
151
+ height: 400,
152
+ data: [
153
+ { id: "06", value: 50 },
154
+ { id: "36", value: 5 },
155
+ ],
156
+ colorScale: [
157
+ { min: 10, color: "#high" },
158
+ { min: 0, color: "#low" },
159
+ ],
160
+ },
161
+ });
162
+ const california = wrapper
163
+ .findAll(".state-path")
164
+ .find((p) => p.find("title").text().includes("California"));
165
+ const ny = wrapper
166
+ .findAll(".state-path")
167
+ .find((p) => p.find("title").text().includes("New York"));
168
+ expect(california!.attributes("fill")).toBe("#high");
169
+ expect(ny!.attributes("fill")).toBe("#low");
170
+ });
171
+
172
+ it("applies categorical color scale with string values", () => {
173
+ const wrapper = mount(ChoroplethMap, {
174
+ props: {
175
+ width: 600,
176
+ height: 400,
177
+ data: [
178
+ { id: "06", value: "high" },
179
+ { id: "36", value: "low" },
180
+ { id: "48", value: "medium" },
181
+ ],
182
+ colorScale: [
183
+ { value: "low", color: "#green1" },
184
+ { value: "medium", color: "#yellow1" },
185
+ { value: "high", color: "#red1" },
186
+ ],
187
+ },
188
+ });
189
+ const california = wrapper
190
+ .findAll(".state-path")
191
+ .find((p) => p.find("title").text().includes("California"));
192
+ const ny = wrapper
193
+ .findAll(".state-path")
194
+ .find((p) => p.find("title").text().includes("New York"));
195
+ const tx = wrapper
196
+ .findAll(".state-path")
197
+ .find((p) => p.find("title").text().includes("Texas"));
198
+ expect(california!.attributes("fill")).toBe("#red1");
199
+ expect(ny!.attributes("fill")).toBe("#green1");
200
+ expect(tx!.attributes("fill")).toBe("#yellow1");
201
+ });
202
+
203
+ it("categorical scale returns noDataColor for unmatched values", () => {
204
+ const wrapper = mount(ChoroplethMap, {
205
+ props: {
206
+ width: 600,
207
+ height: 400,
208
+ data: [{ id: "06", value: "unknown" }],
209
+ colorScale: [{ value: "high", color: "#red" }],
210
+ },
211
+ });
212
+ const california = wrapper
213
+ .findAll(".state-path")
214
+ .find((p) => p.find("title").text().includes("California"));
215
+ expect(california!.attributes("fill")).toBe("#ddd");
216
+ });
217
+
218
+ it("categorical scale shows string value in tooltip", () => {
219
+ const wrapper = mount(ChoroplethMap, {
220
+ props: {
221
+ width: 600,
222
+ height: 400,
223
+ data: [{ id: "06", value: "high" }],
224
+ colorScale: [{ value: "high", color: "#red" }],
225
+ },
226
+ });
227
+ const california = wrapper
228
+ .findAll(".state-path")
229
+ .find((p) => p.find("title").text().includes("California"));
230
+ expect(california!.find("title").text()).toContain("high");
231
+ });
232
+
233
+ it("uses noDataColor for states without data", () => {
234
+ const wrapper = mount(ChoroplethMap, {
235
+ props: {
236
+ width: 600,
237
+ height: 400,
238
+ data: [{ id: "06", value: 50 }],
239
+ noDataColor: "#eee",
240
+ },
241
+ });
242
+ const ny = wrapper
243
+ .findAll(".state-path")
244
+ .find((p) => p.find("title").text().includes("New York"));
245
+ expect(ny!.attributes("fill")).toBe("#eee");
246
+ });
247
+
248
+ it("emits stateClick on path click", async () => {
249
+ const wrapper = mount(ChoroplethMap, {
250
+ props: { width: 600, height: 400 },
251
+ });
252
+ const firstPath = wrapper.find(".state-path");
253
+ await firstPath.trigger("click");
254
+ expect(wrapper.emitted("stateClick")).toHaveLength(1);
255
+ const payload = wrapper.emitted("stateClick")![0][0] as {
256
+ id: string;
257
+ name: string;
258
+ };
259
+ expect(payload.id).toBeDefined();
260
+ expect(payload.name).toBeDefined();
261
+ });
262
+
263
+ it("renders categorical legend with circles and labels", () => {
264
+ const wrapper = mount(ChoroplethMap, {
265
+ props: {
266
+ width: 600,
267
+ height: 400,
268
+ data: [{ id: "06", value: "high" }],
269
+ colorScale: [
270
+ { value: "low", color: "#aaa" },
271
+ { value: "high", color: "#f00" },
272
+ ],
273
+ legendTitle: "Risk",
274
+ },
275
+ });
276
+ const legend = wrapper.find(".choropleth-legend");
277
+ expect(legend.exists()).toBe(true);
278
+ const texts = legend.findAll("text");
279
+ expect(texts[0].text()).toBe("Risk");
280
+ expect(texts[1].text()).toBe("low");
281
+ expect(texts[2].text()).toBe("high");
282
+ const rects = legend.findAll("rect");
283
+ expect(rects).toHaveLength(2);
284
+ });
285
+
286
+ it("renders threshold legend with circles and labels", () => {
287
+ const wrapper = mount(ChoroplethMap, {
288
+ props: {
289
+ width: 600,
290
+ height: 400,
291
+ data: [{ id: "06", value: 50 }],
292
+ colorScale: [
293
+ { min: 0, color: "#aaa", label: "Low" },
294
+ { min: 50, color: "#f00", label: "High" },
295
+ ],
296
+ legendTitle: "Level",
297
+ },
298
+ });
299
+ const legend = wrapper.find(".choropleth-legend");
300
+ expect(legend.exists()).toBe(true);
301
+ const texts = legend.findAll("text");
302
+ expect(texts[0].text()).toBe("Level");
303
+ expect(texts[1].text()).toBe("Low");
304
+ expect(texts[2].text()).toBe("High");
305
+ });
306
+
307
+ it("renders threshold legend with min values when no label", () => {
308
+ const wrapper = mount(ChoroplethMap, {
309
+ props: {
310
+ width: 600,
311
+ height: 400,
312
+ data: [{ id: "06", value: 50 }],
313
+ colorScale: [
314
+ { min: 0, color: "#aaa" },
315
+ { min: 50, color: "#f00" },
316
+ ],
317
+ },
318
+ });
319
+ const legend = wrapper.find(".choropleth-legend");
320
+ const texts = legend.findAll("text");
321
+ expect(texts[0].text()).toBe("0");
322
+ expect(texts[1].text()).toBe("50");
323
+ });
324
+
325
+ it("renders continuous legend with gradient rect and ticks", () => {
326
+ const wrapper = mount(ChoroplethMap, {
327
+ props: {
328
+ width: 600,
329
+ height: 400,
330
+ data: [
331
+ { id: "06", value: 0 },
332
+ { id: "36", value: 100 },
333
+ ],
334
+ legendTitle: "Severity",
335
+ },
336
+ });
337
+ const legend = wrapper.find(".choropleth-legend");
338
+ expect(legend.exists()).toBe(true);
339
+ const texts = legend.findAll("text");
340
+ expect(texts[0].text()).toBe("Severity");
341
+ expect(legend.find("rect").exists()).toBe(true);
342
+ expect(legend.find("linearGradient").exists()).toBe(true);
343
+ // tick labels
344
+ expect(texts.length).toBeGreaterThanOrEqual(3);
345
+ });
346
+
347
+ it("hides legend when legend=false", () => {
348
+ const wrapper = mount(ChoroplethMap, {
349
+ props: {
350
+ width: 600,
351
+ height: 400,
352
+ data: [{ id: "06", value: 50 }],
353
+ legend: false,
354
+ },
355
+ });
356
+ expect(wrapper.find(".choropleth-legend").exists()).toBe(false);
357
+ });
358
+
359
+ it("renders county paths when geoType is counties", () => {
360
+ const wrapper = mount(ChoroplethMap, {
361
+ props: { width: 600, height: 400, geoType: "counties" },
362
+ });
363
+ const paths = wrapper.findAll(".state-path");
364
+ // us-atlas counties-10m has 3231 county geometries
365
+ expect(paths.length).toBeGreaterThanOrEqual(3000);
366
+ });
367
+
368
+ it("colors counties by FIPS id", () => {
369
+ const wrapper = mount(ChoroplethMap, {
370
+ props: {
371
+ width: 600,
372
+ height: 400,
373
+ geoType: "counties",
374
+ data: [
375
+ { id: "04015", value: 100 }, // Mohave County, AZ
376
+ { id: "06037", value: 0 }, // Los Angeles County, CA
377
+ ],
378
+ },
379
+ });
380
+ const mohave = wrapper
381
+ .findAll(".state-path")
382
+ .find((p) => p.find("title").text().includes("Mohave"));
383
+ expect(mohave).toBeDefined();
384
+ expect(mohave!.attributes("fill")).not.toBe("#ddd");
385
+ });
386
+
387
+ it("renders state borders overlay in county mode", () => {
388
+ const wrapper = mount(ChoroplethMap, {
389
+ props: { width: 600, height: 400, geoType: "counties" },
390
+ });
391
+ // State borders path rendered after the county paths group
392
+ const allPaths = wrapper.findAll("path");
393
+ const borderPath = allPaths.find(
394
+ (p) =>
395
+ p.attributes("fill") === "none" &&
396
+ p.attributes("pointer-events") === "none",
397
+ );
398
+ expect(borderPath).toBeDefined();
399
+ });
400
+
401
+ it("does not render state borders in states mode", () => {
402
+ const wrapper = mount(ChoroplethMap, {
403
+ props: { width: 600, height: 400 },
404
+ });
405
+ const allPaths = wrapper.findAll("path");
406
+ const borderPath = allPaths.find(
407
+ (p) =>
408
+ p.attributes("fill") === "none" &&
409
+ p.attributes("pointer-events") === "none",
410
+ );
411
+ expect(borderPath).toBeUndefined();
412
+ });
413
+
414
+ it("renders HSA paths when geoType is hsas", () => {
415
+ const wrapper = mount(ChoroplethMap, {
416
+ props: { width: 600, height: 400, geoType: "hsas" },
417
+ });
418
+ const paths = wrapper.findAll(".state-path");
419
+ // 949 unique HSAs
420
+ expect(paths.length).toBeGreaterThanOrEqual(900);
421
+ expect(paths.length).toBeLessThan(1000);
422
+ });
423
+
424
+ it("colors HSAs by HSA code", () => {
425
+ const wrapper = mount(ChoroplethMap, {
426
+ props: {
427
+ width: 600,
428
+ height: 400,
429
+ geoType: "hsas",
430
+ data: [
431
+ { id: "010259", value: 100 }, // Butler, AL
432
+ { id: "010177", value: 0 }, // Calhoun (Anniston), AL
433
+ ],
434
+ },
435
+ });
436
+ const butler = wrapper
437
+ .findAll(".state-path")
438
+ .find((p) => p.find("title").text().includes("Butler, AL"));
439
+ expect(butler).toBeDefined();
440
+ expect(butler!.attributes("fill")).not.toBe("#ddd");
441
+ });
442
+
443
+ it("renders state borders overlay in HSA mode", () => {
444
+ const wrapper = mount(ChoroplethMap, {
445
+ props: { width: 600, height: 400, geoType: "hsas" },
446
+ });
447
+ const allPaths = wrapper.findAll("path");
448
+ const borderPath = allPaths.find(
449
+ (p) =>
450
+ p.attributes("fill") === "none" &&
451
+ p.attributes("pointer-events") === "none",
452
+ );
453
+ expect(borderPath).toBeDefined();
454
+ });
455
+
456
+ it("emits stateHover on mouseenter/mouseleave", async () => {
457
+ const wrapper = mount(ChoroplethMap, {
458
+ props: { width: 600, height: 400 },
459
+ });
460
+ const firstPath = wrapper.find(".state-path");
461
+ await firstPath.trigger("mouseenter");
462
+ expect(wrapper.emitted("stateHover")).toHaveLength(1);
463
+ expect(wrapper.emitted("stateHover")![0][0]).not.toBeNull();
464
+ await firstPath.trigger("mouseleave");
465
+ expect(wrapper.emitted("stateHover")).toHaveLength(2);
466
+ expect(wrapper.emitted("stateHover")![1][0]).toBeNull();
467
+ });
468
+ });