@pithyjs/pidl-catalog 0.1.0-beta.1 → 0.1.0-beta.2

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.
@@ -0,0 +1,328 @@
1
+ {
2
+ "kind": "component",
3
+ "id": "ui.line-area",
4
+ "name": "LineArea",
5
+ "displayName": "Area Chart",
6
+ "version": "0.1.0",
7
+ "level": "atom",
8
+ "domain": "data",
9
+ "bidi": true,
10
+ "description": "One series over an ordered category axis, drawn as a line with the area beneath it filled. The chart for a single trend — how one quantity moved across a sequence a reader already understands as ordered: weeks, releases, cohorts. The plot is SVG because a polyline is a path; the axis labels beneath it are DOM, so a long category name can ellipsis, a numeric axis can use tabular figures, and the type rescales with the reader's own font setting. The drawing and the axis are both aria-hidden; a visually-hidden data table carries every exact reading and is the chart's accessible representation. Readings may be negative. A non-finite reading is a hole the line breaks across rather than drawing through. Loading, empty (no reading anywhere — an all-zero series stays ready, because a flat line is a real answer) and error states are first-class.",
11
+ "props": [
12
+ {
13
+ "name": "label",
14
+ "label": "Accessible name",
15
+ "type": "string",
16
+ "required": true,
17
+ "placeholder": "Sessions per week",
18
+ "description": "Captions the visually-hidden data table screen readers read instead of the graphic. Required — it is the chart's accessible name."
19
+ },
20
+ {
21
+ "name": "values",
22
+ "label": "Readings",
23
+ "type": "number[] | Signal<number[]>",
24
+ "required": true,
25
+ "reactive": true,
26
+ "placeholder": [120, 138, 131, 164, 152, 187],
27
+ "description": "The readings, in axis order. Each must be a finite number; either sign is allowed, unlike the bar and donut charts. A non-finite entry is treated as a HOLE — the line breaks across it rather than drawing a straight run through data nobody measured — is DEV-warned, and is listed in the accessible table as an em dash."
28
+ },
29
+ {
30
+ "name": "labels",
31
+ "label": "Category names",
32
+ "type": "string[] | Signal<string[]>",
33
+ "required": true,
34
+ "reactive": true,
35
+ "placeholder": ["W1", "W2", "W3", "W4", "W5", "W6"],
36
+ "description": "Category names, in axis order — one per reading. The labels define the axis: more labels than readings means the trailing points have not reported yet and are drawn as holes; more readings than labels means the extra points take a positional stand-in name. Past eight categories the axis shows every other label, always keeping the first and the last."
37
+ },
38
+ {
39
+ "name": "color",
40
+ "label": "Series colour",
41
+ "editor": { "type": "token-color" },
42
+ "type": "enum",
43
+ "enum": [
44
+ "primary",
45
+ "secondary",
46
+ "tertiary",
47
+ "success",
48
+ "info",
49
+ "warning",
50
+ "danger",
51
+ "categorical-1",
52
+ "categorical-2",
53
+ "categorical-3",
54
+ "categorical-4",
55
+ "categorical-5",
56
+ "categorical-6"
57
+ ],
58
+ "default": "categorical-1",
59
+ "description": "Design-token name for the line, its fill and its marker. A trend is a quantity, not a verdict, so the default is a categorical slot rather than a semantic colour — reach for 'danger' or 'success' only when the series genuinely means one."
60
+ },
61
+ {
62
+ "name": "min",
63
+ "label": "Scale minimum",
64
+ "type": "number | Signal<number>",
65
+ "reactive": true,
66
+ "placeholder": 0,
67
+ "description": "The value drawn at the bottom of the plot. Defaults to zero, or to the lowest reading when the data goes below it. May be negative. Pass one to hold the scale still as data changes, or to make two charts comparable. A bound that is not a finite number, or a min/max pair that does not increase, is DEV-warned and ignored."
68
+ },
69
+ {
70
+ "name": "max",
71
+ "label": "Scale maximum",
72
+ "type": "number | Signal<number>",
73
+ "reactive": true,
74
+ "placeholder": 200,
75
+ "description": "The value drawn at the top of the plot. Defaults to the highest reading plus 8% headroom, which keeps the peak's marker inside the plot instead of clipping it in half. A reading past this is drawn ON the top edge and DEV-warned; the accessible table still reports its true value."
76
+ },
77
+ {
78
+ "name": "caption",
79
+ "label": "Caption",
80
+ "type": "string | Signal<string>",
81
+ "reactive": true,
82
+ "placeholder": "sessions",
83
+ "description": "Small caption under the plot naming what is counted — 'sessions', 'p95 ms'. Dropped below 8rem of chart width, where words stop fitting; the data table names the unit for a screen reader either way. Unlike the circular charts, size='sm' does NOT make this inert, because this chart clears the per-size width cap."
84
+ },
85
+ {
86
+ "name": "size",
87
+ "label": "Size",
88
+ "editor": { "tab": "layout" },
89
+ "type": "enum",
90
+ "enum": ["sm", "md", "lg"],
91
+ "default": "md",
92
+ "description": "Sets the type scale of the axis and the caption. It does NOT cap the chart's width, unlike the circular charts: a line chart's inline extent is its TIME AXIS, and capping it at md's 12rem would put twelve weekly readings 16px apart. 'sm' and 'md' share --font-size-xs, which is the design system's smallest step and already the floor for a tabular axis; 'lg' steps up."
93
+ },
94
+ {
95
+ "name": "loading",
96
+ "label": "Loading",
97
+ "editor": { "tab": "settings" },
98
+ "type": "boolean | Signal<boolean>",
99
+ "default": false,
100
+ "reactive": true,
101
+ "description": "Hides the drawing and the table, shows a pulsing skeleton in the drawing's slot, and sets aria-busy on the drawing's wrapper. Outranks every other state."
102
+ },
103
+ {
104
+ "name": "error",
105
+ "label": "Error",
106
+ "editor": { "tab": "settings" },
107
+ "type": "boolean | Signal<boolean>",
108
+ "default": false,
109
+ "reactive": true,
110
+ "description": "Hides the drawing and the table and shows the error message. Outranks the empty state — a failed fetch usually leaves an empty array behind it."
111
+ },
112
+ {
113
+ "name": "errorMessage",
114
+ "label": "Error message",
115
+ "editor": { "tab": "settings" },
116
+ "type": "string | Signal<string>",
117
+ "default": "Chart data unavailable",
118
+ "reactive": true,
119
+ "description": "Copy shown in the error state."
120
+ },
121
+ {
122
+ "name": "emptyMessage",
123
+ "label": "Empty message",
124
+ "editor": { "tab": "settings" },
125
+ "type": "string",
126
+ "default": "No data to display",
127
+ "description": "Copy shown when there is no reading anywhere. NOT shown for an all-zero series, which stays ready and draws a flat line along the floor."
128
+ },
129
+ {
130
+ "name": "loadingLabel",
131
+ "label": "Loading message",
132
+ "editor": { "tab": "settings" },
133
+ "type": "string",
134
+ "default": "Loading chart…",
135
+ "description": "Copy shown in the loading state."
136
+ },
137
+ {
138
+ "name": "id",
139
+ "label": "ID",
140
+ "editor": { "tab": "settings" },
141
+ "type": "string",
142
+ "description": "Explicit element ID on the <figure> root."
143
+ }
144
+ ],
145
+ "events": [],
146
+ "slots": [],
147
+ "tokens": {
148
+ "line.stroke": "--color-categorical-1 … --color-categorical-6, or any semantic colour",
149
+ "line.stroke.fallback": "--color-text-muted (only when an explicit colour is not a recognised token)",
150
+ "area.fill": "the same token as the line, at 13% opacity",
151
+ "dot.fill": "the same token as the line",
152
+ "dot.separator": "--color-surface",
153
+ "dot.separator.width": "--border-width-thin",
154
+ "grid.stroke": "--color-border",
155
+ "axis.color": "--color-text-muted",
156
+ "axis.font": "--font-family-mono",
157
+ "axis.size": "--font-size-xs (--font-size-sm at lg)",
158
+ "axis.tracking": "typography $letter-spacings 'wide'",
159
+ "caption.color": "--color-text-muted",
160
+ "caption.size": "--font-size-xs (--font-size-sm at lg)",
161
+ "line.motion.duration": "--motion-duration-slow",
162
+ "line.motion.easing": "--motion-easing-emphasized",
163
+ "status.color": "--color-text-muted",
164
+ "status.error.color": "--color-danger",
165
+ "placeholder.background": "--color-surface-variant",
166
+ "placeholder.radius": "--radius-md"
167
+ },
168
+ "spacing": {
169
+ "internal": {
170
+ "plot-to-axis": {
171
+ "relationship": "lovers",
172
+ "token": "xs",
173
+ "note": "The axis is part of the drawing, not a caption of it. Any more air reads as a second element below the chart."
174
+ },
175
+ "axis-to-caption": {
176
+ "relationship": "close_friends",
177
+ "token": "sm",
178
+ "note": "One reads as a caption of the other, with the axis row already between them."
179
+ },
180
+ "graphic-to-status": { "relationship": "close_friends", "token": "sm" }
181
+ }
182
+ },
183
+ "constraints": [
184
+ "Use logical properties only — no left/right, no width/height where inline-size/block-size applies",
185
+ "Colour is a design-token NAME, never a CSS value — there is no prop through which a hex code can reach a stylesheet",
186
+ "No function-valued props. Every prop has to be editable and serialisable through PIDL, so there is no formatValue callback; numbers go through the kit's own formatter, which the data table uses too",
187
+ "The PLOT is SVG and the AXIS LABELS are DOM, and the split follows the argument rather than the component. A polyline through N readings IS a path, so the plot cannot be DOM. Axis labels can be, and must be: SVG <text> has no text-overflow: ellipsis, no font-variant-numeric: tabular-nums, and scales with the viewBox transform rather than with the reader's font setting (WCAG 1.4.4)",
188
+ "The axis row is a grid of 2(N-1) equal columns, with each inner label spanning two so its centre falls exactly on its point. N equal columns — one per label — is wrong by (i+0.5)/N against i/(N-1), which for five points is 27 of 540 user units and reads as belonging to the next category",
189
+ "NOTHING outside an axis label may clip it. text-overflow paints its ellipsis at the label's own end edge, so an ancestor overflow: hidden would remove the cue along with the characters and leave a silently wrong category name",
190
+ "The final axis label flips its box direction rather than its text alignment. Under text-align: end an over-long label overflows the START edge, where no ellipsis is ever painted — the truncation then happens in silence",
191
+ "The graphic and the axis are both aria-hidden — the visually-hidden data table is the accessible representation, so `label` is required",
192
+ "The chart is width-agnostic: it fills its column and `size` sets the type scale instead. A line chart's width is its time axis, and capping it compresses the axis rather than shrinking the chart",
193
+ "A SINGLE reading is centred and carried by its marker, with no line and no area. `i / (n - 1)` is 0/0 for one observation, which is a metric's first day rather than a pathological input; a line through one point is not a line",
194
+ "A non-finite reading BREAKS the line rather than being bridged. Drawing a straight run through missing data reads as a measurement that was taken",
195
+ "A reading outside an explicit min/max is drawn at the edge of the plot and DEV-warned; the accessible table reports the true value. The clamp is a drawing decision and may not reach the data",
196
+ "An inverted or collapsed domain (min >= max) is not a domain — it is DEV-warned and the derived one is used, rather than drawing the chart upside down or dividing by nothing",
197
+ "A series whose readings are ALL zero stays `ready` and draws a flat line along the floor. The axis still names every point and the table still carries every reading, so going empty would hide a chart a sighted reader can see. Only 'no reading anywhere' is empty. <Donut> and <StackedBar> go empty on the same input, and should: their parts of a zero whole have nothing to draw and no label anywhere but the key",
198
+ "The <tfoot> states the PLOTTED RANGE, never a total. Adding Monday's queue depth to Tuesday's produces a number that is not a quantity of anything. It also deliberately does not state the drawing's domain, unlike <Bars>: a bar's LENGTH is its reading so its ceiling is a missing denominator, where a line chart's readings are all in the table already and the domain is a property of the aria-hidden picture",
199
+ "The drawing and the data table are hidden together in every non-ready state so the two cannot disagree about whether there is data",
200
+ "The line's shape transition is inside a motion-safe block"
201
+ ],
202
+ "a11y": [
203
+ "The SVG carries aria-hidden='true' — it is a picture of the table beside it",
204
+ "The axis row carries aria-hidden='true' too: every name on it is already a <th scope='row'> in the table, and exposing both reads the category list twice without the readings attached",
205
+ "A <table class='u-sr-only'> carries every exact reading, captioned with the required `label` prop",
206
+ "Table headers are scoped: <th scope='col'> in the head, <th scope='row'> per row",
207
+ "A missing reading appears in the table as an em dash, never as the zero the geometry substitutes to keep its arithmetic finite",
208
+ "A reading clamped to the edge of an explicit domain appears in the table at its true value — the table is the reading of record wherever the drawing is an approximation",
209
+ "The <tfoot> states the plotted range, which stays true under an explicit min/max and when the data is empty",
210
+ "Axis text rescales with the reader's font-size setting rather than with the drawing, which SVG text cannot do (WCAG 1.4.4)",
211
+ "Colour is not the sole encoding: there is one series, and the chart's own label names it",
212
+ "Status messages live in a role='status' region present from first render — a live region inserted when it gains content is often not announced",
213
+ "aria-busy='true' while loading goes on the drawing's wrapper (.ui-chart-body), NEVER on the root. The role='status' region is a child of the root, and a live region inside an aria-busy subtree has its changes DEFERRED until busy clears — which happens in the same call that blanks the region",
214
+ "In forced-colors the 13%-opacity area fill is dropped rather than left as a band of noise; the stroke keeps its colour, because in a chart the colour IS the data",
215
+ "The caption is aria-hidden — the table's own <caption> already carries the chart's name",
216
+ "The line's shape transition is inside a prefers-reduced-motion: no-preference block"
217
+ ],
218
+ "ai": {
219
+ "synonyms": [
220
+ "area chart",
221
+ "filled line chart",
222
+ "trend chart",
223
+ "sparkline",
224
+ "time series",
225
+ "trend over time",
226
+ "growth chart",
227
+ "usage over time",
228
+ "single-series line"
229
+ ],
230
+ "avoid": [
231
+ "Passing a CSS colour — the color prop takes a design-token name",
232
+ "Using it for several series — a second wash reads as a third colour that means nothing. Use Line, which takes up to six",
233
+ "Using it for unordered categories — a line implies a sequence, and 'Search, Direct, Referral' is not one. Use Bars",
234
+ "Using it for parts of one whole — that is StackedBar or Donut",
235
+ "Omitting min/max when two charts are meant to be compared, or when the scale should stay still as data changes: without them the domain follows the data and a growing value can look flat",
236
+ "Passing labels and values of different lengths without meaning to — the shorter one decides where the readings stop and the longer one decides the axis",
237
+ "Wrapping it in a narrow fixed column: the chart fills its column, and a compressed time axis is unreadable",
238
+ "Relying on the graphic alone for accessibility — the table is the accessible representation and `label` captions it"
239
+ ],
240
+ "patterns": [
241
+ "Basic: <LineArea label=\"'Sessions per week'\" values=\"weekly\" labels=\"weeks\" />",
242
+ "Stable scale: <LineArea label=\"'Sessions'\" values=\"weekly\" labels=\"weeks\" min=\"0\" max=\"500\" />",
243
+ "Named unit: <LineArea label=\"'Sessions per week'\" values=\"weekly\" labels=\"weeks\" caption=\"'sessions'\" />",
244
+ "Live: <LineArea label=\"'Sessions'\" values=\"() => weekly()\" labels=\"weeks\" loading=\"isFetching\" />",
245
+ "Signed: values=[-4, 2, -1, 6] draws below the axis, which Bars and Donut cannot"
246
+ ]
247
+ },
248
+ "examples": [
249
+ {
250
+ "id": "basic",
251
+ "title": "Six weekly readings on a derived scale",
252
+ "pithy": "<LineArea label=\"'Sessions per week'\" values=\"weekly\" labels=\"weeks\" />",
253
+ "assertions": [
254
+ "figure.ui-chart.ui-line-area with data-status='ready'",
255
+ "one .ui-line-area-line, one .ui-line-area-fill closed with Z, and exactly one .ui-line-area-dot",
256
+ "the marker sits on the LAST reading",
257
+ "six .ui-chart-axis-label elements, none of them SVG text",
258
+ "no axe violations"
259
+ ]
260
+ },
261
+ {
262
+ "id": "single-reading",
263
+ "title": "A metric's first day",
264
+ "pithy": "<LineArea label=\"'Sessions per week'\" values=\"[42]\" labels=\"['W1']\" />",
265
+ "assertions": [
266
+ "the marker is centred and no path is emitted at all",
267
+ "no NaN anywhere in the markup — `i / (n - 1)` is 0/0 for one point",
268
+ "data-status='ready'"
269
+ ]
270
+ },
271
+ {
272
+ "id": "hole",
273
+ "title": "A week with no reading",
274
+ "pithy": "<LineArea label=\"'Sessions'\" values=\"[1,2,NaN,3,4]\" labels=\"weeks\" />",
275
+ "assertions": [
276
+ "the line's `d` carries TWO subpaths — it breaks rather than bridging",
277
+ "the table's third row reads an em dash, not 0",
278
+ "a single DEV warning names the index"
279
+ ]
280
+ },
281
+ {
282
+ "id": "clamped",
283
+ "title": "A spike past an explicit ceiling",
284
+ "pithy": "<LineArea label=\"'Sessions'\" values=\"[150]\" labels=\"['W1']\" min=\"0\" max=\"100\" />",
285
+ "assertions": [
286
+ "the marker is drawn at y=0, the top of the plot",
287
+ "the table still reads 150",
288
+ "a DEV warning says the reading is outside min/max"
289
+ ]
290
+ },
291
+ {
292
+ "id": "all-zero",
293
+ "title": "A week when nothing happened",
294
+ "pithy": "<LineArea label=\"'Sessions'\" values=\"[0,0,0]\" labels=\"weeks\" />",
295
+ "assertions": [
296
+ "data-status='ready', NOT 'empty' — a flat line is a real answer",
297
+ "every y coordinate sits on the plot floor",
298
+ "no NaN or Infinity anywhere in the markup"
299
+ ]
300
+ },
301
+ {
302
+ "id": "empty",
303
+ "title": "No readings at all",
304
+ "pithy": "<LineArea label=\"'Sessions'\" values=\"[]\" labels=\"[]\" />",
305
+ "assertions": [
306
+ "data-status='empty'",
307
+ "graphic and table both hidden, role='status' region reads 'No data to display'"
308
+ ]
309
+ },
310
+ {
311
+ "id": "loading",
312
+ "title": "Loading",
313
+ "pithy": "<LineArea label=\"'Sessions'\" values=\"weekly\" labels=\"weeks\" loading=\"true\" />",
314
+ "assertions": [
315
+ "aria-busy='true' on .ui-chart-body, and absent from the root",
316
+ "graphic and table both hidden",
317
+ "role='status' region reads 'Loading chart…'"
318
+ ]
319
+ }
320
+ ],
321
+ "designGuidance": {
322
+ "surface": "No background of its own — the plot sits on whatever surface hosts it. Four grid rules in --color-border give the eye a height reference without competing with the line; the fill is the series' own colour at 13%, which reads as belonging to the stroke above it rather than as a second colour, and works with every palette slot in both themes where a fixed tint would not.",
323
+ "depth": "Flat. No shadow under the line and no gradient in the fill: both add apparent height that is not data, and a gradient in particular makes the same value look different at different points along the axis. The only layering is order — grid, then fill, then stroke, then the terminal marker.",
324
+ "borders": "None around the figure. The marker carries a hairline of --color-surface so it reads as a terminus of the line rather than as a blob sitting on it; in forced-colors that becomes Canvas, and the fill is dropped entirely because at 13% it is invisible there anyway.",
325
+ "interactivity": "None. Nothing is focusable and there is no hover treatment: the axis names every point, the table carries every value, and a tooltip would put readings somewhere keyboard and touch users cannot reach. If a reader needs per-point numbers on screen, the chart is being asked to be a table.",
326
+ "bestPractices": "Use it for ONE trend over an ordered sequence — weeks, releases, cohorts. For several trends use Line; for unordered categories use Bars; for parts of a whole use StackedBar or Donut. Keep to about a dozen points: past eight the axis starts showing every other label, and past twenty the line is texture rather than a trend. Pass min and max whenever the scale should stay still as data changes, and whenever two charts sit near each other — without them each chart re-scales on every poll and a growing value can look flat. Start min at zero unless the data goes below it: a scale that starts at the smallest reading exaggerates every wobble. Give the chart room; it is width-agnostic on purpose, and a compressed time axis is the one way to make a correct trend unreadable. Always give a specific label, and prefer an emptyMessage that says what is missing ('No sessions recorded yet') over the generic default."
327
+ }
328
+ }