mirror of
https://github.com/tabler/tabler.git
synced 2026-10-12 05:06:55 +04:00
Add a scale of several colors to the vector map
- data-bs-colors takes two to five colors, mixed in CSS with color-mix()
This commit is contained in:
1 parent
b24064c5d1
commit
b5662881d1
9 files changed
+343
-1
No files matched your search
@@ -4,4 +4,4 @@
|
||||
"@tabler/docs": minor
|
||||
---
|
||||
|
||||
Added the `VectorMap` plugin in `tabler-vector-map.js`: an SVG world map with values, markers, lines, tooltips and zoom.
|
||||
Added the `VectorMap` plugin in `tabler-vector-map.js`: an SVG world map with values, color scales, markers, lines, tooltips and zoom.
|
||||
@@ -54,6 +54,7 @@ type ComponentConfig = {
|
||||
values: VectorMapValues
|
||||
min: number | null
|
||||
max: number | null
|
||||
colors: string[]
|
||||
markers: VectorMapMarker[]
|
||||
lines: VectorMapLine[]
|
||||
tooltip: VectorMapTooltip
|
||||
@@ -95,6 +96,7 @@ const CLASS_NAME_MARKER = 'vector-map-marker'
|
||||
const CLASS_NAME_MARKER_HALO = 'vector-map-marker-halo'
|
||||
const CLASS_NAME_MARKER_DOT = 'vector-map-marker-dot'
|
||||
const CLASS_NAME_TOOLTIP = 'vector-map-tooltip'
|
||||
const CLASS_NAME_COLORS = 'vector-map-colors'
|
||||
const CLASS_NAME_ZOOMABLE = 'vector-map-zoomable'
|
||||
const CLASS_NAME_DRAGGING = 'vector-map-dragging'
|
||||
const CLASS_NAME_CONTROLS = 'vector-map-controls'
|
||||
@@ -132,6 +134,11 @@ const SELECTOR_DATA_TOGGLE = `[data-bs-toggle="${NAME}"], [data-tblr-toggle="${N
|
||||
// Declared on `.vector-map-region` in scss/ui/_vector-map.scss, where it gets
|
||||
// the `--tblr-` prefix at build time.
|
||||
const CSS_VAR_VALUE = '--tblr-vector-map-value'
|
||||
// The colours of a scale, numbered from 1, and how many of them there are.
|
||||
// scss/ui/_vector-map.scss mixes them, for at most this many.
|
||||
const CSS_VAR_COLOR = '--tblr-vector-map-color-'
|
||||
const CSS_VAR_COLORS = '--tblr-vector-map-colors'
|
||||
const COLORS_MAX = 5
|
||||
const SVG_NS = 'http://www.w3.org/2000/svg'
|
||||
|
||||
const Default: ComponentConfig = {
|
||||
@@ -139,6 +146,7 @@ const Default: ComponentConfig = {
|
||||
values: {},
|
||||
min: null,
|
||||
max: null,
|
||||
colors: [],
|
||||
markers: [],
|
||||
lines: [],
|
||||
tooltip: true,
|
||||
@@ -155,6 +163,7 @@ const DefaultType: Record<keyof ComponentConfig, string> = {
|
||||
values: 'object',
|
||||
min: '(number|null)',
|
||||
max: '(number|null)',
|
||||
colors: 'array',
|
||||
markers: 'array',
|
||||
lines: 'array',
|
||||
tooltip: '(boolean|function)',
|
||||
@@ -203,6 +212,13 @@ const toMarkers = (input: unknown): VectorMapMarker[] => {
|
||||
return markers
|
||||
}
|
||||
|
||||
// A scale needs two colours at least; with one, or none, the map keeps the
|
||||
// single colour of `--tblr-vector-map-color`.
|
||||
const toColors = (input: unknown): string[] => {
|
||||
const colors = (Array.isArray(input) ? (input as unknown[]) : []).filter((color): color is string => typeof color === 'string' && color.trim() !== '')
|
||||
return colors.length < 2 ? [] : colors.slice(0, COLORS_MAX)
|
||||
}
|
||||
|
||||
const toLines = (input: unknown): VectorMapLine[] => {
|
||||
const lines: VectorMapLine[] = []
|
||||
for (const item of Array.isArray(input) ? (input as unknown[]) : []) {
|
||||
@@ -353,6 +369,7 @@ class VectorMap extends BaseComponent {
|
||||
this._getSvg()?.remove()
|
||||
this._element.append(svg)
|
||||
this._renderControls()
|
||||
this._applyColors()
|
||||
this._setView({ x: 0, y: 0, scale: 1 })
|
||||
this._applyValues()
|
||||
|
||||
@@ -385,6 +402,8 @@ class VectorMap extends BaseComponent {
|
||||
this._getSvg()?.remove()
|
||||
this._getControls()?.remove()
|
||||
this._tooltip?.remove()
|
||||
this._config.colors = []
|
||||
this._applyColors()
|
||||
this._element.classList.remove(CLASS_NAME_ZOOMABLE, CLASS_NAME_DRAGGING)
|
||||
this._element.removeEventListener('wheel', this._onWheel)
|
||||
super.dispose()
|
||||
@@ -393,6 +412,7 @@ class VectorMap extends BaseComponent {
|
||||
// Private
|
||||
_configAfterMerge(config: BaseConfig): BaseConfig {
|
||||
config.values = toValues(config.values)
|
||||
config.colors = toColors(config.colors)
|
||||
config.markers = toMarkers(config.markers)
|
||||
config.lines = toLines(config.lines)
|
||||
return config
|
||||
@@ -739,6 +759,30 @@ class VectorMap extends BaseComponent {
|
||||
}
|
||||
}
|
||||
|
||||
// Hands the colours of the scale to the stylesheet, which mixes the colour
|
||||
// of every region from them and from its share. Nothing is computed here.
|
||||
_applyColors(): void {
|
||||
const { colors } = this._config
|
||||
const { style } = this._element
|
||||
|
||||
for (let index = 0; index < COLORS_MAX; index++) {
|
||||
const color = colors[index]
|
||||
if (color === undefined) {
|
||||
style.removeProperty(`${CSS_VAR_COLOR}${index + 1}`)
|
||||
} else {
|
||||
style.setProperty(`${CSS_VAR_COLOR}${index + 1}`, color)
|
||||
}
|
||||
}
|
||||
|
||||
if (colors.length > 0) {
|
||||
style.setProperty(CSS_VAR_COLORS, String(colors.length))
|
||||
} else {
|
||||
style.removeProperty(CSS_VAR_COLORS)
|
||||
}
|
||||
|
||||
this._element.classList.toggle(CLASS_NAME_COLORS, colors.length > 0)
|
||||
}
|
||||
|
||||
// Shades every region by where its value sits between `min` and `max`. Only
|
||||
// that 0 to 1 ratio is written here; the colour is mixed in CSS.
|
||||
_applyValues(): void {
|
||||
|
||||
@@ -168,6 +168,64 @@ describe('VectorMap', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('colors', () => {
|
||||
const colorVar = (index: number): string => el().style.getPropertyValue(`--tblr-vector-map-color-${index}`)
|
||||
|
||||
it('should hand a scale of colors to the stylesheet', () => {
|
||||
fixtureEl.innerHTML = `<div class="vector-map" data-bs-colors='["red", "yellow", "green"]'></div>`
|
||||
|
||||
new VectorMap(el(), { map: squares, values: { AA: 1, BB: 2, CC: 3 } })
|
||||
|
||||
expect(el().classList.contains('vector-map-colors')).toBe(true)
|
||||
expect(el().style.getPropertyValue('--tblr-vector-map-colors')).toBe('3')
|
||||
expect(colorVar(1)).toBe('red')
|
||||
expect(colorVar(2)).toBe('yellow')
|
||||
expect(colorVar(3)).toBe('green')
|
||||
expect(colorVar(4)).toBe('')
|
||||
// The share of a region stays the same, only the stylesheet reads it differently
|
||||
expect(ratio('BB')).toBe('0.5')
|
||||
})
|
||||
|
||||
it('should keep the single color with fewer than two colors', () => {
|
||||
fixtureEl.innerHTML = '<div class="vector-map"></div>'
|
||||
|
||||
new VectorMap(el(), { map: squares, colors: ['red'] })
|
||||
|
||||
expect(el().classList.contains('vector-map-colors')).toBe(false)
|
||||
expect(el().style.getPropertyValue('--tblr-vector-map-colors')).toBe('')
|
||||
expect(colorVar(1)).toBe('')
|
||||
})
|
||||
|
||||
it('should use the first five colors', () => {
|
||||
fixtureEl.innerHTML = '<div class="vector-map"></div>'
|
||||
|
||||
new VectorMap(el(), { map: squares, colors: ['#111', '#222', '#333', '#444', '#555', '#666'] })
|
||||
|
||||
expect(el().style.getPropertyValue('--tblr-vector-map-colors')).toBe('5')
|
||||
expect(colorVar(5)).toBe('#555')
|
||||
expect(colorVar(6)).toBe('')
|
||||
})
|
||||
|
||||
it('should drop the scale when render() no longer has one, and on dispose', () => {
|
||||
fixtureEl.innerHTML = `<div class="vector-map" data-bs-colors='["red", "green"]'></div>`
|
||||
|
||||
const map = new VectorMap(el(), { map: squares })
|
||||
el().removeAttribute('data-bs-colors')
|
||||
map.render()
|
||||
|
||||
expect(el().classList.contains('vector-map-colors')).toBe(false)
|
||||
expect(colorVar(1)).toBe('')
|
||||
|
||||
el().setAttribute('data-bs-colors', '["red", "green"]')
|
||||
map.render()
|
||||
expect(colorVar(2)).toBe('green')
|
||||
|
||||
map.dispose()
|
||||
expect(el().classList.contains('vector-map-colors')).toBe(false)
|
||||
expect(colorVar(2)).toBe('')
|
||||
})
|
||||
})
|
||||
|
||||
describe('points', () => {
|
||||
const islands: VectorMapData = {
|
||||
...squares,
|
||||
|
||||
@@ -2271,6 +2271,7 @@ $vector-map-region-fill: var(--border-color) !default;
|
||||
$vector-map-region-stroke: var(--bg-surface) !default;
|
||||
$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-marker-size: 0.5rem !default;
|
||||
$vector-map-marker-color: var(--vector-map-color) !default;
|
||||
|
||||
@@ -82,6 +82,22 @@
|
||||
vector-effect: non-scaling-stroke;
|
||||
}
|
||||
|
||||
// A scale of several colours, from `--vector-map-color-1` for the lowest value
|
||||
// to the last one for the highest. `--vector-map-colors` says how many there
|
||||
// are. Every further colour is mixed over the result so far: not at all below
|
||||
// its part of the scale, more and more inside it, and fully above it, so each
|
||||
// part runs between two neighbours. A colour past the last one never gets a
|
||||
// share, which is why one rule serves any number of them.
|
||||
.vector-map-colors .vector-map-region[data-value] {
|
||||
$color: var(--vector-map-color-1, transparent);
|
||||
|
||||
@for $i from 2 through $vector-map-colors-max {
|
||||
$color: color-mix(in oklab, #{$color}, var(--vector-map-color-#{$i}, transparent) calc(clamp(0, var(--vector-map-value, 1) * (var(--vector-map-colors, 2) - 1) - #{$i - 2}, 1) * 100%));
|
||||
}
|
||||
|
||||
--vector-map-region-color: #{$color};
|
||||
}
|
||||
|
||||
// A country too small for a shape: a dot in the colour of the region, with the
|
||||
// ring of a marker around it. Without a value it says nothing and only adds
|
||||
// noise next to the markers, so it is left out.
|
||||
|
||||
@@ -11,6 +11,9 @@ classnames:
|
||||
component:
|
||||
- class: vector-map
|
||||
desc: Box that holds the map, as wide as its parent
|
||||
style:
|
||||
- class: vector-map-colors
|
||||
desc: Shades the values with a scale of several colors, set by JavaScript for data-bs-colors
|
||||
part:
|
||||
- class: vector-map-svg
|
||||
desc: The rendered SVG, added by JavaScript
|
||||
@@ -96,6 +99,24 @@ The map uses the primary color. Set `--tblr-vector-map-color` to change it.
|
||||
<div class="vector-map" data-bs-toggle="vector-map" data-bs-label="Growth by country" data-bs-min="0" data-bs-values='{"US": 4, "CA": 7, "BR": 9, "DE": 3, "PL": 8, "IN": 10, "CN": 6, "AU": 5}' style="--tblr-vector-map-color: var(--tblr-green)"></div>
|
||||
</Example>
|
||||
|
||||
### Color scale
|
||||
|
||||
One color shows how much. A scale of several colors also shows good and bad, or cold and hot. Pass two to five colors in `data-bs-colors`, as a JSON array. The first color is for the lowest value and the last one for the highest. The values between them get a mix of the two nearest colors.
|
||||
|
||||
<Example>
|
||||
<div class="vector-map" data-bs-toggle="vector-map" data-bs-label="Customer satisfaction by country" data-bs-min="0" data-bs-max="100" data-bs-colors='["var(--tblr-red)", "var(--tblr-yellow)", "var(--tblr-green)"]' data-bs-values='{"US": 82, "CA": 91, "MX": 48, "BR": 35, "AR": 22, "GB": 74, "DE": 96, "FR": 63, "ES": 55, "PL": 88, "RU": 12, "IN": 41, "CN": 58, "JP": 79, "AU": 100, "ZA": 28, "EG": 6, "NG": 17, "SA": 67, "ID": 45}'></div>
|
||||
</Example>
|
||||
|
||||
Any CSS color works, also a Tabler color such as `var(--tblr-red)`. The browser mixes the colors with `color-mix()`, so they follow the theme and you can change them later with CSS.
|
||||
|
||||
The plugin writes the colors to `--tblr-vector-map-color-1`, `--tblr-vector-map-color-2` and so on, and their number to `--tblr-vector-map-colors`. To set a scale without JavaScript options, add the `vector-map-colors` class and these properties yourself:
|
||||
|
||||
```html
|
||||
<div class="vector-map vector-map-colors" data-bs-toggle="vector-map" style="--tblr-vector-map-colors: 2; --tblr-vector-map-color-1: var(--tblr-blue); --tblr-vector-map-color-2: var(--tblr-red)"></div>
|
||||
```
|
||||
|
||||
With a scale, the lowest value gets the first color in full, so `--tblr-vector-map-value-min` is not used. Markers keep the color from `--tblr-vector-map-color`.
|
||||
|
||||
## Markers
|
||||
|
||||
Markers point at single places, such as offices or customers. Pass them in `data-bs-markers` as a JSON array. Each marker has a `lat`, a `lng` and an optional `name`. A marker keeps its size when the map is resized or zoomed.
|
||||
@@ -145,6 +166,7 @@ Set options with data attributes or pass them to the constructor. For a data att
|
||||
| `values` | object | `{}` | A number per region code. |
|
||||
| `min` | number, null | `null` | Value that gets the lightest shade. The lowest value when `null`. |
|
||||
| `max` | number, null | `null` | Value that gets the full color. The highest value when `null`. |
|
||||
| `colors` | array | `[]` | Two to five colors of a scale, from the lowest value to the highest. |
|
||||
| `markers` | array | `[]` | Places as `{ name, lat, lng }`. |
|
||||
| `lines` | array | `[]` | Connections as `{ from, to }`, by marker name. |
|
||||
| `tooltip` | boolean, function | `true` | Shows a tooltip, or builds its text. |
|
||||
@@ -213,6 +235,8 @@ The map reads its colors and sizes from custom properties on the element. Change
|
||||
| `--tblr-vector-map-region-stroke` | `var(--tblr-bg-surface)` | Border between countries. |
|
||||
| `--tblr-vector-map-region-stroke-width` | `1px` | Width of the border. |
|
||||
| `--tblr-vector-map-value-min` | `0.1` | Share of the color the lowest value gets, from 0 to 1. |
|
||||
| `--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-marker-size` | `0.5rem` | Diameter of a marker. |
|
||||
| `--tblr-vector-map-marker-color` | `var(--tblr-vector-map-color)` | Color of a marker. |
|
||||
|
||||
@@ -34,6 +34,14 @@ import DocsLink from '@ui/DocsLink.astro'
|
||||
</CardBody>
|
||||
</Card>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<Card>
|
||||
<CardBody>
|
||||
<CardTitle>Map with a color scale</CardTitle>
|
||||
<MapVector mapId="world-colors" />
|
||||
</CardBody>
|
||||
</Card>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<Card>
|
||||
<CardBody>
|
||||
|
||||
@@ -6,6 +6,7 @@ interface MapData {
|
||||
map: string
|
||||
color?: string
|
||||
zoom?: boolean
|
||||
colors?: string[]
|
||||
values?: Record<string, number>
|
||||
markers?: { name: string; lat: number; lng: number }[]
|
||||
lines?: { from: string; to: string }[]
|
||||
@@ -30,6 +31,7 @@ const color = colorProp ?? data?.color ?? 'primary'
|
||||
data-bs-map={data.map}
|
||||
data-bs-label={data.title}
|
||||
data-bs-zoom={data.zoom ? 'true' : undefined}
|
||||
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)}
|
||||
data-bs-lines={data.lines && JSON.stringify(data.lines)}
|
||||
|
||||
@@ -344,5 +344,194 @@
|
||||
"RO": 2,
|
||||
"CZ": 5
|
||||
}
|
||||
},
|
||||
"world-colors": {
|
||||
"title": "Map with a color scale",
|
||||
"map": "world",
|
||||
"colors": ["var(--tblr-red)", "var(--tblr-yellow)", "var(--tblr-green)"],
|
||||
"values": {
|
||||
"AF": 2,
|
||||
"AL": 2,
|
||||
"DZ": 4,
|
||||
"AO": 3,
|
||||
"AG": 1,
|
||||
"AR": 5,
|
||||
"AM": 1,
|
||||
"AU": 7,
|
||||
"AT": 5,
|
||||
"AZ": 3,
|
||||
"BS": 1,
|
||||
"BH": 2,
|
||||
"BD": 4,
|
||||
"BB": 1,
|
||||
"BY": 3,
|
||||
"BE": 5,
|
||||
"BZ": 1,
|
||||
"BJ": 1,
|
||||
"BT": 1,
|
||||
"BO": 2,
|
||||
"BA": 2,
|
||||
"BW": 2,
|
||||
"BR": 8,
|
||||
"BN": 2,
|
||||
"BG": 2,
|
||||
"BF": 1,
|
||||
"BI": 1,
|
||||
"KH": 2,
|
||||
"CM": 2,
|
||||
"CA": 7,
|
||||
"CV": 1,
|
||||
"CF": 1,
|
||||
"TD": 1,
|
||||
"CL": 4,
|
||||
"CN": 9,
|
||||
"CO": 5,
|
||||
"KM": 1,
|
||||
"CD": 2,
|
||||
"CG": 2,
|
||||
"CR": 2,
|
||||
"CI": 2,
|
||||
"HR": 3,
|
||||
"CY": 2,
|
||||
"CZ": 4,
|
||||
"DK": 5,
|
||||
"DJ": 1,
|
||||
"DM": 1,
|
||||
"DO": 3,
|
||||
"EC": 3,
|
||||
"EG": 5,
|
||||
"SV": 2,
|
||||
"GQ": 2,
|
||||
"ER": 1,
|
||||
"EE": 2,
|
||||
"ET": 2,
|
||||
"FJ": 1,
|
||||
"FI": 5,
|
||||
"FR": 8,
|
||||
"GA": 2,
|
||||
"GM": 1,
|
||||
"GE": 2,
|
||||
"DE": 8,
|
||||
"GH": 2,
|
||||
"GR": 5,
|
||||
"GD": 1,
|
||||
"GT": 2,
|
||||
"GN": 1,
|
||||
"GW": 1,
|
||||
"GY": 1,
|
||||
"HT": 1,
|
||||
"HN": 2,
|
||||
"HK": 5,
|
||||
"HU": 4,
|
||||
"IS": 2,
|
||||
"IN": 7,
|
||||
"ID": 6,
|
||||
"IR": 5,
|
||||
"IQ": 3,
|
||||
"IE": 5,
|
||||
"IL": 5,
|
||||
"IT": 8,
|
||||
"JM": 2,
|
||||
"JP": 9,
|
||||
"JO": 2,
|
||||
"KZ": 4,
|
||||
"KE": 2,
|
||||
"KI": 1,
|
||||
"KR": 6,
|
||||
"KW": 4,
|
||||
"KG": 1,
|
||||
"LA": 1,
|
||||
"LV": 2,
|
||||
"LB": 2,
|
||||
"LS": 1,
|
||||
"LR": 1,
|
||||
"LY": 3,
|
||||
"LT": 2,
|
||||
"LU": 3,
|
||||
"MK": 1,
|
||||
"MG": 1,
|
||||
"MW": 1,
|
||||
"MY": 5,
|
||||
"MV": 1,
|
||||
"ML": 1,
|
||||
"MT": 1,
|
||||
"MR": 1,
|
||||
"MU": 1,
|
||||
"MX": 7,
|
||||
"MD": 1,
|
||||
"MN": 1,
|
||||
"ME": 1,
|
||||
"MA": 3,
|
||||
"MZ": 2,
|
||||
"MM": 2,
|
||||
"NA": 2,
|
||||
"NP": 2,
|
||||
"NL": 6,
|
||||
"NZ": 4,
|
||||
"NI": 1,
|
||||
"NE": 1,
|
||||
"NG": 5,
|
||||
"NO": 5,
|
||||
"OM": 3,
|
||||
"PK": 4,
|
||||
"PA": 2,
|
||||
"PG": 1,
|
||||
"PY": 2,
|
||||
"PE": 4,
|
||||
"PH": 4,
|
||||
"PL": 10,
|
||||
"PT": 5,
|
||||
"QA": 4,
|
||||
"RO": 4,
|
||||
"RU": 7,
|
||||
"RW": 1,
|
||||
"WS": 1,
|
||||
"ST": 1,
|
||||
"SA": 5,
|
||||
"SN": 2,
|
||||
"RS": 2,
|
||||
"SC": 1,
|
||||
"SL": 1,
|
||||
"SG": 5,
|
||||
"SK": 3,
|
||||
"SI": 2,
|
||||
"SB": 1,
|
||||
"ZA": 5,
|
||||
"ES": 7,
|
||||
"LK": 2,
|
||||
"KN": 1,
|
||||
"LC": 1,
|
||||
"VC": 1,
|
||||
"SD": 3,
|
||||
"SR": 1,
|
||||
"SZ": 1,
|
||||
"SE": 5,
|
||||
"CH": 6,
|
||||
"SY": 3,
|
||||
"TW": 5,
|
||||
"TJ": 1,
|
||||
"TZ": 2,
|
||||
"TH": 5,
|
||||
"TL": 1,
|
||||
"TG": 1,
|
||||
"TO": 1,
|
||||
"TT": 2,
|
||||
"TN": 2,
|
||||
"TR": 6,
|
||||
"TM": 1,
|
||||
"UG": 2,
|
||||
"UA": 4,
|
||||
"AE": 5,
|
||||
"GB": 8,
|
||||
"US": 10,
|
||||
"UY": 2,
|
||||
"UZ": 2,
|
||||
"VU": 1,
|
||||
"VE": 5,
|
||||
"VN": 4,
|
||||
"YE": 2,
|
||||
"ZM": 2,
|
||||
"ZW": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user