layerchart 2.0.0-next.25 → 2.0.0-next.27

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.
@@ -1,5 +1,6 @@
1
1
  import type { Numeric } from 'd3-array';
2
2
  import { extent as d3extent } from 'd3-array';
3
+ import { type Accessor } from './common.js';
3
4
  /**
4
5
  * Wrapper around d3-array's `extent()` but remove [undefined, undefined] return type
5
6
  */
@@ -11,3 +12,13 @@ export declare function extent<T extends Numeric>(iterable: Parameters<typeof d3
11
12
  * of making a set
12
13
  */
13
14
  export declare function arraysEqual(arr1: unknown[], arr2: unknown[]): boolean;
15
+ /**
16
+ * Add `lanes` property to each element in the data array support densely packing.
17
+ * This is useful for visualizing overlapping events in a timeline / Gantt chart.
18
+ */
19
+ export declare function applyLanes<T extends Record<string, any>>(data: T[], options?: {
20
+ start: Accessor<T>;
21
+ end: Accessor<T>;
22
+ }): (T & {
23
+ lane: number;
24
+ })[];
@@ -1,4 +1,5 @@
1
1
  import { extent as d3extent } from 'd3-array';
2
+ import { accessor } from './common.js';
2
3
  /**
3
4
  * Wrapper around d3-array's `extent()` but remove [undefined, undefined] return type
4
5
  */
@@ -18,3 +19,25 @@ export function arraysEqual(arr1, arr2) {
18
19
  return arr2.includes(k);
19
20
  });
20
21
  }
22
+ /**
23
+ * Add `lanes` property to each element in the data array support densely packing.
24
+ * This is useful for visualizing overlapping events in a timeline / Gantt chart.
25
+ */
26
+ export function applyLanes(data, options = {
27
+ start: 'start',
28
+ end: 'end',
29
+ }) {
30
+ const result = [];
31
+ let stack = [];
32
+ const startAccessor = accessor(options.start);
33
+ const endAccessor = accessor(options.end);
34
+ for (const d of data) {
35
+ let lane = stack.findIndex((s) => endAccessor(s) <= startAccessor(d) && startAccessor(s) < startAccessor(d));
36
+ if (lane === -1) {
37
+ lane = stack.length;
38
+ }
39
+ result.push({ ...d, lane });
40
+ stack[lane] = d;
41
+ }
42
+ return result;
43
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,200 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { applyLanes } from './array.js';
3
+ describe('applyLanes', () => {
4
+ it('should assign same lane to non-overlapping events', () => {
5
+ const data = [
6
+ { id: 1, start: new Date('2023-01-01'), end: new Date('2023-01-02') },
7
+ { id: 2, start: new Date('2023-01-03'), end: new Date('2023-01-05') },
8
+ { id: 3, start: new Date('2023-01-06'), end: new Date('2023-01-08') },
9
+ ];
10
+ const result = applyLanes(data);
11
+ expect(result).toEqual([
12
+ { id: 1, start: new Date('2023-01-01'), end: new Date('2023-01-02'), lane: 0 },
13
+ { id: 2, start: new Date('2023-01-03'), end: new Date('2023-01-05'), lane: 0 },
14
+ { id: 3, start: new Date('2023-01-06'), end: new Date('2023-01-08'), lane: 0 },
15
+ ]);
16
+ });
17
+ it('should assign different lanes to overlapping events', () => {
18
+ const data = [
19
+ { id: 1, start: new Date('2023-01-01'), end: new Date('2023-01-03') },
20
+ { id: 2, start: new Date('2023-01-02'), end: new Date('2023-01-04') },
21
+ { id: 3, start: new Date('2023-01-02T12:00:00'), end: new Date('2023-01-05') },
22
+ ];
23
+ const result = applyLanes(data);
24
+ expect(result).toEqual([
25
+ { id: 1, start: new Date('2023-01-01'), end: new Date('2023-01-03'), lane: 0 },
26
+ { id: 2, start: new Date('2023-01-02'), end: new Date('2023-01-04'), lane: 1 },
27
+ { id: 3, start: new Date('2023-01-02T12:00:00'), end: new Date('2023-01-05'), lane: 2 },
28
+ ]);
29
+ });
30
+ it('should reuse lanes when events no longer overlap', () => {
31
+ const data = [
32
+ { id: 1, start: new Date('2023-01-01'), end: new Date('2023-01-02') },
33
+ { id: 2, start: new Date('2023-01-01T12:00:00'), end: new Date('2023-01-03') },
34
+ { id: 3, start: new Date('2023-01-04'), end: new Date('2023-01-06') }, // starts after id: 1 ends
35
+ { id: 4, start: new Date('2023-01-05'), end: new Date('2023-01-07') }, // starts after id: 2 ends
36
+ ];
37
+ const result = applyLanes(data);
38
+ expect(result).toEqual([
39
+ { id: 1, start: new Date('2023-01-01'), end: new Date('2023-01-02'), lane: 0 },
40
+ { id: 2, start: new Date('2023-01-01T12:00:00'), end: new Date('2023-01-03'), lane: 1 },
41
+ { id: 3, start: new Date('2023-01-04'), end: new Date('2023-01-06'), lane: 0 }, // reuses lane 0
42
+ { id: 4, start: new Date('2023-01-05'), end: new Date('2023-01-07'), lane: 1 }, // reuses lane 1
43
+ ]);
44
+ });
45
+ it('should handle events that start exactly when another ends', () => {
46
+ const data = [
47
+ { id: 1, start: new Date('2023-01-01'), end: new Date('2023-01-02') },
48
+ { id: 2, start: new Date('2023-01-02'), end: new Date('2023-01-04') }, // starts exactly when id: 1 ends
49
+ { id: 3, start: new Date('2023-01-01T12:00:00'), end: new Date('2023-01-03') }, // overlaps with both
50
+ ];
51
+ const result = applyLanes(data);
52
+ expect(result).toEqual([
53
+ { id: 1, start: new Date('2023-01-01'), end: new Date('2023-01-02'), lane: 0 },
54
+ { id: 2, start: new Date('2023-01-02'), end: new Date('2023-01-04'), lane: 0 }, // can reuse lane 0
55
+ { id: 3, start: new Date('2023-01-01T12:00:00'), end: new Date('2023-01-03'), lane: 1 }, // overlaps, needs new lane
56
+ ]);
57
+ });
58
+ it('should work with string keys for start and end', () => {
59
+ const data = [
60
+ { name: 'Task 1', startTime: new Date('2023-01-01'), endTime: new Date('2023-01-03') },
61
+ { name: 'Task 2', startTime: new Date('2023-01-02'), endTime: new Date('2023-01-04') },
62
+ ];
63
+ const result = applyLanes(data, { start: 'startTime', end: 'endTime' });
64
+ expect(result).toEqual([
65
+ {
66
+ name: 'Task 1',
67
+ startTime: new Date('2023-01-01'),
68
+ endTime: new Date('2023-01-03'),
69
+ lane: 0,
70
+ },
71
+ {
72
+ name: 'Task 2',
73
+ startTime: new Date('2023-01-02'),
74
+ endTime: new Date('2023-01-04'),
75
+ lane: 1,
76
+ },
77
+ ]);
78
+ });
79
+ it('should work with nested string keys for start and end', () => {
80
+ const data = [
81
+ { name: 'Task 1', duration: { start: new Date('2023-01-01'), end: new Date('2023-01-02') } },
82
+ { name: 'Task 2', duration: { start: new Date('2023-01-03'), end: new Date('2023-01-04') } },
83
+ ];
84
+ const result = applyLanes(data, { start: 'duration.start', end: 'duration.end' });
85
+ expect(result).toEqual([
86
+ {
87
+ name: 'Task 1',
88
+ duration: { start: new Date('2023-01-01'), end: new Date('2023-01-02') },
89
+ lane: 0,
90
+ },
91
+ {
92
+ name: 'Task 2',
93
+ duration: { start: new Date('2023-01-03'), end: new Date('2023-01-04') },
94
+ lane: 0,
95
+ },
96
+ ]);
97
+ });
98
+ it('should work with function accessors for start and end', () => {
99
+ const data = [
100
+ { name: 'Task 1', duration: { start: new Date('2023-01-01'), end: new Date('2023-01-02') } },
101
+ { name: 'Task 2', duration: { start: new Date('2023-01-03'), end: new Date('2023-01-04') } },
102
+ ];
103
+ const result = applyLanes(data, { start: (d) => d.duration.start, end: (d) => d.duration.end });
104
+ expect(result).toEqual([
105
+ {
106
+ name: 'Task 1',
107
+ duration: { start: new Date('2023-01-01'), end: new Date('2023-01-02') },
108
+ lane: 0,
109
+ },
110
+ {
111
+ name: 'Task 2',
112
+ duration: { start: new Date('2023-01-03'), end: new Date('2023-01-04') },
113
+ lane: 0,
114
+ },
115
+ ]);
116
+ });
117
+ it('should handle empty array', () => {
118
+ const data = [];
119
+ const result = applyLanes(data);
120
+ expect(result).toEqual([]);
121
+ });
122
+ it('should handle single event', () => {
123
+ const data = [{ id: 1, start: new Date('2023-01-01'), end: new Date('2023-01-02') }];
124
+ const result = applyLanes(data);
125
+ expect(result).toEqual([
126
+ { id: 1, start: new Date('2023-01-01'), end: new Date('2023-01-02'), lane: 0 },
127
+ ]);
128
+ });
129
+ it('should handle complex overlapping scenario', () => {
130
+ const data = [
131
+ { id: 1, start: new Date('2023-01-01'), end: new Date('2023-01-05') }, // long event
132
+ { id: 2, start: new Date('2023-01-02'), end: new Date('2023-01-03') }, // short event inside
133
+ { id: 3, start: new Date('2023-01-02T12:00:00'), end: new Date('2023-01-04') }, // overlaps with both
134
+ { id: 4, start: new Date('2023-01-03'), end: new Date('2023-01-04T12:00:00') }, // overlaps with 1 and 3
135
+ { id: 5, start: new Date('2023-01-06'), end: new Date('2023-01-08') }, // separate event
136
+ ];
137
+ const result = applyLanes(data);
138
+ expect(result).toEqual([
139
+ { id: 1, start: new Date('2023-01-01'), end: new Date('2023-01-05'), lane: 0 },
140
+ { id: 2, start: new Date('2023-01-02'), end: new Date('2023-01-03'), lane: 1 },
141
+ { id: 3, start: new Date('2023-01-02T12:00:00'), end: new Date('2023-01-04'), lane: 2 },
142
+ { id: 4, start: new Date('2023-01-03'), end: new Date('2023-01-04T12:00:00'), lane: 1 }, // can reuse lane 1 since id: 2 ended
143
+ { id: 5, start: new Date('2023-01-06'), end: new Date('2023-01-08'), lane: 0 }, // can reuse lane 0 since id: 1 ended
144
+ ]);
145
+ });
146
+ it('should preserve all original properties', () => {
147
+ const data = [
148
+ {
149
+ id: 1,
150
+ start: new Date('2023-01-01'),
151
+ end: new Date('2023-01-02'),
152
+ name: 'First',
153
+ priority: 'high',
154
+ metadata: { foo: 'bar' },
155
+ },
156
+ {
157
+ id: 2,
158
+ start: new Date('2023-01-01T12:00:00'),
159
+ end: new Date('2023-01-03'),
160
+ name: 'Second',
161
+ priority: 'low',
162
+ metadata: { baz: 'qux' },
163
+ },
164
+ ];
165
+ const result = applyLanes(data);
166
+ expect(result).toEqual([
167
+ {
168
+ id: 1,
169
+ start: new Date('2023-01-01'),
170
+ end: new Date('2023-01-02'),
171
+ name: 'First',
172
+ priority: 'high',
173
+ metadata: { foo: 'bar' },
174
+ lane: 0,
175
+ },
176
+ {
177
+ id: 2,
178
+ start: new Date('2023-01-01T12:00:00'),
179
+ end: new Date('2023-01-03'),
180
+ name: 'Second',
181
+ priority: 'low',
182
+ metadata: { baz: 'qux' },
183
+ lane: 1,
184
+ },
185
+ ]);
186
+ });
187
+ it('should work with numeric values', () => {
188
+ const data = [
189
+ { id: 1, start: 0, end: 3 },
190
+ { id: 2, start: 1, end: 4 },
191
+ { id: 3, start: 5, end: 7 },
192
+ ];
193
+ const result = applyLanes(data);
194
+ expect(result).toEqual([
195
+ { id: 1, start: 0, end: 3, lane: 0 },
196
+ { id: 2, start: 1, end: 4, lane: 1 },
197
+ { id: 3, start: 5, end: 7, lane: 0 }, // can reuse lane 0 since id: 1 ended
198
+ ]);
199
+ });
200
+ });
@@ -1,3 +1,4 @@
1
+ export { applyLanes } from './array.js';
1
2
  export * from './canvas.js';
2
3
  export * from './common.js';
3
4
  export * from './geo.js';
@@ -1,3 +1,4 @@
1
+ export { applyLanes } from './array.js';
1
2
  export * from './canvas.js';
2
3
  export * from './common.js';
3
4
  export * from './geo.js';
package/package.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "author": "Sean Lynch <techniq35@gmail.com>",
5
5
  "license": "MIT",
6
6
  "repository": "techniq/layerchart",
7
- "version": "2.0.0-next.25",
7
+ "version": "2.0.0-next.27",
8
8
  "devDependencies": {
9
9
  "@changesets/cli": "^2.29.4",
10
10
  "@iconify-json/lucide": "^1.2.48",
@@ -56,7 +56,7 @@
56
56
  "svelte": "5.34.1",
57
57
  "svelte-check": "^4.2.1",
58
58
  "svelte-json-tree": "^2.2.0",
59
- "svelte-ux": "2.0.0-next.11",
59
+ "svelte-ux": "2.0.0-next.13",
60
60
  "svelte2tsx": "^0.7.39",
61
61
  "tailwindcss": "^4.1.10",
62
62
  "topojson-client": "^3.1.0",