@cfasim-ui/docs 0.7.5 → 0.7.6
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.
|
@@ -370,6 +370,41 @@ before and after zooming in.
|
|
|
370
370
|
</template>
|
|
371
371
|
</ComponentDemo>
|
|
372
372
|
|
|
373
|
+
### Tighter national fit (`tight-fit`)
|
|
374
|
+
|
|
375
|
+
The Albers USA projection places Alaska and Hawaii in the bottom-left corner, and by default the whole composite is fit into view — so Alaska's western tail pushes the contiguous US in from the edges. Set `tight-fit` to crop that overhang and let the lower-48 fill more of the frame: Alaska's tail (and Hawaii) clip into the lower-left corner. Pass a number in `0`–`1` (e.g. `:tight-fit="0.5"`) to crop only partway.
|
|
376
|
+
|
|
377
|
+
Only affects the national (multi-state) view — it's a no-op in single-state mode and on national HSA maps (HSA ids aren't FIPS codes, so Alaska/Hawaii can't be split out).
|
|
378
|
+
|
|
379
|
+
<ComponentDemo>
|
|
380
|
+
<ChoroplethMap
|
|
381
|
+
:topology="statesTopo"
|
|
382
|
+
tight-fit
|
|
383
|
+
:data="[
|
|
384
|
+
{ id: '06', value: 100 },
|
|
385
|
+
{ id: '36', value: 80 },
|
|
386
|
+
{ id: '48', value: 90 },
|
|
387
|
+
{ id: '12', value: 70 },
|
|
388
|
+
{ id: '17', value: 60 },
|
|
389
|
+
]"
|
|
390
|
+
title="Cases by State (tight fit)"
|
|
391
|
+
:legend-title="'Cases'"
|
|
392
|
+
:height="400"
|
|
393
|
+
/>
|
|
394
|
+
|
|
395
|
+
<template #code>
|
|
396
|
+
|
|
397
|
+
```vue
|
|
398
|
+
<!-- Crop Alaska's overhang so the lower-48 fills the frame -->
|
|
399
|
+
<ChoroplethMap :topology="statesTopo" tight-fit :data="data" />
|
|
400
|
+
|
|
401
|
+
<!-- ...or crop only partway -->
|
|
402
|
+
<ChoroplethMap :topology="statesTopo" :tight-fit="0.5" :data="data" />
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
</template>
|
|
406
|
+
</ComponentDemo>
|
|
407
|
+
|
|
373
408
|
### HSA-level map
|
|
374
409
|
|
|
375
410
|
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.
|
|
@@ -918,6 +953,7 @@ tech).
|
|
|
918
953
|
| `state` | `string` | No | — |
|
|
919
954
|
| `width` | `number` | No | — |
|
|
920
955
|
| `height` | `number` | No | — |
|
|
956
|
+
| `tightFit` | `boolean \| number` | No | `false` |
|
|
921
957
|
| `colorScale` | `ChoroplethColorScale \| ThresholdStop[] \| CategoricalStop[]` | No | — |
|
|
922
958
|
| `title` | `string` | No | — |
|
|
923
959
|
| `titleStyle` | `TitleStyle` | No | — |
|
|
@@ -142,6 +142,16 @@ const props = withDefaults(
|
|
|
142
142
|
state?: string;
|
|
143
143
|
width?: number;
|
|
144
144
|
height?: number;
|
|
145
|
+
/**
|
|
146
|
+
* Tighten the national (multi-state) fit by cropping the Alaska/Hawaii
|
|
147
|
+
* overhang so the contiguous US fills more of the frame. `false`/`0`
|
|
148
|
+
* (default) fits every region into view; `true`/`1` fits the lower-48 to
|
|
149
|
+
* the frame and lets Alaska's western tail (and Hawaii) clip into the
|
|
150
|
+
* lower-left corner. A number in between (e.g. `0.5`) crops partway. No
|
|
151
|
+
* effect in single-state mode or on a national HSA map (HSA ids aren't
|
|
152
|
+
* FIPS, so AK/HI can't be split out).
|
|
153
|
+
*/
|
|
154
|
+
tightFit?: boolean | number;
|
|
145
155
|
colorScale?: ChoroplethColorScale | ThresholdStop[] | CategoricalStop[];
|
|
146
156
|
/**
|
|
147
157
|
* Map title. `\n` in the string creates additional lines, each
|
|
@@ -301,6 +311,7 @@ const props = withDefaults(
|
|
|
301
311
|
tooltipClamp: "chart",
|
|
302
312
|
focusZoomLevel: 4,
|
|
303
313
|
focusZoom: true,
|
|
314
|
+
tightFit: false,
|
|
304
315
|
},
|
|
305
316
|
);
|
|
306
317
|
|
|
@@ -1357,6 +1368,31 @@ const stateBordersPath = computed(() => {
|
|
|
1357
1368
|
// flush against the SVG edge. Only applied in single-state mode.
|
|
1358
1369
|
const STATE_FIT_INSET = 12;
|
|
1359
1370
|
|
|
1371
|
+
// Resolved `tightFit` amount in [0,1]: false/0 → 0 (full fit), true → 1.
|
|
1372
|
+
const tightFitAmount = computed(() => {
|
|
1373
|
+
const v = props.tightFit;
|
|
1374
|
+
if (v === true) return 1;
|
|
1375
|
+
if (!v) return 0;
|
|
1376
|
+
return Math.max(0, Math.min(1, Number(v)));
|
|
1377
|
+
});
|
|
1378
|
+
|
|
1379
|
+
// The national fit target with Alaska (FIPS 02) and Hawaii (15) removed, so
|
|
1380
|
+
// the projection can be re-fit to just the contiguous US. Null when the crop
|
|
1381
|
+
// is off, in single-state mode, on an HSA map (ids aren't FIPS), or when
|
|
1382
|
+
// nothing was removed — the projection then uses the plain full fit.
|
|
1383
|
+
const conusFeaturesGeo = computed(() => {
|
|
1384
|
+
if (tightFitAmount.value <= 0 || stateFips.value) return null;
|
|
1385
|
+
if (props.geoType === "hsas") return null;
|
|
1386
|
+
const fc = featuresGeo.value;
|
|
1387
|
+
const pad = props.geoType === "counties" ? 5 : 2;
|
|
1388
|
+
const kept = fc.features.filter((f) => {
|
|
1389
|
+
const st = String(f.id).padStart(pad, "0").slice(0, 2);
|
|
1390
|
+
return st !== "02" && st !== "15";
|
|
1391
|
+
});
|
|
1392
|
+
if (kept.length === fc.features.length) return null;
|
|
1393
|
+
return { type: "FeatureCollection" as const, features: kept };
|
|
1394
|
+
});
|
|
1395
|
+
|
|
1360
1396
|
const projection = computed(() => {
|
|
1361
1397
|
const outline = stateOutlineFeature.value;
|
|
1362
1398
|
if (stateFips.value && outline) {
|
|
@@ -1376,13 +1412,27 @@ const projection = computed(() => {
|
|
|
1376
1412
|
if (Number.isFinite(c[0]) && Number.isFinite(c[1])) return albers;
|
|
1377
1413
|
return geoMercator().fitExtent(extent, outline);
|
|
1378
1414
|
}
|
|
1379
|
-
|
|
1380
|
-
[
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
);
|
|
1415
|
+
const extent: [[number, number], [number, number]] = [
|
|
1416
|
+
[0, 0],
|
|
1417
|
+
[width.value, height.value],
|
|
1418
|
+
];
|
|
1419
|
+
const full = geoAlbersUsa().fitExtent(extent, featuresGeo.value);
|
|
1420
|
+
const conus = conusFeaturesGeo.value;
|
|
1421
|
+
if (!conus) return full;
|
|
1422
|
+
// Tighten the fit by interpolating the projection's scale + translate from
|
|
1423
|
+
// the full fit toward the contiguous-US fit. At amount 1 CONUS fills the
|
|
1424
|
+
// frame and Alaska's western tail (and Hawaii) clip into the lower-left
|
|
1425
|
+
// corner, cropped by the SVG/canvas viewport. Everything downstream (paths,
|
|
1426
|
+
// picking, tooltip anchors, focus zoom) derives from this projection, so
|
|
1427
|
+
// both renderers stay consistent.
|
|
1428
|
+
const tight = geoAlbersUsa().fitExtent(extent, conus);
|
|
1429
|
+
const t = tightFitAmount.value;
|
|
1430
|
+
const lerp = (a: number, b: number) => a + (b - a) * t;
|
|
1431
|
+
const [fx, fy] = full.translate();
|
|
1432
|
+
const [tx, ty] = tight.translate();
|
|
1433
|
+
return geoAlbersUsa()
|
|
1434
|
+
.scale(lerp(full.scale(), tight.scale()))
|
|
1435
|
+
.translate([lerp(fx, tx), lerp(fy, ty)]);
|
|
1386
1436
|
});
|
|
1387
1437
|
|
|
1388
1438
|
const pathGenerator = computed(() => geoPath(projection.value));
|
|
@@ -1623,7 +1673,8 @@ function titleText(name: string, value: number | string | undefined): string {
|
|
|
1623
1673
|
// using lastTooltipSize (possibly stale by one frame) → visibility:visible
|
|
1624
1674
|
// 2. tooltipObserver fires when the slot DOM has actually committed → we
|
|
1625
1675
|
// refresh lastTooltipSize and re-apply the position if still visible.
|
|
1626
|
-
// 3. mousemove → rAF-throttled
|
|
1676
|
+
// 3. mousemove → rAF-throttled; re-runs the same flip/clamp placement as
|
|
1677
|
+
// the initial hover (direct DOM write of transform, no reactivity).
|
|
1627
1678
|
// 4. mouseout (leaving the map) → visibility:hidden.
|
|
1628
1679
|
//
|
|
1629
1680
|
// There is no `await` and no token: out-of-order completion is impossible
|
|
@@ -1723,11 +1774,14 @@ function moveTooltip(clientX: number, clientY: number) {
|
|
|
1723
1774
|
if (pendingMoveFrame) return;
|
|
1724
1775
|
pendingMoveFrame = requestAnimationFrame(() => {
|
|
1725
1776
|
pendingMoveFrame = 0;
|
|
1726
|
-
|
|
1727
|
-
if (!el || !tooltipVisible) return;
|
|
1777
|
+
if (!tooltipVisible) return;
|
|
1728
1778
|
lastPointer = { x: pendingMoveX, y: pendingMoveY };
|
|
1729
|
-
//
|
|
1730
|
-
|
|
1779
|
+
// Re-run the same flip/clamp as the initial hover. This is already
|
|
1780
|
+
// rAF-coalesced (at most once per frame), so the flip/clamp cost is
|
|
1781
|
+
// negligible. Hardcoding the right side here instead made the tooltip
|
|
1782
|
+
// ignore the boundary while moving and fight showTooltip's per-feature
|
|
1783
|
+
// flip on dense maps (the tooltip appeared to switch sides).
|
|
1784
|
+
applyTooltipPosition(pendingMoveX, pendingMoveY);
|
|
1731
1785
|
});
|
|
1732
1786
|
}
|
|
1733
1787
|
|
|
@@ -2244,8 +2298,10 @@ function onTouchEnd(event: TouchEvent) {
|
|
|
2244
2298
|
!fullscreen.isFullscreen.value
|
|
2245
2299
|
) {
|
|
2246
2300
|
// Suppress the synthetic click/hover the browser would replay from
|
|
2247
|
-
// this tap (and iOS's hover-first tap).
|
|
2248
|
-
|
|
2301
|
+
// this tap (and iOS's hover-first tap). Guard on cancelable: a
|
|
2302
|
+
// touchend fired while the page is mid-scroll is non-cancelable, and
|
|
2303
|
+
// preventDefault there is a no-op that only logs a console intervention.
|
|
2304
|
+
if (event.cancelable) event.preventDefault();
|
|
2249
2305
|
const prev = lastTap;
|
|
2250
2306
|
lastTap = { x: t.clientX, y: t.clientY, time: event.timeStamp };
|
|
2251
2307
|
const isDoubleTap =
|
|
@@ -2287,8 +2343,10 @@ function onTouchEnd(event: TouchEvent) {
|
|
|
2287
2343
|
const data = tooltipDataById.get(start.featId);
|
|
2288
2344
|
if (!data) return;
|
|
2289
2345
|
// Suppress the synthetic click/hover the browser would replay from this
|
|
2290
|
-
// tap, so selection fires exactly once and never via iOS's hover-first
|
|
2291
|
-
|
|
2346
|
+
// tap, so selection fires exactly once and never via iOS's hover-first
|
|
2347
|
+
// tap. Guard on cancelable — a touchend fired mid page-scroll is
|
|
2348
|
+
// non-cancelable, and preventDefault there only logs an intervention.
|
|
2349
|
+
if (event.cancelable) event.preventDefault();
|
|
2292
2350
|
touchHover(data, t.clientX, t.clientY);
|
|
2293
2351
|
emitSelection(data);
|
|
2294
2352
|
}
|
|
@@ -39,7 +39,13 @@ export function placeTooltip(
|
|
|
39
39
|
};
|
|
40
40
|
|
|
41
41
|
const flip = clientX + GAP + tipW > bounds.right - PAD;
|
|
42
|
-
const
|
|
42
|
+
const rawLeft = flip ? clientX - GAP - tipW : clientX + GAP;
|
|
43
|
+
// Clamp horizontally too: a left-flip near a narrow boundary's left edge
|
|
44
|
+
// (or a right placement in a chart narrower than the tooltip) must not
|
|
45
|
+
// overflow the far side. The upper bound is floored to the lower one so a
|
|
46
|
+
// tooltip wider than the bounds still pins to the left edge.
|
|
47
|
+
const maxLeft = Math.max(bounds.left + PAD, bounds.right - PAD - tipW);
|
|
48
|
+
const left = Math.min(Math.max(rawLeft, bounds.left + PAD), maxLeft);
|
|
43
49
|
const halfH = tipH / 2;
|
|
44
50
|
const top = Math.min(
|
|
45
51
|
Math.max(centerY, bounds.top + PAD + halfH),
|
package/index.json
CHANGED