From 423582493840f034ca2df078753cb5edc5995f64 Mon Sep 17 00:00:00 2001 From: Aaron Plave Date: Tue, 31 Oct 2023 13:47:11 -0700 Subject: [PATCH 01/29] Timeline density improvements and refactoring WIP --- src/components/app/Nav.svelte | 2 +- .../simulation/SimulationPanel.svelte | 6 +- src/components/timeline/LayerActivity.svelte | 206 +++++++--- src/components/timeline/Row.svelte | 378 ++++++++++-------- .../timeline/RowDragHandleHeight.svelte | 14 +- src/components/timeline/RowHeader.svelte | 229 +++++++++-- .../timeline/RowHeaderDragHandleWidth.svelte | 70 ++++ src/components/timeline/RowHeaderMenu.svelte | 132 ++++++ src/components/timeline/RowXAxisTicks.svelte | 2 +- src/components/timeline/RowYAxes.svelte | 163 +++++--- src/components/timeline/RowYAxisTicks.svelte | 25 +- src/components/timeline/Timeline.svelte | 267 ++++++++----- .../timeline/TimelineCursors.svelte | 2 +- .../timeline/TimelineHistogram.svelte | 2 +- src/components/timeline/TimelinePanel.svelte | 50 ++- .../timeline/TimelineSimulationRange.svelte | 1 + .../TimelineSimulationRangeCursor.svelte | 1 - .../timeline/TimelineTimeDisplay.svelte | 76 ++++ src/components/timeline/Tooltip.svelte | 2 +- src/components/timeline/XAxis.svelte | 115 ++++-- .../timeline/form/TimelineEditorPanel.svelte | 1 + src/stores/simulation.ts | 2 + src/types/timeline.ts | 7 +- src/utilities/effects.ts | 9 +- src/utilities/time.ts | 26 ++ src/utilities/timeline.ts | 19 +- src/utilities/view.ts | 2 +- 27 files changed, 1317 insertions(+), 492 deletions(-) create mode 100644 src/components/timeline/RowHeaderDragHandleWidth.svelte create mode 100644 src/components/timeline/RowHeaderMenu.svelte create mode 100644 src/components/timeline/TimelineTimeDisplay.svelte diff --git a/src/components/app/Nav.svelte b/src/components/app/Nav.svelte index 40e1f30148..d29f309887 100644 --- a/src/components/app/Nav.svelte +++ b/src/components/app/Nav.svelte @@ -52,7 +52,7 @@ display: flex; height: var(--nav-header-height); padding: 1rem; - z-index: 2; + z-index: 9; } .divider { diff --git a/src/components/simulation/SimulationPanel.svelte b/src/components/simulation/SimulationPanel.svelte index 4a62fa2f86..29e1c9547d 100644 --- a/src/components/simulation/SimulationPanel.svelte +++ b/src/components/simulation/SimulationPanel.svelte @@ -3,11 +3,11 @@ -
- - -
- {#if hasActivityLayer} - - {/if} - -
-
- -
- - (blur = e)} - on:contextmenu={e => (contextmenu = e)} - on:dragenter|preventDefault={e => (dragenter = e)} - on:dragleave={e => (dragleave = e)} - on:dragover|preventDefault={e => (dragover = e)} - on:drop|preventDefault={e => (drop = e)} - on:focus={e => (focus = e)} - on:mousedown={e => (mousedown = e)} - on:mousemove={e => (mousemove = e)} - on:mouseout={e => (mouseout = e)} - on:mouseup={e => (mouseup = e)} - on:dblclick={e => (dblclick = e)} - /> - - - - - {#if drawWidth > 0} - - - - - - {/if} - - - - -
- {#each layers as layer (layer.id)} - {#if layer.chartType === 'activity'} - - {/if} - {#if layer.chartType === 'line' || layer.chartType === 'x-range'} - - {/if} - {#if layer.chartType === 'line'} - - {/if} - {#if layer.chartType === 'x-range'} - +
+ + + + + +
+ + (blur = e)} + on:contextmenu={e => (contextmenu = e)} + on:dragenter|preventDefault={e => (dragenter = e)} + on:dragleave={e => (dragleave = e)} + on:dragover|preventDefault={e => (dragover = e)} + on:drop|preventDefault={e => (drop = e)} + on:focus={e => (focus = e)} + on:mousedown={e => (mousedown = e)} + on:mousemove={e => (mousemove = e)} + on:mouseout={e => (mouseout = e)} + on:mouseup={e => (mouseup = e)} + on:dblclick={e => (dblclick = e)} + /> + + + + + {#if drawWidth > 0} + + {#if expanded} + + {/if} + + {/if} + + + +
+ {#each layers as layer (layer.id)} + {#if layer.chartType === 'activity'} + + {/if} + {#if layer.chartType === 'line' || layer.chartType === 'x-range'} + + {/if} + {#if layer.chartType === 'line'} + + {/if} + {#if layer.chartType === 'x-range'} + + {/if} + {/each} +
+
@@ -432,7 +448,7 @@ } .row.row-collapsed { - display: none; + /* display: none; */ } :global(.right) { @@ -452,11 +468,27 @@ } .row-root { + border-bottom: 2px solid var(--st-gray-20); + display: flex; + flex-direction: column; position: relative; } - .active-row:after { - border: 1px solid var(--st-utility-blue); + .row-root.expanded { + border-bottom: none; + } + + .row-content { + display: flex; + position: relative; + } + + .active-row { + z-index: 1; + } + + .active-row .row-content:after { + box-shadow: 0 0 0px 1px var(--st-utility-blue); content: ' '; height: 100%; left: 0; diff --git a/src/components/timeline/RowDragHandleHeight.svelte b/src/components/timeline/RowDragHandleHeight.svelte index 5859912d3d..0f2fda7f6a 100644 --- a/src/components/timeline/RowDragHandleHeight.svelte +++ b/src/components/timeline/RowDragHandleHeight.svelte @@ -8,6 +8,7 @@ const dispatch = createEventDispatcher(); let clientY: number | null = null; + let dragging: boolean = false; function onMouseMove(event: MouseEvent): void { if (clientY == null) { @@ -16,18 +17,20 @@ const dy = event.clientY - clientY; const newHeight = rowHeight + dy; - if (newHeight >= 50) { + if (newHeight >= 80) { dispatch('updateRowHeight', { newHeight }); } clientY = event.clientY; } function onMouseDown(event: MouseEvent): void { + dragging = true; clientY = event.clientY; document.addEventListener('mousemove', onMouseMove, false); } function onMouseUp(): void { + dragging = false; clientY = null; document.removeEventListener('mousemove', onMouseMove, false); } @@ -35,18 +38,19 @@ -
+
diff --git a/src/components/timeline/RowHeader.svelte b/src/components/timeline/RowHeader.svelte index e1b2196ff3..3c7a061354 100644 --- a/src/components/timeline/RowHeader.svelte +++ b/src/components/timeline/RowHeader.svelte @@ -2,40 +2,131 @@ -
-
- -
+ function onUpdateYAxesWidth(event: CustomEvent) { + const width = event.detail; + yAxesWidth = width; + } - - {#if $$slots.right} -
-
- +
+ + + {#if labels.length > 0} +
+ {#each labels as label} +
+ {label.label} + {label.units ? `(${label.units})` : ''} +
+ {/each} +
+ {/if} +
+
+ {#if expanded && yAxes.length} +
+
+ + + +
{/if} @@ -43,26 +134,63 @@ diff --git a/src/components/timeline/RowHeaderDragHandleWidth.svelte b/src/components/timeline/RowHeaderDragHandleWidth.svelte new file mode 100644 index 0000000000..513ca7ac86 --- /dev/null +++ b/src/components/timeline/RowHeaderDragHandleWidth.svelte @@ -0,0 +1,70 @@ + + + + + + +
+
+
+ + diff --git a/src/components/timeline/RowHeaderMenu.svelte b/src/components/timeline/RowHeaderMenu.svelte new file mode 100644 index 0000000000..f9c019f5b9 --- /dev/null +++ b/src/components/timeline/RowHeaderMenu.svelte @@ -0,0 +1,132 @@ + + + + +
+ + + + Edit Row + Move Up + Move Down + Duplicate Row + Delete Row + + {#if showVisibilityOptions} + +
+ + + +
+ {/if} +
+
+ + diff --git a/src/components/timeline/RowXAxisTicks.svelte b/src/components/timeline/RowXAxisTicks.svelte index 2c860cec9f..44bdb3519b 100644 --- a/src/components/timeline/RowXAxisTicks.svelte +++ b/src/components/timeline/RowXAxisTicks.svelte @@ -12,7 +12,7 @@ {#each xTicksView as tick} - + {/each} diff --git a/src/components/timeline/RowYAxes.svelte b/src/components/timeline/RowYAxes.svelte index 925e5bebb5..fe24d42d8b 100644 --- a/src/components/timeline/RowYAxes.svelte +++ b/src/components/timeline/RowYAxes.svelte @@ -2,15 +2,19 @@ - + + + + + + + diff --git a/src/components/timeline/RowYAxisTicks.svelte b/src/components/timeline/RowYAxisTicks.svelte index dcf6d1c160..d9404ba596 100644 --- a/src/components/timeline/RowYAxisTicks.svelte +++ b/src/components/timeline/RowYAxisTicks.svelte @@ -1,9 +1,8 @@ @@ -169,6 +215,7 @@ planStartTimeYmd={$plan?.start_time ?? ''} {timeline} {timelineDirectiveVisibilityToggles} + {timelineSpanVisibilityToggles} resourcesByViewLayerId={$resourcesByViewLayerId} selectedActivityDirectiveId={$selectedActivityDirectiveId} selectedSpanId={$selectedSpanId} @@ -186,6 +233,7 @@ on:jumpToSpan={jumpToSpan} on:mouseDown={onMouseDown} on:toggleDirectiveVisibility={({ detail: { rowId, visible } }) => onToggleDirectiveVisibility(rowId, visible)} + on:toggleSpanVisibility={({ detail: { rowId, visible } }) => onToggleSpanVisibility(rowId, visible)} on:toggleRowExpansion={({ detail: { expanded, rowId } }) => { viewUpdateRow('expanded', expanded, timelineId, rowId); }} diff --git a/src/components/timeline/TimelineSimulationRange.svelte b/src/components/timeline/TimelineSimulationRange.svelte index 3b07fb3e90..03eb3a79d9 100644 --- a/src/components/timeline/TimelineSimulationRange.svelte +++ b/src/components/timeline/TimelineSimulationRange.svelte @@ -82,6 +82,7 @@ pointer-events: none; position: absolute; width: 100%; + z-index: 4; } .timeline-simulation-range-header { diff --git a/src/components/timeline/TimelineSimulationRangeCursor.svelte b/src/components/timeline/TimelineSimulationRangeCursor.svelte index 96e9558edf..4c6a5a09d3 100644 --- a/src/components/timeline/TimelineSimulationRangeCursor.svelte +++ b/src/components/timeline/TimelineSimulationRangeCursor.svelte @@ -17,7 +17,6 @@ height: 100%; left: 0; opacity: 1; - pointer-events: all; position: absolute; top: -10px; transform: translateX(0); diff --git a/src/components/timeline/TimelineTimeDisplay.svelte b/src/components/timeline/TimelineTimeDisplay.svelte new file mode 100644 index 0000000000..7f3dc20016 --- /dev/null +++ b/src/components/timeline/TimelineTimeDisplay.svelte @@ -0,0 +1,76 @@ + + +
+
+ {getDoyTime(new Date(viewTimeRange.start), true)} +
+ {#if open} +
+ +
+ {/if} +
+ {getDoyTime(new Date(viewTimeRange.end), true)} +
+
+ + diff --git a/src/components/timeline/Tooltip.svelte b/src/components/timeline/Tooltip.svelte index a9ca4f0b87..b813e99a23 100644 --- a/src/components/timeline/Tooltip.svelte +++ b/src/components/timeline/Tooltip.svelte @@ -57,7 +57,7 @@ tooltipDiv.style('opacity', 1.0); tooltipDiv.style('left', `${xPosition}px`); tooltipDiv.style('top', `${yPosition}px`); - tooltipDiv.style('z-index', 5); + tooltipDiv.style('z-index', 9); const node = tooltipDiv.node() as HTMLElement; const { height, width, x, y } = node.getBoundingClientRect(); diff --git a/src/components/timeline/XAxis.svelte b/src/components/timeline/XAxis.svelte index 2d7f290c3c..f6f6b1b889 100644 --- a/src/components/timeline/XAxis.svelte +++ b/src/components/timeline/XAxis.svelte @@ -4,6 +4,7 @@ import type { ScaleTime } from 'd3-scale'; import type { ConstraintResult } from '../../types/constraint'; import type { TimeRange, XAxisTick } from '../../types/timeline'; + import { getTimeZoneName } from '../../utilities/time'; import ConstraintViolations from './ConstraintViolations.svelte'; import RowXAxisTicks from './RowXAxisTicks.svelte'; @@ -15,44 +16,57 @@ export let xScaleView: ScaleTime | null = null; export let xTicksView: XAxisTick[] = []; + const userTimeZone = getTimeZoneName(); + let axisOffset = 12; let violationsOffset = 0; - - - - - - - - {#if drawWidth > 0} - {#each xTicksView as tick} - {#if !tick.hideLabel} - - {tick.coarseTime} - - - {tick.fineTime} - - {/if} - {/each} - {/if} +
+
+
UTC
+
+ {userTimeZone} +
+
+ + + + + + + + {#if drawWidth > 0} + {#each xTicksView as tick} + {#if !tick.hideLabel} + + {tick.formattedDateUTC} + + + {tick.formattedDateLocal} + + {/if} + {/each} + {/if} + + + + - - - - - + +
diff --git a/src/components/timeline/form/TimelineEditorPanel.svelte b/src/components/timeline/form/TimelineEditorPanel.svelte index 3f306b69b6..49acaa7dda 100644 --- a/src/components/timeline/form/TimelineEditorPanel.svelte +++ b/src/components/timeline/form/TimelineEditorPanel.svelte @@ -860,6 +860,7 @@ class="st-input w-100" name="tickCount" type="number" + min="1" value={yAxis.tickCount} on:input={event => updateYAxisTickCount(event, yAxis)} /> diff --git a/src/stores/simulation.ts b/src/stores/simulation.ts index 9298c43688..f4d0a96c8d 100644 --- a/src/stores/simulation.ts +++ b/src/stores/simulation.ts @@ -28,6 +28,8 @@ export const externalResources: Writable = writable([]); export const resources: Writable = writable([]); +export const fetchingResources: Writable = writable(false); + export const resourceTypes: Writable = writable([]); export const spans: Writable = writable([]); diff --git a/src/types/timeline.ts b/src/types/timeline.ts index 15c911ec14..0ef81958d0 100644 --- a/src/types/timeline.ts +++ b/src/types/timeline.ts @@ -25,6 +25,7 @@ export type Axis = { }; export type BoundingBox = { + maxTimeX: number; maxX: number; maxY: number; minX: number; @@ -166,9 +167,9 @@ export type VerticalGuideSelection = { }; export type XAxisTick = { - coarseTime: string; date: Date; - fineTime: string; + formattedDateUTC: string; + formattedDateLocal: string; hideLabel: boolean; }; @@ -198,3 +199,5 @@ export interface XRangePoint extends Point { } export type DirectiveVisibilityToggleMap = Record; + +export type SpanVisibilityToggleMap = Record; diff --git a/src/utilities/effects.ts b/src/utilities/effects.ts index dd4a8429b8..46fe0c543f 100644 --- a/src/utilities/effects.ts +++ b/src/utilities/effects.ts @@ -16,7 +16,7 @@ import { import { createModelError, createPlanError, creatingModel, creatingPlan, models } from '../stores/plan'; import { schedulingStatus, selectedSpecId } from '../stores/scheduling'; import { commandDictionaries } from '../stores/sequencing'; -import { selectedSpanId, simulationDatasetId } from '../stores/simulation'; +import { fetchingResources, selectedSpanId, simulationDatasetId } from '../stores/simulation'; import { createTagError } from '../stores/tags'; import { applyViewUpdate, view } from '../stores/views'; import type { @@ -2781,11 +2781,15 @@ const effects = { async getResources(datasetId: number, startTimeYmd: string, user: User | null): Promise { try { + fetchingResources.set(true); const data = await reqHasura(gql.GET_PROFILES, { datasetId }, user); const { profile: profiles } = data; - return sampleProfiles(profiles, startTimeYmd); + const sampledProfiles = sampleProfiles(profiles, startTimeYmd); + fetchingResources.set(false); + return sampledProfiles; } catch (e) { catchError(e as Error); + fetchingResources.set(false); return []; } }, @@ -3192,7 +3196,6 @@ const effects = { } } } - return generateDefaultView(activityTypes, resourceTypes); } catch (e) { catchError(e as Error); diff --git a/src/utilities/time.ts b/src/utilities/time.ts index e1fb76d8b6..bbb1088b5a 100644 --- a/src/utilities/time.ts +++ b/src/utilities/time.ts @@ -525,3 +525,29 @@ export function getTimeAgo( export function getShortISOForDate(date: Date) { return date.toISOString().slice(0, 19); } + +/** + * Returns a DOY without milliseconds + * @example getShortDOY("2023-276T00:00:00.000") -> '2023-276T00:00:00' + */ +export function getShortDOY(doy: string) { + return doy.slice(0, 17); +} + +export function getShortTimeZoneName() { + return new Intl.DateTimeFormat('en-US', { timeZoneName: 'short' }).format(new Date()); +} + +export function getTimeZoneName() { + // set up formatter + const formatter = new Intl.DateTimeFormat(undefined, { + timeZoneName: 'short', + }); + // run formatter on current date + return ( + formatter + .formatToParts(Date.now()) + // extract the actual value from the formatter, only reliable way i can find to do this + .find(formatted => formatted.type === 'timeZoneName')['value'] + ); +} diff --git a/src/utilities/timeline.ts b/src/utilities/timeline.ts index 4f2e7e0041..304095ec81 100644 --- a/src/utilities/timeline.ts +++ b/src/utilities/timeline.ts @@ -1,4 +1,4 @@ -import { bisector, tickStep } from 'd3-array'; +import { bisector, range, tickStep } from 'd3-array'; import type { Quadtree, QuadtreeInternalNode, QuadtreeLeaf } from 'd3-quadtree'; import { scaleLinear, scaleTime, type ScaleLinear, type ScaleTime } from 'd3-scale'; import { @@ -381,7 +381,7 @@ export function createYAxis(timelines: Timeline[], args: Partial = {}): Ax const id = getNextYAxisID(timelines); return { - color: '#000000', + color: '#1b1d1e', domainFitMode: 'fitTimeWindow', id, label: { text: 'Label' }, @@ -590,3 +590,18 @@ export function getYAxesWithScaleDomains( return yAxis; }); } + +export function getYAxisTicks(scaleDomain: number[], tickCount: number) { + let ticks: number[] = []; + const [min, max] = scaleDomain; + + const tickStep = (max - min) / ((tickCount > 0 ? tickCount : 1) - 1); + if (tickStep === Infinity || isNaN(tickStep)) { + ticks = [min]; + } else if (tickStep === 0) { + ticks = [min]; + } else { + ticks = range(min, max + tickStep, tickStep); + } + return ticks; +} diff --git a/src/utilities/view.ts b/src/utilities/view.ts index e0e75e7ebb..0fc92839a0 100644 --- a/src/utilities/view.ts +++ b/src/utilities/view.ts @@ -20,7 +20,7 @@ export function generateDefaultView(activityTypes: ActivityType[] = [], resource const now = new Date().toISOString(); const types: string[] = activityTypes.map(({ name }) => name); - const timeline = createTimeline([], { marginLeft: 110, marginRight: 30 }); + const timeline = createTimeline([], { marginLeft: 160, marginRight: 30 }); const timelines = [timeline]; const activityLayer = createTimelineActivityLayer(timelines, { From 0a00a81f72b6db05cacf47bab882d9dfecb2cdad Mon Sep 17 00:00:00 2001 From: Aaron Plave Date: Tue, 31 Oct 2023 14:18:36 -0700 Subject: [PATCH 02/29] Clean --- src/utilities/time.ts | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/src/utilities/time.ts b/src/utilities/time.ts index bbb1088b5a..21a19d96e8 100644 --- a/src/utilities/time.ts +++ b/src/utilities/time.ts @@ -526,14 +526,6 @@ export function getShortISOForDate(date: Date) { return date.toISOString().slice(0, 19); } -/** - * Returns a DOY without milliseconds - * @example getShortDOY("2023-276T00:00:00.000") -> '2023-276T00:00:00' - */ -export function getShortDOY(doy: string) { - return doy.slice(0, 17); -} - export function getShortTimeZoneName() { return new Intl.DateTimeFormat('en-US', { timeZoneName: 'short' }).format(new Date()); } @@ -544,6 +536,7 @@ export function getTimeZoneName() { timeZoneName: 'short', }); // run formatter on current date + /* TODO fix TS issue */ return ( formatter .formatToParts(Date.now()) From 105fdd159de51ddcbcf7786eb2c99238e1533970 Mon Sep 17 00:00:00 2001 From: Aaron Plave Date: Wed, 1 Nov 2023 08:06:24 -0700 Subject: [PATCH 03/29] Fixes --- src/components/timeline/LayerActivity.svelte | 7 +++++-- src/components/timeline/RowHeader.svelte | 2 +- src/components/timeline/RowYAxes.svelte | 3 ++- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/components/timeline/LayerActivity.svelte b/src/components/timeline/LayerActivity.svelte index 4aed105b4e..4dba72d684 100644 --- a/src/components/timeline/LayerActivity.svelte +++ b/src/components/timeline/LayerActivity.svelte @@ -328,8 +328,11 @@ spans = activityDirectives .map(directive => { const rootSpan = getSpanForActivityDirective(directive); - const spanChildren = spanUtilityMaps.spanIdToChildIdsMap[rootSpan.id].map(id => spansMap[id]); - return [rootSpan].concat(spanChildren); + if (rootSpan) { + const spanChildren = (spanUtilityMaps.spanIdToChildIdsMap[rootSpan.id] || []).map(id => spansMap[id]); + return [rootSpan].concat(spanChildren); + } + return []; }) .flat(); } diff --git a/src/components/timeline/RowHeader.svelte b/src/components/timeline/RowHeader.svelte index 3c7a061354..8a304fe207 100644 --- a/src/components/timeline/RowHeader.svelte +++ b/src/components/timeline/RowHeader.svelte @@ -179,7 +179,7 @@ overflow: hidden; text-overflow: ellipsis; user-select: none; - white-space: normal; + white-space: nowrap; } .row-header:not(.expanded) .row-header-title { diff --git a/src/components/timeline/RowYAxes.svelte b/src/components/timeline/RowYAxes.svelte index fe24d42d8b..28a838c877 100644 --- a/src/components/timeline/RowYAxes.svelte +++ b/src/components/timeline/RowYAxes.svelte @@ -84,10 +84,11 @@ const domain = axis.scaleDomain; const scale = getYScale(domain, drawHeight); const tickValues = getYAxisTicks(axis.scaleDomain as number[], tickCount); + console.log(domain, '', tickValues); const axisLeft = d3AxisLeft(scale) .tickSizeInner(0) .tickSizeOuter(0) - .ticks(tickValues.length - 1) + .ticks(tickValues.length) .tickPadding(2) .tickValues(tickValues); From cc3328b669d6524d51ef0bbb10c00d01719f442f Mon Sep 17 00:00:00 2001 From: Aaron Plave Date: Wed, 1 Nov 2023 08:38:25 -0700 Subject: [PATCH 04/29] Color y axis labels if appropriate --- src/components/timeline/RowHeader.svelte | 19 ++++++++++--------- src/components/timeline/RowYAxes.svelte | 19 +++++++++++++++---- 2 files changed, 25 insertions(+), 13 deletions(-) diff --git a/src/components/timeline/RowHeader.svelte b/src/components/timeline/RowHeader.svelte index 8a304fe207..8963a090ab 100644 --- a/src/components/timeline/RowHeader.svelte +++ b/src/components/timeline/RowHeader.svelte @@ -14,7 +14,7 @@ export let expanded: boolean = true; export let height: number = 0; export let layers: Layer[]; - export let resourcesByViewLayerId: Record = {}; + export let resourcesByViewLayerId: Record = {}; /* TODO give this a type */ export let rowDragMoveDisabled: boolean = false; export let rowId: number = 0; export let showDirectives: boolean = true; @@ -122,11 +122,14 @@ {#if expanded && yAxes.length}
- - - - +
{/if} @@ -135,7 +138,6 @@ diff --git a/src/components/timeline/RowYAxes.svelte b/src/components/timeline/RowYAxes.svelte index c6bd2d9b58..9b9659dfc2 100644 --- a/src/components/timeline/RowYAxes.svelte +++ b/src/components/timeline/RowYAxes.svelte @@ -79,6 +79,7 @@ color = (yAxisLayers[0] as LineLayer).lineColor; } + // TODO deprecate these view properties? // const labelColor = axis.label?.color || 'black'; // const labelFontFace = axis.label?.fontFace || 'sans-serif'; // const labelFontSize = axis.label?.fontSize || 12; @@ -126,6 +127,8 @@ const axisGElement: SVGGElement | null = axisG.node(); if (axisGElement !== null) { + // TODO might be able to save minor perf by getting bounding rect of entire + // container instead of each individual axis? totalWidth += axisGElement.getBoundingClientRect().width; } } diff --git a/src/components/timeline/RowYAxisTicks.svelte b/src/components/timeline/RowYAxisTicks.svelte index d9404ba596..e17e02712d 100644 --- a/src/components/timeline/RowYAxisTicks.svelte +++ b/src/components/timeline/RowYAxisTicks.svelte @@ -1,14 +1,16 @@ - {#if activityDirective} + {#if mouseOverOrigin === 'row-header'} + Edit Row + Move Up + Move Down + Duplicate Row + Delete Row + + {#if hasActivityLayer} + +
+ + + +
+ {/if} + {:else if activityDirective} {#if activityDirectiveSpans && activityDirectiveSpans.length} {#each activityDirectiveSpans as activityDirectiveSpan} @@ -341,3 +444,22 @@ {/if}
+ + diff --git a/src/components/timeline/TimelinePanel.svelte b/src/components/timeline/TimelinePanel.svelte index 5f70ab31b7..0de4c86ae4 100644 --- a/src/components/timeline/TimelinePanel.svelte +++ b/src/components/timeline/TimelinePanel.svelte @@ -18,7 +18,14 @@ spans, spansMap, } from '../../stores/simulation'; - import { timelineLockStatus, view, viewTogglePanel, viewUpdateRow, viewUpdateTimeline } from '../../stores/views'; + import { + timelineLockStatus, + view, + viewSetSelectedRow, + viewTogglePanel, + viewUpdateRow, + viewUpdateTimeline, + } from '../../stores/views'; import type { ActivityDirectiveId } from '../../types/activity'; import type { User } from '../../types/app'; import type { @@ -141,10 +148,13 @@ : {}; } - function onToggleDirectiveVisibility(rowId: number, visible: boolean) { + function onToggleDirectiveVisibility(event: CustomEvent<{ row: Row; show: boolean }>) { + const { + detail: { row, show }, + } = event; timelineDirectiveVisibilityToggles = { ...timelineDirectiveVisibilityToggles, - ...toggleDirectiveVisibility(rowId, visible), + ...toggleDirectiveVisibility(row.id, show), }; } @@ -158,16 +168,34 @@ // : {}; // } - function onToggleSpanVisibility(rowId: number, visible: boolean) { + function onToggleSpanVisibility(event: CustomEvent<{ row: Row; show: boolean }>) { + const { + detail: { row, show }, + } = event; timelineSpanVisibilityToggles = { ...timelineSpanVisibilityToggles, - ...toggleSpanVisibility(rowId, visible), + ...toggleSpanVisibility(row.id, show), }; } function toggleSpanVisibility(rowId: number, visible: boolean) { return { [rowId]: visible }; } + + function onEditRow(event: CustomEvent) { + const { detail: row } = event; + + // Open the timeline editor panel on the right. + viewTogglePanel({ state: true, type: 'right', update: { rightComponentTop: 'TimelineEditorPanel' } }); + + // Set row to edit. + viewSetSelectedRow(row.id); + } + + function onDeleteRow(event: CustomEvent) { + const { detail: row } = event; + effects.deleteTimelineRow(row, timeline?.rows ?? [], timelineId); + } @@ -232,8 +260,8 @@ on:jumpToActivityDirective={jumpToActivityDirective} on:jumpToSpan={jumpToSpan} on:mouseDown={onMouseDown} - on:toggleDirectiveVisibility={({ detail: { rowId, visible } }) => onToggleDirectiveVisibility(rowId, visible)} - on:toggleSpanVisibility={({ detail: { rowId, visible } }) => onToggleSpanVisibility(rowId, visible)} + on:toggleDirectiveVisibility={onToggleDirectiveVisibility} + on:toggleSpanVisibility={onToggleSpanVisibility} on:toggleRowExpansion={({ detail: { expanded, rowId } }) => { viewUpdateRow('expanded', expanded, timelineId, rowId); }} @@ -249,6 +277,8 @@ on:viewTimeRangeChanged={({ detail: newViewTimeRange }) => { $viewTimeRange = newViewTimeRange; }} + on:editRow={onEditRow} + on:deleteRow={onDeleteRow} /> diff --git a/src/components/timeline/TimelineTimeDisplay.svelte b/src/components/timeline/TimelineTimeDisplay.svelte index 7f3dc20016..24ec6068c6 100644 --- a/src/components/timeline/TimelineTimeDisplay.svelte +++ b/src/components/timeline/TimelineTimeDisplay.svelte @@ -1,54 +1,18 @@
-
- {getDoyTime(new Date(viewTimeRange.start), true)} +
+ {planStartTimeDoy}
- {#if open} -
- -
- {/if} -
- {getDoyTime(new Date(viewTimeRange.end), true)} + +
+ {planEndTimeDoy}
@@ -68,9 +32,4 @@ padding: 0 4px; width: max-content; } - - .timeline-time-display--input { - position: absolute; - top: 0px; - } diff --git a/src/components/timeline/form/TimelineEditorPanel.svelte b/src/components/timeline/form/TimelineEditorPanel.svelte index 49acaa7dda..efaa8614ff 100644 --- a/src/components/timeline/form/TimelineEditorPanel.svelte +++ b/src/components/timeline/form/TimelineEditorPanel.svelte @@ -38,8 +38,8 @@ XRangeLayer, } from '../../../types/timeline'; import type { ViewGridSection } from '../../../types/view'; + import effects from '../../../utilities/effects'; import { getTarget } from '../../../utilities/generic'; - import { showConfirmModal } from '../../../utilities/modal'; import { getDoyTime } from '../../../utilities/time'; import { createHorizontalGuide, @@ -142,23 +142,6 @@ viewUpdateTimeline('rows', rows); } - function deleteTimelineRow(row: Row) { - const filteredRows = rows.filter(r => r.id !== row.id); - viewUpdateTimeline('rows', filteredRows); - } - - async function handleDeleteRowClick(row: Row) { - const { confirm } = await showConfirmModal( - 'Delete', - 'Are you sure you want to delete this timeline row?', - 'Delete Row', - true, - ); - if (confirm) { - deleteTimelineRow(row); - } - } - function handleDndConsiderRows(e: CustomEvent) { const { detail } = e; rows = detail.items as Row[]; @@ -627,7 +610,9 @@ diff --git a/src/types/timeline.ts b/src/types/timeline.ts index 0ef81958d0..324cfe37fc 100644 --- a/src/types/timeline.ts +++ b/src/types/timeline.ts @@ -100,12 +100,16 @@ export type MouseOver = { e: MouseEvent; gaps?: Point[]; layerId: number; + origin?: MouseOverOrigin; points?: Point[]; + row?: Row; selectedActivityDirectiveId?: number; selectedSpanId?: number; spans?: Span[]; }; +export type MouseOverOrigin = 'row-header' | 'layer-line' | 'layer-activity' | 'layer-x-range'; + export interface Point { id: number; name: string; diff --git a/src/utilities/effects.ts b/src/utilities/effects.ts index 46fe0c543f..c9fb97c513 100644 --- a/src/utilities/effects.ts +++ b/src/utilities/effects.ts @@ -18,7 +18,7 @@ import { schedulingStatus, selectedSpecId } from '../stores/scheduling'; import { commandDictionaries } from '../stores/sequencing'; import { fetchingResources, selectedSpanId, simulationDatasetId } from '../stores/simulation'; import { createTagError } from '../stores/tags'; -import { applyViewUpdate, view } from '../stores/views'; +import { applyViewUpdate, view, viewUpdateTimeline } from '../stores/views'; import type { ActivityDirective, ActivityDirectiveId, @@ -123,6 +123,7 @@ import type { TagsInsertInput, TagsSetInput, } from '../types/tags'; +import type { Row } from '../types/timeline'; import type { View, ViewDefinition, ViewInsertInput, ViewUpdateInput } from '../types/view'; import { ActivityDeletionAction } from './activities'; import { convertToQuery, getSearchParameterNumber, setQueryParam, sleep } from './generic'; @@ -2120,6 +2121,19 @@ const effects = { } }, + async deleteTimelineRow(row: Row, rows: Row[], timelineId: number | null) { + const { confirm } = await showConfirmModal( + 'Delete', + `Are you sure you want to delete timeline row: ${row.name}?`, + 'Delete Row', + true, + ); + if (confirm) { + const filteredRows = rows.filter(r => r.id !== row.id); + viewUpdateTimeline('rows', filteredRows, timelineId); + } + }, + async deleteUserSequence(sequence: UserSequence, user: User | null): Promise { try { if (!queryPermissions.DELETE_USER_SEQUENCE(user, sequence)) { From 912cbeeaeae9361d8f6d74d9ef678fde40bee15d Mon Sep 17 00:00:00 2001 From: Aaron Plave Date: Thu, 2 Nov 2023 15:00:11 -0700 Subject: [PATCH 06/29] Add close on escape behavior to context menu --- src/components/context-menu/ContextMenu.svelte | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/components/context-menu/ContextMenu.svelte b/src/components/context-menu/ContextMenu.svelte index c08d289307..c29060533c 100644 --- a/src/components/context-menu/ContextMenu.svelte +++ b/src/components/context-menu/ContextMenu.svelte @@ -61,9 +61,15 @@ hide(true); } } + + function onKeyDown(event: KeyboardEvent) { + if (event.key === 'Escape') { + hide(true); + } + } - hide(true)} /> + hide(true)} on:keydown={onKeyDown} /> {#if shown} From 0914eb0d2babad72860fdf36c79c83831b230cfc Mon Sep 17 00:00:00 2001 From: Aaron Plave Date: Thu, 2 Nov 2023 17:12:22 -0700 Subject: [PATCH 07/29] Fix for hz guide rendering --- src/components/timeline/Row.svelte | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/components/timeline/Row.svelte b/src/components/timeline/Row.svelte index 5b5ae67f10..86637eda7b 100644 --- a/src/components/timeline/Row.svelte +++ b/src/components/timeline/Row.svelte @@ -280,6 +280,12 @@ {xScaleView} on:mouseOver={onMouseOver} /> + {/if} @@ -377,12 +383,6 @@ {/if} {/each}
-
From 67ddcbb8bdd2666674aeb549e759048cce33ace1 Mon Sep 17 00:00:00 2001 From: Aaron Plave Date: Thu, 2 Nov 2023 17:12:36 -0700 Subject: [PATCH 08/29] Duplicate row functionality --- .../timeline/TimelineContextMenu.svelte | 2 +- src/components/timeline/TimelinePanel.svelte | 22 +++++++++- src/utilities/timeline.ts | 43 +++++++++++++++++++ 3 files changed, 65 insertions(+), 2 deletions(-) diff --git a/src/components/timeline/TimelineContextMenu.svelte b/src/components/timeline/TimelineContextMenu.svelte index 6a33125cd3..b612d96a52 100644 --- a/src/components/timeline/TimelineContextMenu.svelte +++ b/src/components/timeline/TimelineContextMenu.svelte @@ -191,7 +191,7 @@ } function onDuplicateRow() { - dispatch('duplicateRow', 0); + dispatch('duplicateRow', row); } function onShowDirectivesAndActivitiesChange(event: Event) { diff --git a/src/components/timeline/TimelinePanel.svelte b/src/components/timeline/TimelinePanel.svelte index 0de4c86ae4..11fad016e2 100644 --- a/src/components/timeline/TimelinePanel.svelte +++ b/src/components/timeline/TimelinePanel.svelte @@ -37,6 +37,7 @@ } from '../../types/timeline'; import effects from '../../utilities/effects'; import { featurePermissions } from '../../utilities/permissions'; + import { duplicateRow } from '../../utilities/timeline'; import Panel from '../ui/Panel.svelte'; import PanelHeaderActions from '../ui/PanelHeaderActions.svelte'; import Timeline from './Timeline.svelte'; @@ -56,7 +57,9 @@ hasUpdateDirectivePermission = featurePermissions.activityDirective.canUpdate(user, $plan) && !$planReadOnly; hasUpdateSimulationPermission = featurePermissions.simulation.canUpdate(user, $plan) && !$planReadOnly; } - $: timeline = $view?.definition.plan.timelines.find(timeline => { + + $: timelines = $view?.definition.plan.timelines || []; + $: timeline = timelines.find(timeline => { return timeline.id === timelineId; }); @@ -196,6 +199,22 @@ const { detail: row } = event; effects.deleteTimelineRow(row, timeline?.rows ?? [], timelineId); } + + function onDuplicateRow(event: CustomEvent) { + const { detail: row } = event; + if (timeline) { + const newRow = duplicateRow(row, timelines, timeline.id); + if (newRow) { + // Add row after the existing row + const newRows = timeline?.rows ?? []; + const rowIndex = newRows.findIndex(r => r.id === row.id); + if (rowIndex > -1) { + newRows.splice(rowIndex, 0, newRow); + viewUpdateTimeline('rows', newRows, timelineId); + } + } + } + } @@ -279,6 +298,7 @@ }} on:editRow={onEditRow} on:deleteRow={onDeleteRow} + on:duplicateRow={onDuplicateRow} /> diff --git a/src/utilities/timeline.ts b/src/utilities/timeline.ts index 304095ec81..8f4203dc32 100644 --- a/src/utilities/timeline.ts +++ b/src/utilities/timeline.ts @@ -591,6 +591,7 @@ export function getYAxesWithScaleDomains( }); } +/* TODO docs */ export function getYAxisTicks(scaleDomain: number[], tickCount: number) { let ticks: number[] = []; const [min, max] = scaleDomain; @@ -605,3 +606,45 @@ export function getYAxisTicks(scaleDomain: number[], tickCount: number) { } return ticks; } + +/* TODO docs and tests */ +/* TODO this would all be so much easier if we just gave things UUIDs instead of incrementing numerical ids.. */ +export function duplicateRow(row: Row, timelines: Timeline[], timelineId: number): Row | null { + const timelinesClone = structuredClone(timelines); + const timeline = timelinesClone.find(t => t.id === timelineId); + if (!timeline) { + return null; + } + + const rowClone = structuredClone(row); + const { id, layers, yAxes, horizontalGuides, ...rowArgs } = rowClone; + const newRow = createRow(timelines, rowArgs); + timeline.rows.push(newRow); + + yAxes.forEach(axis => { + const { id, ...axisArgs } = axis; + newRow.yAxes.push(createYAxis(timelinesClone, axisArgs)); + }); + + layers.forEach(layer => { + if (layer.chartType === 'activity') { + const { id, ...layerArgs } = layer; + newRow.layers.push(createTimelineActivityLayer(timelinesClone, layerArgs)); + } else if (layer.chartType === 'line') { + const { id, yAxisId, ...layerArgs } = layer; + newRow.layers.push(createTimelineLineLayer(timelinesClone, newRow.yAxes, layerArgs)); + } else if (layer.chartType === 'x-range') { + const { id, yAxisId, ...layerArgs } = layer; + newRow.layers.push(createTimelineXRangeLayer(timelinesClone, newRow.yAxes, layerArgs)); + } else { + console.warn('Unable to clone row layer with chart type:', layer.chartType); + } + }); + + horizontalGuides.forEach(guide => { + const { id, yAxisId, ...guideArgs } = guide; + newRow.horizontalGuides.push(createHorizontalGuide(timelinesClone, newRow.yAxes, guideArgs)); + }); + + return newRow; +} From 7d4caa568c087505080ceb56ac5ba5806cf1a380 Mon Sep 17 00:00:00 2001 From: Aaron Plave Date: Thu, 2 Nov 2023 17:28:20 -0700 Subject: [PATCH 09/29] Fixes and ability to right click anywhere in row header --- src/components/timeline/LayerActivity.svelte | 6 +- src/components/timeline/RowHeader.svelte | 9 +- .../timeline/TimelineContextMenu.svelte | 456 +++++++++--------- 3 files changed, 240 insertions(+), 231 deletions(-) diff --git a/src/components/timeline/LayerActivity.svelte b/src/components/timeline/LayerActivity.svelte index 8e1e89ed40..80327cad58 100644 --- a/src/components/timeline/LayerActivity.svelte +++ b/src/components/timeline/LayerActivity.svelte @@ -362,7 +362,9 @@ } const showContextMenu = !!e && isRightClick(e); if (showContextMenu) { - // delay the context menu a little bit to allow any selection events to occur first + // TODO if we don't move this to the end of the call stack we risk not having selectedActivityDirectiveId or selectedSpanId + // properly selected since the right click has to trigger selected entity store update first.. + // Could potentially do a quadtree search based off right click position instead of relying on these stores? setTimeout(() => { dispatch('contextMenu', { e, @@ -371,7 +373,7 @@ selectedActivityDirectiveId, selectedSpanId, }); - }, 1); + }, 0); } } diff --git a/src/components/timeline/RowHeader.svelte b/src/components/timeline/RowHeader.svelte index 5c6e4259e7..7f7dadf176 100644 --- a/src/components/timeline/RowHeader.svelte +++ b/src/components/timeline/RowHeader.svelte @@ -59,7 +59,14 @@ } -
+