Show small countries on the world map as points

- 29 sovereign states with no shape at 110m, plus Hong Kong and Macao
- a point is a region: it takes a value, a color and a tooltip
This commit is contained in:
codecalm committed 2026-10-05 13:40:13 +02:00
1 parent 874897218f
commit 1595bf8b5e
8 files changed
+243 -27

No files matched your search

+63 -7
View File
@@ -40,6 +40,18 @@ interface MapConfig {
width: number
/** Region codes left out of the map */
exclude: string[]
/**
* Countries too small to have a shape in `source`. They are taken from a
* more detailed file and put on the map as a point.
*/
points?: {
/** Natural Earth file, without the extension */
source: string
/** Natural Earth `TYPE` values that get a point */
types: string[]
/** Region codes that get a point whatever their type */
include: string[]
}
}
const maps: MapConfig[] = [
@@ -48,6 +60,11 @@ const maps: MapConfig[] = [
source: 'ne_110m_admin_0_countries',
width: 1000,
exclude: ['AQ'],
points: {
source: 'ne_50m_admin_0_countries',
types: ['Sovereign country'],
include: ['HK', 'MO'],
},
},
]
@@ -139,14 +156,26 @@ const ringPath = (points: Position[]): string => {
return `M${joinNumbers(rounded[0])}l${joinNumbers(deltas)}z`
}
async function generate(config: MapConfig): Promise<void> {
const url = `${SOURCE}/${config.source}.geojson`
async function load(source: string): Promise<Feature[]> {
const url = `${SOURCE}/${source}.geojson`
const response = await fetch(url)
if (!response.ok) {
throw new Error(`Could not load ${url}: ${response.status}`)
}
const { features } = (await response.json()) as { features: Feature[] }
return features
}
// Where the long name of Natural Earth is the formal one, not the common one
const NAME_OVERRIDES: Record<string, string> = {
CV: 'Cabo Verde',
}
const regionName = (properties: Feature['properties']): string => NAME_OVERRIDES[regionCode(properties) ?? ''] ?? String(properties.NAME_LONG ?? properties.NAME)
async function generate(config: MapConfig): Promise<void> {
const features = await load(config.source)
const regions: { code: string; name: string; rings: Ring[] }[] = []
for (const feature of features) {
@@ -162,7 +191,7 @@ async function generate(config: MapConfig): Promise<void> {
regions.push({
code,
name: String(feature.properties.NAME_LONG ?? feature.properties.NAME),
name: regionName(feature.properties),
rings: ringsOf(feature.geometry).map((ring) => ring.map(miller)),
})
}
@@ -186,14 +215,39 @@ async function generate(config: MapConfig): Promise<void> {
const height = Math.ceil((maxY - minY) * scale)
const round = (value: number): number => Math.round(value * 1000) / 1000
// A point sits where Natural Earth puts the label of the country.
const points: { code: string; name: string; position: Position }[] = []
if (config.points) {
const { source, types, include } = config.points
for (const { properties } of await load(source)) {
const code = regionCode(properties)
if (!code || config.exclude.includes(code) || regions.some((region) => region.code === code)) {
continue
}
if (!types.includes(String(properties.TYPE)) && !include.includes(code)) {
continue
}
const [x, y] = miller([Number(properties.LABEL_X), Number(properties.LABEL_Y)])
const position: Position = [x * scale + translate[0], translate[1] - y * scale]
if (!(position[0] >= 0 && position[0] <= config.width && position[1] >= 0 && position[1] <= height)) {
throw new Error(`Point "${code}" is outside of the map`)
}
points.push({ code, name: regionName(properties), position })
}
}
regions.sort((a, b) => a.code.localeCompare(b.code))
points.sort((a, b) => a.code.localeCompare(b.code))
const data = {
width: config.width,
height,
projection: { type: 'miller', scale: round(scale), translate: translate.map(round) },
regions: Object.fromEntries(
regions.map((region) => {
regions: Object.fromEntries([
...regions.map((region) => {
const path = region.rings.map((ring) => ringPath(ring.map(([x, y]): Position => [x * scale + translate[0], translate[1] - y * scale]))).join('')
if (!path) {
@@ -202,7 +256,9 @@ async function generate(config: MapConfig): Promise<void> {
return [region.code, { name: region.name, path }]
}),
),
// A line of no length: the plugin draws it as a dot
...points.map((point) => [point.code, { name: point.name, path: `M${joinNumbers(point.position.map((value) => Math.round(value * PRECISION)))}h0`, point: true }]),
]),
}
const source = `// Generated by \`pnpm run generate:maps\` from Natural Earth (public domain,
@@ -218,7 +274,7 @@ export const ${config.name}: VectorMapData = ${JSON.stringify(data)}
const formatted = await prettier.format(source, { ...options, filepath: file })
writeFileSync(file, formatted)
console.log(`✓ ${config.name}: ${regions.length} regions, ${config.width}x${height}, ${(formatted.length / 1024).toFixed(1)} kB`)
console.log(`✓ ${config.name}: ${regions.length} regions, ${points.length} points, ${config.width}x${height}, ${(formatted.length / 1024).toFixed(1)} kB`)
}
async function main(): Promise<void> {
+31
View File
@@ -418,5 +418,36 @@ export const world: VectorMapData = {
path: 'M585.4 339.3l1.1 .7 1.2 .4 1.7 .5 1.6 .9 1.3 1.2 .7 2.4-.5 .7-.5 2.3 .5 2.4-.9 .9-.8 2.7 1.5 .7-8.5 2.3 .3 2.1-2.1 .3-1.6 1.2-.3 1-1 .2-2.4 2.3-1.6 1.9-.9 .1-.9-.4-3.1-.3-.5-.2 0-.2-1.1-.7-1.8-.1-2.3 .6-1.8-1.8-1.9-2.3 .1-9 5.8 0-.2-1 .4-1-.5-1.4 .3-1.3-.3-.9 1 .1 .1 .9 1.3-.1 1.8 .2 .9 1.3 2.3 .4 1.7-.9 .6 1.5 2.1 .4 1 1.2 1.2 1.6 2.1 0-.2-3.1-.8 .5-1.9-1.1-.8-.5 .4-2.8 .4-3.3-.6-1.2 .8-1.8 .8-.4 3.7-.4z',
},
ZW: { name: 'Zimbabwe', path: 'M586.6 378.9l-1.4-.3-1 .3-1.3-.5-1.1 0-1.8-1.3-2.2-.5-.8-1.8 0-1-1.2-.3-3.1-3.2-.9-1.7-.6-.5-1-2.3 3.1 .3 .9 .4 .9-.1 1.6-1.9 2.4-2.3 1-.2 .3-1 1.6-1.2 2.1-.3 .2 1 2.3 0 1.3 .6 .6 .7 1.3 .2 1.4 .9 0 3.6-.5 2-.1 2.2 .4 .8-.3 1.7-.4 .3-.7 2.1z' },
AD: { name: 'Andorra', path: 'M504.3 190.2h0', point: true },
AG: { name: 'Antigua and Barbuda', path: 'M328.4 267.4h0', point: true },
BB: { name: 'Barbados', path: 'M334.5 279.3h0', point: true },
BH: { name: 'Bahrain', path: 'M640.4 242h0', point: true },
CV: { name: 'Cabo Verde', path: 'M434.3 273.9h0', point: true },
DM: { name: 'Dominica', path: 'M329.6 272.8h0', point: true },
FM: { name: 'Federated States of Micronesia', path: 'M939.5 296.9h0', point: true },
GD: { name: 'Grenada', path: 'M328.7 282.2h0', point: true },
HK: { name: 'Hong Kong', path: 'M816.9 252.6h0', point: true },
KI: { name: 'Kiribati', path: 'M62.8 311h0', point: true },
KM: { name: 'Comoros', path: 'M620.3 348.8h0', point: true },
KN: { name: 'Saint Kitts and Nevis', path: 'M325.7 267.4h0', point: true },
LC: { name: 'Saint Lucia', path: 'M330.6 277.2h0', point: true },
LI: { name: 'Liechtenstein', path: 'M526.6 174.6h0', point: true },
MC: { name: 'Monaco', path: 'M520.6 186.2h0', point: true },
MH: { name: 'Marshall Islands', path: 'M975.5 296.3h0', point: true },
MO: { name: 'Macao', path: 'M815.4 253.6h0', point: true },
MT: { name: 'Malta', path: 'M540.1 211.9h0', point: true },
MU: { name: 'Mauritius', path: 'M659.9 373.2h0', point: true },
MV: { name: 'Maldives', path: 'M704.2 304.4h0', point: true },
NR: { name: 'Nauru', path: 'M963.7 317.5h0', point: true },
PW: { name: 'Palau', path: 'M873.8 295.1h0', point: true },
SC: { name: 'Seychelles', path: 'M654.1 329h0', point: true },
SG: { name: 'Singapore', path: 'M788.4 312.2h0', point: true },
SM: { name: 'San Marino', path: 'M534.6 185.5h0', point: true },
ST: { name: 'São Tomé and Principe', path: 'M519.5 313.3h0', point: true },
TO: { name: 'Tonga', path: 'M13.4 375.8h0', point: true },
TV: { name: 'Tuvalu', path: 'M997.8 339.7h0', point: true },
VA: { name: 'Vatican', path: 'M534.6 192.4h0', point: true },
VC: { name: 'Saint Vincent and the Grenadines', path: 'M329.6 279.5h0', point: true },
WS: { name: 'Samoa', path: 'M21 354.2h0', point: true },
},
}
+36 -7
View File
@@ -14,6 +14,8 @@ export type VectorMapRegion = {
name: string
/** SVG path data, already projected into the viewBox of the map */
path: string
/** Too small for a shape at the scale of the map: `path` is a single point, drawn as a dot */
point?: boolean
}
export type VectorMapData = {
@@ -85,6 +87,9 @@ const EVENT_CLICK = `click${EVENT_KEY}`
const CLASS_NAME_SVG = 'vector-map-svg'
const CLASS_NAME_REGION = 'vector-map-region'
const CLASS_NAME_POINT = 'vector-map-region-point'
const CLASS_NAME_POINT_HALO = 'vector-map-region-point-halo'
const CLASS_NAME_POINT_DOT = 'vector-map-region-point-dot'
const CLASS_NAME_LINE = 'vector-map-line'
const CLASS_NAME_MARKER = 'vector-map-marker'
const CLASS_NAME_MARKER_HALO = 'vector-map-marker-halo'
@@ -333,12 +338,11 @@ class VectorMap extends BaseComponent {
svg.setAttribute('aria-hidden', 'true')
}
for (const [code, region] of Object.entries(map.regions)) {
const path = svgEl('path')
path.setAttribute('class', CLASS_NAME_REGION)
path.setAttribute(ATTRIBUTE_REGION, code)
path.setAttribute('d', region.path)
svg.append(path)
// The points go in after the shapes, so a small country shows on top of
// the large one around it.
const regions = Object.entries(map.regions)
for (const [code, region] of [...regions.filter(([, { point }]) => !point), ...regions.filter(([, { point }]) => point)]) {
svg.append(region.point ? this._createPoint(code, region) : this._createShape(code, region))
}
this._renderMarkers(svg, map)
@@ -413,6 +417,31 @@ class VectorMap extends BaseComponent {
return data
}
_createShape(code: string, region: VectorMapRegion): SVGElement {
const path = svgEl('path')
path.setAttribute('class', CLASS_NAME_REGION)
path.setAttribute(ATTRIBUTE_REGION, code)
path.setAttribute('d', region.path)
return path
}
// A region like any other, so it takes a value and a tooltip. It is drawn
// the way a marker is: a dot with a ring that sets it apart from the land.
_createPoint(code: string, region: VectorMapRegion): SVGElement {
const group = svgEl('g')
group.setAttribute('class', `${CLASS_NAME_REGION} ${CLASS_NAME_POINT}`)
group.setAttribute(ATTRIBUTE_REGION, code)
for (const className of [CLASS_NAME_POINT_HALO, CLASS_NAME_POINT_DOT]) {
const path = svgEl('path')
path.setAttribute('class', className)
path.setAttribute('d', region.path)
group.append(path)
}
return group
}
// Lines go in first, so the markers they join sit on top of their ends.
_renderMarkers(svg: SVGSVGElement, map: VectorMapData): void {
const { markers, lines } = this._config
@@ -719,7 +748,7 @@ class VectorMap extends BaseComponent {
const max = maxForced ?? Math.max(...numbers)
const span = max - min
for (const path of this._element.querySelectorAll<SVGPathElement>(`.${CLASS_NAME_REGION}`)) {
for (const path of this._element.querySelectorAll<SVGElement>(`.${CLASS_NAME_REGION}`)) {
const value = values[path.getAttribute(ATTRIBUTE_REGION) ?? '']
if (value === undefined) {
+47 -3
View File
@@ -29,7 +29,7 @@ describe('VectorMap', () => {
})
const el = (): HTMLElement => fixtureEl.querySelector('.vector-map')!
const region = (code: string): SVGPathElement => el().querySelector<SVGPathElement>(`[data-region="${code}"]`)!
const region = (code: string): SVGGeometryElement => el().querySelector<SVGGeometryElement>(`[data-region="${code}"]`)!
const ratio = (code: string): string => region(code).style.getPropertyValue('--tblr-vector-map-value')
describe('NAME', () => {
@@ -168,6 +168,40 @@ describe('VectorMap', () => {
})
})
describe('points', () => {
const islands: VectorMapData = {
...squares,
regions: { ZZ: { name: 'Island', path: 'M15 5h0', point: true }, ...squares.regions },
}
it('should draw a region that is a point as a dot with a ring, on top of the shapes', () => {
fixtureEl.innerHTML = '<div class="vector-map"></div>'
new VectorMap(el(), { map: islands })
const point = region('ZZ')
expect(point.tagName).toBe('g')
expect(point.classList.contains('vector-map-region-point')).toBe(true)
expect(point.querySelector('.vector-map-region-point-halo')!.getAttribute('d')).toBe('M15 5h0')
expect(point.querySelector('.vector-map-region-point-dot')!.getAttribute('d')).toBe('M15 5h0')
expect(el().querySelector('svg')!.lastElementChild).toBe(point)
})
it('should give a point a value and a tooltip like any region', () => {
fixtureEl.innerHTML = '<div class="vector-map"></div>'
new VectorMap(el(), { map: islands, values: { AA: 1, ZZ: 3 } })
expect(region('ZZ').getAttribute('data-value')).toBe('3')
expect(ratio('ZZ')).toBe('1')
region('ZZ')
.querySelector('.vector-map-region-point-dot')!
.dispatchEvent(new PointerEvent('pointermove', { bubbles: true }))
expect(el().querySelector('.vector-map-tooltip')!.textContent).toBe('Island: 3')
})
})
describe('markers', () => {
it('should place a marker by its coordinates', () => {
fixtureEl.innerHTML = `<div class="vector-map" data-bs-markers='[{"name": "Null Island", "lat": 0, "lng": 0}]'></div>`
@@ -496,12 +530,22 @@ describe('VectorMap', () => {
expect(world.regions.FR.name).toBe('France')
expect(world.regions.AQ).toBeUndefined()
for (const [code, { path }] of Object.entries(world.regions)) {
for (const [code, { path, point }] of Object.entries(world.regions)) {
expect(code).toMatch(/^[A-Z]{2}$/)
expect(path).toMatch(/^M[-\d. ]+l[-\d. ]+z/)
expect(path).toMatch(point ? /^M[\d. ]+h0$/ : /^M[-\d. ]+l[-\d. ]+z/)
}
})
it('should have the countries too small for a shape as points', () => {
for (const code of ['AD', 'SM', 'MC', 'LI', 'VA', 'MT', 'SG', 'HK', 'MO']) {
expect(world.regions[code].point).toBe(true)
}
expect(world.regions.PL.point).toBeUndefined()
// Dependencies are left out
expect(world.regions.GI).toBeUndefined()
})
it('should draw inside its viewBox', () => {
fixtureEl.innerHTML = '<div class="vector-map" style="width: 500px"></div>'
+1
View File
@@ -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-point-size: 0.375rem !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;
+23 -5
View File
@@ -14,6 +14,7 @@
--vector-map-region-stroke: #{$vector-map-region-stroke};
--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-marker-size: #{$vector-map-marker-size};
--vector-map-marker-color: #{$vector-map-marker-color};
--vector-map-marker-halo-color: #{$vector-map-marker-halo-color};
@@ -37,7 +38,10 @@
}
.vector-map-region {
fill: var(--vector-map-region-fill);
// The colour of the region, used as the fill of a shape and as the dot of a
// region drawn as a point.
--vector-map-region-color: var(--vector-map-region-fill);
fill: var(--vector-map-region-color);
stroke: var(--vector-map-region-stroke);
stroke-linejoin: round;
stroke-width: var(--vector-map-region-stroke-width);
@@ -48,7 +52,7 @@
// value still gets `--vector-map-value-min` of the colour, so it stands out
// from a region with no value at all.
&[data-value] {
fill: color-mix(in oklab, var(--vector-map-color) calc((var(--vector-map-value-min) + var(--vector-map-value, 1) * (1 - var(--vector-map-value-min))) * 100%), var(--vector-map-region-fill));
--vector-map-region-color: color-mix(in oklab, var(--vector-map-color) calc((var(--vector-map-value-min) + var(--vector-map-value, 1) * (1 - var(--vector-map-value-min))) * 100%), var(--vector-map-region-fill));
}
}
@@ -66,9 +70,11 @@
}
}
// Both parts of a marker are a line of no length: its round caps make a dot
// as wide as the stroke, and the non-scaling stroke keeps it that wide in
// pixels however large the map is drawn.
// Both parts of a marker, and of a region drawn as a point, are a line of no
// length: its round caps make a dot as wide as the stroke, and the non-scaling
// stroke keeps it that wide in pixels however large the map is drawn.
.vector-map-region-point-halo,
.vector-map-region-point-dot,
.vector-map-marker-halo,
.vector-map-marker-dot {
fill: none;
@@ -76,6 +82,18 @@
vector-effect: non-scaling-stroke;
}
// A country too small for a shape: a dot in the colour of the region, with the
// ring of a marker around it.
.vector-map-region-point-halo {
stroke: var(--vector-map-marker-halo-color);
stroke-width: calc(var(--vector-map-point-size) + var(--vector-map-marker-halo-width) * 2);
}
.vector-map-region-point-dot {
stroke: var(--vector-map-region-color);
stroke-width: var(--vector-map-point-size);
}
.vector-map-marker-halo {
stroke: var(--vector-map-marker-halo-color);
stroke-width: calc(var(--vector-map-marker-size) + var(--vector-map-marker-halo-width) * 2);
+22 -5
View File
@@ -15,7 +15,13 @@ classnames:
- class: vector-map-svg
desc: The rendered SVG, added by JavaScript
- class: vector-map-region
desc: One country or region, a path with a data-region code
desc: One country or region, with its code in data-region
- class: vector-map-region-point
desc: A country too small for a shape, drawn as a dot
- class: vector-map-region-point-dot
desc: The dot, in the color of the region
- class: vector-map-region-point-halo
desc: Ring around the dot, in the surface color
- class: vector-map-marker
desc: Group that holds the two parts of a marker
- class: vector-map-marker-dot
@@ -188,11 +194,11 @@ visits.update({ PL: 61, DE: 77, FR: 70 });
## Styling a single country
Every country is a path with its code in `data-region`, so one rule can style it:
Every country has its code in `data-region`, so one rule can color it. Set `--tblr-vector-map-region-color`, which works for a shape and for the dot of a small country:
```css
.vector-map [data-region='PL'] {
fill: var(--tblr-red);
--tblr-vector-map-region-color: var(--tblr-red);
}
```
@@ -207,9 +213,10 @@ 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-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. |
| `--tblr-vector-map-marker-halo-color` | `var(--tblr-bg-surface)` | Color of the ring around 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. |
| `--tblr-vector-map-marker-halo-width` | `2px` | Width of the ring. |
| `--tblr-vector-map-line-color` | `var(--tblr-secondary)` | Color of a line. |
| `--tblr-vector-map-line-width` | `1px` | Width of a line. |
@@ -219,7 +226,17 @@ The map reads its colors and sizes from custom properties on the element. Change
## The world map
The world map has 176 countries in the Miller projection, without Antarctica. It is made for an overview, so very small countries, such as Singapore or Malta, are not on it. Values for a code that is not on the map are skipped.
The world map has 207 countries in the Miller projection, without Antarctica. Values for a code that is not on the map are skipped.
### Small countries
31 countries are too small to see as a shape on a map of the world, for example Andorra, San Marino, Malta or Singapore. The map shows each of them as a dot. A dot works like any other country: it takes a value, gets the same shade and has a tooltip. It keeps its size when the map is resized or zoomed.
<Example>
<div class="vector-map" data-bs-toggle="vector-map" data-bs-zoom="true" data-bs-label="Visits from small countries" data-bs-values='{"AD": 4, "SM": 2, "MC": 5, "LI": 3, "VA": 1, "MT": 6, "SG": 9, "HK": 8, "BH": 5, "MU": 3}'></div>
</Example>
The dots are the sovereign states that have no shape on the map, plus Hong Kong and Macao. Dependent territories, such as Gibraltar or Guam, are not on the map. Set the size of a dot with `--tblr-vector-map-point-size`.
Three territories have no ISO code. The map uses `XK` for Kosovo, `XC` for Northern Cyprus and `XS` for Somaliland. Borders follow the default view of Natural Earth.
+20
View File
@@ -13,13 +13,16 @@
"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,
@@ -36,11 +39,13 @@
"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,
@@ -50,6 +55,7 @@
"CZ": 4,
"DK": 5,
"DJ": 1,
"DM": 1,
"DO": 3,
"EC": 3,
"EG": 5,
@@ -67,12 +73,14 @@
"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,
@@ -87,6 +95,7 @@
"JO": 2,
"KZ": 4,
"KE": 2,
"KI": 1,
"KR": 6,
"KW": 4,
"KG": 1,
@@ -102,8 +111,11 @@
"MG": 1,
"MW": 1,
"MY": 5,
"MV": 1,
"ML": 1,
"MT": 1,
"MR": 1,
"MU": 1,
"MX": 7,
"MD": 1,
"MN": 1,
@@ -132,16 +144,23 @@
"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,
@@ -154,6 +173,7 @@
"TH": 5,
"TL": 1,
"TG": 1,
"TO": 1,
"TT": 2,
"TN": 2,
"TR": 6,