@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.
- package/dist/base/uuid.d.ts +1 -0
- package/dist/base/uuid.js +15 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +23 -0
- package/dist/tree-chart/constant.d.ts +12 -0
- package/dist/tree-chart/constant.js +16 -0
- package/dist/tree-chart/index.d.ts +105 -0
- package/dist/tree-chart/index.js +600 -0
- package/dist/tree-chart/tree-chart.d.ts +9 -0
- package/dist/tree-chart/tree-chart.js +13 -0
- package/dist/tree-chart/util.d.ts +14 -0
- package/dist/tree-chart/util.js +33 -0
- package/package.json +23 -0
@@ -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
|
+
}
|
package/dist/index.d.ts
ADDED
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,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
|
+
}
|