@bezda/rhp-mcp 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -3
- package/bin/rhp-mcp.js +5 -1
- package/package.json +3 -3
- package/skill/SKILL.md +112 -25
- package/skill/recipes/area.html +6 -0
- package/skill/recipes/bar.html +6 -0
- package/skill/recipes/box-plot.html +6 -0
- package/skill/recipes/bubble.html +6 -0
- package/skill/recipes/bullet.html +6 -0
- package/skill/recipes/candlestick.html +6 -0
- package/skill/recipes/column.html +18 -5
- package/skill/recipes/dial.html +349 -0
- package/skill/recipes/diverging-bars.html +6 -0
- package/skill/recipes/donut.html +6 -0
- package/skill/recipes/dumbbell.html +6 -0
- package/skill/recipes/gantt.html +14 -4
- package/skill/recipes/grouped-bars.html +6 -0
- package/skill/recipes/heatmap.html +6 -0
- package/skill/recipes/histogram.html +6 -0
- package/skill/recipes/line.html +7 -1
- package/skill/recipes/live.html +6 -0
- package/skill/recipes/log-line.html +493 -0
- package/skill/recipes/lollipop.html +6 -0
- package/skill/recipes/multi-line.html +10 -5
- package/skill/recipes/pyramid.html +6 -0
- package/skill/recipes/race.html +6 -0
- package/skill/recipes/radial-bars.html +6 -0
- package/skill/recipes/scatter.html +6 -0
- package/skill/recipes/silhouettes.html +352 -0
- package/skill/recipes/slope.html +6 -0
- package/skill/recipes/sparklines.html +7 -1
- package/skill/recipes/stacked-100.html +6 -0
- package/skill/recipes/stacked-bars.html +6 -0
- package/skill/recipes/strip.html +6 -0
- package/skill/recipes/unit-stack.html +276 -0
- package/skill/recipes/violin.html +6 -0
- package/skill/recipes/waffle.html +6 -0
- package/skill/recipes/waterfall.html +6 -0
- package/skill/references/api.md +150 -22
- package/skill/references/design.md +303 -326
- package/skill/references/environments.md +11 -4
- package/skill/references/forms.md +41 -4
- package/skill/references/interaction.md +3 -1
- package/skill/references/looks.md +267 -0
- package/skill/references/pitfalls.md +52 -7
- package/src/check/README.md +15 -2
- package/src/check/findings.js +1 -1
- package/src/check/index.js +103 -14
- package/src/check/inpage.js +80 -16
- package/src/check/report.js +4 -1
- package/src/guard/rhp.js +4 -0
- package/src/guard/rules.js +1 -1
- package/src/server.js +9 -8
|
@@ -5,6 +5,12 @@
|
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
6
|
<title>Wealth and health</title>
|
|
7
7
|
<meta name="description" content="Scatter plot of two measures per item on a second axis, with a log scale, a few points labeled (short codes on phones) and a readout of the point nearest the pointer, a tap or the arrow keys. Use it to show how two measures relate across items.">
|
|
8
|
+
<!--
|
|
9
|
+
A recipe gives technique, not a design.
|
|
10
|
+
Take: Dots on two axes with a log scale, a few points labeled (short codes on phones), and a readout of the nearest point.
|
|
11
|
+
Leave: the design. This poster was made for its own subject (wealth and health on banknote paper, a quarter of an engraved seal in the corner).
|
|
12
|
+
Its look, the place of its parts and the way its marks are drawn say that subject: design yours from your own (design.md, section 2).
|
|
13
|
+
-->
|
|
8
14
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
9
15
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
10
16
|
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Bodoni+Moda:opsz,wght@6..96,500..800&family=Manrope:wght@400..700&display=swap">
|
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<title>The world's tallest buildings</title>
|
|
7
|
+
<meta name="description" content="Silhouettes: each item drawn in its own outline (a shape() per row on a vertical Bar) to one scale on one street, its height on its top, the picked one's level carried across to the tallest with a dimension line, names on the street on wide screens and ranks with a key on narrow ones. Use it for things a reader knows by their shape: buildings, mountains, ships, bottles, animals.">
|
|
8
|
+
<!--
|
|
9
|
+
A recipe gives technique, not a design.
|
|
10
|
+
Take: an outline per row built from measured profiles with shape(), drawn to one scale on one ground line; a dashed level from the picked item's top across to the leader, with a dimension line for the gap; a slat type per width (names on the street, or ranks with a key below); and a readout of the picked item.
|
|
11
|
+
Leave: the design. This poster was made for its own subject (the ten tallest buildings as a skyline on one street under a pale blue sky, the headline set in the sky over the lowest towers, the street a dark band holding the names, the readout and the note).
|
|
12
|
+
Its look, the place of its parts and the way its marks are drawn say that subject: design yours from your own (design.md, section 2).
|
|
13
|
+
-->
|
|
14
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
15
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
16
|
+
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=League+Gothic&family=Barlow:wght@400;500;600;700&display=swap">
|
|
17
|
+
<script type="importmap">{ "imports": { "@bezda/rhp/standalone": "https://cdn.jsdelivr.net/npm/@bezda/rhp@2/dist/standalone.js" } }</script>
|
|
18
|
+
<style>
|
|
19
|
+
/* Look: the sky over one imaginary street. A pale daytime sky for the paper, the towers in its own haze,
|
|
20
|
+
the one in focus in deep navy, the street a navy ground, and a surveyor's red for the measurement. */
|
|
21
|
+
:root {
|
|
22
|
+
--page: #9fb9d4; --sky: #c4d9ee; --paper: #dbe8f5; --ink: #0f2540; --soft: #3d5878; --rule: #b7cde2;
|
|
23
|
+
--accent: #c8102e; --quiet: #5b789c;
|
|
24
|
+
--ground: #0f2540; --on-ground: #e4edf7; --ground-soft: #a9bdd6;
|
|
25
|
+
--display: "League Gothic", "Arial Narrow", Impact, sans-serif;
|
|
26
|
+
--text: Barlow, "Helvetica Neue", Arial, sans-serif;
|
|
27
|
+
--pad: 60px;
|
|
28
|
+
}
|
|
29
|
+
* { box-sizing: border-box; }
|
|
30
|
+
body { margin: 0; padding: 48px 16px; background: var(--page, #9fb9d4); }
|
|
31
|
+
|
|
32
|
+
/* The poster: sky on top, the street at the foot. Every part is a grid item, placed per width below. */
|
|
33
|
+
.poster { display: grid; grid-template-columns: minmax(0, 1fr); max-width: 1120px; margin: 0 auto; padding: var(--pad) var(--pad) 0; overflow: hidden;
|
|
34
|
+
color: var(--ink); font: 16px/1.5 var(--text); background: linear-gradient(var(--sky), var(--paper) 72%);
|
|
35
|
+
box-shadow: 0 30px 60px -34px rgb(15 37 64 / .6);
|
|
36
|
+
grid-template-areas: "kicker" "headline" "dek" "stage" "readout" "key" "note"; }
|
|
37
|
+
.poster figcaption { display: contents; }
|
|
38
|
+
.poster .kicker { grid-area: kicker; margin-bottom: 12px; font: 700 13px/1.3 var(--text); letter-spacing: .16em; text-transform: uppercase; color: var(--ink); }
|
|
39
|
+
.poster .kicker::before { content: ""; display: inline-block; width: 9px; height: 9px; margin-right: 10px; border-radius: 50%; background: var(--accent); vertical-align: 1px; }
|
|
40
|
+
.poster .headline { grid-area: headline; max-width: 13em; font: 400 clamp(44px, 5.6vw, 66px)/.95 var(--display); letter-spacing: .005em; text-wrap: balance; }
|
|
41
|
+
.poster .dek { grid-area: dek; max-width: 46ch; margin-top: 16px; font-size: 17px; line-height: 1.45; color: var(--soft); }
|
|
42
|
+
.poster .stage { grid-area: stage; position: relative; min-width: 0; margin-top: 18px; }
|
|
43
|
+
/* The street: a navy ground under the towers, from edge to edge of the poster, as tall as the room for the names */
|
|
44
|
+
.poster .stage::before { content: ""; position: absolute; left: calc(-1 * var(--pad)); right: calc(-1 * var(--pad)); bottom: 0; height: var(--street); background: var(--ground); }
|
|
45
|
+
/* Everything after the chart stands on the street */
|
|
46
|
+
.poster .readout, .poster .key, .poster .note { margin-inline: calc(-1 * var(--pad)); padding-inline: var(--pad); background: var(--ground); color: var(--on-ground); }
|
|
47
|
+
/* Every version of the readout in one grid cell, only the current one shown: it is as tall as the longest at every
|
|
48
|
+
width, so nothing under it moves whichever tower is picked */
|
|
49
|
+
.poster .readout { grid-area: readout; display: grid; margin-top: 0; margin-bottom: 0; padding-top: 8px; padding-bottom: 10px; font-size: 17px; line-height: 1.4; font-variant-numeric: tabular-nums; }
|
|
50
|
+
.poster .readout b { font-weight: 700; }
|
|
51
|
+
.poster .readout > .v { grid-area: 1 / 1; visibility: hidden; }
|
|
52
|
+
.poster .readout > .v.on { visibility: visible; }
|
|
53
|
+
.poster .readout .v span { display: block; color: var(--ground-soft); }
|
|
54
|
+
.poster .key { grid-area: key; margin-block: 0; padding-block: 6px 4px; list-style: none; font-size: 15px; line-height: 1.35; }
|
|
55
|
+
.poster .key li { display: grid; grid-template-columns: 1.6em minmax(0, 1fr) auto; column-gap: 8px; padding: 5px 0; border-top: 1px solid rgb(255 255 255 / .12); cursor: default; }
|
|
56
|
+
.poster .key li b { font: 400 19px/1 var(--display); color: var(--ground-soft); }
|
|
57
|
+
.poster .key li em { font-style: normal; color: var(--ground-soft); }
|
|
58
|
+
.poster .key li span { font-variant-numeric: tabular-nums; }
|
|
59
|
+
/* The picked tower's line is marked by a band, not by weight: bold text would wrap and move what is under it */
|
|
60
|
+
.poster .key li.lit { background: rgb(255 255 255 / .1); box-shadow: -8px 0 0 rgb(255 255 255 / .1), 8px 0 0 rgb(255 255 255 / .1); }
|
|
61
|
+
.poster .key li.lit b { color: var(--on-ground); }
|
|
62
|
+
.poster .note { grid-area: note; display: block; padding-top: 12px; padding-bottom: 26px; border-top: 1px solid rgb(255 255 255 / .16); font-size: 12.5px; line-height: 1.5; color: var(--ground-soft); }
|
|
63
|
+
.poster :focus-visible { outline: 2px solid var(--ink); outline-offset: 2px; }
|
|
64
|
+
|
|
65
|
+
/* Wide screens: the kicker and the headline float in the sky over the five lowest towers, the dek over the next three.
|
|
66
|
+
The columns follow the towers: the plot is the whole stage, one tenth per tower.
|
|
67
|
+
This place suits these heights: with other data, check that the words still clear the tips (pitfalls.md, "Text and
|
|
68
|
+
layers laid over the chart"). */
|
|
69
|
+
@media (min-width: 980px) {
|
|
70
|
+
.poster { grid-template-columns: calc(100% * .5) calc(100% * .3) minmax(0, 1fr); grid-template-rows: auto auto 1fr auto auto;
|
|
71
|
+
grid-template-areas: "kicker dek ." "headline dek ." ". . ." "readout readout readout" "note note note"; }
|
|
72
|
+
.poster .headline, .poster .kicker, .poster .dek { position: relative; z-index: 2; }
|
|
73
|
+
.poster .headline { padding-right: 28px; }
|
|
74
|
+
.poster .dek { align-self: start; margin-top: 2px; padding-right: 20px; font-size: 16px; }
|
|
75
|
+
.poster .stage { grid-area: 1 / 1 / 4 / 4; align-self: end; margin-top: 64px; }
|
|
76
|
+
.poster .key { display: none; }
|
|
77
|
+
}
|
|
78
|
+
@media (max-width: 640px) {
|
|
79
|
+
body { padding: 16px 16px 32px; }
|
|
80
|
+
:root { --pad: 16px; }
|
|
81
|
+
.poster { padding-top: 24px; }
|
|
82
|
+
.poster .kicker { font-size: 12px; margin-bottom: 10px; }
|
|
83
|
+
.poster .dek { margin-top: 12px; font-size: 15px; }
|
|
84
|
+
.poster .stage { margin-top: 14px; }
|
|
85
|
+
.poster .readout { font-size: 15px; }
|
|
86
|
+
}
|
|
87
|
+
</style>
|
|
88
|
+
</head>
|
|
89
|
+
<body>
|
|
90
|
+
<div id="chart"></div>
|
|
91
|
+
<script type="module">
|
|
92
|
+
import { Chart, Plot, Bar, Tick, Label, Place, Poster, Show, slat, shape, nice, html, render, createSignal } from "@bezda/rhp/standalone";
|
|
93
|
+
|
|
94
|
+
// Data: the ten tallest completed buildings in the world, height to the architectural top in metres
|
|
95
|
+
// (spires count, antennas do not), from the Council on Tall Buildings and Urban Habitat, October 2026.
|
|
96
|
+
const BUILDINGS = [
|
|
97
|
+
{ name: "Burj Khalifa", city: "Dubai", year: 2010, floors: 163, height: 828 },
|
|
98
|
+
{ name: "Menara Merdeka Maybank", formerly: "Merdeka 118", city: "Kuala Lumpur", year: 2023, floors: 118, height: 679 },
|
|
99
|
+
{ name: "Shanghai Tower", city: "Shanghai", year: 2015, floors: 128, height: 632 },
|
|
100
|
+
{ name: "Makkah Royal Clock Tower", city: "Mecca", year: 2012, floors: 120, height: 601, clock: { at: 428, size: 43 } }, // its faces: 43 m across, their middle 428 m up
|
|
101
|
+
{ name: "Ping An Finance Center", city: "Shenzhen", year: 2017, floors: 115, height: 599 },
|
|
102
|
+
{ name: "Lotte World Tower", city: "Seoul", year: 2017, floors: 123, height: 555 },
|
|
103
|
+
{ name: "One World Trade Center", city: "New York", year: 2014, floors: 94, height: 541 },
|
|
104
|
+
{ name: "Guangzhou CTF Finance Centre", city: "Guangzhou", year: 2016, floors: 111, height: 530 },
|
|
105
|
+
{ name: "Tianjin CTF Finance Centre", city: "Tianjin", year: 2019, floors: 97, height: 530 },
|
|
106
|
+
{ name: "CITIC Tower", city: "Beijing", year: 2018, floors: 109, height: 528 },
|
|
107
|
+
];
|
|
108
|
+
const AS_OF = 2026;
|
|
109
|
+
const SOURCE = "Source: Council on Tall Buildings and Urban Habitat (CTBUH), The Skyscraper Center, completed buildings, October 2026. "
|
|
110
|
+
+ "Height to the architectural top: spires count, antennas do not. Merdeka 118 was renamed Menara Merdeka Maybank in July 2026. "
|
|
111
|
+
+ "Outlines are simplified side views, all drawn to one scale.";
|
|
112
|
+
|
|
113
|
+
// Outlines: each building seen from the side, as [metres above the street, metres from its axis to its edge] from the
|
|
114
|
+
// street up; the left side, and the right side when it differs (an edge past the axis has a negative distance).
|
|
115
|
+
// Measured every 2 m from Phoenix CZE's to-scale elevation diagram, "Tallest buildings 2026" (Wikimedia Commons,
|
|
116
|
+
// CC BY-SA 4.0), and simplified to within a metre; podiums are left out, and the Clock Tower's crescent, too small to draw,
|
|
117
|
+
// ends its spire in a point. Every outline is drawn on one scale, BOX metres wide.
|
|
118
|
+
const PROFILE = {
|
|
119
|
+
"Burj Khalifa": {
|
|
120
|
+
left: [[0, 47], [125, 47], [126, 38], [223, 38], [224, 29.5], [349, 28.5], [350, 20], [504, 20], [506, 14.5], [568, 14.5], [570, 11], [604, 11], [606, 8.5], [740, 7], [742, 4], [776, 4], [778, 2.5], [790, 2.5], [792, 1], [828, 1]],
|
|
121
|
+
right: [[0, 47.5], [99, 47.5], [100, 39], [191, 39], [192, 30], [345, 30], [346, 21.5], [442, 21.5], [444, 16], [526, 16], [528, 12.5], [586, 12.5], [588, 9.5], [644, 9.5], [646, 7], [694, 6.5], [696, 5], [756, 4], [758, 2.5], [790, 2.5], [792, 1], [828, 1]],
|
|
122
|
+
},
|
|
123
|
+
"Menara Merdeka Maybank": {
|
|
124
|
+
left: [[0, -4.5], [76, 6], [152, -2], [222, 6], [294, -1], [310, -1], [346, 3.5], [380, 6], [452, -2], [566, 10], [568, 4.5], [584, 3], [676, 3], [679, 1.5]],
|
|
125
|
+
right: [[0, 81.5], [106, 81.5], [296, 61.5], [380, 68.5], [528, 53.5], [553, 53.5], [554, 7.5], [556, 5], [594, 3], [676, 3], [679, 1.5]],
|
|
126
|
+
},
|
|
127
|
+
"Shanghai Tower": {
|
|
128
|
+
left: [[0, 36.5], [38, 36.5], [92, 39.5], [178, 39.5], [374, 24.5], [448, 21.5], [632, 21.5]],
|
|
129
|
+
right: [[0, 45.5], [38, 45.5], [96, 39.5], [388, 39.5], [396, 38], [480, 35.5], [536, 31], [588, 24.5], [612, 19], [616, 14.5], [618, 9.5], [620, 7], [632, 7]],
|
|
130
|
+
},
|
|
131
|
+
"Makkah Royal Clock Tower": {
|
|
132
|
+
left: [[0, 47], [357, 47.5], [358, 37.5], [397, 41], [398, 34.5], [402, 34.5], [404, 33], [452, 33], [458, 30], [463, 30], [464, 21], [469, 21], [470, 12.5], [472, 10.5], [486, 13], [490, 11.5], [496, 6], [558, 4.5], [601, 0.6]],
|
|
133
|
+
right: [[0, 48], [357, 48], [358, 37.5], [397, 41], [398, 34.5], [402, 34.5], [404, 33], [452, 33], [458, 30], [460, 30.5], [465, 28], [466, 21], [469, 21], [470, 11.5], [474, 10.5], [486, 13], [492, 10], [496, 6], [558, 4.5], [601, 0.6]],
|
|
134
|
+
},
|
|
135
|
+
"Ping An Finance Center": {
|
|
136
|
+
left: [[0, 41.5], [84, 39], [404, 39], [416, 37.5], [538, 32.5], [550, 31], [599, 0.6]],
|
|
137
|
+
right: [[0, 39.5], [402, 39], [404, 37.5], [412, 36.5], [540, 31], [550, 29.5], [599, 0.6]],
|
|
138
|
+
},
|
|
139
|
+
"Lotte World Tower": {
|
|
140
|
+
left: [[0, 44.5], [138, 44.5], [256, 37.5], [555, 10.5]],
|
|
141
|
+
right: [[0, 45.5], [140, 44.5], [254, 38], [555, 8.5]],
|
|
142
|
+
},
|
|
143
|
+
"One World Trade Center": {
|
|
144
|
+
left: [[0, 31.5], [413, 31.5], [414, 17], [424, 17], [426, 12.5], [452, 6], [464, 1], [541, 1]],
|
|
145
|
+
right: [[0, 31.5], [413, 31.5], [414, 17], [424, 17], [426, 11.5], [446, 7.5], [464, 1], [541, 1]],
|
|
146
|
+
},
|
|
147
|
+
"Guangzhou CTF Finance Centre": {
|
|
148
|
+
left: [[0, 39.5], [530, 39.5]],
|
|
149
|
+
right: [[0, 39.5], [437, 39.5], [438, 24], [500, 24], [508, 8.5], [512, -3], [526, -35.5], [530, -35.5]],
|
|
150
|
+
},
|
|
151
|
+
"Tianjin CTF Finance Centre": {
|
|
152
|
+
// its crown ends in two rounded lobes with a dip between them
|
|
153
|
+
left: [[0, 38], [130, 38], [228, 29.5], [436, 23], [518, 15], [524, 12], [528, 9], [530, 6], [529, 3.5], [525, 0.3]],
|
|
154
|
+
right: [[0, 38], [130, 38], [216, 29.5], [308, 27], [328, 28], [438, 23], [518, 14.5], [524, 11], [528, 8.5], [530, 6], [529, 3.5], [525, 0.3]],
|
|
155
|
+
},
|
|
156
|
+
"CITIC Tower": {
|
|
157
|
+
left: [[0, 43.5], [32, 43.5], [108, 38], [138, 38], [140, 36.5], [150, 35.5], [264, 31], [460, 31], [520, 36.5], [528, 35.5]],
|
|
158
|
+
right: [[0, 44.5], [30, 44.5], [46, 42], [140, 35.5], [250, 32], [470, 32], [518, 35.5], [528, 35]],
|
|
159
|
+
},
|
|
160
|
+
};
|
|
161
|
+
const BOX = 165;
|
|
162
|
+
// A profile as a Bar's outline: along runs from the street (0) to the top (1), across from the left (0) to the right (1);
|
|
163
|
+
// shift moves the building's axis across its slat, in metres
|
|
164
|
+
const outline = ({ left, right = left }, shift = 0) => {
|
|
165
|
+
const top = Math.max(...left.map(([h]) => h), ...right.map(([h]) => h));
|
|
166
|
+
return shape(
|
|
167
|
+
...left.map(([h, w], i) => [i ? "L" : "M", h / top, 0.5 + (shift - w) / BOX]),
|
|
168
|
+
...[...right].reverse().map(([h, w]) => ["L", h / top, 0.5 + (shift + w) / BOX]),
|
|
169
|
+
["Z"],
|
|
170
|
+
);
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
// What depends on the data: ranks, places on the street (lowest on the left), the leader and the runner-up
|
|
174
|
+
const HEIGHT = BUILDINGS.map((b) => b.height);
|
|
175
|
+
const scale = { min: 0, max: Math.ceil(Math.max(...HEIGHT) / 10) * 10 }; // 0 to 830 m: the top of the plot is just over the tallest tip
|
|
176
|
+
const byHeight = BUILDINGS.map((b, i) => i).sort((a, b) => HEIGHT[b] - HEIGHT[a]); // tallest first
|
|
177
|
+
const LEAD = byHeight[0];
|
|
178
|
+
const SECOND = byHeight[1];
|
|
179
|
+
const RANK = HEIGHT.map((h) => 1 + HEIGHT.filter((other) => other > h).length); // ties share a rank, as in CTBUH's table
|
|
180
|
+
const POS = BUILDINGS.map((b, i) => BUILDINGS.length - 1 - byHeight.indexOf(i)); // place on screen, lowest first
|
|
181
|
+
const profileOf = (b) => PROFILE[b.name] ?? { left: [[0, 30], [b.height, 30]] };
|
|
182
|
+
// Even gaps: every building's widest extent is laid along the street with the same space between each, and each axis
|
|
183
|
+
// moves across its slat to where that puts it (SHIFT, a share of the slat, which the labels and lines follow)
|
|
184
|
+
const EXTENT = BUILDINGS.map((b) => {
|
|
185
|
+
const { left, right = left } = profileOf(b);
|
|
186
|
+
return [Math.max(...left.map(([, w]) => w)), Math.max(...right.map(([, w]) => w))];
|
|
187
|
+
});
|
|
188
|
+
const SHIFT = new Array(BUILDINGS.length);
|
|
189
|
+
{
|
|
190
|
+
const placed = POS.map((p, i) => [p, i]).sort((a, b) => a[0] - b[0]).map(([, i]) => i); // left to right
|
|
191
|
+
const space = (BUILDINGS.length * BOX - EXTENT.reduce((sum, [l, r]) => sum + l + r, 0)) / BUILDINGS.length;
|
|
192
|
+
let x = space / 2;
|
|
193
|
+
placed.forEach((i, place) => {
|
|
194
|
+
SHIFT[i] = (x + EXTENT[i][0] - BOX * (place + 0.5)) / BOX;
|
|
195
|
+
x += EXTENT[i][0] + EXTENT[i][1] + space;
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
const OUTLINE = BUILDINGS.map((b, i) => outline(profileOf(b), SHIFT[i] * BOX));
|
|
199
|
+
const WORDS = ["no", "one", "two", "three", "four", "five", "six", "seven", "eight", "nine", "ten", "eleven", "twelve",
|
|
200
|
+
"thirteen", "fourteen", "fifteen", "sixteen", "seventeen", "eighteen", "nineteen", "twenty"];
|
|
201
|
+
const word = (n) => WORDS[n] ?? String(n);
|
|
202
|
+
const cap = (s) => s[0].toUpperCase() + s.slice(1);
|
|
203
|
+
const metres = (m) => `${m} m`;
|
|
204
|
+
const fullName = (i) => BUILDINGS[i].name + (BUILDINGS[i].formerly ? ` (formerly ${BUILDINGS[i].formerly})` : "");
|
|
205
|
+
const lead = BUILDINGS[LEAD];
|
|
206
|
+
const margin = lead.height - BUILDINGS[SECOND].height;
|
|
207
|
+
const cities = new Set(BUILDINGS.map((b) => b.city)).size;
|
|
208
|
+
// The tower the picked one is measured against the leader with: itself, or the runner-up when the leader is picked
|
|
209
|
+
const against = (i) => (i === LEAD ? SECOND : i);
|
|
210
|
+
const compare = (i) => (i === LEAD
|
|
211
|
+
? `${metres(margin)} taller than ${BUILDINGS[SECOND].name}`
|
|
212
|
+
: `${metres(lead.height - HEIGHT[i])} shorter than the ${lead.name}`);
|
|
213
|
+
const facts = (i) => `${metres(HEIGHT[i])}, ${BUILDINGS[i].floors} floors, finished ${BUILDINGS[i].year}`;
|
|
214
|
+
const say = (i) => `${RANK[i]}. ${fullName(i)}, ${BUILDINGS[i].city}: ${facts(i)}; ${compare(i)}.`;
|
|
215
|
+
|
|
216
|
+
// Slat type: one tower in its own outline, its height over its top, its name on the street. The level of the tower the
|
|
217
|
+
// reader picked runs from its top across to the leader, where a red dimension line measures what is left above it.
|
|
218
|
+
const TOWER_CSS = `
|
|
219
|
+
.tower { cursor: default; }
|
|
220
|
+
/* Focus rings the name on the street: a ring round the whole tower would cross the headline set in the sky */
|
|
221
|
+
.tower:focus-visible { outline: none; }
|
|
222
|
+
.tower:focus-visible .name { outline: 2px solid var(--rhp-surface); outline-offset: 3px; }
|
|
223
|
+
.tip { --rhp-label-gap: 5px; font: 400 21px/1 "League Gothic", "Arial Narrow", sans-serif; letter-spacing: .02em; color: var(--rhp-muted); }
|
|
224
|
+
.lit .tip { color: var(--rhp-ink); font-size: 25px; }
|
|
225
|
+
.level { --rhp-tick-width: 2px; background: repeating-linear-gradient(90deg, var(--rhp-ink) 0 5px, transparent 5px 9px); }
|
|
226
|
+
.level.none { visibility: hidden; }
|
|
227
|
+
.level.first { translate: 0 50%; }
|
|
228
|
+
.level.last { translate: -100% 50%; }
|
|
229
|
+
.dim { translate: var(--dx) 0; }
|
|
230
|
+
/* A clock face: a ring cut out of the tower, round whatever the chart's proportions, in the sky's color */
|
|
231
|
+
.clock { position: absolute; left: calc(50% + var(--shift) * 100%); bottom: calc(var(--at) * 100%); width: calc(var(--size) * 100%); aspect-ratio: 1;
|
|
232
|
+
translate: -50% 50%; border: 2px solid #d0e1f2; border-radius: 50%; }
|
|
233
|
+
/* The labels and lines follow the building's axis, moved across the slat by --shift for even gaps */
|
|
234
|
+
.tip, .dim, .cap, .mark, .level.first, .level.last { margin-left: calc(var(--shift) * 100%); }
|
|
235
|
+
.cap { translate: 0 50%; }
|
|
236
|
+
.gap { position: absolute; left: calc(var(--dx) + 7px); top: 0; translate: 0 -50%; font: 400 36px/1 "League Gothic", "Arial Narrow", sans-serif; color: var(--rhp-series-1); white-space: nowrap; }
|
|
237
|
+
.name { --rhp-label-gap: 10px; white-space: normal; text-align: center; line-height: 1.15; font-size: 12.5px; color: var(--rhp-surface); }
|
|
238
|
+
.name b { display: block; padding-bottom: 4px; font-weight: 600; }
|
|
239
|
+
.name i, .name small { display: block; font-style: normal; font-size: 11.5px; color: color-mix(in oklab, var(--rhp-surface) 72%, var(--rhp-ink)); }
|
|
240
|
+
.lit .name b { font-weight: 700; text-decoration: underline 2px; text-underline-offset: 2px; }
|
|
241
|
+
.rank { font: 400 19px/1 "League Gothic", "Arial Narrow", sans-serif; color: color-mix(in oklab, var(--rhp-surface) 72%, var(--rhp-ink)); }
|
|
242
|
+
.lit .rank { color: var(--rhp-surface); }
|
|
243
|
+
@container (max-width: 640px) {
|
|
244
|
+
.tip { font-size: 16px; }
|
|
245
|
+
.lit .tip { font-size: 19px; }
|
|
246
|
+
.tip .u { display: none; }
|
|
247
|
+
.gap { font-size: 26px; }
|
|
248
|
+
.clock { border-width: 1px; }
|
|
249
|
+
}
|
|
250
|
+
`;
|
|
251
|
+
const tower = (d) => html`
|
|
252
|
+
<div class=${() => (d.lit ? "tower lit" : "tower")} data-row=${() => d.index} aria-label=${() => say(d.index)}
|
|
253
|
+
style=${() => `--dx: ${d.wide ? 14 : 8}px; --shift: ${d.shift}`}>
|
|
254
|
+
<${Bar} to=${() => d.height} thick=${1} shape=${() => d.outline} color=${() => (d.lit ? "ink" : "series-2")}>
|
|
255
|
+
<${Show} when=${() => d.clock}>
|
|
256
|
+
<i class="clock" aria-hidden="true" style=${() => `--at: ${d.clock.at / d.height}; --size: ${d.clock.size / BOX}`}></i>
|
|
257
|
+
<//>
|
|
258
|
+
<//>
|
|
259
|
+
<${Tick} at=${() => d.level} thick=${() => ({ mid: 1, first: 0.5 - d.shift, last: 0.5 + d.shift })[d.span] ?? 0.5} class=${() => "level " + d.span} />
|
|
260
|
+
<${Label} at=${() => d.height} class="tip">${() => d.height}<span class="u"> m</span><//>
|
|
261
|
+
<${Label} edge="start" class="name">
|
|
262
|
+
<${Show} when=${() => d.wide} fallback=${html`<b class="rank">${() => d.rank}</b>`}>
|
|
263
|
+
<b>${() => d.name}</b>
|
|
264
|
+
<${Show} when=${() => d.formerly}><i>(${() => d.formerly})</i><//>
|
|
265
|
+
<small>${() => d.city}</small>
|
|
266
|
+
<//>
|
|
267
|
+
<//>
|
|
268
|
+
<${Show} when=${() => d.lead}>
|
|
269
|
+
<${Bar} from=${() => d.level} to=${() => d.height} thick="2px" color="series-1" class="dim" />
|
|
270
|
+
<${Tick} at=${() => d.level} thick=${() => `${d.wide ? 22 : 14}px`} color="series-1" class="cap" />
|
|
271
|
+
<${Tick} at=${() => d.height} thick=${() => `${d.wide ? 22 : 14}px`} color="series-1" class="cap" />
|
|
272
|
+
<${Place} at=${() => (d.level + d.height) / 2} class="mark"><b class="gap">${() => metres(d.height - d.level)}</b><//>
|
|
273
|
+
<//>
|
|
274
|
+
</div>`;
|
|
275
|
+
// The same tower with the room each width needs: names on the street on wide screens, ranks on narrow ones
|
|
276
|
+
const TowerNamed = slat({ room: { start: 88, end: 0, after: 0 }, css: TOWER_CSS }, tower);
|
|
277
|
+
const TowerRanked = slat({ room: { start: 32, end: 32, after: 46 }, css: TOWER_CSS }, tower);
|
|
278
|
+
|
|
279
|
+
// The chart's font and colors, read from the page's tokens
|
|
280
|
+
const THEME = {
|
|
281
|
+
font: "var(--text)", ink: "var(--ink)", muted: "var(--soft)", grid: "var(--rule)", surface: "var(--on-ground)",
|
|
282
|
+
series: ["var(--accent)", "var(--quiet)"],
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
// Chart component: the tower the reader points at, taps or reaches with the keyboard is lit and measured against the leader
|
|
286
|
+
function Skyline() {
|
|
287
|
+
const media = matchMedia("(min-width: 980px)");
|
|
288
|
+
const [wide, setWide] = createSignal(media.matches);
|
|
289
|
+
media.addEventListener("change", (e) => setWide(e.matches));
|
|
290
|
+
const [picked, setPicked] = createSignal(null);
|
|
291
|
+
const lit = () => picked() ?? LEAD;
|
|
292
|
+
const ref = () => against(lit());
|
|
293
|
+
// Where the level line runs: half of the measured tower's band (first), every band between (mid), half of the leader's (last)
|
|
294
|
+
const span = (i) => {
|
|
295
|
+
const p = POS[i], a = POS[ref()], b = POS[LEAD];
|
|
296
|
+
return p === a ? "first" : p === b ? "last" : p > a && p < b ? "mid" : "none";
|
|
297
|
+
};
|
|
298
|
+
// The row of the element an event landed on (a tower, or a line of the key), or null
|
|
299
|
+
const rowOf = (el) => {
|
|
300
|
+
const root = el.closest("[data-row]");
|
|
301
|
+
return root ? +root.dataset.row : null;
|
|
302
|
+
};
|
|
303
|
+
// A mouse or a pen picks as it moves, a tap or a click picks, the keyboard picks; on no tower the pick stays
|
|
304
|
+
const pick = (e) => {
|
|
305
|
+
const row = rowOf(e.target);
|
|
306
|
+
if (row != null) setPicked(row);
|
|
307
|
+
};
|
|
308
|
+
// Leaving the poster: back to the tower that has focus (a keyboard's or a click's pick), or none
|
|
309
|
+
const leave = (e) => {
|
|
310
|
+
if (e.pointerType === "touch") return; // a finger sends pointerleave as it lifts after a tap
|
|
311
|
+
setPicked(e.currentTarget.contains(document.activeElement) ? rowOf(document.activeElement) : null);
|
|
312
|
+
};
|
|
313
|
+
const towers = (type) => html`
|
|
314
|
+
<${Plot} rows=${BUILDINGS} order=${POS} keyboard=${true} rank=${RANK} outline=${OUTLINE} shift=${SHIFT}
|
|
315
|
+
lit=${(d) => d.index === lit()} lead=${(d) => d.index === LEAD} span=${(d) => span(d.index)}
|
|
316
|
+
level=${() => HEIGHT[ref()]} wide=${wide}>${type}<//>`;
|
|
317
|
+
return html`
|
|
318
|
+
<${Poster}
|
|
319
|
+
kicker=${`The world's tallest buildings, ${AS_OF}`}
|
|
320
|
+
title=${`${cap(word(AS_OF - lead.year))} years on, the ${lead.name} still tops the rest by ${metres(margin)}`}
|
|
321
|
+
dek=${`The ${word(BUILDINGS.length)} tallest finished buildings on Earth, from ${word(cities)} cities, share one street here, each in its own outline and all at one scale. Heights in metres.`}
|
|
322
|
+
note=${SOURCE}
|
|
323
|
+
onPointerMove=${(e) => e.pointerType !== "touch" && pick(e)} onClick=${pick} onFocusIn=${pick}
|
|
324
|
+
onPointerLeave=${leave} onFocusOut=${(e) => !e.currentTarget.contains(e.relatedTarget) && setPicked(null)}>
|
|
325
|
+
<div class="stage" style=${() => `--street: ${wide() ? 88 : 32}px`}>
|
|
326
|
+
<${Chart} orientation="vertical" height=${() => (wide() ? 680 : 420)} scale=${[scale.min, scale.max]} ticks=${false}
|
|
327
|
+
label=${`The ${word(BUILDINGS.length)} tallest completed buildings in the world, by height in metres`} theme=${THEME}>
|
|
328
|
+
<${Show} when=${wide} fallback=${() => towers(TowerRanked)}>${() => towers(TowerNamed)}<//>
|
|
329
|
+
<//>
|
|
330
|
+
</div>
|
|
331
|
+
<p class="readout" aria-hidden="true">
|
|
332
|
+
<span class=${() => (picked() == null ? "v on" : "v")}>Tap or point at a tower to measure it against the Burj Khalifa.</span>
|
|
333
|
+
${BUILDINGS.map((b, i) => html`
|
|
334
|
+
<span class=${() => (picked() === i ? "v on" : "v")}><b>${b.name}</b>, ${b.city}<span>${facts(i)}. ${cap(compare(i))}.</span></span>`)}
|
|
335
|
+
</p>
|
|
336
|
+
<${Show} when=${() => !wide()}>
|
|
337
|
+
<ol class="key" aria-hidden="true">
|
|
338
|
+
${byHeight.map((i) => html`
|
|
339
|
+
<li data-row=${i} class=${() => (lit() === i ? "lit" : "")}>
|
|
340
|
+
<b>${RANK[i]}</b>
|
|
341
|
+
<div>${fullName(i)}<em>, ${BUILDINGS[i].city}</em></div>
|
|
342
|
+
<span>${metres(HEIGHT[i])}</span>
|
|
343
|
+
</li>`)}
|
|
344
|
+
</ol>
|
|
345
|
+
<//>
|
|
346
|
+
<//>`;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
render(() => html`<${Skyline} />`, document.getElementById("chart"));
|
|
350
|
+
</script>
|
|
351
|
+
</body>
|
|
352
|
+
</html>
|
package/skill/recipes/slope.html
CHANGED
|
@@ -5,6 +5,12 @@
|
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
6
|
<title>Eggs doubled</title>
|
|
7
7
|
<meta name="description" content="Slope chart: each item's value at two dates joined by a line on a second axis, labels kept apart on both sides, the change written beside each item, and the line nearest the pointer, a tap or the arrow keys picked out. Use it to compare change between two dates across items.">
|
|
8
|
+
<!--
|
|
9
|
+
A recipe gives technique, not a design.
|
|
10
|
+
Take: a Line of two points per item on a second axis, labels kept apart on both sides, and the line nearest the pointer picked out.
|
|
11
|
+
Leave: the design. This poster was made for its own subject (the price of eggs as a till receipt, torn at the top and the bottom, a barcode at its foot).
|
|
12
|
+
Its look, the place of its parts and the way its marks are drawn say that subject: design yours from your own (design.md, section 2).
|
|
13
|
+
-->
|
|
8
14
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
9
15
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
10
16
|
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Azeret+Mono:wght@400..800&display=swap">
|
|
@@ -5,6 +5,12 @@
|
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
6
|
<title>Richer since 1990</title>
|
|
7
7
|
<meta name="description" content="Sparkline table: one small line per item, each scaled to its own peak, with the start level dashed, and a scrub through the years (pointer, tap, a sideways drag or arrow keys) that moves a dot along every line and updates each item's value and its change since the start. Use it for many series side by side, a table of trends.">
|
|
8
|
+
<!--
|
|
9
|
+
A recipe gives technique, not a design.
|
|
10
|
+
Take: a Line in each slat scaled to itself, the start level dashed, numbers in columns, and a year scrub that moves a dot along every line.
|
|
11
|
+
Leave: the design. This poster was made for its own subject (incomes since 1990 as a table page of a statistical yearbook, cream paper between a thick and a thin rule).
|
|
12
|
+
Its look, the place of its parts and the way its marks are drawn say that subject: design yours from your own (design.md, section 2).
|
|
13
|
+
-->
|
|
8
14
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
9
15
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
10
16
|
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Libre+Caslon+Text:wght@400;700&family=Libre+Franklin:wght@400..800&display=swap">
|
|
@@ -105,7 +111,7 @@ const Country = slat({
|
|
|
105
111
|
|
|
106
112
|
// The year being read: a hairline down every slat
|
|
107
113
|
const Crosshair = slat({ room: {}, css: `.hair { --rhp-tick-width: 1px; background: #c2410c; opacity: .55; }` }, (d) => html`
|
|
108
|
-
|
|
114
|
+
<${Tick} at=${() => d.year} thick=${1} class="hair" />`);
|
|
109
115
|
|
|
110
116
|
// Chart component: hovering previews a year; a click, tap, sideways drag or the arrow keys saves it for every slat.
|
|
111
117
|
// With no saved year, the latest shows. The year in the table's head is a slider for the keyboard and screen readers.
|
|
@@ -5,6 +5,12 @@
|
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
6
|
<title>How cities get to work</title>
|
|
7
7
|
<meta name="description" content="100% stacked bar chart of shares per category, labeled where they fit, with a key that lights one part in every bar on hover, focus and tap. Use it to compare how a whole splits across several categories.">
|
|
8
|
+
<!--
|
|
9
|
+
A recipe gives technique, not a design.
|
|
10
|
+
Take: stackUp() of shares() in an overlap inner Plot, labels inside where they fit, and a key that lights one part in every bar.
|
|
11
|
+
Leave: the design. This poster was made for its own subject (ways to commute as a transit map: enamel white, geometric type, the key drawn as a metro line whose stations are the buttons).
|
|
12
|
+
Its look, the place of its parts and the way its marks are drawn say that subject: design yours from your own (design.md, section 2).
|
|
13
|
+
-->
|
|
8
14
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
9
15
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
10
16
|
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Jost:wght@400..800&display=swap">
|
|
@@ -5,6 +5,12 @@
|
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
6
|
<title>Europe's power mix</title>
|
|
7
7
|
<meta name="description" content="Stacked bar chart of parts that add up to a total per category, with a key that moves one part to the start of every bar and ranks the bars by it. Use it for totals made of a few parts.">
|
|
8
|
+
<!--
|
|
9
|
+
A recipe gives technique, not a design.
|
|
10
|
+
Take: stackUp() parts in an overlap inner Plot, and a key that moves one part to the start of every bar and ranks by it.
|
|
11
|
+
Leave: the design. This poster was made for its own subject (Europe's power mix on drafting paper, a line of pylons under the headline).
|
|
12
|
+
Its look, the place of its parts and the way its marks are drawn say that subject: design yours from your own (design.md, section 2).
|
|
13
|
+
-->
|
|
8
14
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
9
15
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
10
16
|
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Oswald:wght@500..700&family=Public+Sans:wght@400..800&display=swap">
|
package/skill/recipes/strip.html
CHANGED
|
@@ -5,6 +5,12 @@
|
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
6
|
<title>Burger places are hit or miss</title>
|
|
7
7
|
<meta name="description" content="Strip plot of restaurant ratings by cuisine: one dot per restaurant in its cuisine's band (an overlap Plot inside each slat), a median tick, and a readout of the restaurant the reader points at, taps or reaches with the arrow keys. Use it to show every value of a few groups.">
|
|
8
|
+
<!--
|
|
9
|
+
A recipe gives technique, not a design.
|
|
10
|
+
Take: an overlap Plot of Dots inside each group's slat, a median Tick, and a readout of the dot under the pointer.
|
|
11
|
+
Leave: the design. This poster was made for its own subject (restaurant ratings as a chalkboard menu in a wooden frame, its slate smudged where it was wiped).
|
|
12
|
+
Its look, the place of its parts and the way its marks are drawn say that subject: design yours from your own (design.md, section 2).
|
|
13
|
+
-->
|
|
8
14
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
9
15
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
10
16
|
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Cabin+Sketch:wght@700&family=Karla:wght@400..800&display=swap">
|