@beespokeio/vector-map
v0.2.2
Published
Vue and JavaScript component for rendering a Portugal vector map by municipalities.
Readme
PortugalMap
Vanilla JavaScript library for rendering an SVG vector map of Portugal by municipalities, using GeoJSON files.
The package is published as @beespokeio/vector-map.
It does not depend on Vue, React, or any other framework. It uses D3 only for geographic projection and SVG path generation.
The Vue wrapper is optional and lives in vue/PortugalMapVue.js. The core js/PortugalMap.js remains framework-agnostic.
Files
js/PortugalMap.js
vue/PortugalMapVue.js
css/portugal-map.css
assets/maps/concelhos_pt_high.geojson
assets/maps/concelhos_pt_low.geojson
assets/maps/distritos_pt_high.geojson
assets/maps/distritos_pt_low.geojson
tests/example1.html
tests/example2.html
tests/example3.htmlInstallation
In Vue/Vite apps, install the package from this folder or publish it to your registry.
cd map
npm install
npm run buildIn a local Vue app, you can install it by path:
npm install /BeeSpoke/components/mapThen import it in the Vue app:
import PortugalMapVue from "@beespokeio/vector-map";
import "@beespokeio/vector-map/style.css";If you are consuming the local build directly before publishing:
import PortugalMapVue from "/path/to/components/map/dist/portugal-map.js";
import "/path/to/components/map/dist/portugal-map.css";The build outputs:
dist/portugal-map.js
dist/portugal-map.umd.cjs
dist/portugal-map.cssFor the JavaScript core, you can also import:
import { PortugalMap } from "@beespokeio/vector-map";d3-geo is imported by the package and included in the bundle. You no longer need to load d3-array or d3-geo globally when using the build.
For direct browser usage without a build step, add an import map for d3-geo:
<script type="importmap">
{
"imports": {
"d3-geo": "https://cdn.jsdelivr.net/npm/d3-geo@3/+esm"
}
}
</script>Example
Minimal usage with the included files:
<link rel="stylesheet" href="/css/portugal-map.css">
<div id="mapa-portugal"></div>
<script type="importmap">
{
"imports": {
"d3-geo": "https://cdn.jsdelivr.net/npm/d3-geo@3/+esm"
}
}
</script>
<script type="module">
import PortugalMap from "/js/PortugalMap.js";
const map = new PortugalMap("#mapa-portugal", {});
map.render();
</script>Example with options:
<link rel="stylesheet" href="/css/portugal-map.css">
<div id="mapa-portugal"></div>
<script type="importmap">
{
"imports": {
"d3-geo": "https://cdn.jsdelivr.net/npm/d3-geo@3/+esm"
}
}
</script>
<script type="module">
import PortugalMap from "/js/PortugalMap.js";
const map = new PortugalMap("#mapa-portugal", {
resolution: "high",
idField: "DICO",
labelField: "Concelho",
selectedId: "1820",
highlightedIds: ["0101", "1106"],
showIslands: true,
allowZoom: true,
zoomLvl: 3,
showTooltip: true,
values: {
"1820": {
label: "TABUAÇO",
value: 10,
color: "#008fbd"
}
},
onSelect(item) {
console.log("Selected:", item);
}
});
map.render();
</script>Constructor
const map = new PortugalMap(container, options);container can be a CSS selector or a DOM element:
new PortugalMap("#mapa-portugal", options);
new PortugalMap(document.querySelector("#mapa-portugal"), options);Options
| Property | Type | Default | Description |
| --- | --- | --- | --- |
| mapUrl | string \| null | null | Manual URL for the GeoJSON file. When set, it takes priority over resolution and mapUrls. |
| mapUrls | object | see below | GeoJSON file URLs by resolution. |
| resolution | "high" \| "low" | "high" | GeoJSON resolution to load when mapUrl is not set. |
| districtMode | boolean | false | Switches the built-in defaults from municipality data to district/area data. It defaults to DI / Distrito and the bundled district GeoJSON files unless you override them explicitly. |
| idField | string | "DICO" | GeoJSON property name used as the municipality ID. |
| labelField | string | "Concelho" | GeoJSON property name used as the municipality label. |
| selectedId | string \| null | null | Initially selected municipality ID. |
| highlightedIds | array | [] | List of highlighted IDs. |
| disabledIds | array | [] | IDs that stay visible but cannot be selected or keyboard-activated. |
| values | object | {} | External data associated by municipality ID. It can define label, labelHtml, value, and color. |
| legends | array | [] | Legend entries rendered inside the map. Each item accepts { color, label }. |
| legendTitle | string | "" | Optional title shown above the legend items. |
| legendPosition | "bottom-left" \| "bottom-right" \| "top-left" \| "top-right" | "bottom-left" | Legend corner position inside the map. |
| showTooltip | boolean | false | Shows a tooltip with the municipality name on hover. |
| tooltipClassName | string | "portugal-map__tooltip" | CSS class applied to the tooltip element. |
| width | number | 600 | SVG viewBox width. |
| height | number | 800 | SVG viewBox height. |
| allowZoom | boolean | false | When true, centers the map on selectedId and applies zoom. |
| zoomOnInit | boolean | true | When false, does not zoom to the initial selectedId on first render. Later selections still apply zoom. |
| allowScrollZoom | boolean | false | Enables mouse wheel zoom over the map. |
| allowDragPan | boolean | false | Enables mouse drag panning while zoom is active. |
| disableAnimations | boolean | false | Disables animated viewport zoom transitions and uses instant transform updates instead. |
| minZoom | number | 1 | Minimum allowed zoom level. |
| maxZoom | number | 8 | Maximum allowed zoom level. |
| zoomStep | number | 0.5 | Increment used by zoom buttons and mouse wheel. |
| zoomLvl | number | 2 | Zoom multiplier used when allowZoom is active. Values lower than 1 are treated as 1. |
| showZoomControls | boolean | false | Shows internal +, -, and reset buttons over the map. |
| zoomControlsPosition | "bottom-left" \| "bottom-right" \| "top-left" \| "top-right" | "top-right" | Corner position for the internal zoom controls. |
| className | string | "portugal-map" | CSS class applied to the SVG. |
| defaultFill | string | "#edf3f5" | Fill color for municipalities without a special state. |
| disabledFill | string | "#e3eaed" | Fill color used for IDs listed in disabledIds. |
| selectedFill | string | "#008fbd" | Fill color for the selected municipality. |
| highlightedFill | string | "#cceaf3" | Fill color for highlighted municipalities. |
| hoverFill | string | "#d8eef6" | Fill color applied on hover. |
| stroke | string | "#aebdc4" | Base municipality stroke color. |
| selectedStroke | string \| null | null | Optional stroke color for the selected municipality. When null, stroke is used. |
| selectedStrokeWidth | number \| null | null | Optional stroke width for the selected municipality. When null, the normal CSS is used. |
| useIslandInsets | boolean | true | When enabled, renders mainland Portugal, the Azores, and Madeira in separate areas, without real geographic distance. |
| showIslands | boolean | true | Shows or hides the Azores and Madeira. |
| insetLabels | boolean | true | Shows labels in the archipelago insets. |
| insetBoxes | object | see below | Defines position, size, and padding for mainland Portugal, the Azores, and Madeira. |
| onSelect | function \| null | null | Callback called when a municipality is selected. |
| onHover | function \| null | null | Callback called when hovering a municipality. |
| onLeave | function \| null | null | Callback called when leaving a municipality. |
GeoJSON Resolution
By default, the widget loads the high-resolution version:
const map = new PortugalMap("#mapa-portugal", {
resolution: "high"
});To load the smaller file:
const map = new PortugalMap("#mapa-portugal", {
resolution: "low"
});Default URLs:
mapUrls: {
high: new URL("../assets/maps/concelhos_pt_high.geojson", import.meta.url).href,
low: new URL("../assets/maps/concelhos_pt_low.geojson", import.meta.url).href
}This means that, when you import js/PortugalMap.js or the package build, the included GeoJSON files are resolved automatically without passing mapUrls.
You can also pass your own URLs:
const map = new PortugalMap("#mapa-portugal", {
resolution: "low",
mapUrls: {
high: "/assets/maps/concelhos_pt_high.geojson",
low: "/assets/maps/concelhos_pt_low.geojson"
}
});If mapUrl is set, that URL is used directly and resolution is ignored:
const map = new PortugalMap("#mapa-portugal", {
mapUrl: "/assets/maps/portugal-concelhos.geojson"
});To render districts or areas by DI instead of municipalities by DICO, use the included district files:
const map = new PortugalMap("#mapa-portugal", {
mapUrl: new URL("../assets/maps/distritos_pt_high.geojson", import.meta.url).href,
idField: "DI",
labelField: "Distrito"
});Island Insets
By default, the widget moves the Azores and Madeira closer to mainland Portugal in small boxes on the left side.
insetBoxes: {
continent: {
x: 220,
y: 20,
width: 340,
height: 740
},
azores: {
x: 20,
y: 430,
width: 170,
height: 125,
padding: 16
},
madeira: {
x: 20,
y: 585,
width: 170,
height: 90,
padding: 18,
shapeOffsetX: 12,
shapeOffsetY: 0
}
}padding moves the shapes away from the box edge. shapeOffsetX and shapeOffsetY let you visually adjust the shapes inside the box.
To hide the islands:
const map = new PortugalMap("#mapa-portugal", {
showIslands: false
});When showIslands is false and useIslandInsets is enabled, mainland Portugal automatically uses the central map area instead of the narrower box used while the archipelagos are visible.
Or after rendering:
map.setShowIslands(false);
map.setShowIslands(true);Values
values associates external data with a municipality by ID.
values: {
"1820": {
label: "TABUAÇO",
labelHtml: "TABUAÇO <strong>10</strong>",
value: 10,
color: "#008fbd"
}
}Color priority:
values[id].colorselectedFillhighlightedFilldefaultFill
If labelHtml exists, the tooltip renders it as HTML. Otherwise, it uses label as plain text.
Tooltip
To show a tooltip with the municipality name on hover:
const map = new PortugalMap("#mapa-portugal", {
showTooltip: true
});The tooltip uses the calculated label for the municipality. If values[id].label exists, it takes priority over the field defined in labelField.
To customize the CSS class:
const map = new PortugalMap("#mapa-portugal", {
showTooltip: true,
tooltipClassName: "my-map-tooltip"
});Zooming To The Selected Municipality
When allowZoom is enabled, the map recenters the municipality defined by selectedId and applies the factor set in zoomLvl.
const map = new PortugalMap("#mapa-portugal", {
selectedId: "1820",
allowZoom: true,
zoomOnInit: false,
allowScrollZoom: true,
allowDragPan: true,
minZoom: 1,
maxZoom: 6,
zoomStep: 0.5,
zoomLvl: 3,
showZoomControls: true
});To disable zoom, set allowZoom: false or clear selectedId.
To keep the initial municipality selected without zooming on first render, use zoomOnInit: false.
For direct interaction, enable allowScrollZoom for the mouse wheel and allowDragPan for dragging the map.
Use minZoom, maxZoom, and zoomStep to control zoom limits and pace.
Event Payload
onSelect, onHover, and onLeave receive:
{
id: "1820",
label: "TABUAÇO",
value: 10,
data: {
label: "TABUAÇO",
value: 10,
color: "#008fbd"
},
feature: {}
}Public Methods
render()
Loads the GeoJSON and draws the map.
await map.render();setSelected(id)
Updates the selected municipality.
map.setSelected("1820");setValues(values)
Updates the external data associated with municipalities.
map.setValues({
"1820": {
value: 20,
color: "#008fbd"
}
});setHighlightedIds(ids)
Updates the highlighted municipalities.
map.setHighlightedIds(["1820", "1106"]);setShowIslands(showIslands)
Shows or hides the Azores and Madeira insets.
map.setShowIslands(false);setShowTooltip(showTooltip)
Shows or hides the hover tooltip.
map.setShowTooltip(true);setAllowZoom(allowZoom)
Enables or disables zoom centered on the selected municipality.
map.setAllowZoom(true);setZoomLvl(zoomLvl)
Updates the zoom multiplier.
map.setZoomLvl(4);setMinZoom(minZoom)
Updates the minimum zoom limit.
map.setMinZoom(1);setMaxZoom(maxZoom)
Updates the maximum zoom limit.
map.setMaxZoom(6);setZoomStep(zoomStep)
Updates the increment used by zoomIn(), zoomOut(), and scroll.
map.setZoomStep(0.5);zoomIn(step)
Increases the current zoom. Automatically enables allowZoom.
map.zoomIn();
map.zoomIn(0.5);zoomOut(step)
Decreases the current zoom without going below 1. Automatically enables allowZoom.
map.zoomOut();
map.zoomOut(0.5);resetZoom()
Returns to the base zoom and disables allowZoom.
map.resetZoom();destroy()
Removes the SVG and clears internal listeners.
map.destroy();CSS Classes
Each municipality always receives:
portugal-map__regionAdditional classes:
is-selected
is-highlighted
has-valueSVG groups receive:
portugal-map__group
portugal-map__group--continent
portugal-map__group--azores
portugal-map__group--madeiraVue Wrapper
The Vue wrapper is a thin layer over the vanilla core. It only handles the Vue lifecycle, synchronizes props, and converts core callbacks into Vue events.
Import:
import PortugalMapVue from "@beespokeio/vector-map";
import "@beespokeio/vector-map/style.css";Usage:
<template>
<PortugalMapVue
v-model:selected-id="selectedId"
:highlighted-ids="highlightedIds"
:values="values"
:allow-zoom="true"
:zoom-on-init="false"
:allow-scroll-zoom="true"
:allow-drag-pan="true"
:min-zoom="1"
:max-zoom="6"
:zoom-step="0.5"
:zoom-lvl="3"
show-zoom-controls
show-tooltip
@select="handleSelect"
/>
</template>Example without v-model:
<template>
<PortugalMapVue
:selected-id="selectedId"
:show-islands="showIslands"
@select="handleSelect"
@hover="handleHover"
@leave="handleLeave"
/>
</template>Emitted events:
update:selectedId
select
hover
leave
ready
errorMain wrapper props:
mapUrl
mapUrls
resolution
districtMode
idField
labelField
selectedId
highlightedIds
disabledIds
values
legends
legendTitle
legendPosition
showTooltip
showIslands
width
height
allowZoom
zoomOnInit
allowScrollZoom
allowDragPan
disableAnimations
minZoom
maxZoom
zoomStep
zoomLvl
showZoomControls
zoomControlsPosition
defaultFill
disabledFill
selectedFill
highlightedFill
hoverFill
stroke
selectedStroke
selectedStrokeWidth
useIslandInsets
insetLabels
insetBoxesIf you need to control zoom programmatically in the Vue wrapper, use a ref:
<template>
<PortugalMapVue ref="mapRef" :selected-id="selectedId" :allow-zoom="true" />
<button @click="mapRef?.zoomIn()">Zoom +</button>
<button @click="mapRef?.zoomOut()">Zoom -</button>
</template>The tests/example3.html file shows the Vue wrapper working via CDN/import map.
Manual Tests
The files in tests/ are HTML examples for manual validation. Serve them over HTTP because the component loads GeoJSON files with fetch().
cd /BeeSpoke/components/map
source /home/fernando/.nvm/nvm.sh
nvm use 22
npm install
npm run build
python3 -m http.server 8000Then open in the browser:
http://localhost:8000/tests/example1.html
http://localhost:8000/tests/example2.html
http://localhost:8000/tests/example3.htmlExpected validation:
example1.html: the map appears, Tabuaço starts selected, hover shows the tooltip, and clicking updates the text.example2.html: the basic map appears without errors.example3.html: the Vue wrapper appears and the buttons select/clear, hide/show islands, and update zoom.example3.html: the Vue wrapper appears and the buttons select/clear, hide/show islands, and the map shows internal zoom and reset controls.
The examples use CDN/import maps for d3-geo and, for Vue, also for vue, so they require internet access.
Legends And District Mode
You can position the legend with legendPosition, give it a title with legendTitle, and switch the built-in defaults to district data with districtMode:
const map = new PortugalMap("#mapa-portugal", {
districtMode: true,
selectedId: "01",
legends: [
{ color: "#008fbd", label: "Selected" },
{ color: "#cceaf3", label: "Highlighted" }
],
legendTitle: "Legend",
legendPosition: "bottom-right"
});Supported legendPosition values:
bottom-left
bottom-right
top-left
top-rightDisabled IDs
You can pass disabledIds to keep some areas visible but non-selectable:
const map = new PortugalMap("#mapa-portugal", {
disabledIds: ["0303", "1820"]
});You can also customize their color and disable zoom/reset animations:
const map = new PortugalMap("#mapa-portugal", {
disabledIds: ["0303", "1820"],
disabledFill: "#d9e1e5",
disableAnimations: true
});