@fusion.dev/helix-core 0.205.2 → 0.207.0

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.
Files changed (59) hide show
  1. package/README.md +22 -13
  2. package/custom-elements.json +1 -37171
  3. package/dist/controllers/aria-forward-controller/aria-forward-controller.js +5 -6
  4. package/dist/controllers/category-axis-controller/category-axis-controller.d.ts +75 -0
  5. package/dist/controllers/category-axis-controller/category-axis-controller.js +128 -0
  6. package/dist/controllers/source-controller/source-controller.d.ts +2 -0
  7. package/dist/controllers/source-controller/source-controller.js +5 -2
  8. package/dist/controllers/virtualize-controller/virtualize-controller.d.ts +9 -0
  9. package/dist/controllers/virtualize-controller/virtualize-controller.js +10 -8
  10. package/dist/helix-element/helix-element.js +3 -21
  11. package/dist/helix-element/react-bridge.d.ts +17 -0
  12. package/dist/helix-element/react-bridge.js +54 -0
  13. package/dist/index.d.ts +8 -2
  14. package/dist/index.js +8 -2
  15. package/dist/mixins/mixin-chart-source/mixin-chart-source.d.ts +28 -0
  16. package/dist/mixins/mixin-chart-source/mixin-chart-source.js +76 -9
  17. package/dist/mixins/mixin-field/mixin-field.js +2 -0
  18. package/dist/mixins/mixin-tokens/mixin-tokens.js +2 -1
  19. package/dist/mixins/mixin-viewport/mixin-viewport.js +3 -2
  20. package/dist/utils/audit-children/audit-children.d.ts +1 -0
  21. package/dist/utils/audit-children/audit-children.js +2 -2
  22. package/dist/utils/audit-name/audit-name.js +2 -2
  23. package/dist/utils/audit-number/audit-number-off.d.ts +12 -0
  24. package/dist/utils/audit-number/audit-number-off.js +2 -0
  25. package/dist/utils/audit-number/audit-number.d.ts +23 -0
  26. package/dist/utils/audit-number/audit-number.js +21 -0
  27. package/dist/utils/category-labels/category-labels.d.ts +74 -0
  28. package/dist/utils/category-labels/category-labels.js +230 -0
  29. package/dist/utils/closest-name/closest-name.d.ts +11 -0
  30. package/dist/utils/closest-name/closest-name.js +35 -0
  31. package/dist/utils/custom-element/custom-element.js +11 -6
  32. package/dist/utils/draw-tooltip/draw-tooltip.js +3 -2
  33. package/dist/utils/host-words/host-words.js +2 -1
  34. package/dist/utils/how-to-register/how-to-register.d.ts +20 -0
  35. package/dist/utils/how-to-register/how-to-register.js +3 -0
  36. package/dist/utils/labels-of/labels-of.d.ts +23 -0
  37. package/dist/utils/labels-of/labels-of.js +8 -0
  38. package/dist/utils/line-path/line-path.d.ts +18 -0
  39. package/dist/utils/line-path/line-path.js +36 -0
  40. package/dist/utils/locale-of/locale-of.d.ts +40 -2
  41. package/dist/utils/locale-of/locale-of.js +30 -16
  42. package/dist/utils/number-converter/number-converter.d.ts +44 -0
  43. package/dist/utils/number-converter/number-converter.js +34 -0
  44. package/dist/utils/read-data/read-data.d.ts +1 -1
  45. package/dist/utils/read-data/read-data.js +23 -10
  46. package/dist/utils/read-series/read-series.js +9 -2
  47. package/dist/utils/report-runtime/report-runtime.d.ts +8 -0
  48. package/dist/utils/scale/scale.d.ts +7 -1
  49. package/dist/utils/scale/scale.js +9 -4
  50. package/dist/utils/scroll-item-into-view/scroll-item-into-view.d.ts +5 -0
  51. package/dist/utils/scroll-item-into-view/scroll-item-into-view.js +3 -2
  52. package/dist/utils/watch-labels/watch-labels.d.ts +4 -2
  53. package/dist/utils/watch-labels/watch-labels.js +14 -14
  54. package/dist/version.d.ts +1 -1
  55. package/dist/version.js +1 -1
  56. package/dist/words/held.d.ts +11 -3
  57. package/dist/words/words-locale.js +20 -8
  58. package/dist/words/words.js +10 -5
  59. package/package.json +16 -4
@@ -1,16 +1,17 @@
1
1
  import { __decorate } from "tslib";
2
2
  import { html, nothing } from 'lit';
3
- import { property } from 'lit/decorators.js';
3
+ import { property, state } from 'lit/decorators.js';
4
4
  import { ChartPointerController } from '../../controllers/chart-pointer-controller/chart-pointer-controller.js';
5
5
  import { SourceController } from '../../controllers/source-controller/source-controller.js';
6
6
  import { seriesKey } from '../../utils/read-series/read-series.js';
7
7
  import { writtenList } from '../../utils/written-list/written-list.js';
8
- import { words } from '../../words/words.js';
8
+ import { formatWords, words } from '../../words/words.js';
9
9
  export const PALETTE = 24;
10
10
  export const seriesIndex = (depth) => (depth % PALETTE) + 1;
11
11
  export const PLOT_WIDTH = 100;
12
12
  export const PLOT_HEIGHT = 100;
13
13
  export const seriesNames = (list) => writtenList(list).map(seriesKey);
14
+ export const READINGS_PAGE = 200;
14
15
  export const POSITION_LABEL = '{position} of {count}';
15
16
  export const MixinChartSource = (Base) => {
16
17
  class ChartSourceElement extends Base {
@@ -22,7 +23,11 @@ export const MixinChartSource = (Base) => {
22
23
  this.for = '';
23
24
  this.label = '';
24
25
  this.locale = '';
26
+ this.nextReadingsLabel = 'Next readings';
25
27
  this.positionLabel = '{position} of {count}';
28
+ this.previousReadingsLabel = 'Previous readings';
29
+ this.readingsLabel = 'Readings {first} to {last} of {count}';
30
+ this.tablePage = 0;
26
31
  }
27
32
  static { this.words = ''; }
28
33
  static get observedAttributes() {
@@ -60,8 +65,16 @@ export const MixinChartSource = (Base) => {
60
65
  }
61
66
  get plottedColumns() {
62
67
  const rows = this.sourceRows;
63
- const width = Math.max(0, ...rows.map((row) => row.cells.length));
64
- return Array.from({ length: width }, (_, column) => column).filter((column) => rows.some((row) => row.cells[column]?.value !== undefined));
68
+ if (this.plotted?.rows === rows) {
69
+ return this.plotted.columns;
70
+ }
71
+ let width = 0;
72
+ for (const row of rows) {
73
+ width = Math.max(width, row.cells.length);
74
+ }
75
+ const columns = Array.from({ length: width }, (_, column) => column).filter((column) => rows.some((row) => row.cells[column]?.value !== undefined));
76
+ this.plotted = { columns, rows };
77
+ return columns;
65
78
  }
66
79
  seriesOf(cell) {
67
80
  const depth = this.plottedColumns.indexOf(cell);
@@ -85,13 +98,52 @@ export const MixinChartSource = (Base) => {
85
98
  if (this.source.element || rows.length === 0) {
86
99
  return nothing;
87
100
  }
88
- const width = Math.max(...rows.map((row) => row.cells.length));
101
+ let width = 0;
102
+ for (const row of rows) {
103
+ width = Math.max(width, row.cells.length);
104
+ }
89
105
  const columns = Array.from({ length: width }, (_, cell) => this.columnName(cell));
90
- return html `<table part="table">
106
+ const pages = Math.ceil(rows.length / READINGS_PAGE);
107
+ const page = Math.min(this.tablePage, pages - 1);
108
+ const first = page * READINGS_PAGE;
109
+ const shown = rows.slice(first, first + READINGS_PAGE);
110
+ const paged = pages > 1;
111
+ return html `<table
112
+ aria-describedby=${paged ? 'helix-chart-readings' : nothing}
113
+ aria-rowcount=${paged ? rows.length + 1 : nothing}
114
+ part="table"
115
+ >
91
116
  ${this.named ? html `<caption>${this.named}</caption>` : nothing}
92
- <thead><tr><th scope="col">${this.data?.columns[0] ?? ''}</th>${columns.map((name) => html `<th scope="col">${name}</th>`)}</tr></thead>
93
- <tbody>${rows.map((row) => html `<tr><th scope="row">${row.label}</th>${columns.map((_, cell) => html `<td>${row.cells[cell]?.text ?? ''}</td>`)}</tr>`)}</tbody>
94
- </table>`;
117
+ <thead><tr aria-rowindex=${paged ? 1 : nothing}><th scope="col">${this.data?.columns[0] ?? ''}</th>${columns.map((name) => html `<th scope="col">${name}</th>`)}</tr></thead>
118
+ <tbody>${shown.map((row, place) => html `<tr aria-rowindex=${paged ? first + place + 2 : nothing}><th scope="row">${row.label}</th>${columns.map((_, cell) => html `<td>${row.cells[cell]?.text ?? ''}</td>`)}</tr>`)}</tbody>
119
+ </table>${paged ? this.renderTablePages(page, pages, first, shown.length, rows.length) : nothing}`;
120
+ }
121
+ renderTablePages(page, pages, first, shown, count) {
122
+ const turn = (by) => () => {
123
+ const next = Math.min(Math.max(page + by, 0), pages - 1);
124
+ if (next !== page) {
125
+ this.tablePage = next;
126
+ }
127
+ };
128
+ return html `<div part="table-pages">
129
+ <span id="helix-chart-readings" role="status">${formatWords(this, 'readingsLabel', {
130
+ count,
131
+ first: first + 1,
132
+ last: first + shown,
133
+ })}</span>
134
+ <button
135
+ aria-disabled=${page === 0 ? 'true' : 'false'}
136
+ tabindex="-1"
137
+ type="button"
138
+ @click=${turn(-1)}
139
+ >${this.previousReadingsLabel}</button>
140
+ <button
141
+ aria-disabled=${page === pages - 1 ? 'true' : 'false'}
142
+ tabindex="-1"
143
+ type="button"
144
+ @click=${turn(1)}
145
+ >${this.nextReadingsLabel}</button>
146
+ </div>`;
95
147
  }
96
148
  }
97
149
  __decorate([
@@ -106,9 +158,24 @@ export const MixinChartSource = (Base) => {
106
158
  __decorate([
107
159
  property()
108
160
  ], ChartSourceElement.prototype, "locale", void 0);
161
+ __decorate([
162
+ words,
163
+ property({ attribute: 'next-readings-label' })
164
+ ], ChartSourceElement.prototype, "nextReadingsLabel", void 0);
109
165
  __decorate([
110
166
  words,
111
167
  property({ attribute: 'position-label' })
112
168
  ], ChartSourceElement.prototype, "positionLabel", void 0);
169
+ __decorate([
170
+ words,
171
+ property({ attribute: 'previous-readings-label' })
172
+ ], ChartSourceElement.prototype, "previousReadingsLabel", void 0);
173
+ __decorate([
174
+ words,
175
+ property({ attribute: 'readings-label' })
176
+ ], ChartSourceElement.prototype, "readingsLabel", void 0);
177
+ __decorate([
178
+ state()
179
+ ], ChartSourceElement.prototype, "tablePage", void 0);
113
180
  return ChartSourceElement;
114
181
  };
@@ -197,5 +197,7 @@ export const MixinField = (Base) => {
197
197
  __decorate([
198
198
  state()
199
199
  ], FieldElement.prototype, "touched", void 0);
200
+ ;
201
+ FieldElement.disableWarning?.('change-in-update');
200
202
  return FieldElement;
201
203
  };
@@ -4,6 +4,7 @@ import { html, nothing } from 'lit';
4
4
  import { property, state } from 'lit/decorators.js';
5
5
  import { drawIcon } from '../../utils/draw-icon/draw-icon.js';
6
6
  import { inlineStep } from '../../utils/inline-step/inline-step.js';
7
+ import { numberConverter } from '../../utils/number-converter/number-converter.js';
7
8
  import { formatWords, words } from '../../words/words.js';
8
9
  export const MixinTokens = (Base) => {
9
10
  class TokensElement extends Base {
@@ -148,7 +149,7 @@ export const MixinTokens = (Base) => {
148
149
  property({ attribute: 'expand-label' })
149
150
  ], TokensElement.prototype, "expandLabel", void 0);
150
151
  __decorate([
151
- property({ attribute: 'max-tags', type: Number })
152
+ property({ attribute: 'max-tags', converter: numberConverter('max-tags', 0) })
152
153
  ], TokensElement.prototype, "maxTags", void 0);
153
154
  __decorate([
154
155
  words,
@@ -5,6 +5,7 @@ import { FullscreenController } from '../../controllers/fullscreen-controller/fu
5
5
  import { IdleController } from '../../controllers/idle-controller/idle-controller.js';
6
6
  import { inlineStep } from '../../utils/inline-step/inline-step.js';
7
7
  import { isRtl } from '../../utils/is-rtl/is-rtl.js';
8
+ import { numberConverter, timeConverter } from '../../utils/number-converter/number-converter.js';
8
9
  import { formatWords, words } from '../../words/words.js';
9
10
  const HINT_SHOWS = 1600;
10
11
  export const MixinViewport = (Base) => {
@@ -323,7 +324,7 @@ export const MixinViewport = (Base) => {
323
324
  property()
324
325
  ], Viewport.prototype, "gestures", void 0);
325
326
  __decorate([
326
- property({ attribute: 'idle-delay', type: Number })
327
+ property({ attribute: 'idle-delay', converter: timeConverter('idle-delay', 0) })
327
328
  ], Viewport.prototype, "idleDelay", void 0);
328
329
  __decorate([
329
330
  words,
@@ -338,7 +339,7 @@ export const MixinViewport = (Base) => {
338
339
  property({ attribute: 'pan-right-label' })
339
340
  ], Viewport.prototype, "panRightLabel", void 0);
340
341
  __decorate([
341
- property({ attribute: 'pan-step', type: Number })
342
+ property({ attribute: 'pan-step', converter: numberConverter('pan-step', 0.25) })
342
343
  ], Viewport.prototype, "panStep", void 0);
343
344
  __decorate([
344
345
  words,
@@ -20,6 +20,7 @@
20
20
  *
21
21
  * `part` is the child's tag after the tier, such as `option`, and `entry` its
22
22
  * family's entry, such as `option`; the host's `tier` gives the rest of both.
23
+ * On a tier of your own the report names the tag to define.
23
24
  * `consequence` says what the missing child costs, after "so". `drawn` is for
24
25
  * children the host draws from data, such as the options of Listbox's `items`:
25
26
  * the page wrote none, so an undefined tag is reported without one in the
@@ -1,3 +1,4 @@
1
+ import { howToRegister } from '../how-to-register/how-to-register.js';
1
2
  import { reportRuntime } from '../report-runtime/report-runtime.js';
2
3
  const reported = new Set();
3
4
  const afterPaint = (check) => {
@@ -29,10 +30,9 @@ export const auditChildren = (host, part, entry, consequence, drawn = false) =>
29
30
  return;
30
31
  }
31
32
  reported.add(key);
32
- const pkg = tier === 'helix-' ? '@fusion.dev/helix' : '@fusion.dev/helix-headless';
33
33
  reportRuntime({
34
34
  kind: 'unregistered-part',
35
- message: `<${tag}> in <${host.localName}> is not registered, so ${consequence}. Import '${pkg}/${entry}'.`,
35
+ message: `<${tag}> in <${host.localName}> is not registered, so ${consequence}. ${howToRegister({ entry, host: host.localName, tag, tier })}`,
36
36
  tag: host.localName,
37
37
  });
38
38
  };
@@ -1,3 +1,4 @@
1
+ import { labelsOf } from '../labels-of/labels-of.js';
1
2
  import { nameText } from '../name-text/name-text.js';
2
3
  import { reportRuntime } from '../report-runtime/report-runtime.js';
3
4
  const namesWithWords = (ids, from, shown) => {
@@ -36,8 +37,7 @@ export const hasName = (element, shown) => {
36
37
  if ((referenced ?? []).some((one) => nameText(one, shown).trim() !== '')) {
37
38
  return true;
38
39
  }
39
- const labels = element.labels;
40
- if ([...(labels ?? [])].some((label) => nameText(label).trim() !== '')) {
40
+ if (labelsOf(element).some((label) => nameText(label).trim() !== '')) {
41
41
  return true;
42
42
  }
43
43
  const role = element.getAttribute('role') ?? implicitRole(element);
@@ -0,0 +1,12 @@
1
+ import type { auditNumber as audit, auditTime as time } from './audit-number.js';
2
+ /**
3
+ * The not-a-number warning in a production build: nothing.
4
+ *
5
+ * `#audit-number` resolves here unless the bundler or runtime asks for the
6
+ * `development` condition, so a production build carries none of the audit's
7
+ * code. The signature is the audit's, so a call site reads the same in both
8
+ * builds. See `audit-number.ts`.
9
+ */
10
+ export declare const auditNumber: typeof audit;
11
+ /** The not-a-time warning in a production build: nothing. */
12
+ export declare const auditTime: typeof time;
@@ -0,0 +1,2 @@
1
+ export const auditNumber = () => { };
2
+ export const auditTime = () => { };
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Reports a numeric attribute that holds something other than a number, such
3
+ * as `page-size="1O"`, which `numberConverter` reads as the property's
4
+ * default.
5
+ *
6
+ * **Once for each attribute, value and default**, since a converter is not
7
+ * handed the element: twenty tables written with `page-size="1O"` are one
8
+ * mistake, and the sentence names what to search the markup for.
9
+ *
10
+ * **In a development build only**, as `missing-name` is: every numeric
11
+ * attribute would pay for a sentence meant for the person writing the page.
12
+ * Core imports it as `#audit-number`, which `package.json` resolves here under
13
+ * the `development` condition and to `audit-number-off.ts`, which does
14
+ * nothing, everywhere else.
15
+ */
16
+ export declare const auditNumber: (attribute: string, value: string, fallback: number | undefined, unit?: string) => void;
17
+ /**
18
+ * Reports an attribute in milliseconds that holds something other than a
19
+ * time, such as `duration="soon"` or `delay="-1s"`, which `timeConverter`
20
+ * reads as the property's default. Once for each, in a development build
21
+ * only, as `auditNumber` is.
22
+ */
23
+ export declare const auditTime: (attribute: string, value: string, fallback: number | undefined) => void;
@@ -0,0 +1,21 @@
1
+ import { reportRuntime } from '../report-runtime/report-runtime.js';
2
+ const reported = new Set();
3
+ const report = (message) => {
4
+ if (reported.has(message)) {
5
+ return;
6
+ }
7
+ reported.add(message);
8
+ reportRuntime({ kind: 'not-a-number', message });
9
+ };
10
+ const kept = (attribute, fallback) => fallback === undefined
11
+ ? `\`${attribute}\` stays unset`
12
+ : `\`${attribute}\` keeps its default of ${fallback}`;
13
+ export const auditNumber = (attribute, value, fallback, unit) => {
14
+ const takes = unit === undefined ? 'a number' : `a number of ${unit}, with no unit after it`;
15
+ report(`${attribute}="${value}" is not a number, so ${kept(attribute, fallback)}. ` +
16
+ `Write \`${attribute}\` as ${takes}.`);
17
+ };
18
+ export const auditTime = (attribute, value, fallback) => {
19
+ report(`${attribute}="${value}" is not a time of zero or more, so ${kept(attribute, fallback)}. ` +
20
+ `Write \`${attribute}\` in milliseconds, such as 300, or as a CSS time, such as 300ms or 0.3s.`);
21
+ };
@@ -0,0 +1,74 @@
1
+ /** One reading on a category axis: its name, and the instant it stands for, where it has one. */
2
+ export interface Category {
3
+ /**
4
+ * The reading's own name, which is what is drawn under it.
5
+ */
6
+ label: string;
7
+ /**
8
+ * The machine-readable instant, from a `<time>` in the row's header.
9
+ */
10
+ when?: string;
11
+ }
12
+ /** One reading's name drawn along a category axis, and the room it is given. */
13
+ export interface PlacedLabel {
14
+ /**
15
+ * The reading's place among the readings, counting from zero.
16
+ */
17
+ at: number;
18
+ /**
19
+ * How much of the axis the name's room takes, as a percentage.
20
+ */
21
+ size: number;
22
+ /**
23
+ * Where the name's room starts, as a percentage of the axis from its start.
24
+ */
25
+ start: number;
26
+ }
27
+ /** How long a category axis is and how long each name on it is, in pixels. */
28
+ export interface AxisLengths {
29
+ /**
30
+ * The least room left between two names.
31
+ */
32
+ gap: number;
33
+ /**
34
+ * The axis' own length.
35
+ */
36
+ length: number;
37
+ /**
38
+ * How long the name of the reading at this place is, along the axis.
39
+ */
40
+ name: (at: number) => number;
41
+ }
42
+ /**
43
+ * How many names are drawn while the axis is not measured: before its first
44
+ * layout, and where there is no layout at all, as on a server or in a DOM
45
+ * without boxes. Every name up to this many, which is more than any axis
46
+ * holds at a readable size, and a share of them past it.
47
+ */
48
+ export declare const UNMEASURED_LABELS = 100;
49
+ /**
50
+ * Which readings a category axis names, and where each name sits.
51
+ *
52
+ * **Every name where every name fits, and a regular share of them where they
53
+ * do not.** One name per reading in a grid of equal columns truncated each name
54
+ * to its column and, past a few hundred readings, laid them over one another:
55
+ * 10,000 readings across 900 pixels gave each name under a tenth of a pixel and
56
+ * built ten thousand elements to draw nothing legible. So the names that fit
57
+ * are drawn and the rest are left out, with the first and the last always kept.
58
+ *
59
+ * **Which share depends on what the names are.** Instants, from a `<time>` in
60
+ * every row, are named where they cross into a new period: the first of each
61
+ * month, the first of each year. Numbers in order, such as years, are named
62
+ * where they cross a round number. Anything else is named every so many
63
+ * readings, counted from the first. Every share is the finest of a ladder of
64
+ * round steps, 1, 2 and 5 at each power of ten, that leaves `gap` between every
65
+ * two names at `length`.
66
+ *
67
+ * Without `lengths` the axis has not been measured, and every name is drawn up
68
+ * to `UNMEASURED_LABELS`, which is what a page with no layout sees.
69
+ *
70
+ * A pure function, so the drawing and its specs share one answer. The names
71
+ * are read for the reading's own words only: a reading's tooltip and its
72
+ * spoken readout say its own name, whether or not the axis draws it.
73
+ */
74
+ export declare const placeLabels: (categories: readonly Category[], lengths?: AxisLengths) => PlacedLabel[];
@@ -0,0 +1,230 @@
1
+ export const UNMEASURED_LABELS = 100;
2
+ const roundStep = (rung) => [1, 2, 5][rung % 3] * 10 ** Math.floor(rung / 3);
3
+ const plainRungs = function* (count) {
4
+ for (let rung = 0;; rung += 1) {
5
+ const step = roundStep(rung);
6
+ yield { bucket: (at) => Math.floor(at / step), estimate: count / step };
7
+ if (step >= count) {
8
+ return;
9
+ }
10
+ }
11
+ };
12
+ const numericRungs = function* (values) {
13
+ let finest = Number.POSITIVE_INFINITY;
14
+ for (let at = 1; at < values.length; at += 1) {
15
+ finest = Math.min(finest, Math.abs(values[at] - values[at - 1]));
16
+ }
17
+ const span = Math.abs(values.at(-1) - values[0]);
18
+ const base = 10 ** Math.floor(Math.log10(finest));
19
+ for (let rung = 0;; rung += 1) {
20
+ const step = base * roundStep(rung);
21
+ yield {
22
+ bucket: (at) => Math.floor(values[at] / step + 1e-9),
23
+ estimate: span / step,
24
+ };
25
+ if (step > span) {
26
+ return;
27
+ }
28
+ }
29
+ };
30
+ const SECOND = 1000;
31
+ const MINUTE = 60 * SECOND;
32
+ const HOUR = 60 * MINUTE;
33
+ const DAY = 24 * HOUR;
34
+ const CLOCK = [
35
+ SECOND,
36
+ 5 * SECOND,
37
+ 15 * SECOND,
38
+ 30 * SECOND,
39
+ MINUTE,
40
+ 5 * MINUTE,
41
+ 15 * MINUTE,
42
+ 30 * MINUTE,
43
+ HOUR,
44
+ 3 * HOUR,
45
+ 6 * HOUR,
46
+ 12 * HOUR,
47
+ DAY,
48
+ 2 * DAY,
49
+ ];
50
+ const timeRungs = function* (walls) {
51
+ const span = Math.abs(walls.at(-1) - walls[0]);
52
+ for (const step of CLOCK) {
53
+ yield { bucket: (at) => Math.floor(walls[at] / step), estimate: span / step };
54
+ }
55
+ yield {
56
+ bucket: (at) => Math.floor((Math.floor(walls[at] / DAY) + 3) / 7),
57
+ estimate: span / (7 * DAY),
58
+ };
59
+ const months = (at) => {
60
+ const date = new Date(walls[at]);
61
+ return date.getUTCFullYear() * 12 + date.getUTCMonth();
62
+ };
63
+ for (const step of [1, 3, 6]) {
64
+ yield { bucket: (at) => Math.floor(months(at) / step), estimate: span / (step * 30 * DAY) };
65
+ }
66
+ for (let rung = 0;; rung += 1) {
67
+ const step = roundStep(rung);
68
+ yield {
69
+ bucket: (at) => Math.floor(Math.floor(months(at) / 12) / step),
70
+ estimate: span / (step * 365 * DAY),
71
+ };
72
+ if (step * 365 * DAY > span) {
73
+ return;
74
+ }
75
+ }
76
+ };
77
+ const ordered = (values, strict) => {
78
+ let rising = true;
79
+ let falling = true;
80
+ for (let at = 1; at < values.length; at += 1) {
81
+ const step = values[at] - values[at - 1];
82
+ rising &&= strict ? step > 0 : step >= 0;
83
+ falling &&= strict ? step < 0 : step <= 0;
84
+ }
85
+ return rising || falling;
86
+ };
87
+ const DATE_ONLY = /^\d{4}(-\d{2}(-\d{2})?)?$/;
88
+ const rungsOf = (categories) => {
89
+ const count = categories.length;
90
+ if (count > 1 && categories.every((one) => one.when !== undefined)) {
91
+ const walls = categories.map((one) => {
92
+ const when = one.when.trim();
93
+ const instant = Date.parse(when);
94
+ return DATE_ONLY.test(when)
95
+ ? instant
96
+ : instant - new Date(instant).getTimezoneOffset() * MINUTE;
97
+ });
98
+ if (walls.every(Number.isFinite) && ordered(walls, false)) {
99
+ return timeRungs(walls);
100
+ }
101
+ }
102
+ if (count > 1) {
103
+ const values = categories.map((one) => one.label.trim() === '' ? Number.NaN : Number(one.label.trim()));
104
+ if (values.every(Number.isFinite) && ordered(values, true)) {
105
+ return numericRungs(values);
106
+ }
107
+ }
108
+ return plainRungs(count);
109
+ };
110
+ const changes = (rung, count) => {
111
+ const found = [0];
112
+ let previous = rung.bucket(0);
113
+ for (let at = 1; at < count; at += 1) {
114
+ const bucket = rung.bucket(at);
115
+ if (bucket !== previous) {
116
+ found.push(at);
117
+ previous = bucket;
118
+ }
119
+ }
120
+ return found;
121
+ };
122
+ const extentOf = (count, lengths) => (at) => {
123
+ const long = lengths.name(at);
124
+ const center = ((at + 0.5) / count) * lengths.length;
125
+ const inset = lengths.gap / 2;
126
+ const low = Math.max(inset, Math.min(center - long / 2, lengths.length - inset - long));
127
+ return [low, low + long];
128
+ };
129
+ const keep = (candidates, count, lengths) => {
130
+ const last = count - 1;
131
+ const extent = extentOf(count, lengths);
132
+ const kept = [0];
133
+ let end = extent(0)[1];
134
+ const tail = last > 0 ? extent(last)[0] : Number.POSITIVE_INFINITY;
135
+ for (const at of candidates) {
136
+ if (at === 0 || at === last) {
137
+ continue;
138
+ }
139
+ const [low, high] = extent(at);
140
+ if (high + lengths.gap > tail) {
141
+ break;
142
+ }
143
+ if (low < end + lengths.gap) {
144
+ if (kept.length === 1) {
145
+ continue;
146
+ }
147
+ return undefined;
148
+ }
149
+ kept.push(at);
150
+ end = high;
151
+ }
152
+ if (last > 0) {
153
+ kept.push(last);
154
+ }
155
+ return kept;
156
+ };
157
+ const short = (value) => Math.round(value * 10_000) / 10_000;
158
+ const percent = (at, low, high, length) => ({
159
+ at,
160
+ size: short(((high - low) / length) * 100),
161
+ start: short((low / length) * 100),
162
+ });
163
+ const rooms = (kept, count, lengths) => {
164
+ const last = kept.length - 1;
165
+ if (last === 0) {
166
+ return [{ at: kept[0], size: 100, start: 0 }];
167
+ }
168
+ if (!lengths) {
169
+ const center = (at) => at + 0.5;
170
+ return kept.map((at, place) => {
171
+ const before = place > 0 ? center(at) - center(kept[place - 1]) : 2 * center(at);
172
+ const after = place < last ? center(kept[place + 1]) - center(at) : 2 * (count - center(at));
173
+ const half = Math.min(before, after) / 2;
174
+ return percent(at, center(at) - half, center(at) + half, count);
175
+ });
176
+ }
177
+ const length = lengths.length;
178
+ const extent = extentOf(count, lengths);
179
+ const spans = kept.map(extent);
180
+ const [head, tail] = [spans[0], spans[last]];
181
+ if (last === 1 && head[1] + lengths.gap > tail[0]) {
182
+ return [
183
+ percent(kept[0], 0, length / 2, length),
184
+ percent(kept[1], length / 2, length, length),
185
+ ];
186
+ }
187
+ return kept.map((at, place) => {
188
+ const [low, high] = spans[place];
189
+ const before = place > 0 ? (low - spans[place - 1][1]) / 2 : low;
190
+ const after = place < last ? (spans[place + 1][0] - high) / 2 : length - high;
191
+ const pad = Math.max(0, Math.min(before, after));
192
+ return percent(at, low - pad, high + pad, length);
193
+ });
194
+ };
195
+ export const placeLabels = (categories, lengths) => {
196
+ const count = categories.length;
197
+ if (count === 0) {
198
+ return [];
199
+ }
200
+ if (!lengths || !(lengths.length > 0)) {
201
+ let step = 1;
202
+ for (let rung = 0; count / step > UNMEASURED_LABELS; rung += 1) {
203
+ step = roundStep(rung + 1);
204
+ }
205
+ const kept = Array.from({ length: Math.ceil(count / step) }, (_, place) => place * step);
206
+ return rooms(kept.at(-1) === count - 1 ? kept : [...kept, count - 1], count, undefined);
207
+ }
208
+ const most = (2 * lengths.length) / Math.max(lengths.gap, 1);
209
+ if (count <= most) {
210
+ const every = Array.from({ length: count }, (_, at) => at);
211
+ const all = keep(every, count, lengths);
212
+ if (all && all.length === count) {
213
+ return rooms(all, count, lengths);
214
+ }
215
+ }
216
+ for (const rung of rungsOf(categories)) {
217
+ if (rung.estimate > most) {
218
+ continue;
219
+ }
220
+ const candidates = changes(rung, count);
221
+ if (candidates.length === count) {
222
+ continue;
223
+ }
224
+ const kept = keep(candidates, count, lengths);
225
+ if (kept) {
226
+ return rooms(kept, count, lengths);
227
+ }
228
+ }
229
+ return rooms(count > 1 ? [0, count - 1] : [0], count, lengths);
230
+ };
@@ -0,0 +1,11 @@
1
+ /**
2
+ * The name in `names` closest to `wrong`, or `undefined` where none is close
3
+ * enough to be the one meant.
4
+ *
5
+ * **Close is the fewest letters added, taken out, changed, or swapped with
6
+ * the one beside them**, a swap counting once, so `chekc` finds `check`.
7
+ * **Up to a third of the name's length**, the ceiling diagnostics' `nearest`
8
+ * keeps, since a confident wrong suggestion costs more than none: `x` is a
9
+ * typo for nothing. The first of two names at the same distance wins.
10
+ */
11
+ export declare const closestName: (wrong: string, names: Iterable<string>) => string | undefined;
@@ -0,0 +1,35 @@
1
+ export const closestName = (wrong, names) => {
2
+ let found;
3
+ let least = Math.floor(wrong.length / 3) + 1;
4
+ for (const name of names) {
5
+ if (Math.abs(name.length - wrong.length) >= least) {
6
+ continue;
7
+ }
8
+ const distance = distanceOf(wrong, name);
9
+ if (distance < least) {
10
+ found = name;
11
+ least = distance;
12
+ }
13
+ }
14
+ return found;
15
+ };
16
+ const distanceOf = (from, to) => {
17
+ const rows = [Array.from({ length: to.length + 1 }, (_, column) => column)];
18
+ for (let row = 1; row <= from.length; row += 1) {
19
+ const above = rows[row - 1];
20
+ const current = [row];
21
+ for (let column = 1; column <= to.length; column += 1) {
22
+ const same = from[row - 1] === to[column - 1];
23
+ let cell = Math.min(above[column] + 1, current[column - 1] + 1, above[column - 1] + (same ? 0 : 1));
24
+ if (row > 1 &&
25
+ column > 1 &&
26
+ from[row - 1] === to[column - 2] &&
27
+ from[row - 2] === to[column - 1]) {
28
+ cell = Math.min(cell, rows[row - 2][column - 2] + 1);
29
+ }
30
+ current[column] = cell;
31
+ }
32
+ rows.push(current);
33
+ }
34
+ return rows[from.length][to.length];
35
+ };