Commit 53fae9ad authored by Mike Greiling's avatar Mike Greiling

Merge branch 'ee-6381-multiseries' into 'master'

multiseries

Closes #50947

See merge request gitlab-org/gitlab-ce!21427
parents c9116b6d 4d2e9a1f
...@@ -82,11 +82,12 @@ export default { ...@@ -82,11 +82,12 @@ export default {
value: 0, value: 0,
}, },
currentXCoordinate: 0, currentXCoordinate: 0,
currentCoordinates: [], currentCoordinates: {},
showFlag: false, showFlag: false,
showFlagContent: false, showFlagContent: false,
timeSeries: [], timeSeries: [],
realPixelRatio: 1, realPixelRatio: 1,
seriesUnderMouse: [],
}; };
}, },
computed: { computed: {
...@@ -126,6 +127,9 @@ export default { ...@@ -126,6 +127,9 @@ export default {
this.draw(); this.draw();
}, },
methods: { methods: {
showDot(path) {
return this.showFlagContent && this.seriesUnderMouse.includes(path);
},
draw() { draw() {
const breakpointSize = bp.getBreakpointSize(); const breakpointSize = bp.getBreakpointSize();
const query = this.graphData.queries[0]; const query = this.graphData.queries[0];
...@@ -155,7 +159,24 @@ export default { ...@@ -155,7 +159,24 @@ export default {
point.y = e.clientY; point.y = e.clientY;
point = point.matrixTransform(this.$refs.graphData.getScreenCTM().inverse()); point = point.matrixTransform(this.$refs.graphData.getScreenCTM().inverse());
point.x += 7; point.x += 7;
const firstTimeSeries = this.timeSeries[0];
this.seriesUnderMouse = this.timeSeries.filter((series) => {
const mouseX = series.timeSeriesScaleX.invert(point.x);
let minDistance = Infinity;
const closestTickMark = Object.keys(this.allXAxisValues).reduce((closest, x) => {
const distance = Math.abs(Number(new Date(x)) - Number(mouseX));
if (distance < minDistance) {
minDistance = distance;
return x;
}
return closest;
});
return series.values.find(v => v.time.toString() === closestTickMark);
});
const firstTimeSeries = this.seriesUnderMouse[0];
const timeValueOverlay = firstTimeSeries.timeSeriesScaleX.invert(point.x); const timeValueOverlay = firstTimeSeries.timeSeriesScaleX.invert(point.x);
const overlayIndex = bisectDate(firstTimeSeries.values, timeValueOverlay, 1); const overlayIndex = bisectDate(firstTimeSeries.values, timeValueOverlay, 1);
const d0 = firstTimeSeries.values[overlayIndex - 1]; const d0 = firstTimeSeries.values[overlayIndex - 1];
...@@ -190,6 +211,17 @@ export default { ...@@ -190,6 +211,17 @@ export default {
axisXScale.domain(d3.extent(allValues, d => d.time)); axisXScale.domain(d3.extent(allValues, d => d.time));
axisYScale.domain([0, d3.max(allValues.map(d => d.value))]); axisYScale.domain([0, d3.max(allValues.map(d => d.value))]);
this.allXAxisValues = this.timeSeries.reduce((obj, series) => {
const seriesKeys = {};
series.values.forEach(v => {
seriesKeys[v.time] = true;
});
return {
...obj,
...seriesKeys,
};
}, {});
const xAxis = d3 const xAxis = d3
.axisBottom() .axisBottom()
.scale(axisXScale) .scale(axisXScale)
...@@ -277,9 +309,8 @@ export default { ...@@ -277,9 +309,8 @@ export default {
:line-style="path.lineStyle" :line-style="path.lineStyle"
:line-color="path.lineColor" :line-color="path.lineColor"
:area-color="path.areaColor" :area-color="path.areaColor"
:current-coordinates="currentCoordinates[index]" :current-coordinates="currentCoordinates[path.metricTag]"
:current-time-series-index="index" :show-dot="showDot(path)"
:show-dot="showFlagContent"
/> />
<graph-deployment <graph-deployment
:deployment-data="reducedDeploymentData" :deployment-data="reducedDeploymentData"
...@@ -303,7 +334,7 @@ export default { ...@@ -303,7 +334,7 @@ export default {
:graph-height="graphHeight" :graph-height="graphHeight"
:graph-height-offset="graphHeightOffset" :graph-height-offset="graphHeightOffset"
:show-flag-content="showFlagContent" :show-flag-content="showFlagContent"
:time-series="timeSeries" :time-series="seriesUnderMouse"
:unit-of-display="unitOfDisplay" :unit-of-display="unitOfDisplay"
:legend-title="legendTitle" :legend-title="legendTitle"
:deployment-flag-data="deploymentFlagData" :deployment-flag-data="deploymentFlagData"
......
...@@ -52,7 +52,7 @@ export default { ...@@ -52,7 +52,7 @@ export default {
required: true, required: true,
}, },
currentCoordinates: { currentCoordinates: {
type: Array, type: Object,
required: true, required: true,
}, },
}, },
...@@ -91,8 +91,8 @@ export default { ...@@ -91,8 +91,8 @@ export default {
}, },
methods: { methods: {
seriesMetricValue(seriesIndex, series) { seriesMetricValue(seriesIndex, series) {
const indexFromCoordinates = this.currentCoordinates[seriesIndex] const indexFromCoordinates = this.currentCoordinates[series.metricTag]
? this.currentCoordinates[seriesIndex].currentDataIndex : 0; ? this.currentCoordinates[series.metricTag].currentDataIndex : 0;
const index = this.deploymentFlagData const index = this.deploymentFlagData
? this.deploymentFlagData.seriesIndex ? this.deploymentFlagData.seriesIndex
: indexFromCoordinates; : indexFromCoordinates;
......
...@@ -50,19 +50,24 @@ const mixins = { ...@@ -50,19 +50,24 @@ const mixins = {
}, },
positionFlag() { positionFlag() {
const timeSeries = this.timeSeries[0]; const timeSeries = this.seriesUnderMouse[0];
const hoveredDataIndex = bisectDate(timeSeries.values, this.hoverData.hoveredDate, 1); if (!timeSeries) {
return;
}
const hoveredDataIndex = bisectDate(timeSeries.values, this.hoverData.hoveredDate);
this.currentData = timeSeries.values[hoveredDataIndex]; this.currentData = timeSeries.values[hoveredDataIndex];
this.currentXCoordinate = Math.floor(timeSeries.timeSeriesScaleX(this.currentData.time)); this.currentXCoordinate = Math.floor(timeSeries.timeSeriesScaleX(this.currentData.time));
this.currentCoordinates = this.timeSeries.map((series) => { this.currentCoordinates = {};
const currentDataIndex = bisectDate(series.values, this.hoverData.hoveredDate, 1);
this.seriesUnderMouse.forEach((series) => {
const currentDataIndex = bisectDate(series.values, this.hoverData.hoveredDate);
const currentData = series.values[currentDataIndex]; const currentData = series.values[currentDataIndex];
const currentX = Math.floor(series.timeSeriesScaleX(currentData.time)); const currentX = Math.floor(series.timeSeriesScaleX(currentData.time));
const currentY = Math.floor(series.timeSeriesScaleY(currentData.value)); const currentY = Math.floor(series.timeSeriesScaleY(currentData.value));
return { this.currentCoordinates[series.metricTag] = {
currentX, currentX,
currentY, currentY,
currentDataIndex, currentDataIndex,
......
...@@ -2,7 +2,7 @@ import _ from 'underscore'; ...@@ -2,7 +2,7 @@ import _ from 'underscore';
import { scaleLinear, scaleTime } from 'd3-scale'; import { scaleLinear, scaleTime } from 'd3-scale';
import { line, area, curveLinear } from 'd3-shape'; import { line, area, curveLinear } from 'd3-shape';
import { extent, max, sum } from 'd3-array'; import { extent, max, sum } from 'd3-array';
import { timeMinute } from 'd3-time'; import { timeMinute, timeSecond } from 'd3-time';
import { capitalizeFirstCharacter } from '~/lib/utils/text_utility'; import { capitalizeFirstCharacter } from '~/lib/utils/text_utility';
const d3 = { const d3 = {
...@@ -14,6 +14,7 @@ const d3 = { ...@@ -14,6 +14,7 @@ const d3 = {
extent, extent,
max, max,
timeMinute, timeMinute,
timeSecond,
sum, sum,
}; };
...@@ -51,6 +52,24 @@ function queryTimeSeries(query, graphWidth, graphHeight, graphHeightOffset, xDom ...@@ -51,6 +52,24 @@ function queryTimeSeries(query, graphWidth, graphHeight, graphHeightOffset, xDom
return defaultColorPalette[pick]; return defaultColorPalette[pick];
} }
function findByDate(series, time) {
const val = series.find(v => Math.abs(d3.timeSecond.count(time, v.time)) < 60);
if (val) {
return val.value;
}
return NaN;
}
// The timeseries data may have gaps in it
// but we need a regularly-spaced set of time/value pairs
// this gives us a complete range of one minute intervals
// offset the same amount as the original data
const [minX, maxX] = xDom;
const offset = d3.timeMinute(minX) - Number(minX);
const datesWithoutGaps = d3.timeSecond.every(60)
.range(d3.timeMinute.offset(minX, -1), maxX)
.map(d => d - offset);
query.result.forEach((timeSeries, timeSeriesNumber) => { query.result.forEach((timeSeries, timeSeriesNumber) => {
let metricTag = ''; let metricTag = '';
let lineColor = ''; let lineColor = '';
...@@ -119,9 +138,14 @@ function queryTimeSeries(query, graphWidth, graphHeight, graphHeightOffset, xDom ...@@ -119,9 +138,14 @@ function queryTimeSeries(query, graphWidth, graphHeight, graphHeightOffset, xDom
}); });
} }
const values = datesWithoutGaps.map(time => ({
time,
value: findByDate(timeSeries.values, time),
}));
timeSeriesParsed.push({ timeSeriesParsed.push({
linePath: lineFunction(timeSeries.values), linePath: lineFunction(values),
areaPath: areaFunction(timeSeries.values), areaPath: areaFunction(values),
timeSeriesScaleX, timeSeriesScaleX,
timeSeriesScaleY, timeSeriesScaleY,
values: timeSeries.values, values: timeSeries.values,
......
---
title: Allow gaps in multiseries metrics charts
merge_request: 21427
author:
type: fixed
...@@ -35,7 +35,7 @@ const defaultValuesComponent = { ...@@ -35,7 +35,7 @@ const defaultValuesComponent = {
unitOfDisplay: 'ms', unitOfDisplay: 'ms',
currentDataIndex: 0, currentDataIndex: 0,
legendTitle: 'Average', legendTitle: 'Average',
currentCoordinates: [], currentCoordinates: {},
}; };
const deploymentFlagData = { const deploymentFlagData = {
......
...@@ -113,6 +113,9 @@ describe('Graph', () => { ...@@ -113,6 +113,9 @@ describe('Graph', () => {
projectPath, projectPath,
}); });
// simulate moving mouse over data series
component.seriesUnderMouse = component.timeSeries;
component.positionFlag(); component.positionFlag();
expect(component.currentData).toBe(component.timeSeries[0].values[10]); expect(component.currentData).toBe(component.timeSeries[0].values[10]);
}); });
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment