@mim/histui 0.1.0 → 0.2.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 +40 -25
- package/package.json +1 -1
- package/src/default-config.js +5 -1
- package/src/index.d.ts +23 -2
- package/src/index.js +18 -1
- package/src/styles.css +154 -0
- package/src/timeline-view.js +142 -0
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Histui
|
|
2
2
|
|
|
3
|
-
Histui is a reusable, framework-agnostic interactive history timeline package. It can render PastStruct datasets or already-normalized records into a zoomable, pannable, responsive timeline with LOD, clustering, a zoom navigator, hover-linked connectors, axis placement controls, themes, Persian/English UI strings, and explode mode.
|
|
3
|
+
Histui is a reusable, framework-agnostic interactive history timeline package. It can render PastStruct datasets or already-normalized records into a zoomable, pannable, responsive timeline with LOD, clustering, a zoom navigator, hover-linked connectors, blueprint-style measurement indicators, axis placement controls, themes, Persian/English UI strings, and explode mode.
|
|
4
4
|
|
|
5
5
|
## Files
|
|
6
6
|
|
|
@@ -13,29 +13,34 @@ Histui is a reusable, framework-agnostic interactive history timeline package. I
|
|
|
13
13
|
|
|
14
14
|
## Basic Usage
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
container: "#timeline",
|
|
24
|
-
data: pastStructDataset,
|
|
25
|
-
language: "en",
|
|
26
|
-
themeId: "obsidian-lab",
|
|
27
|
-
explodeEnabled: false,
|
|
28
|
-
onSelect(record) {
|
|
29
|
-
console.log("selected", record.id);
|
|
30
|
-
},
|
|
31
|
-
onViewportChange(viewport) {
|
|
32
|
-
console.log(viewport);
|
|
33
|
-
}
|
|
34
|
-
});
|
|
16
|
+
Install the package from npm:
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npm install @mim/histui
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Import the JavaScript API and required stylesheet:
|
|
35
23
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
24
|
+
```js
|
|
25
|
+
import { createHistuiTimeline } from "@mim/histui";
|
|
26
|
+
import "@mim/histui/styles.css";
|
|
27
|
+
|
|
28
|
+
const histui = createHistuiTimeline({
|
|
29
|
+
container: "#timeline",
|
|
30
|
+
data: pastStructDataset,
|
|
31
|
+
language: "en",
|
|
32
|
+
themeId: "obsidian-lab",
|
|
33
|
+
explodeEnabled: false,
|
|
34
|
+
onSelect(record) {
|
|
35
|
+
console.log("selected", record.id);
|
|
36
|
+
},
|
|
37
|
+
onViewportChange(viewport) {
|
|
38
|
+
console.log(viewport);
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
histui.setExplodeEnabled(true);
|
|
43
|
+
histui.setFilters({ minSignificance: 7 });
|
|
39
44
|
```
|
|
40
45
|
|
|
41
46
|
## Local Development
|
|
@@ -56,7 +61,7 @@ Use a custom port when needed:
|
|
|
56
61
|
PORT=5180 npm run dev
|
|
57
62
|
```
|
|
58
63
|
|
|
59
|
-
Keep this server running while editing package files. For testing the package inside `histui-app-2`,
|
|
64
|
+
Keep this server running while editing package files. For testing the package inside `histui-app-2`, run `npm run histui:local` in the app repo to point `@mim/histui` at `../histui`, then run `npm run histui:published` when you want to switch the app back to the published package.
|
|
60
65
|
|
|
61
66
|
## Public API
|
|
62
67
|
|
|
@@ -69,7 +74,7 @@ import {
|
|
|
69
74
|
createDefaultFilters,
|
|
70
75
|
filterRecords,
|
|
71
76
|
DEFAULT_HISTUI_CONFIG
|
|
72
|
-
} from "histui";
|
|
77
|
+
} from "@mim/histui";
|
|
73
78
|
```
|
|
74
79
|
|
|
75
80
|
### `createHistuiTimeline(options)`
|
|
@@ -92,6 +97,7 @@ Common options:
|
|
|
92
97
|
- `axisPlacement`: `{ horizontal, vertical }`, each `"center"`, `"side-start"`, or `"side-end"`.
|
|
93
98
|
- `lodEnabled`: boolean.
|
|
94
99
|
- `explodeEnabled`: boolean.
|
|
100
|
+
- `measurement`: optional override for `config.timeline.measurement`.
|
|
95
101
|
- `analytics.measurementId`: optional Google Analytics measurement id.
|
|
96
102
|
- `onSelect(record, instance)`: event callback.
|
|
97
103
|
- `onViewportChange(viewport, instance)`: event callback.
|
|
@@ -112,6 +118,8 @@ Common options:
|
|
|
112
118
|
- `setAxisPlacement(orientation, placement)`
|
|
113
119
|
- `setLodEnabled(enabled)`
|
|
114
120
|
- `setExplodeEnabled(enabled)`
|
|
121
|
+
- `setMeasurementOptions(options)`
|
|
122
|
+
- `setMeasurementEnabled(enabled)`
|
|
115
123
|
- `setLanguage(language, direction)`
|
|
116
124
|
- `setTheme(themeOrId)`
|
|
117
125
|
- `getState()`
|
|
@@ -146,6 +154,11 @@ createHistuiTimeline({
|
|
|
146
154
|
data,
|
|
147
155
|
config: {
|
|
148
156
|
timeline: {
|
|
157
|
+
measurement: {
|
|
158
|
+
enabled: true,
|
|
159
|
+
transient: true,
|
|
160
|
+
fadeOutMs: 1200
|
|
161
|
+
},
|
|
149
162
|
explode: {
|
|
150
163
|
maxVisible: 42,
|
|
151
164
|
layers: 8,
|
|
@@ -156,6 +169,8 @@ createHistuiTimeline({
|
|
|
156
169
|
});
|
|
157
170
|
```
|
|
158
171
|
|
|
172
|
+
`timeline.measurement.enabled` draws a dimension-style line across the currently visible timeline span and labels it with the visible year count. Set `timeline.measurement.transient` to `true` to show it only after the viewport changes; it fades out after `fadeOutMs` milliseconds, defaulting to `1200`.
|
|
173
|
+
|
|
159
174
|
## Check
|
|
160
175
|
|
|
161
176
|
```bash
|
package/package.json
CHANGED
package/src/default-config.js
CHANGED
|
@@ -29,6 +29,11 @@ export const DEFAULT_HISTUI_CONFIG = {
|
|
|
29
29
|
trackInsetPx: 18,
|
|
30
30
|
minSelectionPixels: 10
|
|
31
31
|
},
|
|
32
|
+
measurement: {
|
|
33
|
+
enabled: false,
|
|
34
|
+
transient: false,
|
|
35
|
+
fadeOutMs: 1200
|
|
36
|
+
},
|
|
32
37
|
lod: {
|
|
33
38
|
enabled: true,
|
|
34
39
|
thresholds: [
|
|
@@ -117,4 +122,3 @@ export const DEFAULT_HISTUI_CONFIG = {
|
|
|
117
122
|
}
|
|
118
123
|
]
|
|
119
124
|
};
|
|
120
|
-
|
package/src/index.d.ts
CHANGED
|
@@ -7,6 +7,24 @@ export interface HistuiTheme {
|
|
|
7
7
|
colors: Record<string, string>;
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
+
export interface HistuiMeasurementConfig {
|
|
11
|
+
enabled?: boolean;
|
|
12
|
+
transient?: boolean;
|
|
13
|
+
showOnChangeOnly?: boolean;
|
|
14
|
+
visibleOnChangeOnly?: boolean;
|
|
15
|
+
fadeOutMs?: number;
|
|
16
|
+
hideAfterMs?: number;
|
|
17
|
+
offsetPx?: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface HistuiTimelineConfig {
|
|
21
|
+
minZoomSpanYears?: number;
|
|
22
|
+
maxZoomMultiplier?: number;
|
|
23
|
+
defaultPaddingRatio?: number;
|
|
24
|
+
measurement?: HistuiMeasurementConfig;
|
|
25
|
+
[key: string]: unknown;
|
|
26
|
+
}
|
|
27
|
+
|
|
10
28
|
export interface HistuiConfig {
|
|
11
29
|
app?: {
|
|
12
30
|
name?: string;
|
|
@@ -22,7 +40,7 @@ export interface HistuiConfig {
|
|
|
22
40
|
analytics?: {
|
|
23
41
|
googleAnalyticsMeasurementId?: string;
|
|
24
42
|
};
|
|
25
|
-
timeline?:
|
|
43
|
+
timeline?: HistuiTimelineConfig;
|
|
26
44
|
themes?: HistuiTheme[];
|
|
27
45
|
}
|
|
28
46
|
|
|
@@ -77,6 +95,7 @@ export interface HistuiTimelineOptions<RecordType = any> {
|
|
|
77
95
|
};
|
|
78
96
|
lodEnabled?: boolean;
|
|
79
97
|
explodeEnabled?: boolean;
|
|
98
|
+
measurement?: HistuiMeasurementConfig;
|
|
80
99
|
analytics?: {
|
|
81
100
|
measurementId?: string;
|
|
82
101
|
};
|
|
@@ -104,6 +123,7 @@ export interface HistuiState<RecordType = any> {
|
|
|
104
123
|
};
|
|
105
124
|
lodEnabled: boolean;
|
|
106
125
|
explodeEnabled: boolean;
|
|
126
|
+
measurement: HistuiMeasurementConfig;
|
|
107
127
|
}
|
|
108
128
|
|
|
109
129
|
export class HistuiTimeline<RecordType = any> {
|
|
@@ -120,6 +140,8 @@ export class HistuiTimeline<RecordType = any> {
|
|
|
120
140
|
setAxisPlacement(orientation: "horizontal" | "vertical", placement: HistuiAxisPlacement): this;
|
|
121
141
|
setLodEnabled(enabled: boolean): this;
|
|
122
142
|
setExplodeEnabled(enabled: boolean): this;
|
|
143
|
+
setMeasurementOptions(options: HistuiMeasurementConfig): this;
|
|
144
|
+
setMeasurementEnabled(enabled: boolean): this;
|
|
123
145
|
setLanguage(language: string, direction?: "ltr" | "rtl"): this;
|
|
124
146
|
setTheme(themeOrId: string | HistuiTheme): this;
|
|
125
147
|
applyTheme(theme: HistuiTheme): void;
|
|
@@ -134,4 +156,3 @@ export function createDefaultFilters(records: any[], facets?: unknown): HistuiFi
|
|
|
134
156
|
export function filterRecords<RecordType = any>(records: RecordType[], filters: HistuiFilters): RecordType[];
|
|
135
157
|
export function normalizeFilters(filters?: HistuiFilters, baseFilters?: HistuiFilters): HistuiFilters;
|
|
136
158
|
export const DEFAULT_HISTUI_CONFIG: HistuiConfig;
|
|
137
|
-
|
package/src/index.js
CHANGED
|
@@ -35,6 +35,10 @@ export class HistuiTimeline {
|
|
|
35
35
|
...options
|
|
36
36
|
};
|
|
37
37
|
this.config = mergeConfig(DEFAULT_HISTUI_CONFIG, options.config || {});
|
|
38
|
+
if (options.measurement) {
|
|
39
|
+
this.config.timeline = this.config.timeline || {};
|
|
40
|
+
this.config.timeline.measurement = mergeConfig(this.config.timeline.measurement || {}, options.measurement);
|
|
41
|
+
}
|
|
38
42
|
this.language = options.language || this.config.app.defaultLanguage || "en";
|
|
39
43
|
this.direction = options.direction || dirForLanguage(this.language);
|
|
40
44
|
this.t = options.translator || makeTranslator(this.language);
|
|
@@ -232,6 +236,18 @@ export class HistuiTimeline {
|
|
|
232
236
|
return this;
|
|
233
237
|
}
|
|
234
238
|
|
|
239
|
+
setMeasurementOptions(options = {}) {
|
|
240
|
+
this.config.timeline = this.config.timeline || {};
|
|
241
|
+
this.config.timeline.measurement = mergeConfig(this.config.timeline.measurement || {}, options);
|
|
242
|
+
this.timeline.setMeasurementOptions(this.config.timeline.measurement);
|
|
243
|
+
this.track("timeline_setting_change", { setting: "measurement", value: { ...this.config.timeline.measurement } });
|
|
244
|
+
return this;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
setMeasurementEnabled(enabled) {
|
|
248
|
+
return this.setMeasurementOptions({ enabled: Boolean(enabled) });
|
|
249
|
+
}
|
|
250
|
+
|
|
235
251
|
setLanguage(language, direction = dirForLanguage(language)) {
|
|
236
252
|
this.language = language;
|
|
237
253
|
this.direction = direction;
|
|
@@ -272,7 +288,8 @@ export class HistuiTimeline {
|
|
|
272
288
|
orientation: this.orientation,
|
|
273
289
|
axisPlacement: { ...this.axisPlacement },
|
|
274
290
|
lodEnabled: this.lodEnabled,
|
|
275
|
-
explodeEnabled: this.explodeEnabled
|
|
291
|
+
explodeEnabled: this.explodeEnabled,
|
|
292
|
+
measurement: { ...(this.config.timeline?.measurement || {}) }
|
|
276
293
|
};
|
|
277
294
|
}
|
|
278
295
|
|
package/src/styles.css
CHANGED
|
@@ -167,6 +167,160 @@
|
|
|
167
167
|
pointer-events: none;
|
|
168
168
|
}
|
|
169
169
|
|
|
170
|
+
.histui-measurement-line {
|
|
171
|
+
position: absolute;
|
|
172
|
+
z-index: 240;
|
|
173
|
+
opacity: 0;
|
|
174
|
+
pointer-events: none;
|
|
175
|
+
transform: translateY(-4px);
|
|
176
|
+
transition:
|
|
177
|
+
opacity 240ms ease,
|
|
178
|
+
transform 260ms cubic-bezier(0.16, 0.84, 0.28, 1);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
.histui-measurement-line[hidden] {
|
|
182
|
+
display: none;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
.histui-measurement-line.is-visible {
|
|
186
|
+
opacity: 1;
|
|
187
|
+
transform: translateY(0);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
.histui-measurement-line[data-orientation="vertical"] {
|
|
191
|
+
transform: translateX(-4px);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
.histui-measurement-line[data-orientation="vertical"].is-visible {
|
|
195
|
+
transform: translateX(0);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
.histui-measurement-rule {
|
|
199
|
+
position: absolute;
|
|
200
|
+
inset: 0;
|
|
201
|
+
color: var(--accent2);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
.histui-measurement-rule::before,
|
|
205
|
+
.histui-measurement-rule::after {
|
|
206
|
+
position: absolute;
|
|
207
|
+
background: color-mix(in srgb, var(--accent2) 72%, var(--line));
|
|
208
|
+
content: "";
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
.histui-measurement-line[data-orientation="horizontal"] .histui-measurement-rule {
|
|
212
|
+
top: 50%;
|
|
213
|
+
right: 0;
|
|
214
|
+
left: 0;
|
|
215
|
+
height: 0;
|
|
216
|
+
border-top: 1px dashed color-mix(in srgb, var(--accent2) 72%, var(--line));
|
|
217
|
+
box-shadow: 0 0 18px color-mix(in srgb, var(--accent2) 12%, transparent);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
.histui-measurement-line[data-orientation="horizontal"] .histui-measurement-rule::before,
|
|
221
|
+
.histui-measurement-line[data-orientation="horizontal"] .histui-measurement-rule::after {
|
|
222
|
+
top: -7px;
|
|
223
|
+
width: 1px;
|
|
224
|
+
height: 14px;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
.histui-measurement-line[data-orientation="horizontal"] .histui-measurement-rule::before {
|
|
228
|
+
left: 0;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
.histui-measurement-line[data-orientation="horizontal"] .histui-measurement-rule::after {
|
|
232
|
+
right: 0;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
.histui-measurement-line[data-orientation="vertical"] .histui-measurement-rule {
|
|
236
|
+
top: 0;
|
|
237
|
+
bottom: 0;
|
|
238
|
+
left: 50%;
|
|
239
|
+
width: 0;
|
|
240
|
+
border-left: 1px dashed color-mix(in srgb, var(--accent2) 72%, var(--line));
|
|
241
|
+
box-shadow: 0 0 18px color-mix(in srgb, var(--accent2) 12%, transparent);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
.histui-measurement-line[data-orientation="vertical"] .histui-measurement-rule::before,
|
|
245
|
+
.histui-measurement-line[data-orientation="vertical"] .histui-measurement-rule::after {
|
|
246
|
+
left: -7px;
|
|
247
|
+
width: 14px;
|
|
248
|
+
height: 1px;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
.histui-measurement-line[data-orientation="vertical"] .histui-measurement-rule::before {
|
|
252
|
+
top: 0;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
.histui-measurement-line[data-orientation="vertical"] .histui-measurement-rule::after {
|
|
256
|
+
bottom: 0;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
.histui-measurement-arrow {
|
|
260
|
+
position: absolute;
|
|
261
|
+
width: 0;
|
|
262
|
+
height: 0;
|
|
263
|
+
filter: drop-shadow(0 0 8px color-mix(in srgb, var(--accent2) 22%, transparent));
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
.histui-measurement-line[data-orientation="horizontal"] .histui-measurement-arrow {
|
|
267
|
+
top: 50%;
|
|
268
|
+
border-top: 5px solid transparent;
|
|
269
|
+
border-bottom: 5px solid transparent;
|
|
270
|
+
transform: translateY(-50%);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
.histui-measurement-line[data-orientation="horizontal"] .histui-measurement-arrow-start {
|
|
274
|
+
left: 0;
|
|
275
|
+
border-left: 9px solid color-mix(in srgb, var(--accent2) 88%, var(--text));
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
.histui-measurement-line[data-orientation="horizontal"] .histui-measurement-arrow-end {
|
|
279
|
+
right: 0;
|
|
280
|
+
border-right: 9px solid color-mix(in srgb, var(--accent2) 88%, var(--text));
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
.histui-measurement-line[data-orientation="vertical"] .histui-measurement-arrow {
|
|
284
|
+
left: 50%;
|
|
285
|
+
border-right: 5px solid transparent;
|
|
286
|
+
border-left: 5px solid transparent;
|
|
287
|
+
transform: translateX(-50%);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
.histui-measurement-line[data-orientation="vertical"] .histui-measurement-arrow-start {
|
|
291
|
+
top: 0;
|
|
292
|
+
border-top: 9px solid color-mix(in srgb, var(--accent2) 88%, var(--text));
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
.histui-measurement-line[data-orientation="vertical"] .histui-measurement-arrow-end {
|
|
296
|
+
bottom: 0;
|
|
297
|
+
border-bottom: 9px solid color-mix(in srgb, var(--accent2) 88%, var(--text));
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
.histui-measurement-label {
|
|
301
|
+
position: absolute;
|
|
302
|
+
top: 50%;
|
|
303
|
+
left: 50%;
|
|
304
|
+
max-width: min(220px, calc(100vw - 72px));
|
|
305
|
+
padding: 5px 9px;
|
|
306
|
+
overflow: hidden;
|
|
307
|
+
border: 1px solid color-mix(in srgb, var(--accent2) 62%, var(--line));
|
|
308
|
+
border-radius: 999px;
|
|
309
|
+
background:
|
|
310
|
+
linear-gradient(180deg, color-mix(in srgb, var(--surface-raised) 92%, var(--accent2) 8%), color-mix(in srgb, var(--panel) 92%, transparent));
|
|
311
|
+
box-shadow:
|
|
312
|
+
0 10px 22px color-mix(in srgb, var(--shadow) 62%, transparent),
|
|
313
|
+
inset 0 0 0 1px color-mix(in srgb, var(--text) 8%, transparent);
|
|
314
|
+
color: var(--accent2);
|
|
315
|
+
font-family: var(--mono-font);
|
|
316
|
+
font-size: 11px;
|
|
317
|
+
line-height: 1;
|
|
318
|
+
text-align: center;
|
|
319
|
+
text-overflow: ellipsis;
|
|
320
|
+
transform: translate(-50%, -50%);
|
|
321
|
+
white-space: nowrap;
|
|
322
|
+
}
|
|
323
|
+
|
|
170
324
|
.event-card {
|
|
171
325
|
position: absolute;
|
|
172
326
|
left: var(--x);
|
package/src/timeline-view.js
CHANGED
|
@@ -64,6 +64,9 @@ export class TimelineView {
|
|
|
64
64
|
this.viewportAnimation = null;
|
|
65
65
|
this.motionTimer = 0;
|
|
66
66
|
this.explodeAnimationTimer = 0;
|
|
67
|
+
this.measurementFadeTimer = 0;
|
|
68
|
+
this.lastMeasurementKey = "";
|
|
69
|
+
this.suppressMeasurementChange = false;
|
|
67
70
|
this.lastFrame = 0;
|
|
68
71
|
this.lastMetrics = null;
|
|
69
72
|
this.lastItems = { all: [], display: [], hidden: [] };
|
|
@@ -73,6 +76,7 @@ export class TimelineView {
|
|
|
73
76
|
this.clusterTooltip.className = "cluster-tooltip";
|
|
74
77
|
this.clusterTooltip.hidden = true;
|
|
75
78
|
this.stage.append(this.clusterTooltip);
|
|
79
|
+
this.setupMeasurementLine();
|
|
76
80
|
this.stage.classList.toggle("is-explode-mode", this.explodeEnabled);
|
|
77
81
|
this.setupZoomBar();
|
|
78
82
|
|
|
@@ -177,6 +181,22 @@ export class TimelineView {
|
|
|
177
181
|
this.zoomBar.addEventListener("keydown", (event) => this.handleZoomKeydown(event));
|
|
178
182
|
}
|
|
179
183
|
|
|
184
|
+
setupMeasurementLine() {
|
|
185
|
+
this.measurementLine = document.createElement("div");
|
|
186
|
+
this.measurementLine.className = "histui-measurement-line";
|
|
187
|
+
this.measurementLine.hidden = true;
|
|
188
|
+
this.measurementLine.setAttribute("aria-hidden", "true");
|
|
189
|
+
this.measurementLine.innerHTML = `
|
|
190
|
+
<span class="histui-measurement-rule" aria-hidden="true">
|
|
191
|
+
<span class="histui-measurement-arrow histui-measurement-arrow-start"></span>
|
|
192
|
+
<span class="histui-measurement-arrow histui-measurement-arrow-end"></span>
|
|
193
|
+
</span>
|
|
194
|
+
<span class="histui-measurement-label"></span>
|
|
195
|
+
`;
|
|
196
|
+
this.measurementLabel = this.measurementLine.querySelector(".histui-measurement-label");
|
|
197
|
+
this.stage.append(this.measurementLine);
|
|
198
|
+
}
|
|
199
|
+
|
|
180
200
|
setTranslator(t) {
|
|
181
201
|
this.t = t;
|
|
182
202
|
if (this.zoomBar) {
|
|
@@ -231,7 +251,19 @@ export class TimelineView {
|
|
|
231
251
|
this.render();
|
|
232
252
|
}
|
|
233
253
|
|
|
254
|
+
setMeasurementOptions(options = {}) {
|
|
255
|
+
this.config.timeline = this.config.timeline || {};
|
|
256
|
+
this.config.timeline.measurement = {
|
|
257
|
+
...(this.config.timeline.measurement || {}),
|
|
258
|
+
...options
|
|
259
|
+
};
|
|
260
|
+
this.lastMeasurementKey = "";
|
|
261
|
+
if (!this.getMeasurementConfig().enabled) this.hideMeasurementLine({ immediate: true });
|
|
262
|
+
this.render();
|
|
263
|
+
}
|
|
264
|
+
|
|
234
265
|
setRecords(records, { resetView = false } = {}) {
|
|
266
|
+
this.suppressMeasurementChange = true;
|
|
235
267
|
this.records = records;
|
|
236
268
|
this.idMap = new Map(records.map((record) => [record.id, record]));
|
|
237
269
|
this.hoveredClusterId = null;
|
|
@@ -860,6 +892,7 @@ export class TimelineView {
|
|
|
860
892
|
this.drawClusters(metrics, colors, items.hidden);
|
|
861
893
|
this.renderClusterTooltip(metrics);
|
|
862
894
|
this.renderZoomBar(colors);
|
|
895
|
+
this.renderMeasurementLine(metrics);
|
|
863
896
|
if (renderCards) this.renderCards(metrics, items.display);
|
|
864
897
|
else this.updateCardHighlightClasses();
|
|
865
898
|
this.renderHint(metrics, items);
|
|
@@ -1486,6 +1519,112 @@ export class TimelineView {
|
|
|
1486
1519
|
this.clusterTooltip.hidden = false;
|
|
1487
1520
|
}
|
|
1488
1521
|
|
|
1522
|
+
renderMeasurementLine(metrics) {
|
|
1523
|
+
if (!this.measurementLine || !this.measurementLabel) return;
|
|
1524
|
+
const measurement = this.getMeasurementConfig();
|
|
1525
|
+
this.stage.classList.toggle("has-measurement-line", measurement.enabled);
|
|
1526
|
+
|
|
1527
|
+
if (!measurement.enabled || metrics.axisLength < 80) {
|
|
1528
|
+
this.hideMeasurementLine({ immediate: true });
|
|
1529
|
+
return;
|
|
1530
|
+
}
|
|
1531
|
+
|
|
1532
|
+
const span = Math.max(1, Math.round(this.view.end - this.view.start));
|
|
1533
|
+
this.measurementLabel.textContent = this.t("zoomLevel", { span });
|
|
1534
|
+
this.measurementLine.dataset.orientation = metrics.orientation;
|
|
1535
|
+
this.measurementLine.hidden = false;
|
|
1536
|
+
|
|
1537
|
+
if (metrics.orientation === "horizontal") {
|
|
1538
|
+
const y = this.measurementCoordinate(metrics, measurement);
|
|
1539
|
+
this.measurementLine.style.left = `${metrics.axisStart}px`;
|
|
1540
|
+
this.measurementLine.style.top = `${y - 16}px`;
|
|
1541
|
+
this.measurementLine.style.width = `${metrics.axisLength}px`;
|
|
1542
|
+
this.measurementLine.style.height = "32px";
|
|
1543
|
+
} else {
|
|
1544
|
+
const x = this.measurementCoordinate(metrics, measurement);
|
|
1545
|
+
this.measurementLine.style.left = `${x - 16}px`;
|
|
1546
|
+
this.measurementLine.style.top = `${metrics.axisStart}px`;
|
|
1547
|
+
this.measurementLine.style.width = "32px";
|
|
1548
|
+
this.measurementLine.style.height = `${metrics.axisLength}px`;
|
|
1549
|
+
}
|
|
1550
|
+
|
|
1551
|
+
if (!measurement.transient) {
|
|
1552
|
+
this.showMeasurementLine({ persistent: true });
|
|
1553
|
+
return;
|
|
1554
|
+
}
|
|
1555
|
+
|
|
1556
|
+
const key = [
|
|
1557
|
+
metrics.orientation,
|
|
1558
|
+
Math.round(this.view.start * 1000) / 1000,
|
|
1559
|
+
Math.round(this.view.end * 1000) / 1000,
|
|
1560
|
+
metrics.axisLength
|
|
1561
|
+
].join(":");
|
|
1562
|
+
|
|
1563
|
+
if (!this.lastMeasurementKey || this.suppressMeasurementChange) {
|
|
1564
|
+
this.lastMeasurementKey = key;
|
|
1565
|
+
this.suppressMeasurementChange = false;
|
|
1566
|
+
this.hideMeasurementLine();
|
|
1567
|
+
return;
|
|
1568
|
+
}
|
|
1569
|
+
|
|
1570
|
+
if (key !== this.lastMeasurementKey) {
|
|
1571
|
+
this.lastMeasurementKey = key;
|
|
1572
|
+
this.showMeasurementLine({ fadeOutMs: measurement.fadeOutMs });
|
|
1573
|
+
}
|
|
1574
|
+
}
|
|
1575
|
+
|
|
1576
|
+
getMeasurementConfig() {
|
|
1577
|
+
const measurement = this.config.timeline?.measurement || {};
|
|
1578
|
+
const fadeOutMs = Number(measurement.fadeOutMs ?? measurement.hideAfterMs ?? 1200);
|
|
1579
|
+
return {
|
|
1580
|
+
enabled: measurement.enabled === true,
|
|
1581
|
+
transient: measurement.transient === true ||
|
|
1582
|
+
measurement.showOnChangeOnly === true ||
|
|
1583
|
+
measurement.visibleOnChangeOnly === true,
|
|
1584
|
+
fadeOutMs: Number.isFinite(fadeOutMs) ? Math.max(0, fadeOutMs) : 1200,
|
|
1585
|
+
offsetPx: Number.isFinite(Number(measurement.offsetPx)) ? Number(measurement.offsetPx) : null
|
|
1586
|
+
};
|
|
1587
|
+
}
|
|
1588
|
+
|
|
1589
|
+
measurementCoordinate(metrics, measurement) {
|
|
1590
|
+
const offset = clamp(measurement.offsetPx ?? 32, 20, 110);
|
|
1591
|
+
if (metrics.orientation === "horizontal") {
|
|
1592
|
+
if (metrics.placement === "side-start") return clamp(metrics.axisCoordinate + offset, 18, metrics.height - 18);
|
|
1593
|
+
if (metrics.placement === "side-end") return clamp(metrics.axisCoordinate - offset, 18, metrics.height - 18);
|
|
1594
|
+
return clamp(offset, 18, metrics.height - 18);
|
|
1595
|
+
}
|
|
1596
|
+
|
|
1597
|
+
if (metrics.placement === "center") {
|
|
1598
|
+
return clamp(this.direction === "rtl" ? metrics.width - offset : offset, 18, metrics.width - 18);
|
|
1599
|
+
}
|
|
1600
|
+
|
|
1601
|
+
const side = metrics.axisCoordinate < metrics.width / 2 ? 1 : -1;
|
|
1602
|
+
return clamp(metrics.axisCoordinate + side * offset, 18, metrics.width - 18);
|
|
1603
|
+
}
|
|
1604
|
+
|
|
1605
|
+
showMeasurementLine({ persistent = false, fadeOutMs = 1200 } = {}) {
|
|
1606
|
+
if (!this.measurementLine) return;
|
|
1607
|
+
const wasHidden = this.measurementLine.hidden;
|
|
1608
|
+
this.measurementLine.hidden = false;
|
|
1609
|
+
if (wasHidden) void this.measurementLine.offsetWidth;
|
|
1610
|
+
this.measurementLine.classList.add("is-visible");
|
|
1611
|
+
if (this.measurementFadeTimer) window.clearTimeout(this.measurementFadeTimer);
|
|
1612
|
+
this.measurementFadeTimer = 0;
|
|
1613
|
+
if (persistent) return;
|
|
1614
|
+
this.measurementFadeTimer = window.setTimeout(() => {
|
|
1615
|
+
this.measurementFadeTimer = 0;
|
|
1616
|
+
this.hideMeasurementLine();
|
|
1617
|
+
}, fadeOutMs);
|
|
1618
|
+
}
|
|
1619
|
+
|
|
1620
|
+
hideMeasurementLine({ immediate = false } = {}) {
|
|
1621
|
+
if (!this.measurementLine) return;
|
|
1622
|
+
if (this.measurementFadeTimer) window.clearTimeout(this.measurementFadeTimer);
|
|
1623
|
+
this.measurementFadeTimer = 0;
|
|
1624
|
+
this.measurementLine.classList.remove("is-visible");
|
|
1625
|
+
if (immediate) this.measurementLine.hidden = true;
|
|
1626
|
+
}
|
|
1627
|
+
|
|
1489
1628
|
renderCards(metrics, displayItems) {
|
|
1490
1629
|
const axis = metrics.axisCoordinate;
|
|
1491
1630
|
const mode = this.getLod(this.view.end - this.view.start).labelMode;
|
|
@@ -1835,11 +1974,14 @@ export class TimelineView {
|
|
|
1835
1974
|
if (this.viewportAnimationFrame) cancelAnimationFrame(this.viewportAnimationFrame);
|
|
1836
1975
|
if (this.motionTimer) window.clearTimeout(this.motionTimer);
|
|
1837
1976
|
if (this.explodeAnimationTimer) window.clearTimeout(this.explodeAnimationTimer);
|
|
1977
|
+
if (this.measurementFadeTimer) window.clearTimeout(this.measurementFadeTimer);
|
|
1838
1978
|
this.clusterTooltip?.remove();
|
|
1979
|
+
this.measurementLine?.remove();
|
|
1839
1980
|
this.animationFrame = 0;
|
|
1840
1981
|
this.viewportAnimationFrame = 0;
|
|
1841
1982
|
this.motionTimer = 0;
|
|
1842
1983
|
this.explodeAnimationTimer = 0;
|
|
1984
|
+
this.measurementFadeTimer = 0;
|
|
1843
1985
|
}
|
|
1844
1986
|
}
|
|
1845
1987
|
|