@fusion.dev/helix-core 0.206.0 → 0.207.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 +22 -13
- package/custom-elements.json +1 -37171
- package/dist/controllers/aria-forward-controller/aria-forward-controller.js +5 -6
- package/dist/controllers/category-axis-controller/category-axis-controller.d.ts +75 -0
- package/dist/controllers/category-axis-controller/category-axis-controller.js +128 -0
- package/dist/controllers/source-controller/source-controller.d.ts +2 -0
- package/dist/controllers/source-controller/source-controller.js +5 -2
- package/dist/controllers/virtualize-controller/virtualize-controller.d.ts +9 -0
- package/dist/controllers/virtualize-controller/virtualize-controller.js +10 -8
- package/dist/helix-element/helix-element.js +3 -21
- package/dist/helix-element/react-bridge.d.ts +17 -0
- package/dist/helix-element/react-bridge.js +54 -0
- package/dist/index.d.ts +8 -2
- package/dist/index.js +8 -2
- package/dist/mixins/mixin-chart-source/mixin-chart-source.d.ts +28 -0
- package/dist/mixins/mixin-chart-source/mixin-chart-source.js +76 -9
- package/dist/mixins/mixin-field/mixin-field.js +2 -0
- package/dist/mixins/mixin-tokens/mixin-tokens.js +2 -1
- package/dist/mixins/mixin-viewport/mixin-viewport.js +3 -2
- package/dist/utils/audit-children/audit-children.d.ts +1 -0
- package/dist/utils/audit-children/audit-children.js +2 -2
- package/dist/utils/audit-name/audit-name.js +2 -2
- package/dist/utils/audit-number/audit-number-off.d.ts +12 -0
- package/dist/utils/audit-number/audit-number-off.js +2 -0
- package/dist/utils/audit-number/audit-number.d.ts +23 -0
- package/dist/utils/audit-number/audit-number.js +21 -0
- package/dist/utils/category-labels/category-labels.d.ts +74 -0
- package/dist/utils/category-labels/category-labels.js +230 -0
- package/dist/utils/closest-name/closest-name.d.ts +11 -0
- package/dist/utils/closest-name/closest-name.js +35 -0
- package/dist/utils/custom-element/custom-element.js +11 -6
- package/dist/utils/draw-tooltip/draw-tooltip.js +3 -2
- package/dist/utils/host-words/host-words.js +2 -1
- package/dist/utils/how-to-register/how-to-register.d.ts +20 -0
- package/dist/utils/how-to-register/how-to-register.js +3 -0
- package/dist/utils/labels-of/labels-of.d.ts +23 -0
- package/dist/utils/labels-of/labels-of.js +8 -0
- package/dist/utils/line-path/line-path.d.ts +18 -0
- package/dist/utils/line-path/line-path.js +36 -0
- package/dist/utils/locale-of/locale-of.d.ts +40 -2
- package/dist/utils/locale-of/locale-of.js +30 -16
- package/dist/utils/number-converter/number-converter.d.ts +44 -0
- package/dist/utils/number-converter/number-converter.js +34 -0
- package/dist/utils/read-data/read-data.d.ts +1 -1
- package/dist/utils/read-data/read-data.js +23 -10
- package/dist/utils/read-series/read-series.js +9 -2
- package/dist/utils/report-runtime/report-runtime.d.ts +8 -0
- package/dist/utils/scale/scale.d.ts +7 -1
- package/dist/utils/scale/scale.js +9 -4
- package/dist/utils/scroll-item-into-view/scroll-item-into-view.d.ts +5 -0
- package/dist/utils/scroll-item-into-view/scroll-item-into-view.js +3 -2
- package/dist/utils/watch-labels/watch-labels.d.ts +4 -2
- package/dist/utils/watch-labels/watch-labels.js +14 -14
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/words/held.d.ts +11 -3
- package/dist/words/words-locale.js +20 -8
- package/dist/words/words.js +10 -5
- 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
|
-
|
|
64
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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>${
|
|
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
|
};
|
|
@@ -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',
|
|
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',
|
|
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',
|
|
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}.
|
|
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
|
-
|
|
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,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
|
+
};
|