Merge branch 'ee-6381-multiseries' into 'master'
multiseries Closes #50947 See merge request gitlab-org/gitlab-ce!21427
This commit is contained in:
commit
53fae9ad84
7 changed files with 86 additions and 18 deletions
|
@ -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 {
|
||||||
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 {
|
||||||
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 {
|
||||||
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 {
|
||||||
: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 {
|
||||||
: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 {
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
currentCoordinates: {
|
currentCoordinates: {
|
||||||
type: Array,
|
type: Object,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -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 = {
|
||||||
},
|
},
|
||||||
|
|
||||||
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';
|
||||||
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 = {
|
||||||
extent,
|
extent,
|
||||||
max,
|
max,
|
||||||
timeMinute,
|
timeMinute,
|
||||||
|
timeSecond,
|
||||||
sum,
|
sum,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -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
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
|
5
changelogs/unreleased/ee-6381-multiseries.yml
Normal file
5
changelogs/unreleased/ee-6381-multiseries.yml
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
title: Allow gaps in multiseries metrics charts
|
||||||
|
merge_request: 21427
|
||||||
|
author:
|
||||||
|
type: fixed
|
|
@ -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', () => {
|
||||||
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]);
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in a new issue