diff --git a/package-lock.json b/package-lock.json index ccd96fc780..af1e63a8e6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -53,6 +53,7 @@ "@types/d3-axis": "^3.0.2", "@types/d3-brush": "^3.0.2", "@types/d3-drag": "^3.0.2", + "@types/d3-format": "^3.0.4", "@types/d3-quadtree": "^3.0.2", "@types/d3-scale": "^4.0.3", "@types/d3-scale-chromatic": "^3.0.0", @@ -66,6 +67,7 @@ "@typescript-eslint/parser": "^5.60.1", "@vitest/ui": "^0.32.2", "cloc": "^2.11.0", + "d3-format": "^3.1.0", "esbuild": "^0.18.10", "eslint": "^8.43.0", "eslint-config-prettier": "^8.8.0", @@ -1216,6 +1218,12 @@ "@types/d3-selection": "*" } }, + "node_modules/@types/d3-format": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-format/-/d3-format-3.0.4.tgz", + "integrity": "sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==", + "dev": true + }, "node_modules/@types/d3-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.0.0.tgz", @@ -8196,6 +8204,12 @@ "@types/d3-selection": "*" } }, + "@types/d3-format": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-format/-/d3-format-3.0.4.tgz", + "integrity": "sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==", + "dev": true + }, "@types/d3-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.0.0.tgz", diff --git a/package.json b/package.json index 3be8e1ccb9..6dd49078d5 100644 --- a/package.json +++ b/package.json @@ -80,6 +80,7 @@ "@types/d3-axis": "^3.0.2", "@types/d3-brush": "^3.0.2", "@types/d3-drag": "^3.0.2", + "@types/d3-format": "^3.0.4", "@types/d3-quadtree": "^3.0.2", "@types/d3-scale": "^4.0.3", "@types/d3-scale-chromatic": "^3.0.0", @@ -93,6 +94,7 @@ "@typescript-eslint/parser": "^5.60.1", "@vitest/ui": "^0.32.2", "cloc": "^2.11.0", + "d3-format": "^3.1.0", "esbuild": "^0.18.10", "eslint": "^8.43.0", "eslint-config-prettier": "^8.8.0", 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/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} diff --git a/src/components/context-menu/ContextMenuItem.svelte b/src/components/context-menu/ContextMenuItem.svelte index e83ac64945..ae1a5a43cb 100644 --- a/src/components/context-menu/ContextMenuItem.svelte +++ b/src/components/context-menu/ContextMenuItem.svelte @@ -22,6 +22,7 @@ .context-menu-item { cursor: pointer; padding: 4px; + user-select: none; white-space: nowrap; } 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'} - - {/if} - {/each} +
+ + (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} +
@@ -431,10 +427,6 @@ display: flex; } - .row.row-collapsed { - display: none; - } - :global(.right) { z-index: 0; } @@ -452,11 +444,23 @@ } .row-root { + border-bottom: 2px solid var(--st-gray-20); + display: flex; + flex-direction: column; + position: relative; + } + + .row-root.expanded { + border-bottom: none; + } + + .row-content { + display: flex; position: relative; } - .active-row:after { - border: 1px solid var(--st-utility-blue); + .active-row .row-content:after { + box-shadow: 0 0 0px 1px inset var(--st-utility-blue); content: ' '; height: 100%; left: 0; @@ -464,5 +468,13 @@ position: absolute; top: 0; width: 100%; + z-index: 9; + } + + .active-row .row-content { + background: rgba(47, 128, 237, 0.06); + } + .active-row :global(.row-header) { + background: rgba(47, 128, 237, 0.06); } diff --git a/src/components/timeline/RowDragHandleHeight.svelte b/src/components/timeline/RowDragHandleHeight.svelte index 5859912d3d..f269479f2a 100644 --- a/src/components/timeline/RowDragHandleHeight.svelte +++ b/src/components/timeline/RowDragHandleHeight.svelte @@ -2,6 +2,7 @@ -
-
- -
+ function onUpdateYAxesWidth(event: CustomEvent) { + const width = event.detail; + yAxesWidth = width; + } - - {#if $$slots.right} -
-
- +
+ + + {#if resourceLabels.length > 0} +
+ {#each resourceLabels as label} +
+ + ‎{label.label} + {#if label.unit} + ({label.unit}) + {:else} + ‎  + {/if} +
+ {/each} +
+ {/if} +
+
+ {#if expanded && yAxes.length} +
+
+
{/if} @@ -43,26 +149,63 @@ diff --git a/src/components/timeline/RowHeaderDragHandleWidth.svelte b/src/components/timeline/RowHeaderDragHandleWidth.svelte new file mode 100644 index 0000000000..2a043a2b63 --- /dev/null +++ b/src/components/timeline/RowHeaderDragHandleWidth.svelte @@ -0,0 +1,72 @@ + + + + + + +
+
+
+ + diff --git a/src/components/timeline/RowHeaderMenu.svelte b/src/components/timeline/RowHeaderMenu.svelte new file mode 100644 index 0000000000..8880bdd71a --- /dev/null +++ b/src/components/timeline/RowHeaderMenu.svelte @@ -0,0 +1,46 @@ + + + + +
+ +
+ + 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..e40c474506 100644 --- a/src/components/timeline/RowYAxes.svelte +++ b/src/components/timeline/RowYAxes.svelte @@ -2,107 +2,143 @@ - + + + + + + + diff --git a/src/components/timeline/RowYAxisTicks.svelte b/src/components/timeline/RowYAxisTicks.svelte index dcf6d1c160..50f792cd58 100644 --- a/src/components/timeline/RowYAxisTicks.svelte +++ b/src/components/timeline/RowYAxisTicks.svelte @@ -2,30 +2,46 @@
-
- + -
-
- -
- - -
- {#each rows as row (row.id)} - + +
+
+
+ +
+ { - contextMenu = e.detail; - tooltip.hide(); - }} - on:dblClick - on:deleteActivityDirective - on:mouseDown={onMouseDown} - on:mouseDownRowMove={onMouseDownRowMove} - on:mouseOver={e => (mouseOver = e.detail)} - on:toggleRowExpansion={onToggleRowExpansion} - on:toggleDirectiveVisibility={e => onToggleDirectiveVisibility(row.id, e.detail)} - on:updateRowHeight={onUpdateRowHeight} + on:viewTimeRangeChanged /> - {/each} +
+ + + +
+ {#each rows as row (row.id)} +
+ onContextMenu(e, row)} + on:dblClick + on:deleteActivityDirective + on:mouseDown={onMouseDown} + on:mouseDownRowMove={onMouseDownRowMove} + on:mouseUpRowMove={onMouseUpRowMove} + on:mouseOver={e => (mouseOver = e.detail)} + on:toggleRowExpansion={onToggleRowExpansion} + on:updateRowHeight={onUpdateRowHeight} + /> +
+ {/each} +
@@ -381,15 +444,25 @@ on:updateVerticalGuides on:viewTimeRangeChanged on:viewTimeRangeReset={() => dispatch('viewTimeRangeChanged', maxTimeRange)} + on:viewTimeRangeChanged={onHistogramViewTimeRangeChanged} {simulation} {simulationDataset} {spansMap} {spanUtilityMaps} {plan} + {timelineDirectiveVisibilityToggles} + {timelineSpanVisibilityToggles} {planStartTimeYmd} verticalGuides={timeline?.verticalGuides ?? []} {xScaleView} {user} + on:toggleDirectiveVisibility + on:toggleSpanVisibility + on:editRow + on:deleteRow + on:moveRow={onMoveRow} + on:duplicateRow + on:insertRow />
@@ -403,6 +476,7 @@ } .timeline { + background-color: var(--st-gray-15); height: 100%; overflow-x: hidden; overflow-y: hidden; @@ -412,4 +486,25 @@ .x-axis { pointer-events: none; } + + .timeline-time-row { + background: white; + border-bottom: 1px solid var(--st-gray-20); + display: flex; + } + + .timeline-histogram-container { + padding: 4px 8px 4px 0px; + } + + .timeline-padded-content { + background: white; + border-radius: 4px; + } + + :global(#dnd-action-dragged-el .row-root) { + background: white; + border: 1px solid var(--st-gray-40); + box-shadow: var(--st-shadow-popover); + } diff --git a/src/components/timeline/TimelineContextMenu.svelte b/src/components/timeline/TimelineContextMenu.svelte index 2aafee3737..b0c3d4a188 100644 --- a/src/components/timeline/TimelineContextMenu.svelte +++ b/src/components/timeline/TimelineContextMenu.svelte @@ -11,9 +11,18 @@ import type { User } from '../../types/app'; import type { Plan } from '../../types/plan'; import type { Simulation, SimulationDataset, Span, SpanUtilityMaps, SpansMap } from '../../types/simulation'; - import type { MouseOver, TimeRange, VerticalGuide } from '../../types/timeline'; + import type { + DirectiveVisibilityToggleMap, + MouseOver, + MouseOverOrigin, + Row, + SpanVisibilityToggleMap, + TimeRange, + VerticalGuide, + } from '../../types/timeline'; import { getAllSpansForActivityDirective, getSpanRootParent } from '../../utilities/activities'; import effects from '../../utilities/effects'; + import { getTarget } from '../../utilities/generic'; import { permissionHandler } from '../../utilities/permissionHandler'; import { getDoyTime, getIntervalInMs, getUnixEpochTimeFromInterval } from '../../utilities/time'; import { createVerticalGuide } from '../../utilities/timeline'; @@ -23,16 +32,18 @@ import ContextSubMenuItem from '../context-menu/ContextSubMenuItem.svelte'; export let activityDirectivesMap: ActivityDirectivesMap; + export let contextMenu: MouseOver | null; export let hasUpdateDirectivePermission: boolean = false; export let hasUpdateSimulationPermission: boolean = false; export let maxTimeRange: TimeRange = { end: 0, start: 0 }; + export let plan: Plan | null = null; + export let planStartTimeYmd: string; export let simulation: Simulation | null; export let simulationDataset: SimulationDataset | null = null; export let spansMap: SpansMap; export let spanUtilityMaps: SpanUtilityMaps; - export let plan: Plan | null = null; - export let planStartTimeYmd: string; - export let contextMenu: MouseOver | null; + export let timelineDirectiveVisibilityToggles: DirectiveVisibilityToggleMap; + export let timelineSpanVisibilityToggles: SpanVisibilityToggleMap; export let verticalGuides: VerticalGuide[]; export let xScaleView: ScaleTime | null = null; export let user: User | null; @@ -43,16 +54,32 @@ let activityDirectiveSpans: Span[] | null = []; let activityDirectiveStartDate: Date | null = null; let span: Span | null; + let hasActivityLayer: boolean = false; + let mouseOverOrigin: MouseOverOrigin | undefined = undefined; + let row: Row | undefined = undefined; + let showDirectives: boolean = false; + let showSpans: boolean = false; // TODO imports here could be better, should we handle the vertical guide creation in Timeline? $: timelines = $view?.definition.plan.timelines ?? []; $: if (contextMenu && contextMenuComponent) { - const { e, selectedActivityDirectiveId, selectedSpanId } = contextMenu; + const { e, selectedActivityDirectiveId, selectedSpanId, origin, row: selectedRow } = contextMenu; + row = selectedRow; + mouseOverOrigin = origin; contextMenuComponent.show(e); activityDirective = null; span = null; activityDirectiveSpans = null; + hasActivityLayer = false; + showDirectives = false; + showSpans = false; + + if (row) { + showDirectives = timelineDirectiveVisibilityToggles[row.id]; + showSpans = timelineSpanVisibilityToggles[row.id]; + hasActivityLayer = !!row.layers.find(layer => layer.chartType === 'activity'); + } if (selectedActivityDirectiveId != null) { activityDirective = activityDirectivesMap[selectedActivityDirectiveId]; @@ -64,6 +91,9 @@ activityDirective = null; span = null; activityDirectiveSpans = null; + hasActivityLayer = false; + showDirectives = false; + showSpans = false; } $: startYmd = simulationDataset?.simulation_start_time ?? planStartTimeYmd; @@ -140,204 +170,301 @@ function onZoomHome() { dispatch('viewTimeRangeReset'); } + + function onEditRow() { + dispatch('editRow', row); + } + + function onDeleteRow() { + dispatch('deleteRow', row); + } + + function onMoveRowUp() { + dispatch('moveRow', { direction: 'up', row }); + } + + function onMoveRowDown() { + dispatch('moveRow', { direction: 'down', row }); + } + + function onInsertRow() { + dispatch('insertRow', row); + } + + function onDuplicateRow() { + dispatch('duplicateRow', row); + } + + function onShowDirectivesAndActivitiesChange(event: Event) { + const { value } = getTarget(event); + const newShowDirectives = value !== 'show-spans'; + const newShowSpans = value !== 'show-directives'; + dispatch('toggleDirectiveVisibility', { row, show: newShowDirectives }); + dispatch('toggleSpanVisibility', { row, show: newShowSpans }); + } - {#if activityDirective} - {#if activityDirectiveSpans && activityDirectiveSpans.length} - - {#each activityDirectiveSpans as activityDirectiveSpan} - dispatch('jumpToSpan', activityDirectiveSpan.id)}> - {activityDirectiveSpan.type} ({activityDirectiveSpan.id}) - - {/each} - - - {/if} - - {#if activityDirective.anchor_id !== null} + + {#if mouseOverOrigin !== 'row-header'} + {#if activityDirective} + {#if activityDirectiveSpans && activityDirectiveSpans.length} + + {#each activityDirectiveSpans as activityDirectiveSpan} + dispatch('jumpToSpan', activityDirectiveSpan.id)}> + {activityDirectiveSpan.type} ({activityDirectiveSpan.id}) + + {/each} + + + {/if} + {#if activityDirective.anchor_id !== null} + { + if (activityDirective !== null) { + dispatch('jumpToActivityDirective', activityDirective.anchor_id); + } + }} + > + Jump to Anchor Directive ({activityDirective.anchor_id}) + + {/if} { - if (activityDirective !== null) { - dispatch('jumpToActivityDirective', activityDirective.anchor_id); + if (activityDirectiveStartDate !== null) { + addVerticalGuide(activityDirectiveStartDate); } }} > - Jump to Anchor Directive ({activityDirective.anchor_id}) + Place Guide at Directive Start - {/if} - { - if (activityDirectiveStartDate !== null) { - addVerticalGuide(activityDirectiveStartDate); - } - }} - > - Place Guide at Directive Start - - - updateSimulationStartTime(activityDirectiveStartDate)} - use={[ - [ - permissionHandler, - { - hasPermission: hasUpdateSimulationPermission && !$planReadOnly, - permissionError: $planReadOnly - ? PlanStatusMessages.READ_ONLY - : 'You do not have permission to update this simulation', - }, - ], - ]} - > - Set Simulation Start at Directive Start - - updateSimulationEndTime(activityDirectiveStartDate)} - use={[ - [ - permissionHandler, - { - hasPermission: hasUpdateSimulationPermission && !$planReadOnly, - permissionError: $planReadOnly - ? PlanStatusMessages.READ_ONLY - : 'You do not have permission to update this simulation', - }, - ], - ]} - > - Set Simulation End at Directive Start - - - { - if (activityDirective !== null) { - dispatch('deleteActivityDirective', activityDirective.id); - } - }} - use={[ - [ - permissionHandler, - { - hasPermission: hasUpdateDirectivePermission && !$planReadOnly, - permissionError: $planReadOnly - ? PlanStatusMessages.READ_ONLY - : 'You do not have permission to delete this activity', - }, - ], - ]} - > - Delete Directive - - {:else if span} - Jump to Activity Directive - - - span && addVerticalGuide(getSpanDate(span))}> - At Simulated Activity Start + + updateSimulationStartTime(activityDirectiveStartDate)} + use={[ + [ + permissionHandler, + { + hasPermission: hasUpdateSimulationPermission && !$planReadOnly, + permissionError: $planReadOnly + ? PlanStatusMessages.READ_ONLY + : 'You do not have permission to update this simulation', + }, + ], + ]} + > + Set Simulation Start at Directive Start - span && addVerticalGuide(getSpanDate(span, true))}> - At Simulated Activity End + updateSimulationEndTime(activityDirectiveStartDate)} + use={[ + [ + permissionHandler, + { + hasPermission: hasUpdateSimulationPermission && !$planReadOnly, + permissionError: $planReadOnly + ? PlanStatusMessages.READ_ONLY + : 'You do not have permission to update this simulation', + }, + ], + ]} + > + Set Simulation End at Directive Start - - - - span && updateSimulationStartTime(getSpanDate(span))}> - At Simulated Activity Start + + { + if (activityDirective !== null) { + dispatch('deleteActivityDirective', activityDirective.id); + } + }} + use={[ + [ + permissionHandler, + { + hasPermission: hasUpdateDirectivePermission && !$planReadOnly, + permissionError: $planReadOnly + ? PlanStatusMessages.READ_ONLY + : 'You do not have permission to delete this activity', + }, + ], + ]} + > + Delete Directive - span && updateSimulationStartTime(getSpanDate(span, true))}> - At Simulated Activity End + {:else if span} + Jump to Activity Directive + + + span && addVerticalGuide(getSpanDate(span))}> + At Simulated Activity Start + + span && addVerticalGuide(getSpanDate(span, true))}> + At Simulated Activity End + + + + + span && updateSimulationStartTime(getSpanDate(span))}> + At Simulated Activity Start + + span && updateSimulationStartTime(getSpanDate(span, true))}> + At Simulated Activity End + + + + span && updateSimulationEndTime(getSpanDate(span))}> + At Simulated Activity Start + + span && updateSimulationEndTime(getSpanDate(span, true))}> + At Simulated Activity End + + + {:else} + xScaleView && contextMenu && addVerticalGuide(xScaleView.invert(contextMenu.e.offsetX))} + > + Place Guide - - - span && updateSimulationEndTime(getSpanDate(span))}> - At Simulated Activity Start + + + xScaleView && contextMenu && updateSimulationStartTime(xScaleView.invert(contextMenu.e.offsetX))} + > + Set Simulation Start - span && updateSimulationEndTime(getSpanDate(span, true))}> - At Simulated Activity End + xScaleView && contextMenu && updateSimulationEndTime(xScaleView.invert(contextMenu.e.offsetX))} + > + Set Simulation End - - {:else} - xScaleView && contextMenu && addVerticalGuide(xScaleView.invert(contextMenu.e.offsetX))} - > - Place Guide - + {/if} - xScaleView && contextMenu && updateSimulationStartTime(xScaleView.invert(contextMenu.e.offsetX))} - > - Set Simulation Start - - xScaleView && contextMenu && updateSimulationEndTime(xScaleView.invert(contextMenu.e.offsetX))} - > - Set Simulation End - - {/if} - - {#if span} - - onZoomHome()}>Reset Zoom + {#if span} + + onZoomHome()}>Reset Zoom + + onFocus(TIME_MS.MILLISECOND)}>1 Millisecond Padding + onFocus(TIME_MS.MILLISECOND * 10)}>10 Millisecond Padding + onFocus(TIME_MS.MILLISECOND * 50)}>50 Millisecond Padding + onFocus(TIME_MS.SECOND)}>1 Second Padding + onFocus(TIME_MS.SECOND * 30)}>30 Second Padding + onFocus(TIME_MS.MINUTE)}>1 Minute Padding + onFocus(TIME_MS.MINUTE * 30)}>30 Minute Padding + onFocus(TIME_MS.HOUR)}>1 Hour Padding + onFocus(TIME_MS.HOUR * 12)}>12 Hour Padding + onFocus(TIME_MS.DAY)}>1 Day Padding + onFocus(TIME_MS.DAY * 3)}>3 Day Padding + onFocus(TIME_MS.DAY * 7)}>1 Week Padding + onFocus(TIME_MS.MONTH)}>1 Month Padding + onFocus(TIME_MS.YEAR)}>1 Year Padding + - onFocus(TIME_MS.MILLISECOND)}>1 Millisecond Padding - onFocus(TIME_MS.MILLISECOND * 10)}>10 Millisecond Padding - onFocus(TIME_MS.MILLISECOND * 50)}>50 Millisecond Padding - onFocus(TIME_MS.SECOND)}>1 Second Padding - onFocus(TIME_MS.SECOND * 30)}>30 Second Padding - onFocus(TIME_MS.MINUTE)}>1 Minute Padding - onFocus(TIME_MS.MINUTE * 30)}>30 Minute Padding - onFocus(TIME_MS.HOUR)}>1 Hour Padding - onFocus(TIME_MS.HOUR * 12)}>12 Hour Padding - onFocus(TIME_MS.DAY)}>1 Day Padding - onFocus(TIME_MS.DAY * 3)}>3 Day Padding - onFocus(TIME_MS.DAY * 7)}>1 Week Padding - onFocus(TIME_MS.MONTH)}>1 Month Padding - onFocus(TIME_MS.YEAR)}>1 Year Padding - - {:else} - - onZoomHome()}>Reset Zoom + {:else} + + onZoomHome()}>Reset Zoom + + onZoom(TIME_MS.MILLISECOND)}>1 Millisecond + onZoom(TIME_MS.MILLISECOND * 10)}>10 Milliseconds + onZoom(TIME_MS.MILLISECOND * 50)}>50 Milliseconds + onZoom(TIME_MS.SECOND)}>1 Second + onZoom(TIME_MS.SECOND * 30)}>30 Seconds + onZoom(TIME_MS.MINUTE)}>1 Minute + onZoom(TIME_MS.MINUTE * 30)}>30 Minutes + onZoom(TIME_MS.HOUR)}>1 Hour + onZoom(TIME_MS.HOUR * 12)}>12 Hours + onZoom(TIME_MS.DAY)}>1 Day + onZoom(TIME_MS.DAY * 3)}>3 Days + onZoom(TIME_MS.DAY * 7)}>1 Week + onZoom(TIME_MS.MONTH)}>1 Month + onZoom(TIME_MS.YEAR)}>1 Year + - onZoom(TIME_MS.MILLISECOND)}>1 Millisecond - onZoom(TIME_MS.MILLISECOND * 10)}>10 Milliseconds - onZoom(TIME_MS.MILLISECOND * 50)}>50 Milliseconds - onZoom(TIME_MS.SECOND)}>1 Second - onZoom(TIME_MS.SECOND * 30)}>30 Seconds - onZoom(TIME_MS.MINUTE)}>1 Minute - onZoom(TIME_MS.MINUTE * 30)}>30 Minutes - onZoom(TIME_MS.HOUR)}>1 Hour - onZoom(TIME_MS.HOUR * 12)}>12 Hours - onZoom(TIME_MS.DAY)}>1 Day - onZoom(TIME_MS.DAY * 3)}>3 Days - onZoom(TIME_MS.DAY * 7)}>1 Week - onZoom(TIME_MS.MONTH)}>1 Month - onZoom(TIME_MS.YEAR)}>1 Year - + {/if} + {/if} + Edit Row + Move Row Up + Move Row Down + Insert Row + Duplicate Row + Delete Row + {#if hasActivityLayer} + +
+ + + +
{/if}
+ + diff --git a/src/components/timeline/TimelineCursors.svelte b/src/components/timeline/TimelineCursors.svelte index e3c2c90f2a..d9266c2ab9 100644 --- a/src/components/timeline/TimelineCursors.svelte +++ b/src/components/timeline/TimelineCursors.svelte @@ -195,7 +195,7 @@ pointer-events: none; position: absolute; width: 100%; - z-index: 2; + z-index: 4; } .timeline-cursor-header { diff --git a/src/components/timeline/TimelineHistogram.svelte b/src/components/timeline/TimelineHistogram.svelte index 2dedf6de30..05e5197359 100644 --- a/src/components/timeline/TimelineHistogram.svelte +++ b/src/components/timeline/TimelineHistogram.svelte @@ -18,7 +18,6 @@ export let cursorEnabled: boolean = true; export let drawHeight: number = 40; export let drawWidth: number = 0; - export let marginLeft: number = 50; export let mouseOver: MouseOver | null; export let planStartTimeYmd: string; export let simulationDataset: SimulationDataset | null = null; @@ -290,7 +289,7 @@
{ + + $: timelines = $view?.definition.plan.timelines || []; + $: timeline = timelines.find(timeline => { return timeline.id === timelineId; }); @@ -50,6 +66,10 @@ ? generateDirectiveVisibilityToggles(timeline, timelineDirectiveVisibilityToggles) : {}; + $: timelineSpanVisibilityToggles = timeline + ? generateSpanVisibilityToggles(timeline, timelineSpanVisibilityToggles) + : {}; + function deleteActivityDirective(event: CustomEvent) { const { detail: activityDirectiveId } = event; if ($plan) { @@ -84,6 +104,24 @@ }, {}); } + function generateSpanVisibilityToggles( + timeline: TimelineType, + currentVisibilityMap: SpanVisibilityToggleMap, + visible?: boolean, + ): SpanVisibilityToggleMap { + return (timeline?.rows ?? []).reduce((prevToggles: SpanVisibilityToggleMap, row: Row) => { + const { id, layers } = row; + const containsActivityLayer: boolean = layers.find(layer => layer.chartType === 'activity') !== undefined; + if (containsActivityLayer) { + return { + ...prevToggles, + ...toggleSpanVisibility(id, visible ?? currentVisibilityMap[id] ?? true), + }; + } + return prevToggles; + }, {}); + } + function jumpToActivityDirective(event: CustomEvent) { const { detail: activityDirectiveId } = event; selectActivity(activityDirectiveId, null); @@ -112,16 +150,77 @@ : {}; } - 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), }; } function toggleDirectiveVisibility(rowId: number, visible: boolean) { return { [rowId]: visible }; } + + // function onToggleAllSpanVisibility(visible: boolean) { + // timelineSpanVisibilityToggles = timeline + // ? generateSpanVisibilityToggles(timeline, timelineSpanVisibilityToggles, visible) + // : {}; + // } + + function onToggleSpanVisibility(event: CustomEvent<{ row: Row; show: boolean }>) { + const { + detail: { row, show }, + } = event; + timelineSpanVisibilityToggles = { + ...timelineSpanVisibilityToggles, + ...toggleSpanVisibility(row.id, show), + }; + } + + function toggleSpanVisibility(rowId: number, visible: boolean) { + return { [rowId]: visible }; + } + + function editRow(row: Row) { + // Open the timeline editor panel on the right. + viewTogglePanel({ state: true, type: 'right', update: { rightComponentTop: 'TimelineEditorPanel' } }); + + // Set row to edit. + viewSetSelectedRow(row.id); + } + + function onEditRow(event: CustomEvent) { + const { detail: row } = event; + editRow(row); + } + + function onDeleteRow(event: CustomEvent) { + const { detail: row } = event; + effects.deleteTimelineRow(row, timeline?.rows ?? [], timelineId); + } + + function onDuplicateRow(event: CustomEvent) { + const { detail: row } = event; + if (timeline) { + const newRow = effects.duplicateTimelineRow(row, timeline, timelines); + if (newRow) { + editRow(newRow); + } + } + } + + function onInsertRow(event: CustomEvent) { + const { detail: row } = event; + if (timeline) { + const newRow = effects.insertTimelineRow(row, timeline, timelines); + if (newRow) { + editRow(newRow); + } + } + } @@ -169,6 +268,7 @@ planStartTimeYmd={$plan?.start_time ?? ''} {timeline} {timelineDirectiveVisibilityToggles} + {timelineSpanVisibilityToggles} resourcesByViewLayerId={$resourcesByViewLayerId} selectedActivityDirectiveId={$selectedActivityDirectiveId} selectedSpanId={$selectedSpanId} @@ -185,7 +285,8 @@ on:jumpToActivityDirective={jumpToActivityDirective} on:jumpToSpan={jumpToSpan} on:mouseDown={onMouseDown} - on:toggleDirectiveVisibility={({ detail: { rowId, visible } }) => onToggleDirectiveVisibility(rowId, visible)} + on:toggleDirectiveVisibility={onToggleDirectiveVisibility} + on:toggleSpanVisibility={onToggleSpanVisibility} on:toggleRowExpansion={({ detail: { expanded, rowId } }) => { viewUpdateRow('expanded', expanded, timelineId, rowId); }} @@ -201,6 +302,10 @@ on:viewTimeRangeChanged={({ detail: newViewTimeRange }) => { $viewTimeRange = newViewTimeRange; }} + on:editRow={onEditRow} + on:deleteRow={onDeleteRow} + on:duplicateRow={onDuplicateRow} + on:insertRow={onInsertRow} /> 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..624d676b33 --- /dev/null +++ b/src/components/timeline/TimelineTimeDisplay.svelte @@ -0,0 +1,36 @@ + + +
+
+ {planStartTimeDoy} +
+ +
+ {planEndTimeDoy} +
+
+ + 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/LayerLineForm.svelte b/src/components/timeline/form/LayerLineForm.svelte deleted file mode 100644 index 3aafc3e58b..0000000000 --- a/src/components/timeline/form/LayerLineForm.svelte +++ /dev/null @@ -1,46 +0,0 @@ - - -{#if lineLayer && lineLayer.chartType === 'line'} -
- - - - -
- - - - - - - - - - - - - - - - - -{/if} diff --git a/src/components/timeline/form/LayerXRangeForm.svelte b/src/components/timeline/form/LayerXRangeForm.svelte deleted file mode 100644 index ccb5abf935..0000000000 --- a/src/components/timeline/form/LayerXRangeForm.svelte +++ /dev/null @@ -1,41 +0,0 @@ - - -{#if lineLayer && lineLayer.chartType === 'x-range'} - - - - - - - - - - - -{/if} diff --git a/src/components/timeline/form/TimelineEditorLayerSettings.svelte b/src/components/timeline/form/TimelineEditorLayerSettings.svelte index c0105ac743..6c5607a62a 100644 --- a/src/components/timeline/form/TimelineEditorLayerSettings.svelte +++ b/src/components/timeline/form/TimelineEditorLayerSettings.svelte @@ -40,14 +40,18 @@ } - +
{#if layer.chartType === 'activity'} @@ -63,6 +67,18 @@ /> {:else if layer.chartType === 'line'} + + + + + + +