@bencamus/tree-chart-core 0.1.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 declare function uuid(): string;
@@ -0,0 +1,15 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.uuid = uuid;
4
+ /*this is used for assign an id to the nodes of the tree */
5
+ function uuid() {
6
+ const s = [];
7
+ const hexDigits = '0123456789abcdef';
8
+ for (let i = 0; i < 36; i++) {
9
+ s[i] = hexDigits.substr(Math.floor(Math.random() * 0x10), 1);
10
+ }
11
+ s[14] = '4';
12
+ s[19] = hexDigits.substr((s[19] & 0x3) | 0x8, 1);
13
+ s[8] = s[13] = s[18] = s[23] = '-';
14
+ return s.join('');
15
+ }
@@ -0,0 +1,4 @@
1
+ import TreeChartCore from "./tree-chart/index";
2
+ export default TreeChartCore;
3
+ export * from './tree-chart/constant';
4
+ export * from './tree-chart/tree-chart';
package/dist/index.js ADDED
@@ -0,0 +1,23 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ var __importDefault = (this && this.__importDefault) || function (mod) {
17
+ return (mod && mod.__esModule) ? mod : { "default": mod };
18
+ };
19
+ Object.defineProperty(exports, "__esModule", { value: true });
20
+ const index_1 = __importDefault(require("./tree-chart/index"));
21
+ exports.default = index_1.default;
22
+ __exportStar(require("./tree-chart/constant"), exports);
23
+ __exportStar(require("./tree-chart/tree-chart"), exports);
@@ -0,0 +1,12 @@
1
+ export declare const DEFAULT_NODE_WIDTH = 100;
2
+ export declare const DEFAULT_NODE_HEIGHT = 100;
3
+ export declare const DEFAULT_LEVEL_HEIGHT = 200;
4
+ /**
5
+ * Used to decrement the height of the 'initTransformY' to center diagrams.
6
+ * This is only a hotfix caused by the addition of '__invisible_root' node
7
+ * for multi root purposes.
8
+ */
9
+ export declare const DEFAULT_HEIGHT_DECREMENT = 200;
10
+ export declare const ANIMATION_DURATION = 10;
11
+ export declare const MATCH_TRANSLATE_REGEX: RegExp;
12
+ export declare const MATCH_SCALE_REGEX: RegExp;
@@ -0,0 +1,16 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.MATCH_SCALE_REGEX = exports.MATCH_TRANSLATE_REGEX = exports.ANIMATION_DURATION = exports.DEFAULT_HEIGHT_DECREMENT = exports.DEFAULT_LEVEL_HEIGHT = exports.DEFAULT_NODE_HEIGHT = exports.DEFAULT_NODE_WIDTH = void 0;
4
+ exports.DEFAULT_NODE_WIDTH = 100;
5
+ exports.DEFAULT_NODE_HEIGHT = 100;
6
+ exports.DEFAULT_LEVEL_HEIGHT = 200;
7
+ /**
8
+ * Used to decrement the height of the 'initTransformY' to center diagrams.
9
+ * This is only a hotfix caused by the addition of '__invisible_root' node
10
+ * for multi root purposes.
11
+ */
12
+ exports.DEFAULT_HEIGHT_DECREMENT = 200;
13
+ //animation of the branches of the tree
14
+ exports.ANIMATION_DURATION = 10; //orginal 800, 10~>instant
15
+ exports.MATCH_TRANSLATE_REGEX = /translate\((-?\d+)px, ?(-?\d+)px\)/i;
16
+ exports.MATCH_SCALE_REGEX = /scale\((\S*)\)/i;
@@ -0,0 +1,105 @@
1
+ import * as d3 from "d3";
2
+ import { TreeDataset, Direction, TreeLinkStyle } from "./tree-chart";
3
+ interface TreeConfig {
4
+ nodeWidth: number;
5
+ nodeHeight: number;
6
+ levelHeight: number;
7
+ focusToNode: boolean;
8
+ initiallyCollapsed: boolean;
9
+ useMobileZoom?: boolean;
10
+ useMouseZoom?: boolean;
11
+ collapseDepth?: number;
12
+ }
13
+ interface TreeChartCoreParams {
14
+ treeConfig?: TreeConfig;
15
+ linkStyle?: TreeLinkStyle;
16
+ direction?: Direction;
17
+ collapseEnabled: boolean;
18
+ dataset: TreeDataset;
19
+ svgElement: SVGElement;
20
+ domElement: HTMLDivElement;
21
+ treeContainer: HTMLDivElement;
22
+ }
23
+ interface D3Link {
24
+ source: {
25
+ x: number;
26
+ y: number;
27
+ data: any;
28
+ };
29
+ target: {
30
+ x: number;
31
+ y: number;
32
+ data: any;
33
+ };
34
+ }
35
+ export default class TreeChartCore {
36
+ interactionMode: "drag" | "zoom" | null;
37
+ treeConfig: TreeConfig;
38
+ linkStyle: TreeLinkStyle;
39
+ direction: Direction;
40
+ collapseEnabled: boolean;
41
+ dataset: TreeDataset;
42
+ svgElement: SVGElement;
43
+ svgSelection: any;
44
+ domElement: HTMLDivElement;
45
+ treeContainer: HTMLDivElement;
46
+ nodeDataList: d3.HierarchyPointNode<any>[];
47
+ linkDataList: D3Link[];
48
+ initTransformX: number;
49
+ initTransformY: number;
50
+ currentScale: number;
51
+ constructor(params: TreeChartCoreParams);
52
+ init(): Promise<void>;
53
+ getNodeDataList(): d3.HierarchyPointNode<any>[];
54
+ private enableMouseZoom;
55
+ getInitialTransformStyle(): Record<string, string>;
56
+ private adjustZoom;
57
+ zoomIn(): void;
58
+ zoomOut(): void;
59
+ private enablePinchZoom;
60
+ restoreScale(): void;
61
+ restorePosition(): void;
62
+ setScale(scaleNum: number): void;
63
+ getTranslate(): number[];
64
+ isVertical(): boolean;
65
+ /**
66
+ * 根据link数据,生成svg path data
67
+ */
68
+ private generateLinkPath;
69
+ private generateCurceLinkPath;
70
+ private generateStraightLinkPath;
71
+ updateDataList(): void;
72
+ private draw;
73
+ private assignDepth;
74
+ /**
75
+ * Returns updated dataset by deep copying every nodes from the externalData and adding unique '_key' attributes.
76
+ **/
77
+ private updatedInternalData;
78
+ private collapseAllNodes;
79
+ private expandNodesByDepth;
80
+ private buildTree;
81
+ private enableDrag;
82
+ private cancelTransition;
83
+ reinitTransform(): void;
84
+ private initTransform;
85
+ private collapseNodesAtSameLevel;
86
+ onClickNode(index: number): void;
87
+ expandNodeByLevelAndPosition(level: number, nodeIndex: number): void;
88
+ private collapseSiblingNodes;
89
+ /**
90
+ * Focus on a specific node in the tree
91
+ * @param index Index of the node to focus on
92
+ */
93
+ focusToNode(index: number): void;
94
+ /**
95
+ * call this function to update dataset
96
+ * notice : you need to update the view rendered by `nodeDataList` too
97
+ * @param dataset the new dataset to show in chart
98
+ */
99
+ updateDataset(dataset: TreeDataset): void;
100
+ /**
101
+ * release all dom reference
102
+ */
103
+ destroy(): void;
104
+ }
105
+ export {};
@@ -0,0 +1,600 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ const d3 = __importStar(require("d3"));
37
+ const constant_1 = require("./constant");
38
+ const tree_chart_1 = require("./tree-chart");
39
+ const util_1 = require("./util");
40
+ class TreeChartCore {
41
+ constructor(params) {
42
+ this.interactionMode = null;
43
+ this.treeConfig = {
44
+ nodeWidth: constant_1.DEFAULT_NODE_WIDTH,
45
+ nodeHeight: constant_1.DEFAULT_NODE_HEIGHT,
46
+ levelHeight: constant_1.DEFAULT_LEVEL_HEIGHT,
47
+ focusToNode: false,
48
+ initiallyCollapsed: false,
49
+ useMobileZoom: false,
50
+ collapseDepth: 0, // default value
51
+ useMouseZoom: false,
52
+ };
53
+ this.linkStyle = tree_chart_1.TreeLinkStyle.CURVE;
54
+ this.direction = tree_chart_1.Direction.VERTICAL;
55
+ this.collapseEnabled = true;
56
+ this.nodeDataList = [];
57
+ this.linkDataList = [];
58
+ this.currentScale = 1;
59
+ if (params.treeConfig) {
60
+ this.treeConfig = params.treeConfig;
61
+ }
62
+ this.collapseEnabled = params.collapseEnabled;
63
+ this.svgElement = params.svgElement;
64
+ this.domElement = params.domElement;
65
+ this.treeContainer = params.treeContainer;
66
+ this.dataset = this.updatedInternalData(params.dataset);
67
+ if (params.direction)
68
+ this.direction = params.direction;
69
+ if (params.linkStyle)
70
+ this.linkStyle = params.linkStyle;
71
+ }
72
+ async init() {
73
+ this.draw();
74
+ this.enableDrag();
75
+ this.enablePinchZoom();
76
+ this.enableMouseZoom();
77
+ this.initTransform();
78
+ //To Do: dont work at reload
79
+ //this.expandNodeByLevelAndPosition(2, 1);
80
+ }
81
+ getNodeDataList() {
82
+ return this.nodeDataList;
83
+ }
84
+ enableMouseZoom() {
85
+ if (!this.treeConfig.useMouseZoom)
86
+ return;
87
+ const handleWheel = (event) => {
88
+ event.preventDefault();
89
+ // zoom direction
90
+ const zoomFactor = event.deltaY > 0 ? 0.9 : 1.1;
91
+ //adjust zoom
92
+ const originTransformStr = this.domElement.style.transform;
93
+ const scaleMatchResult = originTransformStr.match(constant_1.MATCH_SCALE_REGEX);
94
+ const originScale = scaleMatchResult
95
+ ? parseFloat(scaleMatchResult[1])
96
+ : 1;
97
+ // Calculate new scale
98
+ const newScale = originScale * zoomFactor;
99
+ this.setScale(newScale);
100
+ };
101
+ this.treeContainer.addEventListener("wheel", handleWheel, {
102
+ passive: false,
103
+ });
104
+ }
105
+ getInitialTransformStyle() {
106
+ return {
107
+ transform: `scale(1) translate(${this.initTransformX}px, ${this.initTransformY}px)`,
108
+ transformOrigin: "center",
109
+ };
110
+ }
111
+ adjustZoom(factor) {
112
+ const originTransformStr = this.domElement.style.transform;
113
+ const scaleMatchResult = originTransformStr.match(constant_1.MATCH_SCALE_REGEX);
114
+ const originScale = scaleMatchResult ? parseFloat(scaleMatchResult[1]) : 1;
115
+ this.setScale(originScale * factor);
116
+ }
117
+ zoomIn() {
118
+ this.adjustZoom(1.2);
119
+ }
120
+ zoomOut() {
121
+ this.adjustZoom(1 / 1.2);
122
+ }
123
+ enablePinchZoom() {
124
+ if (!this.treeConfig.useMobileZoom)
125
+ return;
126
+ let initialDistance = null;
127
+ let initialScale = this.currentScale;
128
+ const handleTouchStart = (event) => {
129
+ if (event.touches.length === 2) {
130
+ this.interactionMode = "zoom"; // change interaction mode to zoom
131
+ event.preventDefault();
132
+ initialDistance = Math.hypot(event.touches[0].clientX - event.touches[1].clientX, event.touches[0].clientY - event.touches[1].clientY);
133
+ initialScale = this.currentScale;
134
+ }
135
+ };
136
+ const handleTouchMove = (event) => {
137
+ if (this.interactionMode === "zoom" &&
138
+ event.touches.length === 2 &&
139
+ initialDistance !== null) {
140
+ event.preventDefault();
141
+ const currentDistance = Math.hypot(event.touches[0].clientX - event.touches[1].clientX, event.touches[0].clientY - event.touches[1].clientY);
142
+ const scale = (currentDistance / initialDistance) * initialScale;
143
+ this.setScale(scale);
144
+ }
145
+ };
146
+ const handleTouchEnd = (event) => {
147
+ if (event.touches.length < 2) {
148
+ this.interactionMode = null;
149
+ initialDistance = null;
150
+ }
151
+ };
152
+ this.treeContainer.addEventListener("touchstart", handleTouchStart, {
153
+ passive: false,
154
+ });
155
+ this.treeContainer.addEventListener("touchmove", handleTouchMove, {
156
+ passive: false,
157
+ });
158
+ this.treeContainer.addEventListener("touchend", handleTouchEnd, {
159
+ passive: false,
160
+ });
161
+ }
162
+ restoreScale() {
163
+ this.setScale(1);
164
+ }
165
+ restorePosition() {
166
+ // Enable transition for smooth animation
167
+ this.svgElement.style.transition = "transform 0.4s ease";
168
+ this.domElement.style.transition = "transform 0.4s ease";
169
+ // Reset scale to 1 and translate to initial position
170
+ const transformString = `scale(1) translate(${this.initTransformX}px, ${this.initTransformY}px)`;
171
+ this.svgElement.style.transform = transformString;
172
+ this.domElement.style.transform = transformString;
173
+ // Reset current scale
174
+ this.currentScale = 1;
175
+ // Clear transition after animation
176
+ setTimeout(() => {
177
+ this.svgElement.style.transition = "";
178
+ this.domElement.style.transition = "";
179
+ }, 400); // Match the transition duration
180
+ }
181
+ setScale(scaleNum) {
182
+ if (typeof scaleNum !== "number")
183
+ return;
184
+ const pos = this.getTranslate();
185
+ const translateString = `translate(${pos[0]}px, ${pos[1]}px)`;
186
+ this.svgElement.style.transform = `scale(${scaleNum}) ` + translateString;
187
+ this.domElement.style.transform = `scale(${scaleNum}) ` + translateString;
188
+ this.currentScale = scaleNum;
189
+ }
190
+ getTranslate() {
191
+ const string = this.svgElement.style.transform;
192
+ const match = string.match(constant_1.MATCH_TRANSLATE_REGEX);
193
+ if (match === null) {
194
+ return [null, null];
195
+ }
196
+ const x = parseInt(match[1]);
197
+ const y = parseInt(match[2]);
198
+ return [x, y];
199
+ }
200
+ isVertical() {
201
+ return this.direction === tree_chart_1.Direction.VERTICAL;
202
+ }
203
+ /**
204
+ * 根据link数据,生成svg path data
205
+ */
206
+ generateLinkPath(d) {
207
+ const self = this;
208
+ if (this.linkStyle === tree_chart_1.TreeLinkStyle.CURVE) {
209
+ return this.generateCurceLinkPath(self, d);
210
+ }
211
+ if (this.linkStyle === tree_chart_1.TreeLinkStyle.STRAIGHT) {
212
+ // the link path is: source -> secondPoint -> thirdPoint -> target
213
+ return this.generateStraightLinkPath(d);
214
+ }
215
+ return "";
216
+ }
217
+ generateCurceLinkPath(self, d) {
218
+ // Fix for D3 linkHorizontal/linkVertical which expect [number, number] format
219
+ // but we're using {x, y} format
220
+ const linkPath = this.isVertical()
221
+ ? d3.linkVertical()
222
+ : d3.linkHorizontal();
223
+ // Set accessor functions to return coordinates in the format D3 expects
224
+ linkPath
225
+ .x(function (point) {
226
+ return point.x;
227
+ })
228
+ .y(function (point) {
229
+ return point.y;
230
+ })
231
+ .source(function (d) {
232
+ const sourcePoint = {
233
+ x: d.source.x,
234
+ y: d.source.y,
235
+ };
236
+ return self.direction === tree_chart_1.Direction.VERTICAL
237
+ ? sourcePoint
238
+ : (0, util_1.rotatePoint)(sourcePoint);
239
+ })
240
+ .target(function (d) {
241
+ const targetPoint = {
242
+ x: d.target.x,
243
+ y: d.target.y,
244
+ };
245
+ return self.direction === tree_chart_1.Direction.VERTICAL
246
+ ? targetPoint
247
+ : (0, util_1.rotatePoint)(targetPoint);
248
+ });
249
+ return linkPath(d);
250
+ }
251
+ generateStraightLinkPath(d) {
252
+ const linkPath = d3.path();
253
+ let sourcePoint = { x: d.source.x, y: d.source.y };
254
+ let targetPoint = { x: d.target.x, y: d.target.y };
255
+ if (!this.isVertical()) {
256
+ sourcePoint = (0, util_1.rotatePoint)(sourcePoint);
257
+ targetPoint = (0, util_1.rotatePoint)(targetPoint);
258
+ }
259
+ const xOffset = targetPoint.x - sourcePoint.x;
260
+ const yOffset = targetPoint.y - sourcePoint.y;
261
+ const secondPoint = this.isVertical()
262
+ ? { x: sourcePoint.x, y: sourcePoint.y + yOffset / 2 }
263
+ : { x: sourcePoint.x + xOffset / 2, y: sourcePoint.y };
264
+ const thirdPoint = this.isVertical()
265
+ ? { x: targetPoint.x, y: sourcePoint.y + yOffset / 2 }
266
+ : { x: sourcePoint.x + xOffset / 2, y: targetPoint.y };
267
+ linkPath.moveTo(sourcePoint.x, sourcePoint.y);
268
+ linkPath.lineTo(secondPoint.x, secondPoint.y);
269
+ linkPath.lineTo(thirdPoint.x, thirdPoint.y);
270
+ linkPath.lineTo(targetPoint.x, targetPoint.y);
271
+ return linkPath.toString();
272
+ }
273
+ updateDataList() {
274
+ const [nodeDataList, linkDataList] = this.buildTree();
275
+ nodeDataList.splice(0, 1);
276
+ // Fix the type issue by casting to the proper D3Link type
277
+ this.linkDataList = linkDataList
278
+ .filter((x) => x.source.data.name !== "__invisible_root");
279
+ this.nodeDataList = nodeDataList;
280
+ }
281
+ draw() {
282
+ this.updateDataList();
283
+ const identifier = this.dataset["identifier"];
284
+ const specialLinks = this.dataset["links"];
285
+ if (specialLinks && identifier) {
286
+ for (const link of specialLinks) {
287
+ let parent, children = undefined;
288
+ if (identifier === "value") {
289
+ parent = this.nodeDataList.find((d) => {
290
+ return d[identifier] == link.parent;
291
+ });
292
+ children = this.nodeDataList.filter((d) => {
293
+ return d[identifier] == link.child;
294
+ });
295
+ }
296
+ else {
297
+ parent = this.nodeDataList.find((d) => {
298
+ return d["data"][identifier] == link.parent;
299
+ });
300
+ children = this.nodeDataList.filter((d) => {
301
+ return d["data"][identifier] == link.child;
302
+ });
303
+ }
304
+ if (parent && children) {
305
+ for (const child of children) {
306
+ const new_link = {
307
+ source: parent,
308
+ target: child,
309
+ };
310
+ this.linkDataList.push(new_link);
311
+ }
312
+ }
313
+ }
314
+ }
315
+ this.svgSelection = d3.select(this.svgElement);
316
+ const self = this;
317
+ const links = this.svgSelection
318
+ .selectAll(".link")
319
+ .data(this.linkDataList, (d) => {
320
+ return `${d.source.data._key}-${d.target.data._key}`;
321
+ });
322
+ links
323
+ .enter()
324
+ .append("path")
325
+ .style("opacity", 0)
326
+ .transition()
327
+ .duration(constant_1.ANIMATION_DURATION)
328
+ .ease(d3.easeCubicInOut)
329
+ .style("opacity", 1)
330
+ //.attr("class", "link")
331
+ .attr("class", (d) => (d.source.depth === 1 ? "link-hidden" : "link")) // 1 are the branches of the root node
332
+ .attr("d", function (d) {
333
+ return self.generateLinkPath(d);
334
+ });
335
+ links
336
+ .transition()
337
+ .duration(constant_1.ANIMATION_DURATION)
338
+ .ease(d3.easeCubicInOut)
339
+ .attr("d", function (d) {
340
+ return self.generateLinkPath(d);
341
+ });
342
+ links
343
+ .exit()
344
+ .transition()
345
+ .duration(constant_1.ANIMATION_DURATION / 2)
346
+ .ease(d3.easeCubicInOut)
347
+ .style("opacity", 0)
348
+ .remove();
349
+ }
350
+ assignDepth(node, currentDepth) {
351
+ node.depth = currentDepth;
352
+ if (node.children) {
353
+ node.children.forEach((child) => this.assignDepth(child, currentDepth + 1));
354
+ }
355
+ }
356
+ /**
357
+ * Returns updated dataset by deep copying every nodes from the externalData and adding unique '_key' attributes.
358
+ **/
359
+ updatedInternalData(externalData) {
360
+ const data = {
361
+ name: "__invisible_root",
362
+ children: [],
363
+ };
364
+ if (!externalData)
365
+ return data;
366
+ const minExpandedDepth = this.treeConfig.collapseDepth; // min level to expand nodes (-1 is the root)
367
+ if (Array.isArray(externalData)) {
368
+ for (let i = externalData.length - 1; i >= 0; i--) {
369
+ const rootNode = (0, util_1.deepCopy)(externalData[i]);
370
+ this.assignDepth(rootNode, 0);
371
+ if (this.treeConfig.initiallyCollapsed) {
372
+ this.collapseAllNodes(rootNode);
373
+ }
374
+ this.expandNodesByDepth(rootNode, minExpandedDepth || 0);
375
+ data.children.push(rootNode);
376
+ }
377
+ }
378
+ else {
379
+ const rootNode = (0, util_1.deepCopy)(externalData);
380
+ this.assignDepth(rootNode, 0);
381
+ if (this.treeConfig.initiallyCollapsed) {
382
+ this.collapseAllNodes(rootNode);
383
+ }
384
+ this.expandNodesByDepth(rootNode, minExpandedDepth || 0);
385
+ data.children.push(rootNode);
386
+ }
387
+ return data;
388
+ }
389
+ collapseAllNodes(node) {
390
+ if (node.children) {
391
+ node._children = node.children;
392
+ node.children = null;
393
+ node._collapsed = true;
394
+ node._children.forEach((child) => this.collapseAllNodes(child));
395
+ }
396
+ }
397
+ expandNodesByDepth(node, minDepth) {
398
+ if (node.depth > minDepth) {
399
+ return;
400
+ }
401
+ if (node.depth <= minDepth && node._children) {
402
+ node.children = node._children;
403
+ node._children = null;
404
+ node._collapsed = false;
405
+ }
406
+ node.children?.forEach((child) => this.expandNodesByDepth(child, minDepth));
407
+ }
408
+ buildTree() {
409
+ const treeBuilder = d3
410
+ .tree()
411
+ .nodeSize([this.treeConfig.nodeWidth, this.treeConfig.levelHeight]);
412
+ const tree = treeBuilder(d3.hierarchy(this.dataset));
413
+ return [tree.descendants(), tree.links()];
414
+ }
415
+ enableDrag() {
416
+ let startX = 0;
417
+ let startY = 0;
418
+ let isDrag = false;
419
+ let mouseDownTransform = "";
420
+ this.treeContainer.onpointerdown = (event) => {
421
+ if (this.interactionMode !== null)
422
+ return;
423
+ this.cancelTransition(); // cancel transition to avoid conflict with drag
424
+ this.interactionMode = "drag";
425
+ mouseDownTransform = this.svgElement.style.transform;
426
+ startX = event.clientX;
427
+ startY = event.clientY;
428
+ isDrag = true;
429
+ };
430
+ this.treeContainer.onpointermove = (event) => {
431
+ if (!isDrag || this.interactionMode !== "drag")
432
+ return;
433
+ const originTransform = mouseDownTransform;
434
+ let originOffsetX = 0;
435
+ let originOffsetY = 0;
436
+ if (originTransform) {
437
+ const result = originTransform.match(constant_1.MATCH_TRANSLATE_REGEX);
438
+ if (result !== null && result.length !== 0) {
439
+ const [offsetX, offsetY] = result.slice(1);
440
+ originOffsetX = parseInt(offsetX);
441
+ originOffsetY = parseInt(offsetY);
442
+ }
443
+ }
444
+ const newX = Math.floor((event.clientX - startX) / this.currentScale) +
445
+ originOffsetX;
446
+ const newY = Math.floor((event.clientY - startY) / this.currentScale) +
447
+ originOffsetY;
448
+ let transformStr = `translate(${newX}px, ${newY}px)`;
449
+ if (originTransform) {
450
+ transformStr = originTransform.replace(constant_1.MATCH_TRANSLATE_REGEX, transformStr);
451
+ }
452
+ this.svgElement.style.transform = transformStr;
453
+ this.domElement.style.transform = transformStr;
454
+ };
455
+ this.treeContainer.onpointerup = () => {
456
+ startX = 0;
457
+ startY = 0;
458
+ isDrag = false;
459
+ this.interactionMode = null;
460
+ };
461
+ }
462
+ cancelTransition() {
463
+ this.svgElement.style.transition = "none";
464
+ this.domElement.style.transition = "none";
465
+ const currentTransform = this.svgElement.style.transform;
466
+ this.svgElement.style.transform = currentTransform;
467
+ this.domElement.style.transform = currentTransform;
468
+ }
469
+ reinitTransform() {
470
+ this.initTransform();
471
+ }
472
+ initTransform() {
473
+ requestAnimationFrame(() => {
474
+ const containerWidth = this.domElement.offsetWidth;
475
+ const containerHeight = this.domElement.offsetHeight;
476
+ if (this.isVertical()) {
477
+ this.initTransformX = Math.floor(containerWidth / 2);
478
+ this.initTransformY = Math.floor(this.treeConfig.nodeHeight - constant_1.DEFAULT_HEIGHT_DECREMENT);
479
+ }
480
+ else {
481
+ this.initTransformX = Math.floor(this.treeConfig.nodeWidth - constant_1.DEFAULT_HEIGHT_DECREMENT);
482
+ this.initTransformY = Math.floor(containerHeight / 2);
483
+ }
484
+ // Apply initial transform
485
+ const transformStyle = this.getInitialTransformStyle();
486
+ this.svgElement.style.transform = transformStyle.transform;
487
+ this.domElement.style.transform = transformStyle.transform;
488
+ });
489
+ }
490
+ collapseNodesAtSameLevel(clickedNode) {
491
+ // Find the parent node to get siblings
492
+ const parent = this.nodeDataList.find((node) => node.data.children?.some((child) => child._key === clickedNode.data._key) ||
493
+ node.data._children?.some((child) => child._key === clickedNode.data._key));
494
+ if (!parent)
495
+ return;
496
+ // Get all siblings (including the clicked node)
497
+ const siblings = parent.data.children || [];
498
+ // Collapse all siblings except the clicked node
499
+ siblings.forEach((sibling) => {
500
+ if (sibling._key !== clickedNode.data._key) {
501
+ if (sibling.children) {
502
+ sibling._children = sibling.children;
503
+ sibling.children = null;
504
+ sibling._collapsed = true;
505
+ }
506
+ }
507
+ });
508
+ }
509
+ onClickNode(index) {
510
+ //dont collapse the root node
511
+ if (index === 0)
512
+ return;
513
+ if (this.collapseEnabled) {
514
+ const curNode = this.nodeDataList[index];
515
+ if (curNode.data.children) {
516
+ curNode.data._children = curNode.data.children;
517
+ curNode.data.children = null;
518
+ curNode.data._collapsed = true;
519
+ }
520
+ else {
521
+ curNode.data.children = curNode.data._children;
522
+ curNode.data._children = null;
523
+ curNode.data._collapsed = false;
524
+ // When expanding a node, collapse its siblings
525
+ this.collapseNodesAtSameLevel(curNode);
526
+ }
527
+ this.draw();
528
+ }
529
+ }
530
+ expandNodeByLevelAndPosition(level, nodeIndex) {
531
+ const nodesAtLevel = this.nodeDataList.filter((node) => node.depth === level);
532
+ if (nodeIndex < 0 || nodeIndex >= nodesAtLevel.length) {
533
+ console.error("Node not found at level", level, "position", nodeIndex);
534
+ return;
535
+ }
536
+ const targetNode = nodesAtLevel[nodeIndex];
537
+ const nodeData = targetNode.data;
538
+ if (nodeData._children) {
539
+ nodeData.children = nodeData._children;
540
+ nodeData._children = null;
541
+ nodeData._collapsed = false;
542
+ this.draw();
543
+ }
544
+ }
545
+ collapseSiblingNodes(node) {
546
+ const parent = this.nodeDataList.find((n) => n.data.children?.some((child) => child._key === node.data._key) ||
547
+ n.data._children?.some((child) => child._key === node.data._key));
548
+ if (parent) {
549
+ parent.data.children?.forEach((child) => {
550
+ if (child._key !== node.data._key && child.children) {
551
+ child._children = child.children;
552
+ child.children = null;
553
+ child._collapsed = true;
554
+ }
555
+ });
556
+ }
557
+ }
558
+ /**
559
+ * Focus on a specific node in the tree
560
+ * @param index Index of the node to focus on
561
+ */
562
+ focusToNode(index) {
563
+ //this.restoreScale();
564
+ if (!this.treeConfig.focusToNode)
565
+ return;
566
+ if (!this.nodeDataList || index < 0 || index >= this.nodeDataList.length) {
567
+ return;
568
+ }
569
+ const node = this.nodeDataList[index];
570
+ const containerWidth = this.domElement.offsetWidth;
571
+ const containerHeight = this.domElement.offsetHeight;
572
+ const nodeX = this.isVertical() ? node.x : node.y;
573
+ const nodeY = this.isVertical() ? node.y : node.x;
574
+ const translateX = Math.floor(containerWidth / 2 - nodeX);
575
+ const translateY = Math.floor(containerHeight / 2 - nodeY - constant_1.DEFAULT_NODE_HEIGHT * 2 - 10); // -2 nodes to center the diagram +10 of root node
576
+ const transformString = `scale(${this.currentScale}) translate(${translateX}px, ${translateY}px)`;
577
+ this.svgElement.style.transition = "transform 0.3s ease";
578
+ this.domElement.style.transition = "transform 0.3s ease";
579
+ this.svgElement.style.transform = transformString;
580
+ this.domElement.style.transform = transformString;
581
+ }
582
+ /**
583
+ * call this function to update dataset
584
+ * notice : you need to update the view rendered by `nodeDataList` too
585
+ * @param dataset the new dataset to show in chart
586
+ */
587
+ updateDataset(dataset) {
588
+ this.dataset = this.updatedInternalData(dataset);
589
+ this.draw();
590
+ }
591
+ /**
592
+ * release all dom reference
593
+ */
594
+ destroy() {
595
+ this.svgElement = null;
596
+ this.domElement = null;
597
+ this.treeContainer = null;
598
+ }
599
+ }
600
+ exports.default = TreeChartCore;
@@ -0,0 +1,9 @@
1
+ export declare enum Direction {
2
+ VERTICAL = "vertical",
3
+ HORIZONTAL = "horizontal"
4
+ }
5
+ export declare enum TreeLinkStyle {
6
+ CURVE = "curve",
7
+ STRAIGHT = "straight"
8
+ }
9
+ export type TreeDataset = Object | Object[];
@@ -0,0 +1,13 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.TreeLinkStyle = exports.Direction = void 0;
4
+ var Direction;
5
+ (function (Direction) {
6
+ Direction["VERTICAL"] = "vertical";
7
+ Direction["HORIZONTAL"] = "horizontal";
8
+ })(Direction || (exports.Direction = Direction = {}));
9
+ var TreeLinkStyle;
10
+ (function (TreeLinkStyle) {
11
+ TreeLinkStyle["CURVE"] = "curve";
12
+ TreeLinkStyle["STRAIGHT"] = "straight";
13
+ })(TreeLinkStyle || (exports.TreeLinkStyle = TreeLinkStyle = {}));
@@ -0,0 +1,14 @@
1
+ export declare function rotatePoint({ x, y }: {
2
+ x: number;
3
+ y: number;
4
+ }): {
5
+ x: number;
6
+ y: number;
7
+ };
8
+ /**
9
+ * Returns a deep copy of selected node (copy of itself and it's children).
10
+ * If selected node or it's children have no '_key' attribute it will assign a new one.
11
+ **/
12
+ export declare function deepCopy(node: any): {
13
+ _key: string;
14
+ };
@@ -0,0 +1,33 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.rotatePoint = rotatePoint;
4
+ exports.deepCopy = deepCopy;
5
+ const uuid_1 = require("../base/uuid");
6
+ function rotatePoint({ x, y }) {
7
+ return {
8
+ x: y,
9
+ y: x,
10
+ };
11
+ }
12
+ /**
13
+ * Returns a deep copy of selected node (copy of itself and it's children).
14
+ * If selected node or it's children have no '_key' attribute it will assign a new one.
15
+ **/
16
+ function deepCopy(node) {
17
+ let obj = { _key: (0, uuid_1.uuid)() };
18
+ for (var key in node) {
19
+ if (node[key] === null) {
20
+ obj[key] = null;
21
+ }
22
+ else if (Array.isArray(node[key])) {
23
+ obj[key] = node[key].map((x) => deepCopy(x));
24
+ }
25
+ else if (typeof node[key] === "object") {
26
+ obj[key] = deepCopy(node[key]);
27
+ }
28
+ else {
29
+ obj[key] = node[key];
30
+ }
31
+ }
32
+ return obj;
33
+ }
package/package.json ADDED
@@ -0,0 +1,23 @@
1
+ {
2
+ "name": "@bencamus/tree-chart-core",
3
+ "version": "0.1.0",
4
+ "main": "dist/index.js",
5
+ "types": "dist/index.d.ts",
6
+ "files": [
7
+ "dist"
8
+ ],
9
+ "scripts": {
10
+ "build": "tsc",
11
+ "test": "echo \"Error: no test specified\" && exit 1"
12
+ },
13
+ "dependencies": {
14
+ "d3": "^7.9.0"
15
+ },
16
+ "devDependencies": {
17
+ "@types/d3": "^7.4.3",
18
+ "ts-loader": "^9.5.2",
19
+ "typescript": "^5.8.2"
20
+ },
21
+ "license": "MIT",
22
+ "description": "Core library for tree chart visualization."
23
+ }