Files
tabler/shared/ui/MapVector.astro
T

158 lines
4.3 KiB
Plaintext

---
import mapsVector from '@data/maps-vector.json';
import CaptureScript from '../components/CaptureScript.astro';
interface MapData {
title?: string;
map: string;
color?: string;
filled?: boolean;
zoom?: boolean;
values?: Record<string, string>;
markers?: { name: string; coords: string }[];
lines?: { from: string; to: string }[];
}
interface Props {
mapId: string;
color?: string;
ratio?: string;
}
const { mapId, color: colorProp, ratio = '4x3' } = Astro.props;
const data = (mapsVector as Record<string, MapData>)[mapId];
const color = colorProp ?? data?.color ?? 'primary';
// scale1..scale10 — a 10-step color-mix ramp against var(--tblr-primary).
const scale = Object.fromEntries(
Array.from({ length: 10 }, (_, n) => [
`scale${n + 1}`,
`color-mix(in srgb, transparent, var(--tblr-primary) ${(n + 1) * 10}%)`,
]),
);
const markers = data?.markers?.map((marker) => ({
coords: marker.coords.split(',').map((n) => Number(n.trim())),
name: marker.name,
}));
const mapKey = `map-${mapId}`;
const mapSelector = `#map-${mapId}`;
const mapLabel = data?.title ?? 'Map';
// jsVectorMap renders its own zoom +/- buttons inside this container when
// zoom is enabled. `role="img"` flattens all descendants for assistive
// tech, which would swallow those buttons — so only apply it when there's
// no interactive chrome to hide.
const hasZoomControls = Boolean(data?.zoom);
const mapOptions = data
? {
backgroundColor: 'transparent',
regionStyle: {
initial: {
fill: 'var(--tblr-bg-surface-secondary)',
stroke: data.filled ? '#fff' : 'var(--tblr-border-color)',
strokeWidth: data.filled ? 1 : 2,
},
},
zoomOnScroll: Boolean(data.zoom),
zoomButtons: Boolean(data.zoom),
...(data.values
? { series: { regions: [{ attribute: 'fill', scale, values: data.values }] } }
: {}),
...(markers
? {
markers,
markerStyle: {
initial: {
r: 4,
stroke: '#fff',
opacity: 1,
strokeWidth: 3,
strokeOpacity: 0.5,
fill: `var(--tblr-${color})`,
},
hover: { fill: `var(--tblr-${color})`, stroke: `var(--tblr-${color})` },
},
markerLabelStyle: { initial: { fontSize: 10 } },
}
: {}),
...(data.lines
? {
lines: data.lines,
lineStyle: {
strokeDasharray: '4 4',
animation: true,
stroke: 'rgba(98, 105, 118, .75)',
strokeWidth: 0.5,
},
}
: {}),
}
: undefined;
---
{
data && (
<Fragment>
<div class={`ratio ratio-${ratio}`}>
<div>
<div
id={mapKey}
class="w-100 h-100"
role={hasZoomControls ? undefined : 'img'}
aria-label={mapLabel}
/>
</div>
</div>
<CaptureScript>
<!-- BEGIN MAP VECTOR -->
<script is:inline define:vars={{ mapKey, mapSelector, mapName: data.map, mapOptions }}>
window.tabler_map_vector ??= {};
function createMapVector() {
const map = (window.tabler_map_vector[mapKey] = new jsVectorMap({
selector: mapSelector,
map: mapName,
...mapOptions,
// labels.markers.render is a function, not JSON-serializable — added here
// rather than baked into mapOptions.
...(mapOptions.markers ? { labels: { markers: { render: (marker) => marker.name } } } : {}),
}));
window.addEventListener('resize', () => {
map.updateSize();
});
}
function initMapVector() {
const container = document.querySelector(mapSelector);
// jsVectorMap computes its initial scale from the container's layout
// size. If the container is still 0x0 (e.g. layout hasn't settled
// yet), that scale ends up 0, and a later resize divides by it,
// producing a NaN transform. Wait for real dimensions first.
if (container.offsetWidth && container.offsetHeight) {
createMapVector();
return;
}
const observer = new ResizeObserver((entries) => {
if (entries[0].contentRect.width && entries[0].contentRect.height) {
observer.disconnect();
createMapVector();
}
});
observer.observe(container);
}
document.readyState !== 'loading' ? initMapVector() : document.addEventListener('DOMContentLoaded', initMapVector, { once: true });
</script>
<!-- END MAP VECTOR -->
</CaptureScript>
</Fragment>
)
}