Skip to content

Commit 29c5afd

Browse files
committed
Component | Timeline: Misc tweaks
- Dummy arrow points to smooth transitions - Make scrolling work when hovering over labels - More robust bleed calculations - More robust `arrowPolylinePath` function - Clip path transition
1 parent eac1ba7 commit 29c5afd

File tree

3 files changed

+58
-34
lines changed

3 files changed

+58
-34
lines changed

packages/ts/src/components/timeline/index.ts

Lines changed: 39 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,12 @@ export class Timeline<Datum> extends XYComponentCore<Datum, TimelineConfigInterf
3939
public config: TimelineConfigInterface<Datum> = this._defaultConfig
4040

4141
events = {
42+
[Timeline.selectors.background]: {
43+
wheel: this._onMouseWheel.bind(this),
44+
},
45+
[Timeline.selectors.label]: {
46+
wheel: this._onMouseWheel.bind(this),
47+
},
4248
[Timeline.selectors.rows]: {
4349
wheel: this._onMouseWheel.bind(this),
4450
},
@@ -134,7 +140,7 @@ export class Timeline<Datum> extends XYComponentCore<Datum, TimelineConfigInterf
134140
.call(trimSVGText, config.rowMaxLabelWidth ?? config.maxLabelWidth)
135141

136142
const labelWidth = label.node().getBBox().width
137-
this._labelsGroup.empty()
143+
label.remove()
138144

139145
const tolerance = 1.15 // Some characters are wider than others so we add a little of extra space to take that into account
140146
this._labelWidth = labelWidth ? tolerance * labelWidth + this._labelMargin : 0
@@ -156,7 +162,7 @@ export class Timeline<Datum> extends XYComponentCore<Datum, TimelineConfigInterf
156162

157163
// Small segments bleed
158164
const lineBleed = [1, 1] as [number, number]
159-
if (config.showEmptySegments && config.lineCap) {
165+
if (config.showEmptySegments && config.lineCap && firstItem && lastItem) {
160166
const firstItemStart = getNumber(firstItem, config.x, firstItemIdx)
161167
const firstItemEnd = getNumber(firstItem, config.x, firstItemIdx) + this._getLineDuration(firstItem, firstItemIdx)
162168
const lastItemStart = getNumber(lastItem, config.x, lastItemIdx)
@@ -173,11 +179,11 @@ export class Timeline<Datum> extends XYComponentCore<Datum, TimelineConfigInterf
173179
// Icon bleed
174180
const iconBleed = [0, 0] as [number, number]
175181
if (config.lineStartIcon) {
176-
iconBleed[0] = getIconBleed(firstItem, firstItemIdx, config.lineStartIcon, config.lineStartIconSize, config.lineStartIconArrangement, rowHeight)
182+
iconBleed[0] = max(data, (d, i) => getIconBleed(d, i, config.lineStartIcon, config.lineStartIconSize, config.lineStartIconArrangement, rowHeight))
177183
}
178184

179185
if (config.lineEndIcon) {
180-
iconBleed[1] = getIconBleed(lastItem, lastItemIdx, config.lineEndIcon, config.lineEndIconSize, config.lineEndIconArrangement, rowHeight)
186+
iconBleed[1] = max(data, (d, i) => getIconBleed(d, i, config.lineEndIcon, config.lineEndIconSize, config.lineEndIconArrangement, rowHeight))
181187
}
182188

183189
this._rowIconBleed = iconBleed
@@ -435,7 +441,8 @@ export class Timeline<Datum> extends XYComponentCore<Datum, TimelineConfigInterf
435441
this._updateScrollPosition(0)
436442

437443
// Clip path
438-
this._clipPath.select('rect')
444+
const clipPathRect = this._clipPath.select('rect')
445+
smartTransition(clipPathRect, clipPathRect.attr('width') ? duration : 0)
439446
.attr('x', xStart)
440447
.attr('width', timelineWidth)
441448
.attr('height', this._height)
@@ -470,20 +477,20 @@ export class Timeline<Datum> extends XYComponentCore<Datum, TimelineConfigInterf
470477
this._getRecordKey(d, i), getNumber(d, config.x, i),
471478
].join('-')
472479

473-
const lineHeight = this._getLineWidth(d, i, rowHeight)
480+
const lineWidth = this._getLineWidth(d, i, rowHeight)
474481
const lineLength = this._getLineLength(d, i)
475482

476483
if (lineLength < 0) {
477484
console.warn('Unovis | Timeline: Line segments should not have negative lengths. Setting to 0.')
478485
}
479486

480-
const isLineTooShort = config.showEmptySegments && config.lineCap && (lineLength < lineHeight)
487+
const isLineTooShort = config.showEmptySegments && config.lineCap && (lineLength < lineWidth)
481488
const lineLengthCorrected = config.showEmptySegments
482-
? Math.max(config.lineCap ? lineHeight : 1, lineLength)
489+
? Math.max(config.lineCap ? lineWidth : 1, lineLength)
483490
: Math.max(0, lineLength)
484491

485492
const x = xScale(getNumber(d, config.x, i))
486-
const y = yStart + rowOrdinalScale(this._getRecordKey(d, i)) * rowHeight + (rowHeight - lineHeight) / 2
493+
const y = yStart + rowOrdinalScale(this._getRecordKey(d, i)) * rowHeight + (rowHeight - lineWidth) / 2
487494
const xOffset = isLineTooShort ? -(lineLengthCorrected - lineLength) / 2 : 0
488495

489496
return {
@@ -493,10 +500,10 @@ export class Timeline<Datum> extends XYComponentCore<Datum, TimelineConfigInterf
493500
_yPx: y,
494501
_xOffsetPx: xOffset,
495502
_length: lineLength,
496-
_height: lineHeight,
503+
_height: lineWidth,
497504
_lengthCorrected: lineLengthCorrected,
498-
_startIconSize: getNumber(d, config.lineStartIconSize, i) ?? lineHeight,
499-
_endIconSize: getNumber(d, config.lineEndIconSize, i) ?? lineHeight,
505+
_startIconSize: getNumber(d, config.lineStartIconSize, i) ?? lineWidth,
506+
_endIconSize: getNumber(d, config.lineEndIconSize, i) ?? lineWidth,
500507
_startIconColor: getString(d, config.lineStartIconColor, i),
501508
_endIconColor: getString(d, config.lineEndIconColor, i),
502509
_startIconArrangement: getValue(d, config.lineStartIconArrangement, i) ?? Arrangement.Outside,
@@ -528,28 +535,38 @@ export class Timeline<Datum> extends XYComponentCore<Datum, TimelineConfigInterf
528535
? this.xScale(a.xSource)
529536
: this.xScale(getNumber(sourceLine, config.x, sourceLineIndex)) + this._getLineLength(sourceLine, sourceLineIndex)
530537
) + (a.xSourceOffsetPx ?? 0)
531-
const targetLineStart = this.xScale(getNumber(targetLine, config.x, targetLineIndex))
538+
const targetLineLength = this._getLineLength(targetLine, targetLineIndex)
539+
const isTargetLineTooShort = config.showEmptySegments && config.lineCap && (targetLineLength < targetLineWidth)
540+
const targetLineStart = this.xScale(getNumber(targetLine, config.x, targetLineIndex)) + (isTargetLineTooShort ? -targetLineWidth / 2 : 0)
532541
const x2 = (a.xTarget ? this.xScale(a.xTarget) : targetLineStart) + (a.xTargetOffsetPx ?? 0)
533542
const isX2OutsideTargetLineStart = (x2 < targetLineStart) || (x2 > targetLineStart)
534543

535544
// Points array
536545
const sourceMargin = a.lineSourceMarginPx ?? TIMELINE_DEFAULT_ARROW_MARGIN
537546
const targetMargin = a.lineTargetMarginPx ?? TIMELINE_DEFAULT_ARROW_MARGIN
538-
const y1 = sourceLineY < targetLineY ? sourceLineY + sourceLineWidth / 2 : sourceLineY - sourceLineWidth / 2
539-
const y2 = sourceLineY < targetLineY ? targetLineY - targetLineWidth / 2 : targetLineY + targetLineWidth / 2
540-
const points = [[x1, y1 + sourceMargin]] as [number, number][]
541-
const threshold = 5
547+
const y1 = sourceLineY < targetLineY ? sourceLineY + sourceLineWidth / 2 + sourceMargin : sourceLineY - sourceLineWidth / 2 - sourceMargin
548+
const y2 = sourceLineY < targetLineY ? targetLineY - targetLineWidth / 2 - targetMargin : targetLineY + targetLineWidth / 2 + targetMargin
549+
const arrowHeadLength = a.arrowHeadLength ?? TIMELINE_DEFAULT_ARROW_HEAD_LENGTH
550+
const isForwardArrow = x1 < x2 && !isX2OutsideTargetLineStart
551+
const threshold = arrowHeadLength + (isForwardArrow ? targetMargin : 0)
552+
553+
const points = [[x1, y1]] as [number, number][]
542554
if (Math.abs(x2 - x1) > threshold) {
543-
if ((x1 < x2) && !isX2OutsideTargetLineStart) {
555+
if (isForwardArrow) {
556+
points.push([x1, (y1 + targetLineY) / 2]) // A dummy point to enable smooth transitions when arrows change
544557
points.push([x1, targetLineY])
545558
points.push([x2 - targetMargin, targetLineY])
546559
} else {
547-
points.push([x1, y1 + Math.sign(targetLineY - sourceLineY) * (rowHeight / 2 - sourceMargin)])
548-
points.push([x2, y1 + Math.sign(targetLineY - sourceLineY) * (rowHeight / 2 - sourceMargin)])
549-
points.push([x2, y2 - targetMargin])
560+
const verticalOffset = Math.sign(targetLineY - sourceLineY) * (rowHeight / 4)
561+
points.push([x1, y2 - verticalOffset])
562+
points.push([x2, y2 - verticalOffset])
563+
points.push([x2, y2])
550564
}
551565
} else {
552-
points.push([x1, y2 - targetMargin])
566+
const quarterOffset = (y2 - y1) / 4
567+
points.push([x1, y1 + quarterOffset]) // A dummy point to enable smooth transitions
568+
points.push([x1, y1 + 3 * quarterOffset]) // A dummy point to enable smooth transitions
569+
points.push([x1, y2])
553570
}
554571

555572
return {

packages/ts/src/components/timeline/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ export type TimelineArrow = {
3333
lineSourceMarginPx?: number;
3434
/** The margin between the line target and the arrow in pixels. Default: `undefined` */
3535
lineTargetMarginPx?: number;
36-
/** The length of the arrowhead in pixels. Default: `5` */
36+
/** The length of the arrowhead in pixels. Default: `8` */
3737
arrowHeadLength?: number;
3838
/** The width of the arrowhead in pixels. Default: `6` */
3939
arrowHeadWidth?: number;

packages/ts/src/utils/path.ts

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -215,7 +215,7 @@ export function convertLineToArc (path: Path | string, r: number): string {
215215
*/
216216
export function arrowPolylinePath (
217217
points: [number, number][],
218-
arrowHeadLength = 10,
218+
arrowHeadLength = 8,
219219
arrowHeadWidth = 6,
220220
smoothing = 5
221221
): string {
@@ -279,12 +279,15 @@ export function arrowPolylinePath (
279279
// For a single segment, create a curved path
280280
const [startX, startY] = points[0]
281281

282-
// Calculate control points for a cubic Bézier curve with absolute smoothing
283-
const cp1x = startX + ux * smoothing
284-
const cp1y = startY + uy * smoothing + perpY * smoothing * 0.5
282+
// Adjust smoothing based on segment length
283+
const adjustedSmoothing = Math.min(smoothing, segmentLength / 3)
285284

286-
const cp2x = tailX - ux * smoothing
287-
const cp2y = tailY - uy * smoothing + perpY * smoothing * 0.5
285+
// Calculate control points for a cubic Bézier curve with adjusted smoothing
286+
const cp1x = startX + ux * adjustedSmoothing
287+
const cp1y = startY + uy * adjustedSmoothing + perpY * adjustedSmoothing * 0.5
288+
289+
const cp2x = tailX - ux * adjustedSmoothing
290+
const cp2y = tailY - uy * adjustedSmoothing + perpY * adjustedSmoothing * 0.5
288291

289292
// Start path and add cubic Bézier curve
290293
pathParts.push(`M${startX},${startY}`)
@@ -314,11 +317,15 @@ export function arrowPolylinePath (
314317
const u2x = v2x / len2
315318
const u2y = v2y / len2
316319

317-
// Calculate the corner points and control points with absolute smoothing
318-
const corner1x = x2 - u1x * smoothing
319-
const corner1y = y2 - u1y * smoothing
320-
const corner2x = x2 + u2x * smoothing
321-
const corner2y = y2 + u2y * smoothing
320+
// Adjust smoothing based on the minimum segment length
321+
const minSegmentLength = Math.min(len1, len2)
322+
const adjustedSmoothing = Math.min(smoothing, minSegmentLength / 3)
323+
324+
// Calculate the corner points and control points with adjusted smoothing
325+
const corner1x = x2 - u1x * adjustedSmoothing
326+
const corner1y = y2 - u1y * adjustedSmoothing
327+
const corner2x = x2 + u2x * adjustedSmoothing
328+
const corner2y = y2 + u2y * adjustedSmoothing
322329

323330
// Add line to approach point
324331
pathParts.push(`L${corner1x},${corner1y}`)

0 commit comments

Comments
 (0)