@design.estate/dees-catalog 12.4.0 → 13.0.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.
- package/dist_bundle/bundle.js +2603 -1201
- package/dist_bundle/bundle.js.map +1 -1
- package/dist_ts_web/00_commitinfo_data.js +3 -3
- package/dist_ts_web/elements/00group-chart/dees-chart-area/component.d.ts +26 -16
- package/dist_ts_web/elements/00group-chart/dees-chart-area/component.js +161 -72
- package/dist_ts_web/elements/00group-chart/dees-chart-area/realtime-updates.d.ts +2 -1
- package/dist_ts_web/elements/00group-chart/dees-chart-area/realtime-updates.js +3 -2
- package/dist_ts_web/elements/00group-chart/dees-chart-area/template.js +4 -3
- package/dist_ts_web/elements/00group-input/dees-input-dropdown/dees-input-dropdown-popup.d.ts +13 -0
- package/dist_ts_web/elements/00group-input/dees-input-dropdown/dees-input-dropdown-popup.js +76 -7
- package/dist_ts_web/elements/00group-input/dees-input-reference/dees-input-reference.d.ts +42 -0
- package/dist_ts_web/elements/00group-input/dees-input-reference/dees-input-reference.demo.d.ts +1 -0
- package/dist_ts_web/elements/00group-input/dees-input-reference/dees-input-reference.demo.js +8 -0
- package/dist_ts_web/elements/00group-input/dees-input-reference/dees-input-reference.js +347 -0
- package/dist_ts_web/elements/00group-input/dees-input-reference/document.d.ts +5 -0
- package/dist_ts_web/elements/00group-input/dees-input-reference/document.js +45 -0
- package/dist_ts_web/elements/00group-input/dees-input-reference/index.d.ts +2 -0
- package/dist_ts_web/elements/00group-input/dees-input-reference/index.js +3 -0
- package/dist_ts_web/elements/00group-input/dees-input-reference/interfaces.d.ts +16 -0
- package/dist_ts_web/elements/00group-input/dees-input-reference/interfaces.js +2 -0
- package/dist_ts_web/elements/00group-input/index.d.ts +1 -0
- package/dist_ts_web/elements/00group-input/index.js +2 -1
- package/dist_ts_web/elements/00group-kanban/dees-kanban/dees-kanban.d.ts +58 -0
- package/dist_ts_web/elements/00group-kanban/dees-kanban/dees-kanban.demo.d.ts +1 -0
- package/dist_ts_web/elements/00group-kanban/dees-kanban/dees-kanban.demo.js +9 -0
- package/dist_ts_web/elements/00group-kanban/dees-kanban/dees-kanban.js +536 -0
- package/dist_ts_web/elements/00group-kanban/dees-kanban-card/dees-kanban-card.d.ts +20 -0
- package/dist_ts_web/elements/00group-kanban/dees-kanban-card/dees-kanban-card.js +157 -0
- package/dist_ts_web/elements/00group-kanban/dees-kanban-column/dees-kanban-column.d.ts +23 -0
- package/dist_ts_web/elements/00group-kanban/dees-kanban-column/dees-kanban-column.js +192 -0
- package/dist_ts_web/elements/00group-kanban/dees-kanban-discussion/dees-kanban-discussion.d.ts +24 -0
- package/dist_ts_web/elements/00group-kanban/dees-kanban-discussion/dees-kanban-discussion.js +211 -0
- package/dist_ts_web/elements/00group-kanban/dees-kanban-ticket-detail/dees-kanban-ticket-detail.d.ts +35 -0
- package/dist_ts_web/elements/00group-kanban/dees-kanban-ticket-detail/dees-kanban-ticket-detail.js +269 -0
- package/dist_ts_web/elements/00group-kanban/index.d.ts +6 -0
- package/dist_ts_web/elements/00group-kanban/index.js +7 -0
- package/dist_ts_web/elements/00group-kanban/interfaces.d.ts +84 -0
- package/dist_ts_web/elements/00group-kanban/interfaces.js +2 -0
- package/dist_ts_web/elements/00group-kanban/kanban.demodata.d.ts +7 -0
- package/dist_ts_web/elements/00group-kanban/kanban.demodata.js +60 -0
- package/dist_ts_web/elements/00group-kanban/kanban.helpers.d.ts +6 -0
- package/dist_ts_web/elements/00group-kanban/kanban.helpers.js +11 -0
- package/dist_ts_web/elements/00group-kanban/kanban.styles.d.ts +1 -0
- package/dist_ts_web/elements/00group-kanban/kanban.styles.js +18 -0
- package/dist_ts_web/elements/index.d.ts +1 -0
- package/dist_ts_web/elements/index.js +2 -1
- package/dist_ts_web/services/DeesServiceLibLoader.d.ts +1 -0
- package/dist_ts_web/services/DeesServiceLibLoader.js +2 -1
- package/package.json +1 -1
- package/readme.md +165 -1
- package/scripts/check-packed-consumer.mjs +1 -1
- package/ts_web/00_commitinfo_data.ts +3 -3
- package/ts_web/elements/00group-chart/dees-chart-area/component.ts +195 -79
- package/ts_web/elements/00group-chart/dees-chart-area/realtime-updates.ts +4 -2
- package/ts_web/elements/00group-chart/dees-chart-area/template.ts +3 -2
- package/ts_web/elements/00group-input/dees-input-dropdown/dees-input-dropdown-popup.ts +40 -5
- package/ts_web/elements/00group-input/dees-input-reference/dees-input-reference.demo.ts +9 -0
- package/ts_web/elements/00group-input/dees-input-reference/dees-input-reference.ts +221 -0
- package/ts_web/elements/00group-input/dees-input-reference/document.ts +38 -0
- package/ts_web/elements/00group-input/dees-input-reference/index.ts +2 -0
- package/ts_web/elements/00group-input/dees-input-reference/interfaces.ts +8 -0
- package/ts_web/elements/00group-input/index.ts +1 -0
- package/ts_web/elements/00group-kanban/dees-kanban/dees-kanban.demo.ts +9 -0
- package/ts_web/elements/00group-kanban/dees-kanban/dees-kanban.ts +315 -0
- package/ts_web/elements/00group-kanban/dees-kanban-card/dees-kanban-card.ts +58 -0
- package/ts_web/elements/00group-kanban/dees-kanban-column/dees-kanban-column.ts +60 -0
- package/ts_web/elements/00group-kanban/dees-kanban-discussion/dees-kanban-discussion.ts +81 -0
- package/ts_web/elements/00group-kanban/dees-kanban-ticket-detail/dees-kanban-ticket-detail.ts +114 -0
- package/ts_web/elements/00group-kanban/index.ts +6 -0
- package/ts_web/elements/00group-kanban/interfaces.ts +67 -0
- package/ts_web/elements/00group-kanban/kanban.demodata.ts +56 -0
- package/ts_web/elements/00group-kanban/kanban.helpers.ts +13 -0
- package/ts_web/elements/00group-kanban/kanban.styles.ts +18 -0
- package/ts_web/elements/index.ts +1 -0
- package/ts_web/services/DeesServiceLibLoader.ts +2 -0
|
@@ -19,7 +19,14 @@ import { getEchartsSeriesColors, hexToRgba, getThemeColors } from '../dees-chart
|
|
|
19
19
|
|
|
20
20
|
import { systemFontFamily } from '../../00fonts.js';
|
|
21
21
|
|
|
22
|
-
import type {
|
|
22
|
+
import type {
|
|
23
|
+
AreaData,
|
|
24
|
+
IChartApi,
|
|
25
|
+
ISeriesApi,
|
|
26
|
+
MouseEventParams,
|
|
27
|
+
UTCTimestamp,
|
|
28
|
+
WhitespaceData,
|
|
29
|
+
} from 'lightweight-charts';
|
|
23
30
|
import { DeesServiceLibLoader, type ILightweightChartsBundle } from '../../../services/index.js';
|
|
24
31
|
import '../../00group-layout/dees-tile/dees-tile.js';
|
|
25
32
|
|
|
@@ -27,9 +34,28 @@ export type ChartSeriesConfig = {
|
|
|
27
34
|
name?: string;
|
|
28
35
|
/** Explicit series color (hex or hsl). Falls back to the shared catalog palette by index. */
|
|
29
36
|
color?: string;
|
|
30
|
-
|
|
37
|
+
/** A null reading keeps its timestamp and breaks the drawn area line. */
|
|
38
|
+
data: Array<{ x: any; y: number | null }>;
|
|
31
39
|
}[];
|
|
32
40
|
|
|
41
|
+
export interface IChartSeriesStats {
|
|
42
|
+
name: string;
|
|
43
|
+
/** Null when the newest sample is unavailable, even if older samples exist. */
|
|
44
|
+
latest: number | null;
|
|
45
|
+
min: number | null;
|
|
46
|
+
max: number | null;
|
|
47
|
+
avg: number | null;
|
|
48
|
+
color: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
type TChartAreaData = AreaData<UTCTimestamp> | WhitespaceData<UTCTimestamp>;
|
|
52
|
+
|
|
53
|
+
interface IChartRenderedSegment {
|
|
54
|
+
key: string;
|
|
55
|
+
logicalIndex: number;
|
|
56
|
+
data: TChartAreaData[];
|
|
57
|
+
}
|
|
58
|
+
|
|
33
59
|
export type TChartLegendStat = 'latest' | 'min' | 'max' | 'avg';
|
|
34
60
|
export type TChartPriceLine = 'avg' | 'max';
|
|
35
61
|
|
|
@@ -55,7 +81,7 @@ export class DeesChartArea extends DeesElement {
|
|
|
55
81
|
accessor chart: IChartApi | null = null;
|
|
56
82
|
|
|
57
83
|
@state()
|
|
58
|
-
accessor seriesStats:
|
|
84
|
+
accessor seriesStats: IChartSeriesStats[] = [];
|
|
59
85
|
|
|
60
86
|
@state()
|
|
61
87
|
accessor isFullPage: boolean = false;
|
|
@@ -117,6 +143,9 @@ export class DeesChartArea extends DeesElement {
|
|
|
117
143
|
private autoScrollTimer: number | null = null;
|
|
118
144
|
private lcBundle: ILightweightChartsBundle | null = null;
|
|
119
145
|
private seriesApis: Map<string, ISeriesApi<any>> = new Map();
|
|
146
|
+
/** LC's data() omits whitespace; this mirrors the exact points sent to each series API. */
|
|
147
|
+
private renderedSeriesData: Map<string, IRealtimeChartPoint[]> = new Map();
|
|
148
|
+
private renderedSegments: IChartRenderedSegment[] = [];
|
|
120
149
|
private priceLines: Map<string, any[]> = new Map();
|
|
121
150
|
private tooltipEl: HTMLDivElement | null = null;
|
|
122
151
|
private crosshairActive = false;
|
|
@@ -219,6 +248,8 @@ export class DeesChartArea extends DeesElement {
|
|
|
219
248
|
console.error('Error destroying chart:', error);
|
|
220
249
|
} finally {
|
|
221
250
|
this.seriesApis.clear();
|
|
251
|
+
this.renderedSeriesData.clear();
|
|
252
|
+
this.renderedSegments = [];
|
|
222
253
|
this.priceLines.clear();
|
|
223
254
|
}
|
|
224
255
|
});
|
|
@@ -316,15 +347,17 @@ export class DeesChartArea extends DeesElement {
|
|
|
316
347
|
this.paintRangeOverlay(fromCoordinate, toCoordinate);
|
|
317
348
|
}
|
|
318
349
|
|
|
319
|
-
private convertDataToLC(data:
|
|
320
|
-
const pointsByTime = new Map<number,
|
|
350
|
+
private convertDataToLC(data: ChartSeriesConfig[number]['data']): TChartAreaData[] {
|
|
351
|
+
const pointsByTime = new Map<number, TChartAreaData>();
|
|
321
352
|
for (const point of data) {
|
|
322
353
|
const ms = typeof point.x === 'number' ? point.x : new Date(point.x).getTime();
|
|
323
354
|
const time = Math.floor(ms / 1000);
|
|
324
|
-
if (!Number.isFinite(time) || !Number.isFinite(point.y)) continue;
|
|
355
|
+
if (!Number.isFinite(time) || (point.y !== null && !Number.isFinite(point.y))) continue;
|
|
325
356
|
// Lightweight Charts requires unique ordered timestamps. The latest
|
|
326
|
-
// sample
|
|
327
|
-
pointsByTime.set(time,
|
|
357
|
+
// sample, including an unavailable one, wins within the same second.
|
|
358
|
+
pointsByTime.set(time, point.y === null
|
|
359
|
+
? { time: time as UTCTimestamp }
|
|
360
|
+
: { time: time as UTCTimestamp, value: point.y });
|
|
328
361
|
}
|
|
329
362
|
return [...pointsByTime.values()]
|
|
330
363
|
.sort((a, b) => (a.time as number) - (b.time as number));
|
|
@@ -338,6 +371,45 @@ export class DeesChartArea extends DeesElement {
|
|
|
338
371
|
return `${index}:${this.getSeriesName(series, index)}`;
|
|
339
372
|
}
|
|
340
373
|
|
|
374
|
+
/**
|
|
375
|
+
* Lightweight Charts joins Area points across whitespace. Render each
|
|
376
|
+
* uninterrupted numeric run as its own Area series instead. All missing
|
|
377
|
+
* timestamps stay on the first real series so the time scale keeps them;
|
|
378
|
+
* there is no synthetic numeric value or extra public legend series.
|
|
379
|
+
*/
|
|
380
|
+
private getRenderedSegments(chartSeries: ChartSeriesConfig): IChartRenderedSegment[] {
|
|
381
|
+
const renderedSegments: IChartRenderedSegment[] = [];
|
|
382
|
+
chartSeries.forEach((series, logicalIndex) => {
|
|
383
|
+
const points = this.convertDataToLC(series.data);
|
|
384
|
+
const missingPoints = points.filter((point) => !('value' in point));
|
|
385
|
+
const numericRuns: TChartAreaData[][] = [];
|
|
386
|
+
let currentRun: TChartAreaData[] = [];
|
|
387
|
+
for (const point of points) {
|
|
388
|
+
if ('value' in point) {
|
|
389
|
+
currentRun.push(point);
|
|
390
|
+
} else if (currentRun.length > 0) {
|
|
391
|
+
numericRuns.push(currentRun);
|
|
392
|
+
currentRun = [];
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
if (currentRun.length > 0) numericRuns.push(currentRun);
|
|
396
|
+
if (numericRuns.length === 0) numericRuns.push([]);
|
|
397
|
+
|
|
398
|
+
const baseKey = this.getSeriesKey(series, logicalIndex);
|
|
399
|
+
numericRuns.forEach((run, segmentIndex) => {
|
|
400
|
+
const data = segmentIndex === 0
|
|
401
|
+
? [...run, ...missingPoints].sort((left, right) => left.time - right.time)
|
|
402
|
+
: run;
|
|
403
|
+
renderedSegments.push({
|
|
404
|
+
key: segmentIndex === 0 ? baseKey : `${baseKey}#${segmentIndex}`,
|
|
405
|
+
logicalIndex,
|
|
406
|
+
data,
|
|
407
|
+
});
|
|
408
|
+
});
|
|
409
|
+
});
|
|
410
|
+
return renderedSegments;
|
|
411
|
+
}
|
|
412
|
+
|
|
341
413
|
private getCanonicalChartSeries(chartSeries: ChartSeriesConfig): ChartSeriesConfig {
|
|
342
414
|
const cutoffTime = Date.now() - this.rollingWindow;
|
|
343
415
|
return chartSeries.map((series) => ({
|
|
@@ -348,15 +420,34 @@ export class DeesChartArea extends DeesElement {
|
|
|
348
420
|
return ms > cutoffTime;
|
|
349
421
|
})).map((point) => ({
|
|
350
422
|
x: (point.time as number) * 1000,
|
|
351
|
-
y: point.value,
|
|
423
|
+
y: 'value' in point ? point.value : null,
|
|
352
424
|
})),
|
|
353
425
|
}));
|
|
354
426
|
}
|
|
355
427
|
|
|
356
|
-
private getRenderedSeriesData(
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
428
|
+
private getRenderedSeriesData(segmentKey: string): IRealtimeChartPoint[] {
|
|
429
|
+
const renderedData = this.renderedSeriesData.get(segmentKey);
|
|
430
|
+
if (!renderedData) throw new Error(`Missing rendered chart data for ${segmentKey}`);
|
|
431
|
+
return renderedData;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
private rememberRenderedSeriesData(segmentKey: string, data: readonly TChartAreaData[]): void {
|
|
435
|
+
this.renderedSeriesData.set(segmentKey, data
|
|
436
|
+
.map((point) => 'value' in point
|
|
437
|
+
? { time: point.time as number, value: point.value }
|
|
438
|
+
: { time: point.time as number }));
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
private rememberIncrementalUpdate(segmentKey: string, point: IRealtimeChartPoint): void {
|
|
442
|
+
const renderedData = this.getRenderedSeriesData(segmentKey);
|
|
443
|
+
const existingIndex = renderedData.findIndex((entry) => entry.time === point.time);
|
|
444
|
+
if (existingIndex >= 0) renderedData[existingIndex] = point;
|
|
445
|
+
else renderedData.push(point);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
private toLcPoint(point: IRealtimeChartPoint): TChartAreaData {
|
|
449
|
+
const time = point.time as UTCTimestamp;
|
|
450
|
+
return point.value === undefined ? { time } : { time, value: point.value };
|
|
360
451
|
}
|
|
361
452
|
|
|
362
453
|
private colorToRgba(color: string, alpha: number): string {
|
|
@@ -423,19 +514,21 @@ export class DeesChartArea extends DeesElement {
|
|
|
423
514
|
},
|
|
424
515
|
} as any);
|
|
425
516
|
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
517
|
+
for (const segment of this.renderedSegments) {
|
|
518
|
+
const api = this.seriesApis.get(segment.key);
|
|
519
|
+
if (!api) continue;
|
|
520
|
+
const color = this.resolveSeriesColor(
|
|
521
|
+
this.chartSeries[segment.logicalIndex], segment.logicalIndex, isDark,
|
|
522
|
+
);
|
|
429
523
|
api.applyOptions({
|
|
430
524
|
topColor: this.colorToRgba(color, isDark ? 0.4 : 0.5),
|
|
431
525
|
bottomColor: this.colorToRgba(color, 0),
|
|
432
526
|
lineColor: color,
|
|
433
527
|
});
|
|
434
|
-
idx++;
|
|
435
528
|
}
|
|
436
529
|
}
|
|
437
530
|
|
|
438
|
-
private recreateSeries(chartSeries: ChartSeriesConfig) {
|
|
531
|
+
private recreateSeries(chartSeries: ChartSeriesConfig, renderedSegments = this.getRenderedSegments(chartSeries)) {
|
|
439
532
|
if (!this.chart || !this.lcBundle) return;
|
|
440
533
|
const isDark = !this.goBright;
|
|
441
534
|
|
|
@@ -443,10 +536,13 @@ export class DeesChartArea extends DeesElement {
|
|
|
443
536
|
this.chart.removeSeries(api);
|
|
444
537
|
}
|
|
445
538
|
this.seriesApis.clear();
|
|
539
|
+
this.renderedSeriesData.clear();
|
|
446
540
|
this.priceLines.clear();
|
|
541
|
+
this.renderedSegments = renderedSegments;
|
|
447
542
|
|
|
448
|
-
|
|
449
|
-
const
|
|
543
|
+
renderedSegments.forEach((segment) => {
|
|
544
|
+
const series = chartSeries[segment.logicalIndex];
|
|
545
|
+
const color = this.resolveSeriesColor(series, segment.logicalIndex, isDark);
|
|
450
546
|
const api = this.chart!.addSeries(this.lcBundle!.AreaSeries, {
|
|
451
547
|
topColor: this.colorToRgba(color, isDark ? 0.4 : 0.5),
|
|
452
548
|
bottomColor: this.colorToRgba(color, 0),
|
|
@@ -459,10 +555,12 @@ export class DeesChartArea extends DeesElement {
|
|
|
459
555
|
crosshairMarkerRadius: 4,
|
|
460
556
|
});
|
|
461
557
|
|
|
462
|
-
api.setData(
|
|
463
|
-
this.
|
|
464
|
-
|
|
465
|
-
|
|
558
|
+
api.setData(segment.data);
|
|
559
|
+
this.seriesApis.set(segment.key, api);
|
|
560
|
+
this.rememberRenderedSeriesData(segment.key, segment.data);
|
|
561
|
+
if (segment.key === this.getSeriesKey(series, segment.logicalIndex)) {
|
|
562
|
+
this.updatePriceLines(segment.key, api, series.data, color);
|
|
563
|
+
}
|
|
466
564
|
});
|
|
467
565
|
this.applyYAxisScale(chartSeries);
|
|
468
566
|
this.computeStats(chartSeries);
|
|
@@ -477,17 +575,19 @@ export class DeesChartArea extends DeesElement {
|
|
|
477
575
|
|
|
478
576
|
private updateSeriesPresentation(chartSeries: ChartSeriesConfig) {
|
|
479
577
|
const isDark = !this.goBright;
|
|
480
|
-
|
|
481
|
-
const
|
|
482
|
-
const api = this.seriesApis.get(
|
|
578
|
+
this.renderedSegments.forEach((segment) => {
|
|
579
|
+
const series = chartSeries[segment.logicalIndex];
|
|
580
|
+
const api = this.seriesApis.get(segment.key);
|
|
483
581
|
if (!api) return;
|
|
484
|
-
const color = this.resolveSeriesColor(series,
|
|
582
|
+
const color = this.resolveSeriesColor(series, segment.logicalIndex, isDark);
|
|
485
583
|
api.applyOptions({
|
|
486
584
|
topColor: this.colorToRgba(color, isDark ? 0.4 : 0.5),
|
|
487
585
|
bottomColor: this.colorToRgba(color, 0),
|
|
488
586
|
lineColor: color,
|
|
489
587
|
});
|
|
490
|
-
this.
|
|
588
|
+
if (segment.key === this.getSeriesKey(series, segment.logicalIndex)) {
|
|
589
|
+
this.updatePriceLines(segment.key, api, series.data, color);
|
|
590
|
+
}
|
|
491
591
|
});
|
|
492
592
|
this.computeStats(chartSeries);
|
|
493
593
|
|
|
@@ -500,7 +600,7 @@ export class DeesChartArea extends DeesElement {
|
|
|
500
600
|
if (this.yAxisScaling === 'dynamic') {
|
|
501
601
|
maxValue = 1;
|
|
502
602
|
for (const entry of series) for (const point of entry.data) {
|
|
503
|
-
if (!Number.isFinite(point.y)) continue;
|
|
603
|
+
if (point.y === null || !Number.isFinite(point.y)) continue;
|
|
504
604
|
minValue = Math.min(minValue, Math.floor(point.y * 1.1));
|
|
505
605
|
maxValue = Math.max(maxValue, Math.ceil(point.y * 1.1));
|
|
506
606
|
}
|
|
@@ -533,22 +633,39 @@ export class DeesChartArea extends DeesElement {
|
|
|
533
633
|
) return;
|
|
534
634
|
|
|
535
635
|
const canonicalSeries = this.getCanonicalChartSeries(this.internalChartData);
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
636
|
+
const canonicalSegments = this.getRenderedSegments(canonicalSeries);
|
|
637
|
+
if (!haveSameSeriesLayout(
|
|
638
|
+
[...this.seriesApis.keys()], canonicalSegments.map((segment) => segment.key),
|
|
639
|
+
)) {
|
|
640
|
+
this.clearCrosshairForDataReplacement();
|
|
641
|
+
this.internalChartData = canonicalSeries;
|
|
642
|
+
this.recreateSeries(canonicalSeries, canonicalSegments);
|
|
643
|
+
return;
|
|
644
|
+
}
|
|
645
|
+
canonicalSegments.forEach((segment) => {
|
|
646
|
+
const api = this.seriesApis.get(segment.key);
|
|
647
|
+
if (api) {
|
|
648
|
+
api.setData(segment.data);
|
|
649
|
+
this.rememberRenderedSeriesData(segment.key, segment.data);
|
|
650
|
+
}
|
|
539
651
|
});
|
|
652
|
+
this.internalChartData = canonicalSeries;
|
|
653
|
+
this.renderedSegments = canonicalSegments;
|
|
540
654
|
this.pendingRealtimePrune = false;
|
|
655
|
+
this.updateSeriesPresentation(canonicalSeries);
|
|
541
656
|
}
|
|
542
657
|
|
|
543
658
|
private computeStats(chartSeries: ChartSeriesConfig) {
|
|
544
659
|
const isDark = !this.goBright;
|
|
545
660
|
this.seriesStats = chartSeries.map((s, index) => {
|
|
546
661
|
const color = this.resolveSeriesColor(s, index, isDark);
|
|
547
|
-
const values = s.data
|
|
662
|
+
const values = s.data
|
|
663
|
+
.map((point) => point.y)
|
|
664
|
+
.filter((value): value is number => value !== null && Number.isFinite(value));
|
|
665
|
+
const latest = s.data.length > 0 ? s.data[s.data.length - 1].y : null;
|
|
548
666
|
if (values.length === 0) {
|
|
549
|
-
return { name: s.name || `series-${index}`, latest:
|
|
667
|
+
return { name: s.name || `series-${index}`, latest: null, min: null, max: null, avg: null, color };
|
|
550
668
|
}
|
|
551
|
-
const latest = values[values.length - 1];
|
|
552
669
|
const min = Math.min(...values);
|
|
553
670
|
const max = Math.max(...values);
|
|
554
671
|
const avg = Math.round((values.reduce((sum, v) => sum + v, 0) / values.length) * 100) / 100;
|
|
@@ -556,7 +673,7 @@ export class DeesChartArea extends DeesElement {
|
|
|
556
673
|
});
|
|
557
674
|
}
|
|
558
675
|
|
|
559
|
-
private updatePriceLines(name: string, api: ISeriesApi<any>, data:
|
|
676
|
+
private updatePriceLines(name: string, api: ISeriesApi<any>, data: ChartSeriesConfig[number]['data'], color: string) {
|
|
560
677
|
// Remove existing price lines for this series
|
|
561
678
|
const existing = this.priceLines.get(name);
|
|
562
679
|
if (existing) {
|
|
@@ -565,12 +682,14 @@ export class DeesChartArea extends DeesElement {
|
|
|
565
682
|
}
|
|
566
683
|
}
|
|
567
684
|
|
|
568
|
-
|
|
685
|
+
const values = data
|
|
686
|
+
.map((point) => point.y)
|
|
687
|
+
.filter((value): value is number => value !== null && Number.isFinite(value));
|
|
688
|
+
if (values.length === 0) {
|
|
569
689
|
this.priceLines.set(name, []);
|
|
570
690
|
return;
|
|
571
691
|
}
|
|
572
692
|
|
|
573
|
-
const values = data.map(d => d.y);
|
|
574
693
|
const avg = values.reduce((sum, v) => sum + v, 0) / values.length;
|
|
575
694
|
const max = Math.max(...values);
|
|
576
695
|
|
|
@@ -627,23 +746,24 @@ export class DeesChartArea extends DeesElement {
|
|
|
627
746
|
const borderColor = isDark ? 'hsl(0 0% 14.9%)' : 'hsl(0 0% 89.8%)';
|
|
628
747
|
|
|
629
748
|
let html = '';
|
|
630
|
-
let idx = 0;
|
|
631
749
|
let hasData = false;
|
|
632
|
-
for (const
|
|
750
|
+
for (const segment of this.renderedSegments) {
|
|
751
|
+
const api = this.seriesApis.get(segment.key);
|
|
752
|
+
if (!api) continue;
|
|
633
753
|
const data = param.seriesData.get(api);
|
|
634
|
-
if (data && 'value' in data &&
|
|
754
|
+
if (data && 'value' in data && typeof data.value === 'number') {
|
|
755
|
+
const marginTop = hasData ? '6px' : '0';
|
|
635
756
|
hasData = true;
|
|
636
|
-
const series = this.internalChartData[
|
|
637
|
-
const name =
|
|
638
|
-
const color = this.resolveSeriesColor(series,
|
|
639
|
-
const formatted = this.yAxisFormatter(
|
|
640
|
-
html += `<div style="display:flex;align-items:center;gap:8px;margin:${
|
|
757
|
+
const series = this.internalChartData[segment.logicalIndex];
|
|
758
|
+
const name = this.getSeriesName(series, segment.logicalIndex);
|
|
759
|
+
const color = this.resolveSeriesColor(series, segment.logicalIndex, isDark);
|
|
760
|
+
const formatted = this.yAxisFormatter(data.value);
|
|
761
|
+
html += `<div style="display:flex;align-items:center;gap:8px;margin:${marginTop} 0;">
|
|
641
762
|
<span style="display:inline-block;width:10px;height:10px;background:${color};border-radius:2px;"></span>
|
|
642
763
|
<span style="font-weight:500;">${name}:</span>
|
|
643
764
|
<span style="margin-left:auto;font-weight:600;">${formatted}</span>
|
|
644
765
|
</div>`;
|
|
645
766
|
}
|
|
646
|
-
idx++;
|
|
647
767
|
}
|
|
648
768
|
|
|
649
769
|
if (!hasData) {
|
|
@@ -786,55 +906,55 @@ export class DeesChartArea extends DeesElement {
|
|
|
786
906
|
try {
|
|
787
907
|
void animate;
|
|
788
908
|
const canonicalSeries = this.getCanonicalChartSeries(newSeries);
|
|
909
|
+
const renderedSegments = this.getRenderedSegments(canonicalSeries);
|
|
789
910
|
this.internalChartData = canonicalSeries;
|
|
790
911
|
const renderedKeys = [...this.seriesApis.keys()];
|
|
791
|
-
const canonicalKeys =
|
|
912
|
+
const canonicalKeys = renderedSegments.map((segment) => segment.key);
|
|
792
913
|
|
|
793
914
|
if (!haveSameSeriesLayout(renderedKeys, canonicalKeys)) {
|
|
794
915
|
this.clearCrosshairForDataReplacement();
|
|
795
|
-
this.recreateSeries(canonicalSeries);
|
|
916
|
+
this.recreateSeries(canonicalSeries, renderedSegments);
|
|
796
917
|
this.updateSeriesPresentation(canonicalSeries);
|
|
797
918
|
return;
|
|
798
919
|
}
|
|
920
|
+
this.renderedSegments = renderedSegments;
|
|
799
921
|
|
|
800
922
|
if (!this.realtimeMode || this.rollingWindow <= 0) {
|
|
801
923
|
this.clearCrosshairForDataReplacement();
|
|
802
|
-
|
|
803
|
-
this.seriesApis.get(
|
|
804
|
-
|
|
924
|
+
renderedSegments.forEach((segment) => {
|
|
925
|
+
this.seriesApis.get(segment.key)!.setData(segment.data);
|
|
926
|
+
this.rememberRenderedSeriesData(segment.key, segment.data);
|
|
805
927
|
});
|
|
806
928
|
this.pendingRealtimePrune = false;
|
|
807
929
|
this.updateSeriesPresentation(canonicalSeries);
|
|
808
930
|
return;
|
|
809
931
|
}
|
|
810
932
|
|
|
811
|
-
const plannedUpdates =
|
|
812
|
-
const api = this.seriesApis.get(
|
|
813
|
-
const canonicalData =
|
|
814
|
-
|
|
933
|
+
const plannedUpdates = renderedSegments.map((segment) => {
|
|
934
|
+
const api = this.seriesApis.get(segment.key)!;
|
|
935
|
+
const canonicalData = segment.data.map((point) => 'value' in point
|
|
936
|
+
? { time: point.time as number, value: point.value }
|
|
937
|
+
: { time: point.time as number });
|
|
815
938
|
return {
|
|
939
|
+
key: segment.key,
|
|
816
940
|
api,
|
|
817
941
|
canonicalData,
|
|
818
|
-
plan: planRealtimeChartUpdates(this.getRenderedSeriesData(
|
|
942
|
+
plan: planRealtimeChartUpdates(this.getRenderedSeriesData(segment.key), canonicalData),
|
|
819
943
|
};
|
|
820
944
|
});
|
|
821
945
|
|
|
822
946
|
if (plannedUpdates.some(({ plan }) => plan.replacementRequired)) {
|
|
823
947
|
this.clearCrosshairForDataReplacement();
|
|
824
|
-
for (const { api, canonicalData } of plannedUpdates) {
|
|
825
|
-
api.setData(canonicalData.map((point) => (
|
|
826
|
-
|
|
827
|
-
value: point.value,
|
|
828
|
-
})));
|
|
948
|
+
for (const { key, api, canonicalData } of plannedUpdates) {
|
|
949
|
+
api.setData(canonicalData.map((point) => this.toLcPoint(point)));
|
|
950
|
+
this.renderedSeriesData.set(key, [...canonicalData]);
|
|
829
951
|
}
|
|
830
952
|
this.pendingRealtimePrune = false;
|
|
831
953
|
} else {
|
|
832
|
-
for (const { api, plan } of plannedUpdates) {
|
|
954
|
+
for (const { key, api, plan } of plannedUpdates) {
|
|
833
955
|
for (const update of plan.updates) {
|
|
834
|
-
api.update(
|
|
835
|
-
|
|
836
|
-
value: update.point.value,
|
|
837
|
-
}, update.historicalUpdate);
|
|
956
|
+
api.update(this.toLcPoint(update.point), update.historicalUpdate);
|
|
957
|
+
this.rememberIncrementalUpdate(key, update.point);
|
|
838
958
|
}
|
|
839
959
|
}
|
|
840
960
|
|
|
@@ -842,20 +962,16 @@ export class DeesChartArea extends DeesElement {
|
|
|
842
962
|
const pruneRecommended = plannedUpdates.some(({ plan }) => plan.pruneRecommended);
|
|
843
963
|
if (pruneRequired) {
|
|
844
964
|
this.clearCrosshairForDataReplacement();
|
|
845
|
-
for (const { api, canonicalData } of plannedUpdates) {
|
|
846
|
-
api.setData(canonicalData.map((point) => (
|
|
847
|
-
|
|
848
|
-
value: point.value,
|
|
849
|
-
})));
|
|
965
|
+
for (const { key, api, canonicalData } of plannedUpdates) {
|
|
966
|
+
api.setData(canonicalData.map((point) => this.toLcPoint(point)));
|
|
967
|
+
this.renderedSeriesData.set(key, [...canonicalData]);
|
|
850
968
|
}
|
|
851
969
|
} else if (pruneRecommended && this.crosshairActive) {
|
|
852
970
|
this.pendingRealtimePrune = true;
|
|
853
971
|
} else if (pruneRecommended) {
|
|
854
|
-
for (const { api, canonicalData } of plannedUpdates) {
|
|
855
|
-
api.setData(canonicalData.map((point) => (
|
|
856
|
-
|
|
857
|
-
value: point.value,
|
|
858
|
-
})));
|
|
972
|
+
for (const { key, api, canonicalData } of plannedUpdates) {
|
|
973
|
+
api.setData(canonicalData.map((point) => this.toLcPoint(point)));
|
|
974
|
+
this.renderedSeriesData.set(key, [...canonicalData]);
|
|
859
975
|
}
|
|
860
976
|
this.pendingRealtimePrune = false;
|
|
861
977
|
}
|
|
@@ -879,7 +995,7 @@ export class DeesChartArea extends DeesElement {
|
|
|
879
995
|
} catch (e) { /* range may be invalid */ }
|
|
880
996
|
}
|
|
881
997
|
|
|
882
|
-
public async appendData(newData:
|
|
998
|
+
public async appendData(newData: ChartSeriesConfig) {
|
|
883
999
|
if (!this.chart) return;
|
|
884
1000
|
const nextSeries = this.internalChartData.map((series) => ({
|
|
885
1001
|
...series,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export interface IRealtimeChartPoint {
|
|
2
2
|
time: number;
|
|
3
|
-
|
|
3
|
+
/** Omitted for a time point where this series has no reading. */
|
|
4
|
+
value?: number;
|
|
4
5
|
}
|
|
5
6
|
|
|
6
7
|
export interface IRealtimeChartUpdate {
|
|
@@ -78,7 +79,8 @@ export const planRealtimeChartUpdates = (
|
|
|
78
79
|
continue;
|
|
79
80
|
}
|
|
80
81
|
|
|
81
|
-
if (renderedPoint.value !== canonicalPoint.value
|
|
82
|
+
if (renderedPoint.value !== canonicalPoint.value
|
|
83
|
+
|| ('value' in renderedPoint) !== ('value' in canonicalPoint)) {
|
|
82
84
|
updates.push({
|
|
83
85
|
point: canonicalPoint,
|
|
84
86
|
historicalUpdate: canonicalPoint.time < renderedLastTime,
|
|
@@ -14,7 +14,8 @@ export const renderChartArea = (component: DeesChartArea): TemplateResult => {
|
|
|
14
14
|
<div class="chartContainer">
|
|
15
15
|
<div class="rangeSelectionOverlay" aria-hidden="true"></div>
|
|
16
16
|
</div>
|
|
17
|
-
${!component.chartSeries.some(series => series.data.
|
|
17
|
+
${!component.chartSeries.some(series => series.data.some(point => point.y !== null))
|
|
18
|
+
? html`<div class="chartState" role="status">No data for this period</div>` : ''}
|
|
18
19
|
${component.seriesStats.length > 0 && !component.hideLegend ? html`
|
|
19
20
|
<div slot="footer" class="statsBar">
|
|
20
21
|
${component.seriesStats.map(s => html`
|
|
@@ -24,7 +25,7 @@ export const renderChartArea = (component: DeesChartArea): TemplateResult => {
|
|
|
24
25
|
${component.legendStats
|
|
25
26
|
.filter(key => key === 'latest' || key === 'min' || key === 'max' || key === 'avg')
|
|
26
27
|
.map(key => html`
|
|
27
|
-
<span class="statsItem">${key} <strong>${component.yAxisFormatter(s[key])}</strong></span>
|
|
28
|
+
<span class="statsItem">${key} <strong>${s[key] === null ? '—' : component.yAxisFormatter(s[key])}</strong></span>
|
|
28
29
|
`)}
|
|
29
30
|
</div>
|
|
30
31
|
`)}
|
|
@@ -19,6 +19,9 @@ declare global {
|
|
|
19
19
|
}
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
export interface IDropdownSearchOption { key: string; option: string; payload?: unknown; }
|
|
23
|
+
export type TDropdownSearch = (query: string, signal: AbortSignal) => Promise<IDropdownSearchOption[]>;
|
|
24
|
+
|
|
22
25
|
@customElement('dees-input-dropdown-popup')
|
|
23
26
|
export class DeesInputDropdownPopup extends DeesElement {
|
|
24
27
|
@property({ type: Array })
|
|
@@ -39,6 +42,13 @@ export class DeesInputDropdownPopup extends DeesElement {
|
|
|
39
42
|
@property({ type: String })
|
|
40
43
|
accessor selectedKey = '';
|
|
41
44
|
|
|
45
|
+
/** Optional remote search. Results are authoritative and are not filtered again locally. */
|
|
46
|
+
@property({ attribute: false }) accessor searchOptions: TDropdownSearch | undefined;
|
|
47
|
+
@property({ type: String }) accessor searchLabel = 'Search options';
|
|
48
|
+
@state() accessor loading = false;
|
|
49
|
+
@state() accessor searchError = '';
|
|
50
|
+
private searchAbort?: AbortController;
|
|
51
|
+
|
|
42
52
|
@state()
|
|
43
53
|
accessor filteredOptions: { option: string; key: string; payload?: any }[] = [];
|
|
44
54
|
|
|
@@ -212,9 +222,9 @@ export class DeesInputDropdownPopup extends DeesElement {
|
|
|
212
222
|
<div class="search">
|
|
213
223
|
<input
|
|
214
224
|
type="text"
|
|
215
|
-
placeholder
|
|
225
|
+
placeholder=${this.searchLabel}
|
|
216
226
|
role="combobox"
|
|
217
|
-
aria-label
|
|
227
|
+
aria-label=${this.searchLabel}
|
|
218
228
|
aria-expanded="true"
|
|
219
229
|
aria-controls="dropdown-options"
|
|
220
230
|
aria-activedescendant=${this.filteredOptions.length ? `dropdown-option-${this.highlightedIndex}` : ''}
|
|
@@ -227,8 +237,8 @@ export class DeesInputDropdownPopup extends DeesElement {
|
|
|
227
237
|
: null}
|
|
228
238
|
<div class="options-container" id="dropdown-options" role="listbox" aria-label=${this.ownerComponent?.getAttribute('label') || 'Options'}
|
|
229
239
|
tabindex=${this.enableSearch ? '-1' : '0'} aria-activedescendant=${this.filteredOptions.length ? `dropdown-option-${this.highlightedIndex}` : ''}>
|
|
230
|
-
${this.filteredOptions.length === 0
|
|
231
|
-
? html`<div class="no-options"
|
|
240
|
+
${this.loading || this.searchError || this.filteredOptions.length === 0
|
|
241
|
+
? html`<div class="no-options" role="status">${this.loading ? 'Searching…' : this.searchError || 'No options found'}</div>`
|
|
232
242
|
: this.filteredOptions.map((option, index) => {
|
|
233
243
|
const isHighlighted = this.highlightedIndex === index;
|
|
234
244
|
return html`
|
|
@@ -263,6 +273,8 @@ export class DeesInputDropdownPopup extends DeesElement {
|
|
|
263
273
|
}
|
|
264
274
|
|
|
265
275
|
public async show(): Promise<void> {
|
|
276
|
+
this.loading = false;
|
|
277
|
+
this.searchError = '';
|
|
266
278
|
this.filteredOptions = this.options;
|
|
267
279
|
this.highlightedIndex = Math.max(0, this.options.findIndex((option) => option.key === this.selectedKey));
|
|
268
280
|
this.searchValue = '';
|
|
@@ -287,6 +299,7 @@ export class DeesInputDropdownPopup extends DeesElement {
|
|
|
287
299
|
// Add scroll/resize listeners for repositioning
|
|
288
300
|
window.addEventListener('scroll', this.handleScrollOrResize, { capture: true, passive: true });
|
|
289
301
|
window.addEventListener('resize', this.handleScrollOrResize, { passive: true });
|
|
302
|
+
if (this.searchOptions) void this.searchRemote();
|
|
290
303
|
}
|
|
291
304
|
|
|
292
305
|
public async hide(): Promise<void> {
|
|
@@ -295,6 +308,7 @@ export class DeesInputDropdownPopup extends DeesElement {
|
|
|
295
308
|
return;
|
|
296
309
|
}
|
|
297
310
|
this.isDestroying = true;
|
|
311
|
+
this.searchAbort?.abort();
|
|
298
312
|
|
|
299
313
|
// Remove scroll/resize listeners
|
|
300
314
|
window.removeEventListener('scroll', this.handleScrollOrResize, { capture: true } as EventListenerOptions);
|
|
@@ -331,6 +345,7 @@ export class DeesInputDropdownPopup extends DeesElement {
|
|
|
331
345
|
|
|
332
346
|
public updateOptions(options: { option: string; key: string; payload?: any }[]): void {
|
|
333
347
|
this.options = options;
|
|
348
|
+
if (this.searchOptions) return;
|
|
334
349
|
// Re-filter with current search value
|
|
335
350
|
if (this.searchValue) {
|
|
336
351
|
const searchLower = this.searchValue.toLowerCase();
|
|
@@ -354,6 +369,7 @@ export class DeesInputDropdownPopup extends DeesElement {
|
|
|
354
369
|
private handleSearch = (event: Event): void => {
|
|
355
370
|
const searchTerm = (event.target as HTMLInputElement).value;
|
|
356
371
|
this.searchValue = searchTerm;
|
|
372
|
+
if (this.searchOptions) { void this.searchRemote(); return; }
|
|
357
373
|
const searchLower = searchTerm.toLowerCase();
|
|
358
374
|
this.filteredOptions = this.options.filter((option) =>
|
|
359
375
|
option.option.toLowerCase().includes(searchLower)
|
|
@@ -361,7 +377,25 @@ export class DeesInputDropdownPopup extends DeesElement {
|
|
|
361
377
|
this.highlightedIndex = 0;
|
|
362
378
|
};
|
|
363
379
|
|
|
380
|
+
private async searchRemote(): Promise<void> {
|
|
381
|
+
this.searchAbort?.abort();
|
|
382
|
+
const controller = this.searchAbort = new AbortController();
|
|
383
|
+
this.loading = true;
|
|
384
|
+
this.searchError = '';
|
|
385
|
+
this.filteredOptions = [];
|
|
386
|
+
this.highlightedIndex = 0;
|
|
387
|
+
try {
|
|
388
|
+
const options = await this.searchOptions!(this.searchValue, controller.signal);
|
|
389
|
+
if (!controller.signal.aborted) this.filteredOptions = options;
|
|
390
|
+
} catch (error) {
|
|
391
|
+
if (!controller.signal.aborted) this.searchError = error instanceof Error ? error.message : 'Search failed. Try again.';
|
|
392
|
+
} finally {
|
|
393
|
+
if (!controller.signal.aborted) this.loading = false;
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
364
397
|
private handleSearchKeydown = (event: KeyboardEvent): void => {
|
|
398
|
+
if (event.isComposing) return;
|
|
365
399
|
const key = event.key;
|
|
366
400
|
const maxIndex = this.filteredOptions.length - 1;
|
|
367
401
|
|
|
@@ -370,7 +404,7 @@ export class DeesInputDropdownPopup extends DeesElement {
|
|
|
370
404
|
this.highlightedIndex = this.highlightedIndex + 1 > maxIndex ? 0 : this.highlightedIndex + 1;
|
|
371
405
|
} else if (key === 'ArrowUp') {
|
|
372
406
|
event.preventDefault();
|
|
373
|
-
this.highlightedIndex = this.highlightedIndex - 1 < 0 ? maxIndex : this.highlightedIndex - 1;
|
|
407
|
+
this.highlightedIndex = Math.max(0, this.highlightedIndex - 1 < 0 ? maxIndex : this.highlightedIndex - 1);
|
|
374
408
|
} else if (key === 'Enter') {
|
|
375
409
|
event.preventDefault();
|
|
376
410
|
if (this.filteredOptions[this.highlightedIndex]) {
|
|
@@ -391,6 +425,7 @@ export class DeesInputDropdownPopup extends DeesElement {
|
|
|
391
425
|
};
|
|
392
426
|
|
|
393
427
|
async disconnectedCallback() {
|
|
428
|
+
this.searchAbort?.abort();
|
|
394
429
|
window.removeEventListener('scroll', this.handleScrollOrResize, { capture: true } as EventListenerOptions);
|
|
395
430
|
window.removeEventListener('resize', this.handleScrollOrResize);
|
|
396
431
|
zIndexRegistry.unregister(this);
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { html } from '@design.estate/dees-element';
|
|
2
|
+
import { demoPage, demoSection } from '../../../demos/templates.js';
|
|
3
|
+
|
|
4
|
+
export const demoFunc = () => demoPage('Reference composer', 'Type #_ to search at the cursor. Choose a ticket and continue writing.',
|
|
5
|
+
demoSection('Ticket references', 'References preserve IDs through editing, undo and copy/paste.', html`
|
|
6
|
+
<dees-input-reference .searchReferences=${async (query: string) => [
|
|
7
|
+
{ id: 'ticket-1', label: '#CAT-101 Keyboard navigation' }, { id: 'ticket-2', label: '#CAT-102 Ticket detail' },
|
|
8
|
+
].filter(item => item.label.toLowerCase().includes(query.toLowerCase()))}></dees-input-reference>
|
|
9
|
+
`));
|