diff --git a/pages/examples/testcases/swimLanes/horizontalMarkers.html b/pages/examples/testcases/swimLanes/horizontalMarkers.html new file mode 100644 index 0000000..81ab708 --- /dev/null +++ b/pages/examples/testcases/swimLanes/horizontalMarkers.html @@ -0,0 +1,231 @@ + + + + + Horizontal markers + + + + + +
+
+ + +
+
+

Lane selections:

+ group 0: + +
+ group 1: + +
+ group 2: + +
+ group 3: + +
+ group 4: + +
+ + + \ No newline at end of file diff --git a/src/UXClient/Components/LineChart/LineChart.scss b/src/UXClient/Components/LineChart/LineChart.scss index c44e7bd..77fd9a6 100644 --- a/src/UXClient/Components/LineChart/LineChart.scss +++ b/src/UXClient/Components/LineChart/LineChart.scss @@ -269,6 +269,15 @@ } } } + + .tsi-horizontalMarkerLine { + stroke-dasharray: 2,2; + } + + .tsi-horizontalMarkerText { + font-style: italic; + text-anchor: end; + } &.tsi-dark{ $grays: grays('dark'); diff --git a/src/UXClient/Components/LineChart/LineChart.ts b/src/UXClient/Components/LineChart/LineChart.ts index b072e1e..5463201 100644 --- a/src/UXClient/Components/LineChart/LineChart.ts +++ b/src/UXClient/Components/LineChart/LineChart.ts @@ -15,6 +15,7 @@ import EventsPlot from '../EventsPlot'; import { AxisState } from '../../Models/AxisState'; import Marker from '../Marker'; import { swimlaneLabelConstants} from '../../Constants/Constants' +import { HorizontalMarker } from '../../Utils/Interfaces'; class LineChart extends TemporalXAxisComponent { private targetElement: any; @@ -605,7 +606,7 @@ class LineChart extends TemporalXAxisComponent { marker.render(millis, this.chartOptions, this.chartComponentData, { chartMargins: this.chartMargins, x: this.x, - marginLeft: this.getMarkerMarginLeft(), + marginLeft: this.getMarkerMarginLeft() + (isSeriesLabels ? this.getAdditionalOffsetFromHorizontalMargin() : 0), colorMap: this.colorMap, yMap: this.yMap, onChange: onChange, @@ -1267,7 +1268,12 @@ class LineChart extends TemporalXAxisComponent { this.aggregateExpressionOptions.forEach((aEO) => { aEO.swimLane = 0; }); - this.chartOptions.swimLaneOptions = {0: {yAxisType: this.chartOptions.yAxisState}}; + // consolidate horizontal markers + const horizontalMarkers = []; + Object.values(this.chartOptions.swimLaneOptions).forEach((lane) => { + horizontalMarkers.push(...lane.horizontalMarkers); + }); + this.chartOptions.swimLaneOptions = {0: {yAxisType: this.chartOptions.yAxisState, horizontalMarkers: horizontalMarkers}}; } else { let minimumPresentSwimLane = this.aggregateExpressionOptions.reduce((currMin, aEO) => { return Math.max(aEO.swimLane, currMin); @@ -1287,6 +1293,102 @@ class LineChart extends TemporalXAxisComponent { this.chartOptions.swimLaneOptions = this.originalSwimLaneOptions; } + private getHorizontalMarkersWithYScales () { + let visibleCDOs = this.aggregateExpressionOptions.filter((cDO) => this.chartComponentData.displayState[cDO.aggKey]["visible"]); + const markerList = []; + const pushMarker = (cDO, marker, markerList) => { + if (this.chartOptions.yAxisState === YAxisStates.Overlap) { + return; + } + const domain = this.chartOptions.yAxisState === YAxisStates.Stacked ? + this.swimlaneYExtents[cDO.swimLane] : + this.swimlaneYExtents[0]; + // filter out markers not in the y range of that lane and in lanes that have overlap axis + if (domain && + this.chartOptions.swimLaneOptions?.[cDO.swimLane]?.yAxisType !== YAxisStates.Overlap && + marker.value >= domain[0] && + marker.value <= domain[1]) { + markerList.push({yScale: this.yMap[cDO.aggKey], ...marker}); + } + } + visibleCDOs.forEach((cDO) => { + cDO.horizontalMarkers.forEach((horizontalMarkerParams: HorizontalMarker) => { + pushMarker(cDO, horizontalMarkerParams, markerList); + }); + }); + + // find a visible CDO for a swimlane + const findFirstVisibleCDO = (swimlaneNumber) => { + return visibleCDOs.find((cDO) => { + return (cDO.swimLane === swimlaneNumber); + }); + } + + if (this.chartOptions.yAxisState !== YAxisStates.Overlap && this.chartOptions.swimLaneOptions) { + Object.keys(this.chartOptions.swimLaneOptions).forEach((swimlaneNumber) => { + const swimlaneOptions = this.chartOptions.swimLaneOptions[swimlaneNumber]; + swimlaneOptions.horizontalMarkers?.forEach((horizontalMarkerParams: HorizontalMarker) => { + const firstVisibleCDO = findFirstVisibleCDO(Number(swimlaneNumber)); + if (firstVisibleCDO) { + pushMarker(firstVisibleCDO, horizontalMarkerParams, markerList); + } + }); + }); + } + return markerList; + } + + // having horizontal markers present should add additional right hand margin to allow space for series labels + private getAdditionalOffsetFromHorizontalMargin () { + return this.getHorizontalMarkersWithYScales().length ? 16 : 0; + } + + private drawHorizontalMarkers () { + const markerList = this.getHorizontalMarkersWithYScales(); + const self = this; + + const markerContainers = this.svgSelection.select('.svgGroup').selectAll('.tsi-horizontalMarker') + .data(markerList); + markerContainers + .enter() + .append('g') + .merge(markerContainers) + .attr('class', 'tsi-horizontalMarker') + .attr("transform", (marker) => { + return "translate(" + 0 + "," + marker.yScale(marker.value) + ")"; + }) + .each(function (marker) { + const valueText = d3.select(this) + .selectAll('.tsi-horizontalMarkerText') + .data([marker.value]); + valueText + .enter() + .append('text') + .merge(valueText) + .attr('class', 'tsi-horizontalMarkerText') + .attr('x', self.chartWidth) + .attr('y', -4) + .text((value) => value); + valueText.exit().remove(); + + const valueLine = d3.select(this) + .selectAll('.tsi-horizontalMarkerLine') + .data([marker]); + valueLine + .enter() + .append('line') + .merge(valueLine) + .attr('class', 'tsi-horizontalMarkerLine') + .attr('stroke', marker => marker.color) + .attr('x1', 0) + .attr('y1', 0) + .attr('x2', self.chartWidth) + .attr('y2', 0); + valueLine.exit().remove(); + }); + markerContainers.exit().remove(); + } + private createSwimlaneLabels(offsetsAndHeights, visibleCDOs){ // swimLaneLabels object contains data needed to render each lane label @@ -1892,6 +1994,7 @@ class LineChart extends TemporalXAxisComponent { } this.renderAllMarkers(); + this.drawHorizontalMarkers(); this.voronoiDiagram = this.voronoi(this.getFilteredAndSticky(this.chartComponentData.allValues)); } diff --git a/src/UXClient/Models/ChartDataOptions.ts b/src/UXClient/Models/ChartDataOptions.ts index 51041af..ecb272a 100644 --- a/src/UXClient/Models/ChartDataOptions.ts +++ b/src/UXClient/Models/ChartDataOptions.ts @@ -1,5 +1,6 @@ import { InterpolationFunctions, DataTypes, EventElementTypes } from "../Constants/Enums"; import Utils from "../Utils"; +import { HorizontalMarker } from '../Utils/Interfaces' const DEFAULT_HEIGHT = 40; // Represents an expression that is suitable for use as the expression options parameter in a chart component @@ -35,6 +36,7 @@ class ChartDataOptions { public image: string; public isRawData: boolean; public isVariableAliasShownOnTooltip: boolean; + public horizontalMarkers: Array; constructor (optionsObject: Object){ this.searchSpan = Utils.getValueOrDefault(optionsObject, 'searchSpan'); @@ -67,6 +69,7 @@ class ChartDataOptions { this.startAt = Utils.getValueOrDefault(optionsObject, 'startAt', null); this.isRawData = Utils.getValueOrDefault(optionsObject, 'isRawData', false); this.isVariableAliasShownOnTooltip = Utils.getValueOrDefault(optionsObject, 'isVariableAliasShownOnTooltip', true); + this.horizontalMarkers = Utils.getValueOrDefault(optionsObject, 'horizontalMarkers', []); } } export {ChartDataOptions} diff --git a/src/UXClient/Models/ChartOptions.ts b/src/UXClient/Models/ChartOptions.ts index d7a320a..aab972a 100644 --- a/src/UXClient/Models/ChartOptions.ts +++ b/src/UXClient/Models/ChartOptions.ts @@ -3,13 +3,15 @@ import Utils from '../Utils'; import { Strings } from './Strings'; import { DefaultHierarchyNavigationOptions } from '../Constants/Constants'; import { InterpolationFunctions, YAxisStates } from '../Constants/Enums'; +import { HorizontalMarker } from '../Utils/Interfaces'; // Interfaces interface swimLaneOption { yAxisType: YAxisStates, label?: string, onClick?: (lane: number) => any, - collapseEvents?: string + collapseEvents?: string, + horizontalMarkers?: Array } class ChartOptions { diff --git a/src/UXClient/Utils/Interfaces.ts b/src/UXClient/Utils/Interfaces.ts new file mode 100644 index 0000000..d67b9eb --- /dev/null +++ b/src/UXClient/Utils/Interfaces.ts @@ -0,0 +1,4 @@ +export interface HorizontalMarker { + color: string, + value: number +} \ No newline at end of file