Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 35 additions & 9 deletions web/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -295,18 +295,32 @@ function updateVisuals(payload) {
}
}
}
} else {
maybeCreateUrchinInstance(lastVisualSchedule);
return;
}

if (visualsState.urchin) {
try {
visualsState.urchin.update({ data: visualsState.useLegacy ? null : lastVisualSchedule });
} catch (error) {
console.error('[visuals] failed to update radial urchin:', error);
const readyForRadial = canRenderRadialVisuals();
if (!readyForRadial) {
if (visualsState.urchin) {
resetVisualsInstance();
}
} else if (!visualsState.useLegacy && visualsState.mount && visualsState.mount.childElementCount > 0) {
visualsState.mount.replaceChildren();
return;
}

if (!hasVisualEvents(lastVisualSchedule)) {
resetVisualsInstance();
return;
}

maybeCreateUrchinInstance(lastVisualSchedule);

if (!visualsState.urchin) {
return;
}

try {
visualsState.urchin.update({ data: lastVisualSchedule });
} catch (error) {
console.error('[visuals] failed to update radial urchin:', error);
}
}

Expand All @@ -331,11 +345,23 @@ function hasVisualEvents(payload) {
return Boolean(schedule && Array.isArray(schedule.events) && schedule.events.length > 0);
}

function canRenderRadialVisuals() {
if (visualsState.useLegacy) {
return false;
}
const mount = visualsState.mount;
if (!(mount instanceof HTMLElement) || !mount.isConnected) {
return false;
}
return true;
}

function maybeCreateUrchinInstance(schedule) {
if (
visualsState.useLegacy ||
visualsState.urchin ||
!visualsState.mount ||
!visualsState.mount.isConnected ||
!schedule ||
!hasVisualEvents(schedule)
) {
Expand Down
65 changes: 45 additions & 20 deletions web/ui/visuals/RadialUrchin.js
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,17 @@ export class RadialUrchin {
this.update(this.props);
}

hasValidCenter() {
const center = this.center;
return (
!!center &&
typeof center.x === 'number' &&
Number.isFinite(center.x) &&
typeof center.y === 'number' &&
Number.isFinite(center.y)
);
}

setupDom() {
this.root.classList.add('radial-urchin-root');
this.root.setAttribute('tabindex', '0');
Expand Down Expand Up @@ -663,13 +674,7 @@ export class RadialUrchin {
return;
}

if (
!this.center ||
typeof this.center.x !== 'number' ||
Number.isNaN(this.center.x) ||
typeof this.center.y !== 'number' ||
Number.isNaN(this.center.y)
) {
if (!this.hasValidCenter()) {
if (!this.didWarnInvalidCenter) {
console.warn('[RadialUrchin] invalid center point, skipping render', this.center);
this.didWarnInvalidCenter = true;
Expand All @@ -696,7 +701,9 @@ export class RadialUrchin {
const dpr = window.devicePixelRatio || 1;
ctx.save();
ctx.scale(dpr, dpr);
ctx.translate(this.center.x, this.center.y);
const center = this.center;

ctx.translate(center.x, center.y);
ctx.lineWidth = 1;
ctx.lineCap = 'butt';
ctx.lineJoin = 'round';
Expand Down Expand Up @@ -768,9 +775,13 @@ export class RadialUrchin {
}

processHoverAtPoint(point) {
if (!this.center) {
if (!this.hasValidCenter()) {
this.state.hoverArc = null;
this.hoverPath.setAttribute('d', '');
this.hideTooltip();
return;
}
const center = this.center;
const layout = {
arcs: this.displayArcs.map((arc) => ({
...arc,
Expand All @@ -787,8 +798,8 @@ export class RadialUrchin {
}
this.state.hoverArc = hovered;
const path = describeSegmentPath(
this.center.x,
this.center.y,
center.x,
center.y,
hovered.innerRadius,
hovered.outerRadius,
hovered.startAngle,
Expand Down Expand Up @@ -886,10 +897,15 @@ export class RadialUrchin {
const html = buildTooltipContent(arc);
this.tooltipMeta.innerHTML = html;
this.tooltip.hidden = false;
if (!this.hasValidCenter()) {
this.hideTooltip();
return;
}
const center = this.center;
const angle = arc.centerAngle;
const radius = (arc.innerRadius + arc.outerRadius) / 2;
const x = this.center.x + Math.cos(angle) * radius;
const y = this.center.y + Math.sin(angle) * radius;
const x = center.x + Math.cos(angle) * radius;
const y = center.y + Math.sin(angle) * radius;
this.tooltip.style.left = `${x + 12}px`;
this.tooltip.style.top = `${y + 12}px`;
}
Expand All @@ -900,13 +916,14 @@ export class RadialUrchin {

updateSelectionOverlay() {
const arc = this.state.selectedArc;
if (!arc) {
if (!arc || !this.hasValidCenter()) {
this.selectionPath.setAttribute('d', '');
return;
}
const center = this.center;
const path = describeSegmentPath(
this.center.x,
this.center.y,
center.x,
center.y,
arc.innerRadius,
arc.outerRadius,
arc.startAngle,
Expand All @@ -916,13 +933,21 @@ export class RadialUrchin {
}

updateScrubOverlay() {
if (!this.hasValidCenter()) {
this.scrubLine.setAttribute('x1', '0');
this.scrubLine.setAttribute('y1', '0');
this.scrubLine.setAttribute('x2', '0');
this.scrubLine.setAttribute('y2', '0');
return;
}
const center = this.center;
const minutes = this.state.scrubMinutes;
const angle = this.mapMinutesToAngle(minutes - this.getZoom().start);
const radius = this.displayMaxRadius ?? (this.layout?.maxRadius ?? 160);
const x2 = this.center.x + Math.cos(angle) * radius;
const y2 = this.center.y + Math.sin(angle) * radius;
this.scrubLine.setAttribute('x1', String(this.center.x));
this.scrubLine.setAttribute('y1', String(this.center.y));
const x2 = center.x + Math.cos(angle) * radius;
const y2 = center.y + Math.sin(angle) * radius;
this.scrubLine.setAttribute('x1', String(center.x));
this.scrubLine.setAttribute('y1', String(center.y));
this.scrubLine.setAttribute('x2', String(x2));
this.scrubLine.setAttribute('y2', String(y2));
}
Expand Down