@yiwei016/d3timeline-plugins 1.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.
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,2 @@
1
+ export {};
2
+ // TODO: CursorLine.ts
@@ -0,0 +1,3 @@
1
+ export * from "./zoom-slider/ZoomSlider";
2
+ export * from "./multi-sync-v/MutiSyncV";
3
+ export * from "./cursor-line/CursorLine";
package/dist/index.js ADDED
@@ -0,0 +1,3 @@
1
+ export * from "./zoom-slider/ZoomSlider";
2
+ export * from "./multi-sync-v/MutiSyncV";
3
+ export * from "./cursor-line/CursorLine";
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,2 @@
1
+ export {};
2
+ // TODO: MutiSyncV.ts
@@ -0,0 +1,40 @@
1
+ import EventEmitter from "eventemitter3";
2
+ export type PercentData = {
3
+ leftPercent: number;
4
+ rightPercent: number;
5
+ };
6
+ type Events = {
7
+ "slider:updated": PercentData;
8
+ };
9
+ type Rect = {
10
+ x: number;
11
+ y: number;
12
+ width: number;
13
+ height: number;
14
+ };
15
+ declare class Slider extends EventEmitter<Events> {
16
+ #private;
17
+ private range?;
18
+ private edgeLabelConverter?;
19
+ get el(): HTMLDivElement | undefined;
20
+ get moveEl(): HTMLDivElement | undefined;
21
+ get showEdgeLabels(): boolean;
22
+ set showEdgeLabels(value: boolean);
23
+ get edgeLeftLabel(): string;
24
+ get edgeRightLabel(): string;
25
+ get leftPercent(): number;
26
+ get rightPercent(): number;
27
+ init(range: number[], edgeLabelConverter: (pos: number) => string): this;
28
+ remove(): void;
29
+ private initMoveDragger;
30
+ private initResizeDragger;
31
+ updateEdgeLabels(): void;
32
+ updateByRange(range?: number[]): void;
33
+ updateByRectData(rectData: Rect): PercentData;
34
+ private handleMouseEnter;
35
+ private handleMouseLeave;
36
+ private onSliderUpdated;
37
+ bindEvents(): void;
38
+ unbindEvents(): void;
39
+ }
40
+ export default Slider;
@@ -0,0 +1,237 @@
1
+ import interact from "interactjs";
2
+ import EventEmitter from "eventemitter3";
3
+ class Slider extends EventEmitter {
4
+ #el;
5
+ #moveEl;
6
+ #resizeEls;
7
+ #showEdgeLabels = false;
8
+ range;
9
+ edgeLabelConverter;
10
+ get el() {
11
+ return this.#el;
12
+ }
13
+ get moveEl() {
14
+ return this.#moveEl;
15
+ }
16
+ get showEdgeLabels() {
17
+ return this.#showEdgeLabels;
18
+ }
19
+ set showEdgeLabels(value) {
20
+ this.#showEdgeLabels = value;
21
+ this.#resizeEls?.forEach((el) => {
22
+ const label = el.querySelector(".edge-label");
23
+ if (label) {
24
+ label.style.display = value ? "" : "none";
25
+ }
26
+ });
27
+ }
28
+ get edgeLeftLabel() {
29
+ return this.edgeLabelConverter
30
+ ? this.edgeLabelConverter(this.leftPercent)
31
+ : "";
32
+ }
33
+ get edgeRightLabel() {
34
+ return this.edgeLabelConverter
35
+ ? this.edgeLabelConverter(this.rightPercent)
36
+ : "";
37
+ }
38
+ get leftPercent() {
39
+ if (!this.#el)
40
+ return 0;
41
+ let x = Math.round(parseFloat(this.#el.getAttribute("data-x") ?? "0") || 0);
42
+ let containerWidth = Math.round(this.#el.parentElement?.offsetWidth ?? 1);
43
+ const leftPercent = parseFloat((x / containerWidth).toFixed(2));
44
+ return leftPercent;
45
+ }
46
+ get rightPercent() {
47
+ if (!this.#el)
48
+ return 0;
49
+ let x = Math.round(parseFloat(this.#el.getAttribute("data-x") ?? "0") || 0);
50
+ let width = Math.round(parseFloat(this.#el.getAttribute("data-width") ?? "0") || 0);
51
+ let containerWidth = Math.round(this.#el.parentElement?.offsetWidth ?? 1);
52
+ const rightPercent = parseFloat(((x + width) / containerWidth).toFixed(2));
53
+ return rightPercent;
54
+ }
55
+ init(range, edgeLabelConverter) {
56
+ this.range = range;
57
+ this.edgeLabelConverter = edgeLabelConverter;
58
+ this.showEdgeLabels = false;
59
+ const el = document.createElement("div");
60
+ el.classList.add("slider");
61
+ this.#el = el;
62
+ this.initMoveDragger();
63
+ this.initResizeDragger();
64
+ return this;
65
+ }
66
+ remove() {
67
+ if (this.#resizeEls) {
68
+ this.#resizeEls.forEach((el) => {
69
+ el.remove();
70
+ });
71
+ }
72
+ if (this.#moveEl) {
73
+ interact(this.#moveEl).unset();
74
+ this.#moveEl.remove();
75
+ }
76
+ if (this.#el) {
77
+ interact(this.#el).unset();
78
+ this.#el.remove();
79
+ }
80
+ this.#el = undefined;
81
+ this.#moveEl = undefined;
82
+ this.#resizeEls = undefined;
83
+ }
84
+ initMoveDragger() {
85
+ const el = document.createElement("div");
86
+ el.classList.add("move-dragger");
87
+ this.#moveEl = el;
88
+ this.#el?.appendChild(el);
89
+ }
90
+ initResizeDragger() {
91
+ // left resize dragger
92
+ const leftResizeEl = document.createElement("div");
93
+ leftResizeEl.classList.add("resize-dragger");
94
+ leftResizeEl.classList.add("left");
95
+ const leftLabel = document.createElement("label");
96
+ leftLabel.classList.add("edge-label");
97
+ leftLabel.style.display = this.showEdgeLabels ? "" : "none";
98
+ leftLabel.textContent = this.edgeLeftLabel;
99
+ leftResizeEl.appendChild(leftLabel);
100
+ this.#el?.appendChild(leftResizeEl);
101
+ // right resize dragger
102
+ const rightResizeEl = document.createElement("div");
103
+ rightResizeEl.classList.add("resize-dragger");
104
+ rightResizeEl.classList.add("right");
105
+ const rightLabel = document.createElement("label");
106
+ rightLabel.classList.add("edge-label");
107
+ rightLabel.style.display = this.showEdgeLabels ? "" : "none";
108
+ rightLabel.textContent = this.edgeRightLabel;
109
+ rightResizeEl.appendChild(rightLabel);
110
+ this.#el?.appendChild(rightResizeEl);
111
+ this.#resizeEls = [leftResizeEl, rightResizeEl];
112
+ }
113
+ updateEdgeLabels() {
114
+ if (!this.#resizeEls)
115
+ return;
116
+ const [leftResizeEl, rightResizeEl] = this.#resizeEls;
117
+ const leftLabel = leftResizeEl.querySelector(".edge-label");
118
+ const rightLabel = rightResizeEl.querySelector(".edge-label");
119
+ leftLabel.textContent = this.edgeLeftLabel;
120
+ rightLabel.textContent = this.edgeRightLabel;
121
+ }
122
+ updateByRange(range) {
123
+ range = range ?? this.range ?? [];
124
+ const [startPer, endPer] = range;
125
+ const slider = this.#el;
126
+ if (slider &&
127
+ slider.parentElement &&
128
+ Number.isFinite(startPer) &&
129
+ Number.isFinite(endPer)) {
130
+ const sliderContainer = slider.parentElement;
131
+ const x = sliderContainer.offsetWidth * startPer;
132
+ const width = sliderContainer.offsetWidth * endPer - x;
133
+ slider.style.width = width + "px";
134
+ slider.style.transform = `translateX(${x}px)`;
135
+ slider.setAttribute("data-x", String(x));
136
+ slider.setAttribute("data-width", String(width));
137
+ this.range = [startPer, endPer];
138
+ }
139
+ }
140
+ updateByRectData(rectData) {
141
+ if (this.el) {
142
+ this.el.style.width = rectData.width + "px";
143
+ this.el.style.transform = "translateX(" + rectData.x + "px)";
144
+ this.el.setAttribute("data-x", String(rectData.x));
145
+ this.el.setAttribute("data-width", String(rectData.width));
146
+ }
147
+ return { leftPercent: this.leftPercent, rightPercent: this.rightPercent };
148
+ }
149
+ handleMouseEnter = () => {
150
+ this.updateEdgeLabels();
151
+ this.showEdgeLabels = true;
152
+ };
153
+ handleMouseLeave = () => {
154
+ this.showEdgeLabels = false;
155
+ };
156
+ onSliderUpdated() {
157
+ console.log("slider updated:", {
158
+ leftPercent: this.leftPercent,
159
+ rightPercent: this.rightPercent,
160
+ });
161
+ this.emit("slider:updated", {
162
+ leftPercent: this.leftPercent,
163
+ rightPercent: this.rightPercent,
164
+ });
165
+ }
166
+ bindEvents() {
167
+ if (!this.moveEl)
168
+ return;
169
+ const _self = this;
170
+ interact(this.moveEl).draggable({
171
+ inertia: true,
172
+ modifiers: [
173
+ interact.modifiers.restrictRect({
174
+ restriction: ".slider-window",
175
+ }),
176
+ ],
177
+ listeners: {
178
+ start() {
179
+ _self.showEdgeLabels = true;
180
+ },
181
+ move(event) {
182
+ let target = event.target.parentElement;
183
+ let x = parseFloat(target.getAttribute("data-x") ?? "0") + event.dx;
184
+ target.style.transform = "translateX(" + x + "px)";
185
+ target.setAttribute("data-x", x);
186
+ _self.updateEdgeLabels();
187
+ },
188
+ end() {
189
+ _self.showEdgeLabels = false;
190
+ _self.onSliderUpdated.call(_self);
191
+ },
192
+ },
193
+ });
194
+ if (!this.el)
195
+ return;
196
+ this.el.addEventListener("mouseenter", this.handleMouseEnter, false);
197
+ this.el.addEventListener("mouseleave", this.handleMouseLeave, false);
198
+ interact(this.el).resizable({
199
+ modifiers: [
200
+ interact.modifiers.restrictRect({
201
+ restriction: ".slider-window",
202
+ }),
203
+ ],
204
+ edges: { left: true, right: true },
205
+ listeners: {
206
+ start() {
207
+ _self.showEdgeLabels = true;
208
+ },
209
+ move(event) {
210
+ let target = event.target;
211
+ let x = parseFloat(target.getAttribute("data-x") ?? "0");
212
+ target.style.width = event.rect.width + "px";
213
+ x += event.deltaRect.left;
214
+ target.style.transform = "translateX(" + x + "px)";
215
+ target.setAttribute("data-x", String(x));
216
+ target.setAttribute("data-width", event.rect.width);
217
+ _self.updateEdgeLabels();
218
+ },
219
+ end() {
220
+ _self.showEdgeLabels = false;
221
+ _self.onSliderUpdated.call(_self);
222
+ },
223
+ },
224
+ });
225
+ }
226
+ unbindEvents() {
227
+ if (!this.moveEl)
228
+ return;
229
+ this.moveEl && interact(this.moveEl).unset();
230
+ if (!this.el)
231
+ return;
232
+ interact(this.el).unset();
233
+ this.el.removeEventListener("mouseenter", this.handleMouseEnter, false);
234
+ this.el.removeEventListener("mouseleave", this.handleMouseLeave, false);
235
+ }
236
+ }
237
+ export default Slider;
@@ -0,0 +1,16 @@
1
+ export interface Spark {
2
+ id: string;
3
+ pos: number[];
4
+ color: string;
5
+ class?: string;
6
+ }
7
+ declare class SparkGroup {
8
+ #private;
9
+ private containerWidth;
10
+ get el(): HTMLDivElement | undefined;
11
+ init(sparks: Spark[], containerWidth: number): this;
12
+ remove(): void;
13
+ updateSparks(sparks: Spark[]): void;
14
+ private calcSparkStyle;
15
+ }
16
+ export default SparkGroup;
@@ -0,0 +1,64 @@
1
+ class SparkGroup {
2
+ #el;
3
+ containerWidth = 0;
4
+ get el() {
5
+ return this.#el;
6
+ }
7
+ init(sparks, containerWidth) {
8
+ this.containerWidth = containerWidth;
9
+ const el = document.createElement("div");
10
+ el.classList.add("spark-group");
11
+ this.#el = el;
12
+ this.updateSparks(sparks);
13
+ return this;
14
+ }
15
+ remove() {
16
+ if (this.#el) {
17
+ this.#el.remove();
18
+ this.#el = undefined;
19
+ }
20
+ }
21
+ updateSparks(sparks) {
22
+ if (!this.#el)
23
+ return;
24
+ this.#el.innerHTML = "";
25
+ // 更新containerWidth,防止窗口resize后spark位置不正确
26
+ this.containerWidth = this.#el.parentElement?.offsetWidth ?? 0;
27
+ sparks.forEach((spark) => {
28
+ const sparkEl = document.createElement("div");
29
+ sparkEl.classList.add("spark");
30
+ if (spark.class) {
31
+ sparkEl.classList.add(spark.class);
32
+ }
33
+ const styleObj = this.calcSparkStyle(spark);
34
+ for (const key of Object.keys(styleObj)) {
35
+ sparkEl.style[key] = styleObj[key];
36
+ }
37
+ this.#el?.appendChild(sparkEl);
38
+ });
39
+ }
40
+ calcSparkStyle(spark) {
41
+ let x0 = 0;
42
+ let x1 = 0;
43
+ const styleObj = {};
44
+ if (Number.isFinite(spark.pos[0])) {
45
+ x0 = this.containerWidth * spark.pos[0];
46
+ if (Number.isFinite(spark.pos[1])) {
47
+ x1 = this.containerWidth * spark.pos[1];
48
+ styleObj.borderLeft = "1px solid skyblue";
49
+ styleObj.borderRight = "1px solid skyblue";
50
+ }
51
+ else {
52
+ styleObj.borderLeft = `1px solid ${spark.color}`;
53
+ styleObj.borderRight = `1px solid ${spark.color}`;
54
+ }
55
+ }
56
+ return {
57
+ background: spark.color,
58
+ transform: `translateX(${x0}px)`,
59
+ width: `${x1 > 0 ? x1 - x0 : 0}px`,
60
+ ...styleObj,
61
+ };
62
+ }
63
+ }
64
+ export default SparkGroup;
@@ -0,0 +1,27 @@
1
+ import { type Spark } from "./SparkGroup";
2
+ import { type D3TimelinePlugin, type D3TimelineContext } from "@yiwei016/d3timeline";
3
+ export interface ZoomSliderOptions {
4
+ range: number[];
5
+ sparks: Spark[];
6
+ dateFormat?: (date: Date) => string;
7
+ }
8
+ export declare class ZoomSlider implements D3TimelinePlugin {
9
+ private readonly container;
10
+ private readonly options;
11
+ private context?;
12
+ private observer?;
13
+ private sliderWindow?;
14
+ private slider?;
15
+ private sparkGroup?;
16
+ constructor(container: HTMLElement, options: ZoomSliderOptions);
17
+ install(context: D3TimelineContext): void;
18
+ uninstall(): void;
19
+ onResize: () => void;
20
+ private bindEvents;
21
+ private unbindEvents;
22
+ private onSliderUpdated;
23
+ private handleZoomEnd;
24
+ private drawSliderWindow;
25
+ private drawSparks;
26
+ private drawSlider;
27
+ }
@@ -0,0 +1,165 @@
1
+ import interact from "interactjs";
2
+ import Slider, {} from "./Slider";
3
+ import SparkGroup, {} from "./SparkGroup";
4
+ import {} from "@yiwei016/d3timeline";
5
+ export class ZoomSlider {
6
+ container;
7
+ options;
8
+ context;
9
+ observer;
10
+ sliderWindow;
11
+ slider;
12
+ sparkGroup;
13
+ constructor(container, options) {
14
+ this.container = container;
15
+ this.options = options;
16
+ }
17
+ install(context) {
18
+ this.context = context;
19
+ this.observer = new ResizeObserver((entries) => {
20
+ const height = entries[0].contentRect.height;
21
+ if (height > 0) {
22
+ this.slider?.updateByRange(this.options.range);
23
+ }
24
+ });
25
+ this.drawSliderWindow();
26
+ this.drawSparks();
27
+ this.drawSlider();
28
+ this.bindEvents();
29
+ }
30
+ uninstall() {
31
+ this.observer?.disconnect();
32
+ this.unbindEvents();
33
+ this.observer = undefined;
34
+ this.context = undefined;
35
+ this.sliderWindow?.remove();
36
+ this.slider?.remove();
37
+ this.sparkGroup?.remove();
38
+ this.slider = undefined;
39
+ this.sparkGroup = undefined;
40
+ }
41
+ onResize = () => {
42
+ this.slider?.updateByRange();
43
+ this.sparkGroup?.updateSparks(this.options.sparks);
44
+ };
45
+ bindEvents() {
46
+ if (!this.sliderWindow)
47
+ return;
48
+ let _self = this;
49
+ let isSelecting = false;
50
+ let startPoint = null;
51
+ let selectRect = null;
52
+ let selectRectBg = null;
53
+ interact(this.sliderWindow).draggable({
54
+ cursorChecker: () => {
55
+ return "crosshair";
56
+ },
57
+ listeners: {
58
+ start(event) {
59
+ isSelecting = true;
60
+ selectRectBg = event.target;
61
+ if (!selectRectBg)
62
+ return;
63
+ const rect = selectRectBg.getBoundingClientRect();
64
+ startPoint = {
65
+ x: event.clientX - rect.left,
66
+ y: 0,
67
+ };
68
+ // 创建选择框
69
+ selectRect = document.createElement("div");
70
+ selectRect.className = "select-rect";
71
+ selectRect.style.left = startPoint.x + "px";
72
+ selectRect.style.top = startPoint.y + "px";
73
+ selectRect.style.width = "0px";
74
+ selectRect.style.height = "100%";
75
+ selectRect.style.position = "absolute";
76
+ selectRect.style.zIndex = "1000";
77
+ selectRect.style.background = "rgba(0, 102, 204, 0.15)";
78
+ selectRect.style.pointerEvents = "none";
79
+ selectRectBg.appendChild(selectRect);
80
+ },
81
+ move(event) {
82
+ if (!isSelecting || !selectRect || !selectRectBg || !startPoint)
83
+ return;
84
+ const rect = selectRectBg.getBoundingClientRect();
85
+ const currentX = event.clientX - rect.left;
86
+ const currentY = rect.height;
87
+ const x = Math.min(startPoint.x, currentX);
88
+ const y = Math.min(startPoint.y, currentY);
89
+ const width = Math.abs(currentX - startPoint.x);
90
+ const height = Math.abs(currentY - startPoint.y);
91
+ selectRect.style.left = x + "px";
92
+ selectRect.style.top = y + "px";
93
+ selectRect.style.width = width + "px";
94
+ selectRect.style.height = height + "px";
95
+ },
96
+ end() {
97
+ if (!isSelecting)
98
+ return;
99
+ isSelecting = false;
100
+ let rectData = null;
101
+ if (selectRect) {
102
+ rectData = {
103
+ x: parseFloat(selectRect.style.left),
104
+ y: parseFloat(selectRect.style.top),
105
+ width: parseFloat(selectRect.style.width),
106
+ height: parseFloat(selectRect.style.height),
107
+ };
108
+ const percentData = _self.slider?.updateByRectData(rectData);
109
+ selectRect.remove();
110
+ selectRect = null;
111
+ startPoint = null;
112
+ percentData && _self.onSliderUpdated(percentData);
113
+ }
114
+ },
115
+ },
116
+ });
117
+ this.slider?.bindEvents();
118
+ this.slider?.addListener("slider:updated", this.onSliderUpdated);
119
+ this.context?.instance.on('zoom.end', this.handleZoomEnd);
120
+ }
121
+ unbindEvents() {
122
+ this.sliderWindow && interact(this.sliderWindow).unset();
123
+ this.slider?.unbindEvents();
124
+ this.slider?.removeListener("slider:updated", this.onSliderUpdated);
125
+ this.context?.instance.off('zoom.end', this.handleZoomEnd);
126
+ }
127
+ onSliderUpdated = ({ leftPercent, rightPercent }) => {
128
+ if (this.context) {
129
+ const { start, end } = this.context.options.maxTimeRange;
130
+ const newStart = +start + (+end - +start) * leftPercent;
131
+ const newEnd = +start + (+end - +start) * rightPercent;
132
+ this.context.instance.setTimeRange(new Date(newStart), new Date(newEnd));
133
+ }
134
+ };
135
+ handleZoomEnd = ({ timeRange }) => {
136
+ if (!this.context)
137
+ return;
138
+ const { start, end } = timeRange;
139
+ const { start: maxStart, end: maxEnd } = this.context.options.maxTimeRange;
140
+ const startPos = (+start - +maxStart) / (+maxEnd - +maxStart);
141
+ const endPos = (+end - +maxStart) / (+maxEnd - +maxStart);
142
+ this.slider?.updateByRange([startPos, endPos]);
143
+ };
144
+ drawSliderWindow() {
145
+ this.sliderWindow = document.createElement("div");
146
+ this.sliderWindow.classList.add("slider-window");
147
+ this.container.appendChild(this.sliderWindow);
148
+ }
149
+ drawSparks() {
150
+ this.sparkGroup = new SparkGroup().init(this.options.sparks, this.sliderWindow?.offsetWidth ?? 0);
151
+ this.sparkGroup?.el && this.sliderWindow?.appendChild(this.sparkGroup.el);
152
+ }
153
+ drawSlider() {
154
+ const edgeLabelConverter = (pos) => {
155
+ if (!this.context)
156
+ return "";
157
+ const { start, end } = this.context.options.maxTimeRange;
158
+ const time = +start + (+end - +start) * pos;
159
+ return this.options.dateFormat ? this.options.dateFormat(new Date(time)) : new Date(time).toLocaleString();
160
+ };
161
+ this.slider = new Slider().init(this.options.range, edgeLabelConverter);
162
+ this.slider?.el && this.sliderWindow?.appendChild(this.slider.el);
163
+ this.slider?.updateByRange();
164
+ }
165
+ }
package/package.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "@yiwei016/d3timeline-plugins",
3
+ "version": "1.0.0",
4
+ "description": "D3Timeline plugins",
5
+ "repository": "https://github.com/ElvisWangTech/d3timeline-plugins.git",
6
+ "author": "Elvis Wang <yiwei016@163.com>",
7
+ "publishConfig": {
8
+ "access": "public",
9
+ "registry": "https://registry.npmjs.org/"
10
+ },
11
+ "license": "MIT",
12
+ "type": "module",
13
+ "main": "./dist/index.js",
14
+ "module": "./dist/index.js",
15
+ "types": "./dist/index.d.ts",
16
+ "exports": {
17
+ ".": {
18
+ "import": "./dist/index.js",
19
+ "types": "./dist/index.d.ts"
20
+ },
21
+ "./ZoomSlider.css": "./src/zoom-slider/ZoomSlider.css"
22
+ },
23
+ "files": [
24
+ "dist"
25
+ ],
26
+ "scripts": {
27
+ "build": "tsc"
28
+ },
29
+ "devDependencies": {
30
+ "@types/node": "25.2.3",
31
+ "typescript": "~5.9.3"
32
+ },
33
+ "dependencies": {
34
+ "@yiwei016/d3timeline": "../D3Timeline",
35
+ "eventemitter3": "5.0.4",
36
+ "interactjs": "1.10.27"
37
+ },
38
+ "peerDependencies": {
39
+ "@yiwei016/d3timeline": "../D3Timeline"
40
+ }
41
+ }