From 4fd7c6bc0800d08f640c69e348422cc8df763605 Mon Sep 17 00:00:00 2001 From: Mario Bielert Date: Fri, 4 Oct 2024 16:30:31 +0200 Subject: [PATCH] feat: highlight the metric close to mouse pointer --- src/graticule.js | 34 ++++++++++++++------- src/interact.js | 64 ++++++++++++++++++++++++++++++++-------- src/store/metrics.js | 16 +++++++++- src/ui/metric-legend.vue | 11 ++++++- 4 files changed, 101 insertions(+), 24 deletions(-) diff --git a/src/graticule.js b/src/graticule.js index 1998ad4..5f3dfca 100644 --- a/src/graticule.js +++ b/src/graticule.js @@ -432,15 +432,16 @@ export class Graticule { } getTimeValueAtPoint (positionArr) { - const relationalPos = [positionArr[0] - this.dimensions.x, positionArr[1] - this.dimensions.y] + const x = positionArr[0] - this.dimensions.x + const y = positionArr[1] - this.dimensions.y if (undefined !== this.curTimeRange && undefined !== this.curValueRange && - relationalPos[0] >= 0 && - relationalPos[0] <= this.dimensions.width && - relationalPos[1] >= 0 && - relationalPos[1] <= this.dimensions.height) { - return [Math.round((relationalPos[0] * this.curTimePerPixel) + this.curTimeRange[0]), - ((this.dimensions.height - relationalPos[1]) * this.curValuesPerPixel) + this.curValueRange[0] + x >= 0 && + x <= this.dimensions.width && + y >= 0 && + y <= this.dimensions.height) { + return [Math.round((x * this.curTimePerPixel) + this.curTimeRange[0]), + ((this.dimensions.height - y) * this.curValuesPerPixel) + this.curValueRange[0] ] } else { return undefined @@ -757,19 +758,20 @@ export class Graticule { } drawBands (ctx, timePerPixel, valuesPerPixel, graticuleDimensions) { + const peakedMetric = store.getters['metrics/getPeakedMetric']() for (const metric of this.data.metrics) { const drawState = store.getters['metrics/getMetricDrawState'](metric.name) if (!drawState.draw) continue if (drawState.drawMin && drawState.drawMax) { - this.drawBand(ctx, timePerPixel, valuesPerPixel, graticuleDimensions, metric) + this.drawBand(ctx, timePerPixel, valuesPerPixel, graticuleDimensions, metric, peakedMetric) } } this.resetCtx(ctx) } - drawBand (ctx, timePerPixel, valuesPerPixel, graticuleDimensions, metric) { + drawBand (ctx, timePerPixel, valuesPerPixel, graticuleDimensions, metric, peakedMetric) { const band = metric.band if (band) { const styleOptions = this.parseStyleOptions(band.styleOptions, ctx) @@ -817,11 +819,17 @@ export class Graticule { previousY = y } ctx.closePath() + if (metric.name === peakedMetric) { + ctx.lineWidth = 2 + ctx.globalAlpha = 0.5 + ctx.stroke() + } ctx.fill() } } drawSeries (timeRange, valueRange, timePerPixel, valuesPerPixel, ctx, graticuleDimensions) { + const peakedMetric = store.getters['metrics/getPeakedMetric']() for (let i = 0; i < this.data.metrics.length; ++i) { if (!store.getters['metrics/getMetricDrawState'](this.data.metrics[i].name).draw) continue @@ -846,12 +854,18 @@ export class Graticule { for (const curAggregate in this.data.metrics[i].series) { const curSeries = this.data.metrics[i].series[curAggregate] + if (curSeries) { - const styleOptions = this.parseStyleOptions(curSeries.styleOptions, ctx) + const styleOptions = { ...this.parseStyleOptions(curSeries.styleOptions, ctx) } if (styleOptions.skip || curSeries.points.length === 0) { this.resetCtx(ctx) continue } + + if (peakedMetric === this.data.metrics[i].name) { + styleOptions.pointWidth = 10 + } + const offsiteCanvas = this.generateOffsiteDot(styleOptions) for (let j = 0, x, y, previousX, previousY; j < curSeries.points.length; ++j) { diff --git a/src/interact.js b/src/interact.js index 2dc3b6a..9126b5a 100644 --- a/src/interact.js +++ b/src/interact.js @@ -75,7 +75,7 @@ function uiInteractZoomArea (evtObj) { function uiInteractZoomIn (evtObj) { evtObj.preventDefault() const relativeStart = mouseDown.relativeStartPos - const relativeEnd = calculateActualMousePos(evtObj) + const relativeEnd = calculateActualMousePos(evtObj, window.MetricQWebView.graticule.ele) relativeEnd[0] = Math.max(window.MetricQWebView.graticule.dimensions.x, Math.min(Math.abs(relativeEnd[0]), window.MetricQWebView.graticule.dimensions.width)) @@ -127,7 +127,7 @@ function uiInteractZoomWheel (evtObj) { scrollDirection = 0.2 } scrollDirection *= store.state.configuration.zoomSpeed / 10 - const curPos = calculateActualMousePos(evtObj) + const curPos = calculateActualMousePos(evtObj, window.MetricQWebView.graticule.ele) const curTimeValue = window.MetricQWebView.graticule.getTimeValueAtPoint(curPos) if (curTimeValue) { if (!window.MetricQWebView.handler.zoomTimeAtPoint(curTimeValue, scrollDirection)) { @@ -140,7 +140,7 @@ function uiInteractZoomWheel (evtObj) { } function uiInteractLegend (evtObj) { - const curPosOnCanvas = calculateActualMousePos(evtObj) + const curPosOnCanvas = calculateActualMousePos(evtObj, window.MetricQWebView.graticule.ele) const curPoint = window.MetricQWebView.graticule.getTimeValueAtPoint(curPosOnCanvas) if (!curPoint) { @@ -148,18 +148,26 @@ function uiInteractLegend (evtObj) { } const timeAt = curPoint[0] + const valueAt = curPoint[1] window.MetricQWebView.graticule.draw(false) const myCtx = window.MetricQWebView.graticule.ctx myCtx.fillStyle = 'rgba(0,0,0,0.8)' myCtx.fillRect(curPosOnCanvas[0] - 1, window.MetricQWebView.graticule.dimensions.y, 2, window.MetricQWebView.graticule.dimensions.height) + // myCtx.fillRect(window.MetricQWebView.graticule.dimensions.x, curPosOnCanvas[1] - 1, window.MetricQWebView.graticule.dimensions.width, 2) myCtx.font = '14px ' + window.MetricQWebView.graticule.DEFAULT_FONT // actually it's sans-serif const legendEntries = [] let maxLabelWidth = 0 let maxTextWidth = 0 + let closestMetric + let closestMetricValue + + const range = window.MetricQWebView.graticule.curValueRange + const maxDistance = 0.05 * (range[1] - range[0]) + for (const metric of Object.values(window.MetricQWebView.graticule.data.metrics)) { const metricDrawState = store.getters['metrics/getMetricDrawState'](metric.name) @@ -170,6 +178,16 @@ function uiInteractLegend (evtObj) { const value = metric.series.raw.getValueAtTimeAndIndex(timeAt) if (value === undefined) continue + if ( + Math.abs(valueAt - value[1]) < maxDistance && + ( + closestMetric === undefined || + Math.abs(value[1] - valueAt) < Math.abs(closestMetricValue - valueAt) + ) + ) { + closestMetric = metric.name + closestMetricValue = value[1] + } curText = (Number(value[1])).toFixed(3) } else if (metric.series.min !== undefined && metric.series.max !== undefined && @@ -179,6 +197,19 @@ function uiInteractLegend (evtObj) { const avg = metric.series.avg.getValueAtTimeAndIndex(timeAt) if (min === undefined || max === undefined || avg === undefined) continue + if ( + (valueAt < max[1] && valueAt > min[1] && metricDrawState.drawMin && metricDrawState.drawMax) || + maxDistance > Math.abs(valueAt - avg[1]) + ) { + if ( + closestMetric === undefined || + Math.abs(avg[1] - valueAt) < Math.abs(closestMetricValue - valueAt) + ) { + closestMetric = metric.name + closestMetricValue = avg[1] + } + } + curText = '' if (metricDrawState.drawMin) { curText += '▼ ' + (Number(min[1])).toFixed(3) + ' | ' @@ -217,6 +248,8 @@ function uiInteractLegend (evtObj) { legendEntries.push(newEntry) } + store.dispatch('metrics/updatePeakedMetric', { metric: closestMetric }) + let timeString = new Date(curPoint[0]).toLocaleString() if (window.MetricQWebView.graticule.curTimeRange !== undefined) { @@ -251,7 +284,7 @@ function uiInteractLegend (evtObj) { } drawHoverDate(myCtx, timeString, curPosOnCanvas[0], maxLabelWidth, offsetTop, offsetMid, verticalDiff, distanceToRightEdge) - drawHoverText(myCtx, legendEntries, curPosOnCanvas[0], maxTextWidth, maxLabelWidth, offsetTop, offsetMid, verticalDiff, borderPadding, distanceToRightEdge) + drawHoverText(myCtx, legendEntries, curPosOnCanvas[0], maxTextWidth, maxLabelWidth, offsetTop, offsetMid, verticalDiff, borderPadding, distanceToRightEdge, closestMetric) } function drawHoverDate (myCtx, timeString, curXPosOnCanvas, maxNameWidth, offsetTop, offsetMid, verticalDiff, distanceToRightEdge) { @@ -265,7 +298,7 @@ function drawHoverDate (myCtx, timeString, curXPosOnCanvas, maxNameWidth, offset myCtx.fillText(timeString, curXPosOnCanvas + offsetMid, offsetTop - 0.5 * verticalDiff) } -function drawHoverText (myCtx, metricsArray, curXPosOnCanvas, maxValueWidth, maxLabelWidth, offsetTop, offsetMid, verticalDiff, borderPadding, distanceToRightEdge) { +function drawHoverText (myCtx, metricsArray, curXPosOnCanvas, maxValueWidth, maxLabelWidth, offsetTop, offsetMid, verticalDiff, borderPadding, distanceToRightEdge, peakedMetric) { myCtx.textBaseline = 'middle' myCtx.textAlign = 'left' let offsetRight = 0 @@ -279,9 +312,18 @@ function drawHoverText (myCtx, metricsArray, curXPosOnCanvas, maxValueWidth, max for (let i = 0; i < metricsArray.length; ++i) { const y = offsetTop + i * verticalDiff myCtx.fillStyle = metricsArray[i].metric.color - myCtx.globalAlpha = 0.4 + if (metricsArray[i].metric.name === peakedMetric) { + myCtx.globalAlpha = 0.8 + } else { + myCtx.globalAlpha = 0.4 + } myCtx.fillRect(curXPosOnCanvas + offsetMid - offsetRight - borderPadding, y, maxValueWidth + maxLabelWidth + (offsetMid + borderPadding) * 2, 20) myCtx.fillStyle = '#000000' + if (metricsArray[i].metric.name === peakedMetric) { + myCtx.lineWidth = 2 + myCtx.color = '#000000' + myCtx.strokeRect(curXPosOnCanvas + offsetMid - offsetRight - borderPadding, y, maxValueWidth + maxLabelWidth + (offsetMid + borderPadding) * 2, 20) + } myCtx.globalAlpha = 1 myCtx.fillText(metricsArray[i].curText, curXPosOnCanvas + offsetMid - offsetRight, y + 0.5 * verticalDiff) myCtx.fillText(metricsArray[i].label, curXPosOnCanvas + (offsetMid * 3 + maxValueWidth) - offsetRight, y + 0.5 * verticalDiff) @@ -345,12 +387,10 @@ export function registerCallbacks (anchoringObject) { }) } -function calculateActualMousePos (evtObj) { - const curPos = [evtObj.x - 3 * evtObj.target.offsetLeft, - evtObj.y - evtObj.target.offsetTop] - const scrollOffset = calculateScrollOffset(evtObj.target) - curPos[0] += scrollOffset[0] - curPos[1] += scrollOffset[1] +function calculateActualMousePos (evtObj, target) { + if (target === undefined) target = evtObj.target + const box = target.getBoundingClientRect() + const curPos = [evtObj.x - box.left, evtObj.y - box.top] return curPos } diff --git a/src/store/metrics.js b/src/store/metrics.js index d0bc769..e13f76c 100644 --- a/src/store/metrics.js +++ b/src/store/metrics.js @@ -6,7 +6,8 @@ import * as Error from '@/errors' export default { namespaced: true, state: { - metrics: {} + metrics: {}, + peakedMetric: undefined }, getters: { getMetricDrawState: (state) => (metricName) => { @@ -45,6 +46,9 @@ export default { }, getUnit: (state) => (metric) => { return state.metrics[metric].unit + }, + getPeakedMetric: (state) => () => { + return state.peakedMetric } }, mutations: { @@ -121,7 +125,14 @@ export default { } else { throw new Error('Metric not found!') } + }, + + setPeakedMetric (state, { metric }) { + if (state.metrics[metric] || metric === undefined) { + Vue.set(state, 'peakedMetric', metric) + } } + }, actions: { checkGlobalDrawState ({ commit, getters }) { @@ -248,6 +259,9 @@ export default { }, updateDataPoints ({ state, commit }, { metricKey, pointsAgg, pointsRaw }) { commit('privateSet', { metricKey, metric: { pointsAgg: pointsAgg, pointsRaw: pointsRaw } }) + }, + updatePeakedMetric ({ commit }, { metric }) { + commit('setPeakedMetric', { metric }) } }, modules: {} diff --git a/src/ui/metric-legend.vue b/src/ui/metric-legend.vue index c84f522..2c48cd0 100644 --- a/src/ui/metric-legend.vue +++ b/src/ui/metric-legend.vue @@ -14,7 +14,7 @@