Skip to content

Commit fa1db97

Browse files
authored
Merge pull request #15 from Grihail/master
Individual arrow colors, directions and line types. Add color parameter for individual arrow colors Add direction parameter (0=no arrows, 1=forward, 2=backward, 3=both) Add line parameter (0=solid, 1=dashed) Update README with new parameters documentation
2 parents a73dc4c + 5aa79c3 commit fa1db97

File tree

3 files changed

+186
-142
lines changed

3 files changed

+186
-142
lines changed

README.md

Lines changed: 42 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
Following the issue of vis https://github.com/almende/vis/issues/1699, and thanks to the comments of @frboyer and @JimmyCheng, I have created a class to easily draw lines to connect items in the vis Timeline module.
44

5-
![CapturaTime](https://user-images.githubusercontent.com/36993404/59111595-9d830600-8941-11e9-8cb8-8d7b72701a71.JPG)
5+
![CapturaTime](https://github.com/user-attachments/assets/a8b85ee1-d147-407e-8132-d749804362f3)
66

77

88
## Install & initialize
@@ -32,15 +32,18 @@ const myTimeline = new vis.Timeline(container, items, groups, options);
3232

3333
And optionally:
3434
* title (insert a text and it will show a title if you hover the mouse in the arrow)
35+
* color (set individual arrow color, overrides the global color option)
36+
* direction (arrow direction: 0=no arrows, 1=forward only, 2=backward only, 3=both directions)
37+
* line (line type: 0=solid line, 1=dashed line)
3538

3639
For instance:
3740

3841
```javascript
3942
var arrowsSpecs = [
4043
{ id: 2, id_item_1: 1, id_item_2: 2 },
41-
{ id: 5, id_item_1: 3, id_item_2: 5, title:'Hello!!!' },
42-
{ id: 7, id_item_1: 6, id_item_2: 7 },
43-
{ id: 10, id_item_1: 3, id_item_2: 8, title:'I am a title!!!' }
44+
{ id: 5, id_item_1: 3, id_item_2: 5, title:'Hello!!!', color: '#ff0000', direction: 1 },
45+
{ id: 7, id_item_1: 6, id_item_2: 7, line: 1 },
46+
{ id: 10, id_item_1: 3, id_item_2: 8, title:'I am a title!!!', color: '#00ff00', direction: 3, line: 1 }
4447
];
4548
```
4649

@@ -86,6 +89,41 @@ This method takes two arguments, `el` - the arrow - and `title` - the content of
8689
**hideWhenItemsNotVisible** - defaults to `true`.
8790
When you zoom the timeline and both items go out of the screen. You can set if the arrow is still visible. By default, the arrow hides, but you can change it setting this option to `false`.
8891

92+
## Arrow Properties
93+
94+
Each arrow can have the following individual properties:
95+
96+
**color** - Optional. Set individual arrow color (including the arrowhead). This overrides the global color option.
97+
Example: `color: '#ff0000'`
98+
99+
**direction** - Optional. Controls arrow direction. Defaults to `1`.
100+
101+
* `0` = No arrows (simple line connection)
102+
* `1` = Forward arrow only (from item_1 to item_2)
103+
* `2` = Backward arrow only (from item_2 to item_1)
104+
* `3` = Both directions (arrows on both ends)
105+
106+
**line** - Optional. Controls line style. Defaults to `0`.
107+
108+
* `0` = Solid line (default)
109+
* `1` = Dashed line
110+
111+
**title** - Optional. Text that appears on hover.
112+
113+
Example with all properties:
114+
115+
```javascript
116+
{
117+
id: 1,
118+
id_item_1: 3,
119+
id_item_2: 8,
120+
title: 'Custom arrow',
121+
color: '#00ff00',
122+
direction: 3,
123+
line: 1
124+
}
125+
```
126+
89127
## Methods
90128

91129
I have created the following methods:

arrow.js

Lines changed: 116 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,9 @@
4040
* @property {VisIdType} id_item_1 start timeline item id
4141
* @property {VisIdType} id_item_2 end timeline item id
4242
* @property {string} [title] optional arrow title
43+
* @property {string} [color] optional arrow color
44+
* @property {number} [direction] arrow direction: 0=no arrows, 1=forward only, 2=backward only, 3=both directions
45+
* @property {number} [line] line type: 0=solid (default), 1=dashed
4346
*/
4447

4548
/**
@@ -78,16 +81,8 @@ export default class Arrow {
7881
/** @private @type {boolean} if true, arrows will be hidden when both items is not visible due to timeline zoom */
7982
this._hideWhenItemsNotVisible = options?.hideWhenItemsNotVisible ?? true;
8083

81-
/** @private @type {SVGMarkerElement} */
82-
this._arrowHead = document.createElementNS(
83-
"http://www.w3.org/2000/svg",
84-
"marker"
85-
);
86-
/** @private @type {SVGPathElement} */
87-
this._arrowHeadPath = document.createElementNS(
88-
"http://www.w3.org/2000/svg",
89-
"path"
90-
);
84+
/** @private @type {Map<string, string>} map of color to marker id */
85+
this._colorMarkers = new Map();
9186

9287
this._dependency = [...dependencies];
9388

@@ -111,23 +106,9 @@ export default class Arrow {
111106
this._svg.style.pointerEvents = "none"; // To click through, if we decide to put it above other elements.
112107
this._timeline.dom.center.appendChild(this._svg);
113108

114-
//Configure the arrowHead
115-
this._arrowHead.setAttribute("id", this._arrowHeadId);
116-
this._arrowHead.setAttribute("viewBox", "-10 -5 10 10");
117-
this._arrowHead.setAttribute("refX", "-7");
118-
this._arrowHead.setAttribute("refY", "0");
119-
this._arrowHead.setAttribute("markerUnits", "strokeWidth");
120-
this._arrowHead.setAttribute("markerWidth", "3");
121-
this._arrowHead.setAttribute("markerHeight", "3");
122-
this._arrowHead.setAttribute("orient", "auto-start-reverse");
123-
//Configure the path of the arrowHead (arrowHeadPath)
124-
this._arrowHeadPath.setAttribute("d", "M 0 0 L -10 -5 L -7.5 0 L -10 5 z");
125-
this._arrowHeadPath.style.fill = this._arrowsColor;
126-
this._arrowHead.appendChild(this._arrowHeadPath);
127-
this._svg.appendChild(this._arrowHead);
128109
//Create paths for the started dependency array
129110
for (let i = 0; i < this._dependency.length; i++) {
130-
this._createPath();
111+
this._createPath(this._dependency[i].color, this._dependency[i].line);
131112
}
132113

133114
//NOTE: We hijack the on "changed" event to draw the arrows.
@@ -136,19 +117,68 @@ export default class Arrow {
136117
});
137118

138119
}
120+
121+
/** @private */
122+
_getOrCreateMarker(color) {
123+
const arrowColor = color || this._arrowsColor;
124+
125+
if (!this._colorMarkers.has(arrowColor)) {
126+
const markerId = `arrowhead-${arrowColor.replace('#', '')}-${Math.random().toString(36).substring(2)}`;
127+
128+
const arrowHead = document.createElementNS("http://www.w3.org/2000/svg", "marker");
129+
arrowHead.setAttribute("id", markerId);
130+
arrowHead.setAttribute("viewBox", "-10 -5 10 10");
131+
arrowHead.setAttribute("refX", "-7");
132+
arrowHead.setAttribute("refY", "0");
133+
arrowHead.setAttribute("markerUnits", "strokeWidth");
134+
arrowHead.setAttribute("markerWidth", "3");
135+
arrowHead.setAttribute("markerHeight", "3");
136+
arrowHead.setAttribute("orient", "auto-start-reverse");
137+
138+
const arrowHeadPath = document.createElementNS("http://www.w3.org/2000/svg", "path");
139+
arrowHeadPath.setAttribute("d", "M 0 0 L -10 -5 L -7.5 0 L -10 5 z");
140+
arrowHeadPath.style.fill = arrowColor;
141+
142+
arrowHead.appendChild(arrowHeadPath);
143+
this._svg.appendChild(arrowHead);
144+
145+
this._colorMarkers.set(arrowColor, markerId);
146+
}
147+
148+
return this._colorMarkers.get(arrowColor);
149+
}
139150

140151
/** @private */
141-
_createPath(){
152+
_createPath(color, lineType){
142153
//Add a new path to array dependencyPath and to svg
143154
let somePath = document.createElementNS(
144155
"http://www.w3.org/2000/svg",
145156
"path"
146157
);
147158
somePath.setAttribute("d", "M 0 0");
148-
somePath.style.stroke = this._arrowsColor;
159+
somePath.style.stroke = color || this._arrowsColor;
149160
somePath.style.strokeWidth = this._arrowsStrokeWidth + "px";
150161
somePath.style.fill = "none";
151162
somePath.style.pointerEvents = "auto";
163+
164+
// Устанавливаем тип линии
165+
const line = lineType !== undefined ? lineType : 0; // По умолчанию сплошная линия
166+
if (line === 0) {
167+
// Тип 0: Сплошная линия (по умолчанию)
168+
somePath.style.strokeDasharray = "none";
169+
} else if (line === 1) {
170+
// Тип 1: Пунктирная линия
171+
somePath.style.strokeDasharray = "7,5";
172+
}
173+
// Здесь можно добавить дополнительные типы линий:
174+
// } else if (line === 2) {
175+
// // Тип 2: Точечная линия
176+
// somePath.style.strokeDasharray = "2,3";
177+
// } else if (line === 3) {
178+
// // Тип 3: Штрих-пунктир
179+
// somePath.style.strokeDasharray = "10,5,2,5";
180+
// }
181+
152182
this._dependencyPath.push(somePath);
153183
this._svg.appendChild(somePath);
154184
}
@@ -215,10 +245,48 @@ export default class Arrow {
215245

216246
var curveLen = item_1.height * 2; // Length of straight Bezier segment out of the item.
217247

248+
const markerId = this._getOrCreateMarker(dep.color);
249+
const direction = dep.direction !== undefined ? dep.direction : 1; // По умолчанию направление вперед
250+
251+
// Определяем какие маркеры нужно установить в зависимости от direction
252+
let markerStart = "";
253+
let markerEnd = "";
254+
255+
if (direction === 0) {
256+
// Без стрелок
257+
markerStart = "";
258+
markerEnd = "";
259+
} else if (direction === 1) {
260+
// Направление вперед (по умолчанию)
261+
if (this._followRelationships && item_2.mid_x < item_1.mid_x) {
262+
markerStart = `url(#${markerId})`;
263+
markerEnd = "";
264+
} else {
265+
markerStart = "";
266+
markerEnd = `url(#${markerId})`;
267+
}
268+
} else if (direction === 2) {
269+
// Направление назад
270+
if (this._followRelationships && item_2.mid_x < item_1.mid_x) {
271+
markerStart = "";
272+
markerEnd = `url(#${markerId})`;
273+
} else {
274+
markerStart = `url(#${markerId})`;
275+
markerEnd = "";
276+
}
277+
} else if (direction === 3) {
278+
// Обе стороны
279+
markerStart = `url(#${markerId})`;
280+
markerEnd = `url(#${markerId})`;
281+
}
282+
218283
if (this._followRelationships && item_2.mid_x < item_1.mid_x) {
219-
item_2.right += 10; // Space for the arrowhead.
220-
this._dependencyPath[index].setAttribute("marker-start", `url(#${this._arrowHeadId})`);
221-
this._dependencyPath[index].setAttribute("marker-end", "");
284+
// Добавляем отступы только там, где есть стрелки
285+
if (markerStart !== "") item_2.right += 10; // Space for the arrowhead at start
286+
if (markerEnd !== "") item_1.left -= 10; // Space for the arrowhead at end
287+
288+
this._dependencyPath[index].setAttribute("marker-start", markerStart);
289+
this._dependencyPath[index].setAttribute("marker-end", markerEnd);
222290
this._dependencyPath[index].setAttribute(
223291
"d",
224292
"M " +
@@ -239,9 +307,12 @@ export default class Arrow {
239307
item_1.mid_y
240308
);
241309
} else {
242-
item_2.left -= 10; // Space for the arrowhead.
243-
this._dependencyPath[index].setAttribute("marker-end", `url(#${this._arrowHeadId})`);
244-
this._dependencyPath[index].setAttribute("marker-start", "");
310+
// Добавляем отступы только там, где есть стрелки
311+
if (markerEnd !== "") item_2.left -= 10; // Space for the arrowhead at end
312+
if (markerStart !== "") item_1.right += 10; // Space for the arrowhead at start
313+
314+
this._dependencyPath[index].setAttribute("marker-end", markerEnd);
315+
this._dependencyPath[index].setAttribute("marker-start", markerStart);
245316
this._dependencyPath[index].setAttribute(
246317
"d",
247318
"M " +
@@ -304,7 +375,7 @@ export default class Arrow {
304375
*/
305376
addArrow(dep) {
306377
this._dependency.push(dep);
307-
this._createPath();
378+
this._createPath(dep.color, dep.line);
308379
this._timeline.redraw();
309380
}
310381

@@ -332,19 +403,21 @@ export default class Arrow {
332403
* Función que recibe el id de una flecha y la elimina.
333404
* @param {ArrowIdType} id arrow id
334405
*/
335-
removeArrow(id) {
406+
removeArrow(id) {
336407
const index = this._dependency.findIndex(dep => dep.id === id);
337408

338409
if (index >= 0) {
410+
// Get the path element from our specific array before modifying the arrays
411+
const pathToRemove = this._dependencyPath[index];
339412

340-
//var list = document.getElementsByTagName("path"); //FALTA QUE ESTA SELECCION LA HAGA PARA EL DOM DEL TIMELINE INSTANCIADO!!!!
341-
const list = document.querySelectorAll("#" + this._timeline.dom.container.id + " path");
413+
// Remove the SVG element from the DOM
414+
if (pathToRemove && pathToRemove.parentNode) {
415+
pathToRemove.parentNode.removeChild(pathToRemove);
416+
}
342417

343-
this._dependency.splice(index, 1); //Elimino del array dependency
344-
this._dependencyPath.splice(index, 1); //Elimino del array dependencyPath
345-
346-
list[index + 1].parentNode?.removeChild(list[index + 1]); //Lo elimino del dom
347-
418+
// Now, remove the arrow from internal arrays
419+
this._dependency.splice(index, 1);
420+
this._dependencyPath.splice(index, 1);
348421
}
349422
}
350423

@@ -391,4 +464,4 @@ export default class Arrow {
391464
this.removeItemArrows(id);
392465
}
393466

394-
}
467+
}

0 commit comments

Comments
 (0)