Add click events and country selection to the vector map

- regionclick, markerclick and selected events
- select option with single and multiple modes, getSelected() and setSelected()
- a drag starts after 4 px, so a click on a zoomable map reaches the country
This commit is contained in:
codecalm committed 2026-10-05 14:31:36 +02:00
1 parent 81621e7d97
commit 7a06c66954
10 files changed
+405 -15

No files matched your search

+1 -1
View File
@@ -4,4 +4,4 @@
"@tabler/docs": minor
---
Added the `VectorMap` plugin: an SVG world map with values, color scales, a legend, markers, lines, tooltips and zoom.
Added the `VectorMap` plugin: an SVG world map with values, color scales, a legend, markers, lines, selection, tooltips and zoom.
+138 -10
View File
@@ -46,6 +46,7 @@ export type VectorMapTooltipItem = { type: 'region'; code: string; name: string;
type VectorMapValues = Record<string, number>
type VectorMapTooltip = boolean | ((item: VectorMapTooltipItem) => string)
type VectorMapLegend = boolean | ((value: number) => string)
type VectorMapSelect = 'none' | 'single' | 'multiple'
type Point = [number, number]
// The part of the map in sight: its top left corner and how many times it is magnified
type View = { x: number; y: number; scale: number }
@@ -60,6 +61,8 @@ type ComponentConfig = {
lines: VectorMapLine[]
tooltip: VectorMapTooltip
legend: VectorMapLegend
select: VectorMapSelect
selected: string[]
zoom: boolean
zoomMax: number
zoomOnScroll: boolean
@@ -80,6 +83,9 @@ const EVENT_KEY = `.${DATA_KEY}`
const EVENT_RENDERED = `rendered${EVENT_KEY}`
const EVENT_UPDATED = `updated${EVENT_KEY}`
const EVENT_REGION_CLICK = `regionclick${EVENT_KEY}`
const EVENT_MARKER_CLICK = `markerclick${EVENT_KEY}`
const EVENT_SELECTED = `selected${EVENT_KEY}`
const EVENT_POINTERMOVE = `pointermove${EVENT_KEY}`
const EVENT_POINTERLEAVE = `pointerleave${EVENT_KEY}`
const EVENT_POINTERDOWN = `pointerdown${EVENT_KEY}`
@@ -103,6 +109,8 @@ const CLASS_NAME_LEGEND = 'vector-map-legend'
const CLASS_NAME_LEGEND_LABEL = 'vector-map-legend-label'
const CLASS_NAME_LEGEND_SCALE = 'vector-map-legend-scale'
const CLASS_NAME_LEGEND_STEP = 'vector-map-legend-step'
const CLASS_NAME_SELECTABLE = 'vector-map-selectable'
const CLASS_NAME_SELECTED = 'vector-map-region-selected'
const CLASS_NAME_ZOOMABLE = 'vector-map-zoomable'
const CLASS_NAME_DRAGGING = 'vector-map-dragging'
const CLASS_NAME_CONTROLS = 'vector-map-controls'
@@ -116,6 +124,16 @@ const ATTRIBUTE_ACTION = 'data-bs-vector-map-action'
const SELECTOR_TOOLTIP_TARGET = `.${CLASS_NAME_REGION}, .${CLASS_NAME_MARKER}`
const SELECTOR_ACTION = `[${ATTRIBUTE_ACTION}]`
// What a selected shape is moved in front of, so its outline is not covered
// by the shapes around it but stays under everything drawn over the map
const SELECTOR_OVER_SHAPES = `.${CLASS_NAME_POINT}, .${CLASS_NAME_LINE}, .${CLASS_NAME_MARKER}`
const SELECT_NONE = 'none'
const SELECT_MULTIPLE = 'multiple'
// How far a pointer has to move, in pixels, before a press becomes a drag
// and stops being a click
const DRAG_THRESHOLD = 4
const ACTION_ZOOM_IN = 'zoom-in'
const ACTION_ZOOM_OUT = 'zoom-out'
@@ -159,6 +177,8 @@ const Default: ComponentConfig = {
lines: [],
tooltip: true,
legend: false,
select: 'none',
selected: [],
zoom: false,
zoomMax: 8,
zoomOnScroll: true,
@@ -177,6 +197,8 @@ const DefaultType: Record<keyof ComponentConfig, string> = {
lines: 'array',
tooltip: '(boolean|function)',
legend: '(boolean|function)',
select: 'string',
selected: 'array',
zoom: 'boolean',
zoomMax: 'number',
zoomOnScroll: 'boolean',
@@ -279,6 +301,11 @@ class VectorMap extends BaseComponent {
_frame = 0
// Pointers that are down on the map, with where each was last seen
_pointers = new Map<number, Point>()
// Whether the pointers that are down have moved the map. A click that ends
// a drag is not a click on a region.
_dragged = false
// The codes of the selected regions; filled from `selected` on the first render
_selected: Set<string> | null = null
/** Maps added with `addMap()`, by name */
static maps: Record<string, VectorMapData> = {}
@@ -312,6 +339,9 @@ class VectorMap extends BaseComponent {
// `wheel` is not among the native events EventHandler knows, and the
// listener must not be passive to keep the page from scrolling.
this._element.addEventListener('wheel', this._onWheel, { passive: false })
EventHandler.on(this._element, EVENT_CLICK, (event: MouseEvent) => {
this._onClick(event)
})
EventHandler.on(this._element, EVENT_DBLCLICK, (event: MouseEvent) => {
if (this._config.zoom && this._isOnMap(event)) {
this._zoomBy(ZOOM_STEP, this._toMapPoint(event.clientX, event.clientY), true)
@@ -380,6 +410,7 @@ class VectorMap extends BaseComponent {
this._element.append(svg)
this._renderControls()
this._applyColors()
this._applySelected()
this._setView({ x: 0, y: 0, scale: 1 })
this._applyValues()
@@ -394,6 +425,19 @@ class VectorMap extends BaseComponent {
EventHandler.trigger(this._element, EVENT_UPDATED)
}
// The codes of the selected regions, in the order they were selected
getSelected(): string[] {
return [...(this._selected ?? [])]
}
// Selects exactly these regions. Codes that are not on the map are left
// out, and all but the first when only one region can be selected.
setSelected(codes: string[]): void {
this._selected = this._toSelected(codes)
this._applySelected()
EventHandler.trigger(this._element, EVENT_SELECTED, { selected: this.getSelected() })
}
zoomIn(): void {
this._zoomBy(ZOOM_STEP, null, true)
}
@@ -415,7 +459,7 @@ class VectorMap extends BaseComponent {
this._tooltip?.remove()
this._config.colors = []
this._applyColors()
this._element.classList.remove(CLASS_NAME_ZOOMABLE, CLASS_NAME_DRAGGING)
this._element.classList.remove(CLASS_NAME_ZOOMABLE, CLASS_NAME_DRAGGING, CLASS_NAME_SELECTABLE)
this._element.removeEventListener('wheel', this._onWheel)
super.dispose()
}
@@ -671,17 +715,12 @@ class VectorMap extends BaseComponent {
return
}
if (this._pointers.size === 0) {
this._dragged = false
}
this._pointers.set(event.pointerId, [event.clientX, event.clientY])
cancelAnimationFrame(this._frame)
// The map keeps getting the moves of this pointer when it leaves the map.
// Capturing throws for a pointer the browser does not know, which is what
// an event made in a script has.
try {
this._getSvg()?.setPointerCapture(event.pointerId)
} catch {
// The drag still works while the pointer stays over the map
}
}
_onPointerUp(event: PointerEvent): void {
@@ -700,6 +739,26 @@ class VectorMap extends BaseComponent {
}
const current: Point = [event.clientX, event.clientY]
// A press that barely moves is a click, not a drag. Until the pointer has
// moved far enough the map stays put, and the click reaches the region.
if (!this._dragged) {
if (this._pointers.size === 1 && Math.hypot(current[0] - previous[0], current[1] - previous[1]) < DRAG_THRESHOLD) {
return
}
this._dragged = true
// The map keeps getting the moves of this pointer when it leaves the
// map. Capturing throws for a pointer the browser does not know, which
// is what an event made in a script has.
try {
this._getSvg()?.setPointerCapture(event.pointerId)
} catch {
// The drag still works while the pointer stays over the map
}
}
this._pointers.set(event.pointerId, current)
this._element.classList.add(CLASS_NAME_DRAGGING)
this._hideTooltip()
@@ -770,6 +829,75 @@ class VectorMap extends BaseComponent {
}
}
_toSelected(codes: unknown): Set<string> {
const { select } = this._config
const known = (Array.isArray(codes) ? (codes as unknown[]) : []).filter((code): code is string => typeof code === 'string' && this._map?.regions[code] !== undefined)
if (select === SELECT_NONE) {
return new Set()
}
return new Set(select === SELECT_MULTIPLE ? known : known.slice(0, 1))
}
// Marks the selected regions for the stylesheet. A shape that gets selected
// moves after the other shapes, where nothing covers its outline.
_applySelected(): void {
const svg = this._getSvg()
if (!svg) {
return
}
this._selected ??= this._toSelected(this._config.selected)
this._element.classList.toggle(CLASS_NAME_SELECTABLE, this._config.select !== SELECT_NONE)
const over = svg.querySelector(SELECTOR_OVER_SHAPES)
for (const region of svg.querySelectorAll<SVGElement>(`.${CLASS_NAME_REGION}`)) {
const selected = this._selected.has(region.getAttribute(ATTRIBUTE_REGION) ?? '')
if (selected && !region.classList.contains(CLASS_NAME_SELECTED) && !region.classList.contains(CLASS_NAME_POINT)) {
svg.insertBefore(region, over)
}
region.classList.toggle(CLASS_NAME_SELECTED, selected)
}
}
// A click on a region or on a marker becomes an event of the map. A click on
// a region also selects it, unless the event was prevented.
_onClick(event: MouseEvent): void {
if (this._dragged || !this._isOnMap(event) || !this._map) {
return
}
const target = event.target as Element
const marker = target.closest(`.${CLASS_NAME_MARKER}`)
if (marker) {
const index = [...this._element.querySelectorAll(`.${CLASS_NAME_MARKER}`)].indexOf(marker)
EventHandler.trigger(this._element, EVENT_MARKER_CLICK, { ...this._config.markers[index], relatedTarget: marker })
return
}
const region = target.closest(`.${CLASS_NAME_REGION}`)
const code = region?.getAttribute(ATTRIBUTE_REGION)
if (!region || !code) {
return
}
const value = this._config.values[code]
const clickEvent = EventHandler.trigger(this._element, EVENT_REGION_CLICK, { code, name: this._map.regions[code]?.name ?? code, ...(value === undefined ? {} : { value }), relatedTarget: region })
if (clickEvent?.defaultPrevented || this._config.select === SELECT_NONE) {
return
}
// A click on a selected region takes it out again
const selected = this.getSelected()
const rest = selected.filter((other) => other !== code)
const next = rest.length < selected.length ? rest : this._config.select === SELECT_MULTIPLE ? [...selected, code] : [code]
this._selected = new Set(next)
this._applySelected()
EventHandler.trigger(this._element, EVENT_SELECTED, { selected: next, code })
}
_getLegend(): HTMLElement | null {
return this._element.querySelector<HTMLElement>(`.${CLASS_NAME_LEGEND}`)
}
+177
View File
@@ -226,6 +226,183 @@ describe('VectorMap', () => {
})
})
describe('click', () => {
it('should fire regionclick with the code, the name and the value of the region', () => {
fixtureEl.innerHTML = '<div class="vector-map"></div>'
new VectorMap(el(), { map: squares, values: { AA: 5 } })
const spy = vi.fn()
el().addEventListener('regionclick.bs.vector-map', spy)
region('AA').dispatchEvent(new MouseEvent('click', { bubbles: true }))
region('BB').dispatchEvent(new MouseEvent('click', { bubbles: true }))
expect(spy).toHaveBeenCalledTimes(2)
expect(spy.mock.calls[0][0]).toMatchObject({ code: 'AA', name: 'First', value: 5, relatedTarget: region('AA') })
expect(spy.mock.calls[1][0]).toMatchObject({ code: 'BB', name: 'Second' })
expect('value' in spy.mock.calls[1][0]).toBe(false)
})
it('should fire markerclick with the marker, and no regionclick', () => {
fixtureEl.innerHTML = '<div class="vector-map"></div>'
new VectorMap(el(), {
map: squares,
markers: [
{ name: 'One', lat: 0, lng: 0 },
{ name: 'Two', lat: 1, lng: 2 },
],
})
const markerSpy = vi.fn()
const regionSpy = vi.fn()
el().addEventListener('markerclick.bs.vector-map', markerSpy)
el().addEventListener('regionclick.bs.vector-map', regionSpy)
el()
.querySelectorAll('.vector-map-marker-dot')[1]
.dispatchEvent(new MouseEvent('click', { bubbles: true }))
expect(markerSpy.mock.calls[0][0]).toMatchObject({ name: 'Two', lat: 1, lng: 2 })
expect(regionSpy).not.toHaveBeenCalled()
})
it('should not take the click that ends a drag for a click on a region', () => {
fixtureEl.innerHTML = '<div class="vector-map" style="width: 300px" data-bs-zoom="true"></div>'
new VectorMap(el(), { map: squares, zoomMax: 2 })
el().dispatchEvent(new WheelEvent('wheel', { bubbles: true, cancelable: true, deltaY: -5000 }))
const spy = vi.fn()
el().addEventListener('regionclick.bs.vector-map', spy)
const pointer = (type: string, x: number): void => {
region('BB').dispatchEvent(new PointerEvent(type, { bubbles: true, pointerId: 1, pointerType: 'mouse', button: 0, clientX: x, clientY: 50 }))
}
pointer('pointerdown', 100)
pointer('pointermove', 60)
pointer('pointerup', 60)
region('BB').dispatchEvent(new MouseEvent('click', { bubbles: true }))
expect(spy).not.toHaveBeenCalled()
// A press that moves by a pixel or two is still a click
const viewBox = el().querySelector('svg')!.getAttribute('viewBox')
pointer('pointerdown', 100)
pointer('pointermove', 102)
pointer('pointerup', 102)
region('BB').dispatchEvent(new MouseEvent('click', { bubbles: true }))
expect(spy).toHaveBeenCalledTimes(1)
expect(el().querySelector('svg')!.getAttribute('viewBox')).toBe(viewBox)
})
})
describe('select', () => {
const click = (code: string): void => {
region(code).dispatchEvent(new MouseEvent('click', { bubbles: true }))
}
const isSelected = (code: string): boolean => region(code).classList.contains('vector-map-region-selected')
it('should not select anything by default', () => {
fixtureEl.innerHTML = '<div class="vector-map"></div>'
const map = new VectorMap(el(), { map: squares, selected: ['AA'] })
click('BB')
expect(map.getSelected()).toEqual([])
expect(isSelected('AA')).toBe(false)
expect(el().classList.contains('vector-map-selectable')).toBe(false)
})
it('should select one region at a time, and take it out on a second click', () => {
fixtureEl.innerHTML = '<div class="vector-map" data-bs-select="single"></div>'
const map = new VectorMap(el(), { map: squares })
const spy = vi.fn()
el().addEventListener('selected.bs.vector-map', spy)
expect(el().classList.contains('vector-map-selectable')).toBe(true)
click('AA')
expect(map.getSelected()).toEqual(['AA'])
expect(isSelected('AA')).toBe(true)
expect(spy.mock.calls[0][0]).toMatchObject({ selected: ['AA'], code: 'AA' })
click('BB')
expect(map.getSelected()).toEqual(['BB'])
expect(isSelected('AA')).toBe(false)
click('BB')
expect(map.getSelected()).toEqual([])
expect(spy).toHaveBeenCalledTimes(3)
})
it('should select several regions', () => {
fixtureEl.innerHTML = `<div class="vector-map" data-bs-select="multiple" data-bs-selected='["CC", "ZZ"]'></div>`
const map = new VectorMap(el(), { map: squares })
expect(map.getSelected()).toEqual(['CC'])
click('AA')
expect(map.getSelected()).toEqual(['CC', 'AA'])
click('CC')
expect(map.getSelected()).toEqual(['AA'])
expect(isSelected('CC')).toBe(false)
})
it('should keep the first of several codes when one region can be selected', () => {
fixtureEl.innerHTML = '<div class="vector-map"></div>'
const map = new VectorMap(el(), { map: squares, select: 'single', selected: ['BB', 'CC'] })
expect(map.getSelected()).toEqual(['BB'])
})
it('should not select when regionclick is prevented', () => {
fixtureEl.innerHTML = '<div class="vector-map" data-bs-select="single"></div>'
const map = new VectorMap(el(), { map: squares })
el().addEventListener('regionclick.bs.vector-map', (event) => {
event.preventDefault()
})
click('AA')
expect(map.getSelected()).toEqual([])
})
it('should set the selection from code', () => {
fixtureEl.innerHTML = '<div class="vector-map" data-bs-select="multiple"></div>'
const map = new VectorMap(el(), { map: squares })
const spy = vi.fn()
el().addEventListener('selected.bs.vector-map', spy)
map.setSelected(['AA', 'CC', 'nowhere'])
expect(map.getSelected()).toEqual(['AA', 'CC'])
expect(spy.mock.calls[0][0]).toMatchObject({ selected: ['AA', 'CC'] })
map.setSelected([])
expect(isSelected('AA')).toBe(false)
})
it('should draw a selected shape after the other shapes, under the markers', () => {
fixtureEl.innerHTML = '<div class="vector-map" data-bs-select="single"></div>'
new VectorMap(el(), { map: squares, markers: [{ name: 'One', lat: 0, lng: 0 }] })
click('AA')
const children = [...el().querySelector('svg')!.children]
expect(children.indexOf(region('AA'))).toBeGreaterThan(children.indexOf(region('CC')))
expect(children.indexOf(region('AA'))).toBeLessThan(children.indexOf(el().querySelector('.vector-map-marker')!))
})
it('should keep the selection over update() and render()', () => {
fixtureEl.innerHTML = '<div class="vector-map" data-bs-select="single"></div>'
const map = new VectorMap(el(), { map: squares })
click('BB')
map.update({ AA: 1 })
map.render()
expect(map.getSelected()).toEqual(['BB'])
expect(isSelected('BB')).toBe(true)
})
})
describe('legend', () => {
const legend = (): HTMLElement | null => el().querySelector<HTMLElement>('.vector-map-legend')
const labels = (): string[] => [...el().querySelectorAll('.vector-map-legend-label')].map((label) => label.textContent ?? '')
+4 -4
View File
@@ -247,19 +247,19 @@
},
{
"path": "./dist/js/tabler-vector-map.js",
"maxSize": "11.5 kB"
"maxSize": "13 kB"
},
{
"path": "./dist/js/tabler-vector-map.esm.js",
"maxSize": "11.5 kB"
"maxSize": "13 kB"
},
{
"path": "./dist/js/tabler-vector-map.min.js",
"maxSize": "8 kB"
"maxSize": "9 kB"
},
{
"path": "./dist/js/tabler-vector-map.esm.min.js",
"maxSize": "8 kB"
"maxSize": "9 kB"
},
{
"path": "./dist/js/maps/world.js",
+2
View File
@@ -2273,6 +2273,8 @@ $vector-map-region-stroke-width: 1px !default;
$vector-map-value-min: 0.1 !default;
$vector-map-colors-max: 5 !default;
$vector-map-point-size: 0.375rem !default;
$vector-map-selected-stroke: var(--body-color) !default;
$vector-map-selected-stroke-width: 2px !default;
$vector-map-marker-size: 0.5rem !default;
$vector-map-marker-color: var(--vector-map-color) !default;
$vector-map-marker-halo-color: var(--bg-surface) !default;
+17
View File
@@ -15,6 +15,8 @@
--vector-map-region-stroke-width: #{$vector-map-region-stroke-width};
--vector-map-value-min: #{$vector-map-value-min};
--vector-map-point-size: #{$vector-map-point-size};
--vector-map-selected-stroke: #{$vector-map-selected-stroke};
--vector-map-selected-stroke-width: #{$vector-map-selected-stroke-width};
--vector-map-marker-size: #{$vector-map-marker-size};
--vector-map-marker-color: #{$vector-map-marker-color};
--vector-map-marker-halo-color: #{$vector-map-marker-halo-color};
@@ -198,3 +200,18 @@
flex: 1;
background: var(--vector-map-region-color);
}
// Regions can be clicked to select them
.vector-map-selectable .vector-map-region {
cursor: pointer;
}
// A selected region gets an outline, and keeps the colour of its value
.vector-map-region-selected {
stroke: var(--vector-map-selected-stroke);
stroke-width: var(--vector-map-selected-stroke-width);
.vector-map-region-point-halo {
stroke: var(--vector-map-selected-stroke);
}
}
+48
View File
@@ -170,6 +170,44 @@ Add `data-bs-zoom="true"` to let users look closer. The map gets two buttons, an
While the pointer is over a zoomable map, the wheel zooms the map and does not scroll the page. Set `data-bs-zoom-on-scroll="false"` to keep the wheel for the page. On a touch screen a finger on the map moves the map, not the page, so leave some room around a large map.
## Selecting countries
Set `data-bs-select` to let users pick countries with a click. With `single` a click on another country moves the selection. With `multiple` every click adds a country. A click on a selected country takes it out again. Pass the countries that start selected in `data-bs-selected`.
<Example>
<div class="vector-map" data-bs-toggle="vector-map" data-bs-select="multiple" data-bs-selected='["PL", "FR", "BR"]' data-bs-label="Countries to compare"></div>
</Example>
A selected country gets an outline and keeps the color of its value. Change the outline with `--tblr-vector-map-selected-stroke` and `--tblr-vector-map-selected-stroke-width`.
The map fires `selected.bs.vector-map` after every change. Read the selection from the event, or with `getSelected()`:
```js
const element = document.getElementById('compare');
element.addEventListener('selected.bs.vector-map', (event) => {
console.log(event.selected); // ['PL', 'FR', 'BR', 'DE']
});
tabler.VectorMap.getInstance(element).setSelected(['US', 'CA']);
```
## Clicks
The map fires `regionclick.bs.vector-map` when a country is clicked and `markerclick.bs.vector-map` when a marker is clicked, also when nothing can be selected. Use them to open a page or to filter a table. A drag on a zoomable map is not a click.
```js
element.addEventListener('regionclick.bs.vector-map', (event) => {
console.log(event.code, event.name, event.value); // 'PL', 'Poland', 54
});
element.addEventListener('markerclick.bs.vector-map', (event) => {
console.log(event.name, event.lat, event.lng);
});
```
Call `event.preventDefault()` in `regionclick.bs.vector-map` to keep that click from changing the selection.
## Tooltip
A tooltip shows the name of the country or marker under the pointer, with the value when the country has one. Set `data-bs-tooltip="false"` to turn it off.
@@ -198,6 +236,8 @@ Set options with data attributes or pass them to the constructor. For a data att
| `lines` | array | `[]` | Connections as `{ from, to }`, by marker name. |
| `tooltip` | boolean, function | `true` | Shows a tooltip, or builds its text. |
| `legend` | boolean, function | `false` | Shows the scale under the map, or formats its two numbers. |
| `select` | string | `'none'` | Lets users select countries: `'none'`, `'single'` or `'multiple'`. |
| `selected` | array | `[]` | Codes of the countries that start selected. |
| `zoom` | boolean | `false` | Lets users zoom and drag the map. |
| `zoomMax` | number | `8` | The largest magnification. |
| `zoomOnScroll` | boolean | `true` | Zooms with the mouse wheel. Needs `zoom`. |
@@ -229,6 +269,8 @@ visits.update({ PL: 61, DE: 77, FR: 70 });
| --- | --- |
| `update(values)` | Shades the regions with new values. Fires `updated.bs.vector-map`. |
| `render()` | Reads the attributes again and draws the map from the start. Fires `rendered.bs.vector-map`. |
| `getSelected()` | Returns the codes of the selected countries. |
| `setSelected(codes)` | Selects exactly these countries. Fires `selected.bs.vector-map`. |
| `zoomIn()` | Zooms in one step, around the middle of the view. |
| `zoomOut()` | Zooms out one step. |
| `reset()` | Goes back to the whole map. |
@@ -241,6 +283,9 @@ visits.update({ PL: 61, DE: 77, FR: 70 });
| --- | --- |
| `rendered.bs.vector-map` | Fired on the element after every render. |
| `updated.bs.vector-map` | Fired on the element after `update()`. |
| `regionclick.bs.vector-map` | Fired when a country is clicked. Has `code`, `name`, `value` when the country has one, and the element in `relatedTarget`. Can be prevented. |
| `markerclick.bs.vector-map` | Fired when a marker is clicked. Has `name`, `lat`, `lng` and the element in `relatedTarget`. |
| `selected.bs.vector-map` | Fired after the selection changes. Has the codes in `selected`, and the clicked `code` when a click made the change. |
## Styling a single country
@@ -266,6 +311,8 @@ The map reads its colors and sizes from custom properties on the element. Change
| `--tblr-vector-map-color-1` to `-5` | none | Colors of a scale, from the lowest value to the highest. |
| `--tblr-vector-map-colors` | none | Number of colors in the scale. |
| `--tblr-vector-map-point-size` | `0.375rem` | Diameter of the dot of a small country. |
| `--tblr-vector-map-selected-stroke` | `var(--tblr-body-color)` | Outline of a selected country. |
| `--tblr-vector-map-selected-stroke-width` | `2px` | Width of that outline. |
| `--tblr-vector-map-marker-size` | `0.5rem` | Diameter of a marker. |
| `--tblr-vector-map-marker-color` | `var(--tblr-vector-map-color)` | Color of a marker. |
| `--tblr-vector-map-marker-halo-color` | `var(--tblr-bg-surface)` | Color of the ring around a marker and around the dot of a small country. |
@@ -308,6 +355,7 @@ Three territories have no ISO code. The map uses `XK` for Kosovo, `XC` for North
- The data behind the map has to be reachable another way. Put a table of the same values next to or below the map. It serves screen reader users and anyone who prints the page.
- The tooltip is for the pointer only. Do not put anything there that is not in the text version too.
- Shades of one color are hard to tell apart for many users. Turn on the legend, and for exact numbers add a table.
- Countries and markers can be clicked with a pointer only. They are not in the tab order, because a world map would be two hundred tab stops. When a click or a selection does something, offer the same choice in another control, such as a list of checkboxes or a select, and keep the two in step with `setSelected()`.
- The zoom buttons work with the keyboard and have accessible names. Change the names with `data-bs-zoom-in-label` and `data-bs-zoom-out-label` when the page is not in English.
## Variables
+8
View File
@@ -42,6 +42,14 @@ import DocsLink from '@ui/DocsLink.astro'
</CardBody>
</Card>
</div>
<div class="col-md-6">
<Card>
<CardBody>
<CardTitle>Map with selection</CardTitle>
<MapVector mapId="world-select" />
</CardBody>
</Card>
</div>
<div class="col-md-6">
<Card>
<CardBody>
+4
View File
@@ -8,6 +8,8 @@ interface MapData {
zoom?: boolean
colors?: string[]
legend?: boolean
select?: 'single' | 'multiple'
selected?: string[]
values?: Record<string, number>
markers?: { name: string; lat: number; lng: number }[]
lines?: { from: string; to: string }[]
@@ -36,6 +38,8 @@ const legend = legendProp ?? data?.legend ?? false
data-bs-label={data.title}
data-bs-zoom={data.zoom ? 'true' : undefined}
data-bs-legend={legend ? 'true' : undefined}
data-bs-select={data.select}
data-bs-selected={data.selected && JSON.stringify(data.selected)}
data-bs-colors={data.colors && JSON.stringify(data.colors)}
data-bs-values={data.values && JSON.stringify(data.values)}
data-bs-markers={data.markers && JSON.stringify(data.markers)}
+6
View File
@@ -534,5 +534,11 @@
"ZW": 1
},
"legend": true
},
"world-select": {
"title": "Map with selection",
"map": "world",
"select": "multiple",
"selected": ["PL", "FR", "BR", "AU"]
}
}