@unovis/ts 1.6.0-beta.0 → 1.6.0-bigip.1

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.
Files changed (96) hide show
  1. package/components/axis/config.d.ts +1 -1
  2. package/components/axis/config.js.map +1 -1
  3. package/components/axis/index.d.ts +0 -1
  4. package/components/axis/index.js +2 -10
  5. package/components/axis/index.js.map +1 -1
  6. package/components/brush/config.d.ts +1 -1
  7. package/components/brush/config.js.map +1 -1
  8. package/components/crosshair/config.d.ts +6 -0
  9. package/components/crosshair/config.js +1 -1
  10. package/components/crosshair/config.js.map +1 -1
  11. package/components/crosshair/index.d.ts +7 -0
  12. package/components/crosshair/index.js +192 -45
  13. package/components/crosshair/index.js.map +1 -1
  14. package/components/graph/config.d.ts +8 -0
  15. package/components/graph/config.js +1 -1
  16. package/components/graph/config.js.map +1 -1
  17. package/components/graph/index.d.ts +1 -0
  18. package/components/graph/index.js +14 -9
  19. package/components/graph/index.js.map +1 -1
  20. package/components/graph/modules/layout.js +33 -31
  21. package/components/graph/modules/layout.js.map +1 -1
  22. package/components/graph/modules/link/index.js +1 -1
  23. package/components/graph/modules/link/index.js.map +1 -1
  24. package/components/graph/types.d.ts +2 -1
  25. package/components/graph/types.js.map +1 -1
  26. package/components/plotband/config.d.ts +59 -0
  27. package/components/plotband/config.js +9 -0
  28. package/components/plotband/config.js.map +1 -0
  29. package/components/plotband/constants.d.ts +5 -0
  30. package/components/plotband/constants.js +413 -0
  31. package/components/plotband/constants.js.map +1 -0
  32. package/components/plotband/index.d.ts +15 -0
  33. package/components/plotband/index.js +92 -0
  34. package/components/plotband/index.js.map +1 -0
  35. package/components/plotband/style.d.ts +4 -0
  36. package/components/plotband/style.js +38 -0
  37. package/components/plotband/style.js.map +1 -0
  38. package/components/plotband/types.d.ts +51 -0
  39. package/components/plotband/types.js +27 -0
  40. package/components/plotband/types.js.map +1 -0
  41. package/components/scatter/index.d.ts +1 -0
  42. package/components/scatter/index.js +11 -1
  43. package/components/scatter/index.js.map +1 -1
  44. package/components/timeline/config.d.ts +65 -14
  45. package/components/timeline/config.js +15 -1
  46. package/components/timeline/config.js.map +1 -1
  47. package/components/timeline/constants.d.ts +3 -0
  48. package/components/timeline/constants.js +6 -0
  49. package/components/timeline/constants.js.map +1 -0
  50. package/components/timeline/index.d.ts +21 -10
  51. package/components/timeline/index.js +380 -95
  52. package/components/timeline/index.js.map +1 -1
  53. package/components/timeline/style.d.ts +7 -0
  54. package/components/timeline/style.js +40 -1
  55. package/components/timeline/style.js.map +1 -1
  56. package/components/timeline/types.d.ts +62 -0
  57. package/components/timeline/types.js +2 -0
  58. package/components/timeline/types.js.map +1 -0
  59. package/components/timeline/utils.d.ts +2 -0
  60. package/components/timeline/utils.js +16 -0
  61. package/components/timeline/utils.js.map +1 -0
  62. package/components/xy-labels/index.js +1 -1
  63. package/components/xy-labels/index.js.map +1 -1
  64. package/components.d.ts +4 -2
  65. package/components.js +1 -0
  66. package/components.js.map +1 -1
  67. package/containers/xy-container/config.d.ts +2 -0
  68. package/containers/xy-container/config.js +1 -1
  69. package/containers/xy-container/config.js.map +1 -1
  70. package/containers/xy-container/index.js +2 -2
  71. package/containers/xy-container/index.js.map +1 -1
  72. package/data-models/graph.js +7 -1
  73. package/data-models/graph.js.map +1 -1
  74. package/index.js +4 -2
  75. package/index.js.map +1 -1
  76. package/package.json +4 -3
  77. package/styles/index.js +1 -0
  78. package/styles/index.js.map +1 -1
  79. package/types/position.d.ts +2 -1
  80. package/types/position.js +1 -0
  81. package/types/position.js.map +1 -1
  82. package/types/text.d.ts +1 -1
  83. package/types/text.js.map +1 -1
  84. package/types.d.ts +2 -0
  85. package/types.js +2 -0
  86. package/types.js.map +1 -1
  87. package/utils/data.d.ts +1 -1
  88. package/utils/data.js +2 -2
  89. package/utils/data.js.map +1 -1
  90. package/utils/index.js +2 -2
  91. package/utils/path.d.ts +8 -0
  92. package/utils/path.js +109 -1
  93. package/utils/path.js.map +1 -1
  94. package/utils/text.d.ts +3 -2
  95. package/utils/text.js +21 -9
  96. package/utils/text.js.map +1 -1
@@ -1,15 +1,22 @@
1
1
  import { select } from 'd3-selection';
2
+ import { max, min, minIndex } from 'd3-array';
2
3
  import { scaleOrdinal } from 'd3-scale';
3
4
  import { drag } from 'd3-drag';
4
- import { max } from 'd3-array';
5
5
  import { XYComponentCore } from '../../core/xy-component/index.js';
6
- import { isNumber, unique, arrayOfIndices, getString, getNumber, getMin, getMax } from '../../utils/data.js';
6
+ import { getNumber, isNumber, arrayOfIndices, getValue, isPlainObject, isFunction, getString, groupBy, getMin, getMax } from '../../utils/data.js';
7
7
  import { smartTransition } from '../../utils/d3.js';
8
8
  import { getColor } from '../../utils/color.js';
9
- import { trimSVGText } from '../../utils/text.js';
9
+ import { trimSVGText, textAlignToAnchor } from '../../utils/text.js';
10
+ import { arrowPolylinePath } from '../../utils/path.js';
11
+ import { guid } from '../../utils/misc.js';
12
+ import '../../types.js';
10
13
  import { TimelineDefaultConfig } from './config.js';
11
14
  import * as style from './style.js';
12
- import { background, rows, lines, labels, scrollbar, scrollbarBackground, scrollbarHandle, label, row, rowOdd, line } from './style.js';
15
+ import { background, rows, arrows, lines, labels, rowIcons, scrollbar, scrollbarBackground, scrollbarHandle, label, rowIcon, row, rowOdd, lineGroup, line, lineStartIcon, lineEndIcon, arrow } from './style.js';
16
+ import { TIMELINE_DEFAULT_ARROW_HEAD_LENGTH, TIMELINE_DEFAULT_ARROW_HEAD_WIDTH, TIMELINE_DEFAULT_ARROW_MARGIN } from './constants.js';
17
+ import { getIconBleed } from './utils.js';
18
+ import { TextAlign } from '../../types/text.js';
19
+ import { Arrangement } from '../../types/position.js';
13
20
 
14
21
  class Timeline extends XYComponentCore {
15
22
  constructor(config) {
@@ -17,6 +24,12 @@ class Timeline extends XYComponentCore {
17
24
  this._defaultConfig = TimelineDefaultConfig;
18
25
  this.config = this._defaultConfig;
19
26
  this.events = {
27
+ [Timeline.selectors.background]: {
28
+ wheel: this._onMouseWheel.bind(this),
29
+ },
30
+ [Timeline.selectors.label]: {
31
+ wheel: this._onMouseWheel.bind(this),
32
+ },
20
33
  [Timeline.selectors.rows]: {
21
34
  wheel: this._onMouseWheel.bind(this),
22
35
  },
@@ -30,14 +43,29 @@ class Timeline extends XYComponentCore {
30
43
  this._maxScroll = 0;
31
44
  this._scrollbarHeight = 0;
32
45
  this._labelMargin = 5;
46
+ this._labelWidth = 0; // Will be overridden in `get bleed ()`
47
+ this._rowIconBleed = [0, 0];
48
+ this._lineBleed = [0, 0];
49
+ /** We define a dedicated clipping path for this component because it needs to behave
50
+ * differently than the regular XYContainer's clipPath */
51
+ this._clipPathId = guid();
33
52
  if (config)
34
53
  this.setConfig(config);
35
54
  // Invisible background rect to track events
36
55
  this._background = this.g.append('rect').attr('class', background);
56
+ // Clip path
57
+ this._clipPath = this.g.append('clipPath')
58
+ .attr('id', this._clipPathId);
59
+ this._clipPath.append('rect');
37
60
  // Group for content
38
- this._rowsGroup = this.g.append('g').attr('class', rows);
39
- this._linesGroup = this.g.append('g').attr('class', lines);
61
+ this._rowsGroup = this.g.append('g').attr('class', rows)
62
+ .style('clip-path', `url(#${this._clipPathId})`);
63
+ this._arrowsGroup = this.g.append('g').attr('class', arrows)
64
+ .style('clip-path', `url(#${this._clipPathId})`);
65
+ this._linesGroup = this.g.append('g').attr('class', lines)
66
+ .style('clip-path', `url(#${this._clipPathId})`);
40
67
  this._labelsGroup = this.g.append('g').attr('class', labels);
68
+ this._rowIconsGroup = this.g.append('g').attr('class', rowIcons);
41
69
  this._scrollBarGroup = this.g.append('g').attr('class', scrollbar);
42
70
  // Scroll bar
43
71
  this._scrollBarBackground = this._scrollBarGroup.append('rect')
@@ -49,35 +77,80 @@ class Timeline extends XYComponentCore {
49
77
  .on('drag', this._onScrollbarDrag.bind(this));
50
78
  this._scrollBarHandle.call(dragBehaviour);
51
79
  }
80
+ setConfig(config) {
81
+ super.setConfig(config);
82
+ }
83
+ setData(data) {
84
+ super.setData(data);
85
+ }
52
86
  get bleed() {
87
+ var _a, _b, _c, _d;
53
88
  const { config, datamodel: { data } } = this;
89
+ const rowLabels = this._getRowLabels(data);
90
+ const rowHeight = config.rowHeight || (this._height / (rowLabels.length || 1));
91
+ const hasIcons = rowLabels.some(l => l.iconHref);
92
+ const maxIconSize = max(rowLabels.map(l => l.iconSize || 0));
54
93
  // We calculate the longest label width to set the bleed values accordingly
55
- let labelsBleed = 0;
56
- if (config.showLabels) {
57
- if (config.labelWidth)
58
- labelsBleed = config.labelWidth + this._labelMargin;
94
+ if ((_a = config.showRowLabels) !== null && _a !== void 0 ? _a : config.showLabels) {
95
+ if ((_b = config.rowLabelWidth) !== null && _b !== void 0 ? _b : config.labelWidth)
96
+ this._labelWidth = ((_c = config.rowLabelWidth) !== null && _c !== void 0 ? _c : config.labelWidth) + this._labelMargin;
59
97
  else {
60
- const recordLabels = this._getRecordLabels(data);
61
- const longestLabel = recordLabels.reduce((acc, val) => acc.length > val.length ? acc : val, '');
98
+ const longestLabel = rowLabels.reduce((longestLabel, l) => longestLabel.formattedLabel.length > l.formattedLabel.length ? longestLabel : l, rowLabels[0]);
62
99
  const label$1 = this._labelsGroup.append('text')
63
100
  .attr('class', label)
64
- .text(longestLabel)
65
- .call(trimSVGText, config.maxLabelWidth);
101
+ .text((longestLabel === null || longestLabel === void 0 ? void 0 : longestLabel.formattedLabel) || '')
102
+ .call(trimSVGText, (_d = config.rowMaxLabelWidth) !== null && _d !== void 0 ? _d : config.maxLabelWidth);
66
103
  const labelWidth = label$1.node().getBBox().width;
67
- this._labelsGroup.empty();
104
+ label$1.remove();
68
105
  const tolerance = 1.15; // Some characters are wider than others so we add a little of extra space to take that into account
69
- labelsBleed = labelWidth ? tolerance * labelWidth + this._labelMargin : 0;
106
+ this._labelWidth = labelWidth ? tolerance * labelWidth + this._labelMargin : 0;
70
107
  }
71
108
  }
72
- const maxLineWidth = this._getMaxLineWidth();
109
+ // There can be multiple start / end items with the same timestamp, so we need to find the shortest one
110
+ const minTimestamp = min(data, (d, i) => getNumber(d, config.x, i));
111
+ const dataMin = data.filter((d, i) => getNumber(d, config.x, i) === minTimestamp);
112
+ const dataMinShortestItemIdx = minIndex(dataMin, (d, i) => this._getLineDuration(d, i));
113
+ const firstItemIdx = data.findIndex(d => d === dataMin[dataMinShortestItemIdx]);
114
+ const firstItem = data[firstItemIdx];
115
+ const maxTimestamp = max(data, (d, i) => getNumber(d, config.x, i) + this._getLineDuration(d, i));
116
+ const dataMax = data.filter((d, i) => getNumber(d, config.x, i) + this._getLineDuration(d, i) === maxTimestamp);
117
+ const dataMaxShortestItemIdx = minIndex(dataMax, (d, i) => this._getLineDuration(d, i));
118
+ const lastItemIdx = data.findIndex(d => d === dataMax[dataMaxShortestItemIdx]);
119
+ const lastItem = data[lastItemIdx];
120
+ // Small segments bleed
121
+ const lineBleed = [1, 1];
122
+ if (config.showEmptySegments && config.lineCap && firstItem && lastItem) {
123
+ const firstItemStart = getNumber(firstItem, config.x, firstItemIdx);
124
+ const firstItemEnd = getNumber(firstItem, config.x, firstItemIdx) + this._getLineDuration(firstItem, firstItemIdx);
125
+ const lastItemStart = getNumber(lastItem, config.x, lastItemIdx);
126
+ const lastItemEnd = getNumber(lastItem, config.x, lastItemIdx) + this._getLineDuration(lastItem, lastItemIdx);
127
+ const fullTimeRange = lastItemEnd - firstItemStart;
128
+ const firstItemHeight = this._getLineWidth(firstItem, firstItemIdx, rowHeight);
129
+ const lastItemHeight = this._getLineWidth(lastItem, lastItemIdx, rowHeight);
130
+ if ((firstItemEnd - firstItemStart) / fullTimeRange * this._width < firstItemHeight)
131
+ lineBleed[0] = firstItemHeight / 2;
132
+ if ((lastItemEnd - lastItemStart) / fullTimeRange * this._width < lastItemHeight)
133
+ lineBleed[1] = lastItemHeight / 2;
134
+ }
135
+ this._lineBleed = lineBleed;
136
+ // Icon bleed
137
+ const iconBleed = [0, 0];
138
+ if (config.lineStartIcon) {
139
+ iconBleed[0] = max(data, (d, i) => getIconBleed(d, i, config.lineStartIcon, config.lineStartIconSize, config.lineStartIconArrangement, rowHeight)) || 0;
140
+ }
141
+ if (config.lineEndIcon) {
142
+ iconBleed[1] = max(data, (d, i) => getIconBleed(d, i, config.lineEndIcon, config.lineEndIconSize, config.lineEndIconArrangement, rowHeight)) || 0;
143
+ }
144
+ this._rowIconBleed = iconBleed;
73
145
  return {
74
146
  top: 0,
75
147
  bottom: 0,
76
- left: maxLineWidth / 2 + labelsBleed,
77
- right: maxLineWidth / 2 + this._scrollBarWidth + this._scrollBarMargin,
148
+ left: this._labelWidth + iconBleed[0] + (hasIcons ? maxIconSize : 0) + lineBleed[0],
149
+ right: this._scrollBarWidth + this._scrollBarMargin + iconBleed[1] + lineBleed[1],
78
150
  };
79
151
  }
80
152
  _render(customDuration) {
153
+ var _a;
81
154
  super._render(customDuration);
82
155
  const { config, datamodel: { data } } = this;
83
156
  const duration = isNumber(customDuration) ? customDuration : config.duration;
@@ -85,76 +158,189 @@ class Timeline extends XYComponentCore {
85
158
  const yRange = this.yScale.range();
86
159
  const yStart = Math.min(...yRange);
87
160
  const yHeight = Math.abs(yRange[1] - yRange[0]);
88
- const maxLineWidth = this._getMaxLineWidth();
89
- const recordLabels = this._getRecordLabels(data);
90
- const recordLabelsUnique = unique(recordLabels);
91
- const numUniqueRecords = recordLabelsUnique.length;
92
- // Ordinal scale to handle records on the same type
93
- const ordinalScale = scaleOrdinal();
94
- ordinalScale.range(arrayOfIndices(numUniqueRecords));
161
+ const rowLabels = this._getRowLabels(data);
162
+ const numRowLabels = rowLabels.length;
163
+ const rowHeight = config.rowHeight || (yHeight / (numRowLabels || 1));
164
+ const yOrdinalScale = scaleOrdinal()
165
+ .range(arrayOfIndices(numRowLabels))
166
+ .domain(rowLabels.map(l => l.label));
167
+ const lineDataPrepared = this._prepareLinesData(data, yOrdinalScale, rowHeight);
95
168
  // Invisible Background rect to track events
96
169
  this._background
97
170
  .attr('width', this._width)
98
171
  .attr('height', this._height)
99
172
  .attr('opacity', 0);
173
+ // Row Icons
174
+ const rowIcons = this._rowIconsGroup.selectAll(`.${rowIcon}`)
175
+ .data(rowLabels.filter(d => d.iconSize), l => l === null || l === void 0 ? void 0 : l.label);
176
+ const rowIconsEnter = rowIcons.enter().append('use')
177
+ .attr('class', rowIcon)
178
+ .attr('x', 0)
179
+ .attr('width', l => l.iconSize)
180
+ .attr('height', l => l.iconSize)
181
+ .attr('y', l => yStart + (yOrdinalScale(l.label) + 0.5) * rowHeight - l.iconSize / 2)
182
+ .style('opacity', 0);
183
+ smartTransition(rowIconsEnter.merge(rowIcons), duration)
184
+ .attr('href', l => l.iconHref)
185
+ .attr('x', 0)
186
+ .attr('y', l => yStart + (yOrdinalScale(l.label) + 0.5) * rowHeight - l.iconSize / 2)
187
+ .attr('width', l => l.iconSize)
188
+ .attr('height', l => l.iconSize)
189
+ .style('color', l => l.iconColor)
190
+ .style('opacity', 1);
191
+ smartTransition(rowIcons.exit(), duration)
192
+ .style('opacity', 0)
193
+ .remove();
100
194
  // Labels
101
195
  const labels = this._labelsGroup.selectAll(`.${label}`)
102
- .data(config.showLabels ? recordLabelsUnique : []);
196
+ .data(((_a = config.showRowLabels) !== null && _a !== void 0 ? _a : config.showLabels) ? rowLabels : [], l => l === null || l === void 0 ? void 0 : l.label);
197
+ const labelOffset = config.rowLabelTextAlign === TextAlign.Center ? this._labelWidth / 2
198
+ : config.rowLabelTextAlign === TextAlign.Left ? this._labelWidth
199
+ : this._labelMargin;
200
+ const xStart = xRange[0] - this._rowIconBleed[0] - this._lineBleed[0];
201
+ const labelXStart = xStart - labelOffset;
103
202
  const labelsEnter = labels.enter().append('text')
104
- .attr('class', label);
105
- labelsEnter.merge(labels)
106
- .attr('x', xRange[0] - maxLineWidth / 2 - this._labelMargin)
107
- .attr('y', (label, i) => yStart + (ordinalScale(label) + 0.5) * config.rowHeight)
108
- .text(label => label)
203
+ .attr('class', label)
204
+ .attr('x', labelXStart)
205
+ .attr('y', l => yStart + (yOrdinalScale(l.label) + 0.5) * rowHeight)
206
+ .style('opacity', 0);
207
+ const labelsMerged = labelsEnter.merge(labels)
208
+ .text(l => l.formattedLabel)
109
209
  .each((label, i, els) => {
110
- trimSVGText(select(els[i]), config.labelWidth || config.maxLabelWidth);
111
- });
112
- labels.exit().remove();
210
+ var _a, _b;
211
+ const labelSelection = select(els[i]);
212
+ trimSVGText(labelSelection, ((_a = config.rowLabelWidth) !== null && _a !== void 0 ? _a : config.labelWidth) || ((_b = config.rowMaxLabelWidth) !== null && _b !== void 0 ? _b : config.maxLabelWidth));
213
+ // Apply custom label style if it has been provided
214
+ const customStyle = getValue(label, config.rowLabelStyle);
215
+ if (!isPlainObject(customStyle))
216
+ return;
217
+ for (const [prop, value] of Object.entries(customStyle)) {
218
+ labelSelection.style(prop, value);
219
+ }
220
+ })
221
+ .style('text-anchor', textAlignToAnchor(config.rowLabelTextAlign));
222
+ smartTransition(labelsMerged, duration)
223
+ .attr('x', labelXStart)
224
+ .attr('y', l => yStart + (yOrdinalScale(l.label) + 0.5) * rowHeight)
225
+ .style('opacity', 1);
226
+ smartTransition(labels.exit(), duration)
227
+ .style('opacity', 0)
228
+ .remove();
113
229
  // Row background rects
114
- const xStart = xRange[0];
115
- const numRows = Math.max(Math.floor(yHeight / config.rowHeight), numUniqueRecords);
116
- const recordTypes = Array(numRows).fill(null).map((_, i) => recordLabelsUnique[i]);
230
+ const timelineWidth = xRange[1] - xRange[0] + this._rowIconBleed[0] + this._rowIconBleed[1] + this._lineBleed[0] + this._lineBleed[1];
231
+ const numRows = Math.max(Math.floor(yHeight / rowHeight), numRowLabels);
232
+ const recordTypes = Array(numRows).fill(null).map((_, i) => rowLabels[i]);
117
233
  const rects = this._rowsGroup.selectAll(`.${row}`)
118
234
  .data(recordTypes);
119
235
  const rectsEnter = rects.enter().append('rect')
120
- .attr('class', row);
121
- rectsEnter.merge(rects)
122
- .classed(rowOdd, config.alternatingRowColors ? (_, i) => !(i % 2) : null)
123
- .attr('x', xStart - maxLineWidth / 2)
124
- .attr('width', xRange[1] - xStart + maxLineWidth)
125
- .attr('y', (_, i) => yStart + i * config.rowHeight)
126
- .attr('height', config.rowHeight);
127
- rects.exit().remove();
236
+ .attr('class', row)
237
+ .attr('x', xStart)
238
+ .attr('width', timelineWidth)
239
+ .attr('y', (_, i) => yStart + i * rowHeight)
240
+ .attr('height', rowHeight)
241
+ .style('opacity', 0);
242
+ const rectsMerged = rectsEnter.merge(rects)
243
+ .classed(rowOdd, config.alternatingRowColors ? (_, i) => !(i % 2) : null);
244
+ smartTransition(rectsMerged, duration)
245
+ .attr('x', xStart)
246
+ .attr('width', timelineWidth)
247
+ .attr('y', (_, i) => yStart + i * rowHeight)
248
+ .attr('height', rowHeight)
249
+ .style('opacity', 1);
250
+ smartTransition(rects.exit(), duration)
251
+ .style('opacity', 0)
252
+ .remove();
128
253
  // Lines
129
- const lines = this._linesGroup.selectAll(`.${line}`)
130
- .data(data, (d, i) => {
131
- var _a;
132
- return (_a = getString(d, config.id, i)) !== null && _a !== void 0 ? _a : [
133
- this._getRecordType(d, i), getNumber(d, config.x, i),
134
- ].join('-');
254
+ const lines = this._linesGroup.selectAll(`.${lineGroup}`)
255
+ .data(lineDataPrepared, (d) => d._id);
256
+ const linesEnter = lines.enter().append('g')
257
+ .attr('class', lineGroup)
258
+ .style('opacity', 0)
259
+ .attr('transform', (d, i) => {
260
+ var _a, _b;
261
+ const configuredPos = isFunction(config.animationLineEnterPosition)
262
+ ? config.animationLineEnterPosition(d, i, lineDataPrepared)
263
+ : config.animationLineEnterPosition;
264
+ const [x, y] = [(_a = configuredPos === null || configuredPos === void 0 ? void 0 : configuredPos[0]) !== null && _a !== void 0 ? _a : d._xPx, (_b = configuredPos === null || configuredPos === void 0 ? void 0 : configuredPos[1]) !== null && _b !== void 0 ? _b : d._yPx];
265
+ return `translate(${x}, ${y})`;
135
266
  });
136
- const linesEnter = lines.enter().append('rect')
267
+ linesEnter.append('rect')
137
268
  .attr('class', line)
138
- .classed(rowOdd, config.alternatingRowColors
139
- ? (d, i) => !(recordLabelsUnique.indexOf(this._getRecordType(d, i)) % 2)
140
- : null)
141
- .style('fill', (d, i) => getColor(d, config.color, ordinalScale(this._getRecordType(d, i))))
142
- .call(this._positionLines.bind(this), ordinalScale)
143
- .attr('transform', 'translate(0, 10)')
144
- .style('opacity', 0);
145
- const linesMerged = linesEnter.merge(lines)
146
- .style('fill', (d, i) => getColor(d, config.color, ordinalScale(this._getRecordType(d, i))))
147
- .style('cursor', (d, i) => getString(d, config.cursor, i))
148
- .call(this._positionLines.bind(this), ordinalScale);
269
+ .style('fill', (d, i) => getColor(d, config.color, yOrdinalScale(this._getRecordKey(d, i))))
270
+ .call(this._renderLines.bind(this), rowHeight);
271
+ linesEnter.append('use').attr('class', lineStartIcon);
272
+ linesEnter.append('use').attr('class', lineEndIcon);
273
+ const linesMerged = linesEnter.merge(lines);
149
274
  smartTransition(linesMerged, duration)
150
- .attr('transform', 'translate(0, 0)')
275
+ .attr('transform', d => `translate(${d._xPx + d._xOffsetPx}, ${d._yPx})`)
151
276
  .style('opacity', 1);
152
- smartTransition(lines.exit(), duration)
277
+ const lineRectElementsSelection = linesMerged.selectAll(`.${line}`)
278
+ .data(d => [d]);
279
+ smartTransition(lineRectElementsSelection, duration)
280
+ .style('fill', (d, i) => getColor(d, config.color, yOrdinalScale(this._getRecordKey(d, i))))
281
+ .style('cursor', (d, i) => { var _a; return getString(d, (_a = config.lineCursor) !== null && _a !== void 0 ? _a : config.cursor, i); })
282
+ .call(this._renderLines.bind(this), rowHeight);
283
+ linesMerged.selectAll(`.${lineStartIcon}`)
284
+ .data(d => [d])
285
+ .attr('href', (d, i) => getString(d, config.lineStartIcon, i))
286
+ .attr('x', (d, i) => {
287
+ const iconSize = d._startIconSize;
288
+ const iconArrangement = d._startIconArrangement;
289
+ const offset = iconArrangement === Arrangement.Inside ? 0
290
+ : iconArrangement === Arrangement.Center ? -iconSize / 2
291
+ : -iconSize;
292
+ return offset;
293
+ })
294
+ .attr('y', d => (-(d._startIconSize - d._height) / 2) || 0)
295
+ .attr('width', d => d._startIconSize)
296
+ .attr('height', d => d._startIconSize)
297
+ .style('color', d => d._startIconColor);
298
+ linesMerged.selectAll(`.${lineEndIcon}`)
299
+ .data(d => [d])
300
+ .attr('href', (d, i) => getString(d, config.lineEndIcon, i))
301
+ .attr('x', (d, i) => {
302
+ const lineLength = d._lengthCorrected;
303
+ const iconSize = d._endIconSize;
304
+ const iconArrangement = d._endIconArrangement;
305
+ const offset = iconArrangement === Arrangement.Inside ? -iconSize
306
+ : iconArrangement === Arrangement.Center ? -iconSize / 2
307
+ : 0;
308
+ return lineLength + offset;
309
+ })
310
+ .attr('y', d => -((d._endIconSize - d._height) / 2) || 0)
311
+ .attr('width', d => d._endIconSize)
312
+ .attr('height', d => d._endIconSize)
313
+ .style('color', d => d._endIconColor);
314
+ const linesExit = lines.exit();
315
+ smartTransition(linesExit, duration)
316
+ .style('opacity', 0)
317
+ .attr('transform', (d, i) => {
318
+ var _a, _b;
319
+ const configuredPos = isFunction(config.animationLineExitPosition)
320
+ ? config.animationLineExitPosition(d, i, lineDataPrepared)
321
+ : config.animationLineExitPosition;
322
+ const [x, y] = [(_a = configuredPos === null || configuredPos === void 0 ? void 0 : configuredPos[0]) !== null && _a !== void 0 ? _a : d._xPx, (_b = configuredPos === null || configuredPos === void 0 ? void 0 : configuredPos[1]) !== null && _b !== void 0 ? _b : d._yPx];
323
+ return `translate(${x}, ${y})`;
324
+ })
325
+ .remove();
326
+ // Arrows
327
+ const arrowsData = this._prepareArrowsData(data, yOrdinalScale, rowHeight);
328
+ const arrows = this._arrowsGroup.selectAll(`.${arrow}`)
329
+ .data(arrowsData !== null && arrowsData !== void 0 ? arrowsData : [], d => d.id);
330
+ const arrowsEnter = arrows.enter().append('path')
331
+ .attr('class', arrow)
332
+ .style('opacity', 0);
333
+ smartTransition(arrowsEnter.merge(arrows), duration)
334
+ .attr('d', (d) => {
335
+ var _a, _b;
336
+ return arrowPolylinePath(d._points, (_a = d.arrowHeadLength) !== null && _a !== void 0 ? _a : TIMELINE_DEFAULT_ARROW_HEAD_LENGTH, (_b = d.arrowHeadWidth) !== null && _b !== void 0 ? _b : TIMELINE_DEFAULT_ARROW_HEAD_WIDTH);
337
+ })
338
+ .style('opacity', 1);
339
+ smartTransition(arrows.exit(), duration)
153
340
  .style('opacity', 0)
154
341
  .remove();
155
342
  // Scroll Bar
156
- const contentBBox = this._rowsGroup.node().getBBox(); // We determine content size using the rects group because lines are animated
157
- const absoluteContentHeight = contentBBox.height;
343
+ const absoluteContentHeight = recordTypes.length * rowHeight;
158
344
  this._scrollbarHeight = yHeight * yHeight / absoluteContentHeight || 0;
159
345
  this._maxScroll = Math.max(absoluteContentHeight - yHeight, 0);
160
346
  this._scrollBarGroup
@@ -171,32 +357,119 @@ class Timeline extends XYComponentCore {
171
357
  .attr('rx', this._scrollBarWidth / 2)
172
358
  .attr('ry', this._scrollBarWidth / 2);
173
359
  this._updateScrollPosition(0);
360
+ // Clip path
361
+ const clipPathRect = this._clipPath.select('rect');
362
+ smartTransition(clipPathRect, clipPathRect.attr('width') ? duration : 0)
363
+ .attr('x', xStart)
364
+ .attr('width', timelineWidth)
365
+ .attr('height', this._height);
366
+ }
367
+ _getLineLength(d, i) {
368
+ var _a, _b;
369
+ const { config, xScale } = this;
370
+ const x = getNumber(d, config.x, i);
371
+ const length = (_b = getNumber(d, (_a = config.lineDuration) !== null && _a !== void 0 ? _a : config.length, i)) !== null && _b !== void 0 ? _b : 0;
372
+ const lineLength = xScale(x + length) - xScale(x);
373
+ return lineLength;
174
374
  }
175
- _positionLines(selection, ordinalScale) {
375
+ _getLineWidth(d, i, rowHeight) {
376
+ var _a;
377
+ const { config } = this;
378
+ return (_a = getNumber(d, config.lineWidth, i)) !== null && _a !== void 0 ? _a : Math.max(Math.floor(rowHeight / 2), 1);
379
+ }
380
+ _getLineDuration(d, i) {
381
+ var _a, _b;
382
+ const { config } = this;
383
+ return (_b = getNumber(d, (_a = config.lineDuration) !== null && _a !== void 0 ? _a : config.length, i)) !== null && _b !== void 0 ? _b : 0;
384
+ }
385
+ _prepareLinesData(data, rowOrdinalScale, rowHeight) {
176
386
  const { config, xScale, yScale } = this;
177
387
  const yRange = yScale.range();
178
388
  const yStart = Math.min(...yRange);
179
- selection.each((d, i, elements) => {
180
- var _a;
181
- const x = getNumber(d, config.x, i);
182
- const y = ordinalScale(this._getRecordType(d, i)) * config.rowHeight;
183
- const length = (_a = getNumber(d, config.length, i)) !== null && _a !== void 0 ? _a : 0;
184
- // Rect dimensions
185
- const height = getNumber(d, config.lineWidth, i);
186
- const width = xScale(x + length) - xScale(x);
187
- if (width < 0) {
389
+ return data.map((d, i) => {
390
+ var _a, _b, _c, _d, _e;
391
+ const id = (_a = getString(d, config.id, i)) !== null && _a !== void 0 ? _a : [
392
+ this._getRecordKey(d, i), getNumber(d, config.x, i),
393
+ ].join('-');
394
+ const lineWidth = this._getLineWidth(d, i, rowHeight);
395
+ const lineLength = this._getLineLength(d, i);
396
+ if (lineLength < 0) {
188
397
  console.warn('Unovis | Timeline: Line segments should not have negative lengths. Setting to 0.');
189
398
  }
190
- select(elements[i])
191
- .attr('x', xScale(x))
192
- .attr('y', yStart + y + (config.rowHeight - height) / 2)
193
- .attr('width', config.showEmptySegments
194
- ? Math.max(config.lineCap ? height : 1, width)
195
- : Math.max(0, width))
196
- .attr('height', height)
197
- .attr('rx', config.lineCap ? height / 2 : null);
399
+ const isLineTooShort = config.showEmptySegments && config.lineCap && (lineLength < lineWidth);
400
+ const lineLengthCorrected = config.showEmptySegments
401
+ ? Math.max(config.lineCap ? lineWidth : 1, lineLength)
402
+ : Math.max(0, lineLength);
403
+ const x = xScale(getNumber(d, config.x, i));
404
+ const y = yStart + rowOrdinalScale(this._getRecordKey(d, i)) * rowHeight + (rowHeight - lineWidth) / 2;
405
+ const xOffset = isLineTooShort ? -(lineLengthCorrected - lineLength) / 2 : 0;
406
+ return Object.assign(Object.assign({}, d), { _id: id, _xPx: x, _yPx: y, _xOffsetPx: xOffset, _length: lineLength, _height: lineWidth, _lengthCorrected: lineLengthCorrected, _startIconSize: (_b = getNumber(d, config.lineStartIconSize, i)) !== null && _b !== void 0 ? _b : lineWidth, _endIconSize: (_c = getNumber(d, config.lineEndIconSize, i)) !== null && _c !== void 0 ? _c : lineWidth, _startIconColor: getString(d, config.lineStartIconColor, i), _endIconColor: getString(d, config.lineEndIconColor, i), _startIconArrangement: (_d = getValue(d, config.lineStartIconArrangement, i)) !== null && _d !== void 0 ? _d : Arrangement.Outside, _endIconArrangement: (_e = getValue(d, config.lineEndIconArrangement, i)) !== null && _e !== void 0 ? _e : Arrangement.Outside });
198
407
  });
199
408
  }
409
+ _prepareArrowsData(data, rowOrdinalScale, rowHeight) {
410
+ var _a;
411
+ const { config } = this;
412
+ const arrowsData = (_a = config.arrows) === null || _a === void 0 ? void 0 : _a.map(a => {
413
+ var _a, _b, _c, _d, _e;
414
+ const sourceLineIndex = data.findIndex((d, i) => getString(d, config.id, i) === a.lineSourceId);
415
+ const targetLineIndex = data.findIndex((d, i) => getString(d, config.id, i) === a.lineTargetId);
416
+ const sourceLine = data[sourceLineIndex];
417
+ const targetLine = data[targetLineIndex];
418
+ if (!sourceLine || !targetLine) {
419
+ console.warn('Unovis | Timeline: Arrow references a non-existent line. Skipping...', a);
420
+ return undefined;
421
+ }
422
+ const sourceLineY = rowOrdinalScale(this._getRecordKey(sourceLine, sourceLineIndex)) * rowHeight + rowHeight / 2;
423
+ const targetLineY = rowOrdinalScale(this._getRecordKey(targetLine, targetLineIndex)) * rowHeight + rowHeight / 2;
424
+ const sourceLineWidth = this._getLineWidth(sourceLine, sourceLineIndex, rowHeight);
425
+ const targetLineWidth = this._getLineWidth(targetLine, targetLineIndex, rowHeight);
426
+ const x1 = (a.xSource
427
+ ? this.xScale(a.xSource)
428
+ : this.xScale(getNumber(sourceLine, config.x, sourceLineIndex)) + this._getLineLength(sourceLine, sourceLineIndex)) + ((_a = a.xSourceOffsetPx) !== null && _a !== void 0 ? _a : 0);
429
+ const targetLineLength = this._getLineLength(targetLine, targetLineIndex);
430
+ const isTargetLineTooShort = config.showEmptySegments && config.lineCap && (targetLineLength < targetLineWidth);
431
+ const targetLineStart = this.xScale(getNumber(targetLine, config.x, targetLineIndex)) + (isTargetLineTooShort ? -targetLineWidth / 2 : 0);
432
+ const x2 = (a.xTarget ? this.xScale(a.xTarget) : targetLineStart) + ((_b = a.xTargetOffsetPx) !== null && _b !== void 0 ? _b : 0);
433
+ const isX2OutsideTargetLineStart = (x2 < targetLineStart) || (x2 > targetLineStart);
434
+ // Points array
435
+ const sourceMargin = (_c = a.lineSourceMarginPx) !== null && _c !== void 0 ? _c : TIMELINE_DEFAULT_ARROW_MARGIN;
436
+ const targetMargin = (_d = a.lineTargetMarginPx) !== null && _d !== void 0 ? _d : TIMELINE_DEFAULT_ARROW_MARGIN;
437
+ const y1 = sourceLineY < targetLineY ? sourceLineY + sourceLineWidth / 2 + sourceMargin : sourceLineY - sourceLineWidth / 2 - sourceMargin;
438
+ const y2 = sourceLineY < targetLineY ? targetLineY - targetLineWidth / 2 - targetMargin : targetLineY + targetLineWidth / 2 + targetMargin;
439
+ const arrowHeadLength = (_e = a.arrowHeadLength) !== null && _e !== void 0 ? _e : TIMELINE_DEFAULT_ARROW_HEAD_LENGTH;
440
+ const isForwardArrow = x1 < x2 && !isX2OutsideTargetLineStart;
441
+ const threshold = arrowHeadLength + (isForwardArrow ? targetMargin : 0);
442
+ const points = [[x1, y1]];
443
+ if (Math.abs(x2 - x1) > threshold) {
444
+ if (isForwardArrow) {
445
+ points.push([x1, (y1 + targetLineY) / 2]); // A dummy point to enable smooth transitions when arrows change
446
+ points.push([x1, targetLineY]);
447
+ points.push([x2 - targetMargin, targetLineY]);
448
+ }
449
+ else {
450
+ const verticalOffset = Math.sign(targetLineY - sourceLineY) * (rowHeight / 4);
451
+ points.push([x1, y2 - verticalOffset]);
452
+ points.push([x2, y2 - verticalOffset]);
453
+ points.push([x2, y2]);
454
+ }
455
+ }
456
+ else {
457
+ const quarterOffset = (y2 - y1) / 4;
458
+ points.push([x1, y1 + quarterOffset]); // A dummy point to enable smooth transitions
459
+ points.push([x1, y1 + 3 * quarterOffset]); // A dummy point to enable smooth transitions
460
+ points.push([x1, y2]);
461
+ }
462
+ return Object.assign(Object.assign({}, a), { _points: points });
463
+ }).filter(Boolean);
464
+ return arrowsData;
465
+ }
466
+ _renderLines(selection) {
467
+ const { config } = this;
468
+ selection
469
+ .attr('width', d => d._lengthCorrected)
470
+ .attr('height', d => d._height)
471
+ .attr('rx', d => config.lineCap ? d._height / 2 : null);
472
+ }
200
473
  _onScrollbarDrag(event) {
201
474
  const yRange = this.yScale.range();
202
475
  const yHeight = Math.abs(yRange[1] - yRange[0]);
@@ -219,28 +492,40 @@ class Timeline extends XYComponentCore {
219
492
  this._scrollDistance += diff;
220
493
  this._scrollDistance = Math.max(0, this._scrollDistance);
221
494
  this._scrollDistance = Math.min(this._maxScroll, this._scrollDistance);
495
+ this._clipPath.attr('transform', `translate(0,${this._scrollDistance})`);
222
496
  this._linesGroup.attr('transform', `translate(0,${-this._scrollDistance})`);
223
497
  this._rowsGroup.attr('transform', `translate(0,${-this._scrollDistance})`);
224
498
  this._labelsGroup.attr('transform', `translate(0,${-this._scrollDistance})`);
499
+ this._rowIconsGroup.attr('transform', `translate(0,${-this._scrollDistance})`);
500
+ this._arrowsGroup.attr('transform', `translate(0,${-this._scrollDistance})`);
225
501
  const scrollBarPosition = (this._scrollDistance / this._maxScroll * (yHeight - this._scrollbarHeight)) || 0;
226
502
  this._scrollBarHandle.attr('y', scrollBarPosition);
227
503
  }
228
- _getMaxLineWidth() {
504
+ _getRecordKey(d, i) {
229
505
  var _a;
230
- const { config, datamodel: { data } } = this;
231
- return (_a = max(data, (d, i) => getNumber(d, config.lineWidth, i))) !== null && _a !== void 0 ? _a : 0;
232
- }
233
- _getRecordType(d, i) {
234
- return getString(d, this.config.type) || `__${i}`;
506
+ return getString(d, (_a = this.config.lineRow) !== null && _a !== void 0 ? _a : this.config.type) || `__${i}`;
235
507
  }
236
- _getRecordLabels(data) {
237
- return data.map((d, i) => getString(d, this.config.type) || `${i + 1}`);
508
+ _getRowLabels(data) {
509
+ const grouped = groupBy(data, (d, i) => { var _a; return getString(d, (_a = this.config.lineRow) !== null && _a !== void 0 ? _a : this.config.type) || `${i + 1}`; });
510
+ const rowLabels = Object.entries(grouped).map(([key, items], i) => {
511
+ var _a, _b, _c, _d, _e;
512
+ const icon = (_b = (_a = this.config).rowIcon) === null || _b === void 0 ? void 0 : _b.call(_a, key, items, i);
513
+ return {
514
+ label: key,
515
+ formattedLabel: (_e = (_d = (_c = this.config).rowLabelFormatter) === null || _d === void 0 ? void 0 : _d.call(_c, key, items, i)) !== null && _e !== void 0 ? _e : key,
516
+ iconHref: icon === null || icon === void 0 ? void 0 : icon.href,
517
+ iconSize: icon === null || icon === void 0 ? void 0 : icon.size,
518
+ iconColor: icon === null || icon === void 0 ? void 0 : icon.color,
519
+ data: items,
520
+ };
521
+ });
522
+ return rowLabels;
238
523
  }
239
524
  // Override the default XYComponent getXDataExtent method to take into account line lengths
240
525
  getXDataExtent() {
241
526
  const { config, datamodel } = this;
242
527
  const min = getMin(datamodel.data, config.x);
243
- const max = getMax(datamodel.data, (d, i) => { var _a; return getNumber(d, config.x, i) + ((_a = getNumber(d, config.length, i)) !== null && _a !== void 0 ? _a : 0); });
528
+ const max = getMax(datamodel.data, (d, i) => { var _a, _b; return getNumber(d, config.x, i) + ((_b = getNumber(d, (_a = config.lineDuration) !== null && _a !== void 0 ? _a : config.length, i)) !== null && _b !== void 0 ? _b : 0); });
244
529
  return [min, max];
245
530
  }
246
531
  }