342 lines
10 KiB
Vue
342 lines
10 KiB
Vue
<script>
|
|
import { scaleLinear, scaleTime } from 'd3-scale';
|
|
import { axisLeft, axisBottom } from 'd3-axis';
|
|
import _ from 'underscore';
|
|
import { max, extent } from 'd3-array';
|
|
import { select } from 'd3-selection';
|
|
import GraphAxis from './graph/axis.vue';
|
|
import GraphLegend from './graph/legend.vue';
|
|
import GraphFlag from './graph/flag.vue';
|
|
import GraphDeployment from './graph/deployment.vue';
|
|
import GraphPath from './graph/path.vue';
|
|
import MonitoringMixin from '../mixins/monitoring_mixins';
|
|
import eventHub from '../event_hub';
|
|
import measurements from '../utils/measurements';
|
|
import { bisectDate, timeScaleFormat } from '../utils/date_time_formatters';
|
|
import createTimeSeries from '../utils/multiple_time_series';
|
|
import bp from '../../breakpoints';
|
|
|
|
const d3 = { scaleLinear, scaleTime, axisLeft, axisBottom, max, extent, select };
|
|
|
|
export default {
|
|
components: {
|
|
GraphAxis,
|
|
GraphFlag,
|
|
GraphDeployment,
|
|
GraphPath,
|
|
GraphLegend,
|
|
},
|
|
mixins: [MonitoringMixin],
|
|
props: {
|
|
graphData: {
|
|
type: Object,
|
|
required: true,
|
|
},
|
|
deploymentData: {
|
|
type: Array,
|
|
required: true,
|
|
},
|
|
hoverData: {
|
|
type: Object,
|
|
required: false,
|
|
default: () => ({}),
|
|
},
|
|
projectPath: {
|
|
type: String,
|
|
required: true,
|
|
},
|
|
tagsPath: {
|
|
type: String,
|
|
required: true,
|
|
},
|
|
showLegend: {
|
|
type: Boolean,
|
|
required: false,
|
|
default: true,
|
|
},
|
|
smallGraph: {
|
|
type: Boolean,
|
|
required: false,
|
|
default: false,
|
|
},
|
|
},
|
|
data() {
|
|
return {
|
|
baseGraphHeight: 450,
|
|
baseGraphWidth: 600,
|
|
graphHeight: 450,
|
|
graphWidth: 600,
|
|
graphHeightOffset: 120,
|
|
margin: {},
|
|
unitOfDisplay: '',
|
|
yAxisLabel: '',
|
|
legendTitle: '',
|
|
reducedDeploymentData: [],
|
|
measurements: measurements.large,
|
|
currentData: {
|
|
time: new Date(),
|
|
value: 0,
|
|
},
|
|
currentXCoordinate: 0,
|
|
currentCoordinates: {},
|
|
showFlag: false,
|
|
showFlagContent: false,
|
|
timeSeries: [],
|
|
graphDrawData: {},
|
|
realPixelRatio: 1,
|
|
seriesUnderMouse: [],
|
|
};
|
|
},
|
|
computed: {
|
|
outerViewBox() {
|
|
return `0 0 ${this.baseGraphWidth} ${this.baseGraphHeight}`;
|
|
},
|
|
innerViewBox() {
|
|
return `0 0 ${this.baseGraphWidth - 150} ${this.baseGraphHeight}`;
|
|
},
|
|
axisTransform() {
|
|
return `translate(70, ${this.graphHeight - 100})`;
|
|
},
|
|
paddingBottomRootSvg() {
|
|
return {
|
|
paddingBottom: `${Math.ceil(this.baseGraphHeight * 100) / this.baseGraphWidth || 0}%`,
|
|
};
|
|
},
|
|
deploymentFlagData() {
|
|
return this.reducedDeploymentData.find(deployment => deployment.showDeploymentFlag);
|
|
},
|
|
},
|
|
watch: {
|
|
hoverData() {
|
|
this.positionFlag();
|
|
},
|
|
},
|
|
mounted() {
|
|
this.draw();
|
|
},
|
|
methods: {
|
|
showDot(path) {
|
|
return this.showFlagContent && this.seriesUnderMouse.includes(path);
|
|
},
|
|
draw() {
|
|
const breakpointSize = bp.getBreakpointSize();
|
|
const query = this.graphData.queries[0];
|
|
this.margin = measurements.large.margin;
|
|
if (this.smallGraph || breakpointSize === 'xs' || breakpointSize === 'sm') {
|
|
this.graphHeight = 300;
|
|
this.margin = measurements.small.margin;
|
|
this.measurements = measurements.small;
|
|
}
|
|
this.unitOfDisplay = query.unit || '';
|
|
this.yAxisLabel = this.graphData.y_label || 'Values';
|
|
this.legendTitle = query.label || 'Average';
|
|
this.graphWidth = this.$refs.baseSvg.clientWidth - this.margin.left - this.margin.right;
|
|
this.graphHeight = this.graphHeight - this.margin.top - this.margin.bottom;
|
|
this.baseGraphHeight = this.graphHeight - 50;
|
|
this.baseGraphWidth = this.graphWidth;
|
|
|
|
// pixel offsets inside the svg and outside are not 1:1
|
|
this.realPixelRatio = this.$refs.baseSvg.clientWidth / this.baseGraphWidth;
|
|
|
|
this.renderAxesPaths();
|
|
this.formatDeployments();
|
|
},
|
|
handleMouseOverGraph(e) {
|
|
let point = this.$refs.graphData.createSVGPoint();
|
|
point.x = e.clientX;
|
|
point.y = e.clientY;
|
|
point = point.matrixTransform(this.$refs.graphData.getScreenCTM().inverse());
|
|
point.x += 7;
|
|
|
|
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 overlayIndex = bisectDate(firstTimeSeries.values, timeValueOverlay, 1);
|
|
const d0 = firstTimeSeries.values[overlayIndex - 1];
|
|
const d1 = firstTimeSeries.values[overlayIndex];
|
|
if (d0 === undefined || d1 === undefined) return;
|
|
const evalTime = timeValueOverlay - d0[0] > d1[0] - timeValueOverlay;
|
|
const hoveredDataIndex = evalTime ? overlayIndex : overlayIndex - 1;
|
|
const hoveredDate = firstTimeSeries.values[hoveredDataIndex].time;
|
|
const currentDeployXPos = this.mouseOverDeployInfo(point.x);
|
|
|
|
eventHub.$emit('hoverChanged', {
|
|
hoveredDate,
|
|
currentDeployXPos,
|
|
});
|
|
},
|
|
renderAxesPaths() {
|
|
({ timeSeries: this.timeSeries, graphDrawData: this.graphDrawData } = createTimeSeries(
|
|
this.graphData.queries,
|
|
this.graphWidth,
|
|
this.graphHeight,
|
|
this.graphHeightOffset,
|
|
));
|
|
|
|
if (_.findWhere(this.timeSeries, { renderCanary: true })) {
|
|
this.timeSeries = this.timeSeries.map(series => ({ ...series, renderCanary: true }));
|
|
}
|
|
|
|
const axisXScale = d3.scaleTime().range([0, this.graphWidth - 70]);
|
|
const axisYScale = d3.scaleLinear().range([this.graphHeight - this.graphHeightOffset, 0]);
|
|
|
|
const allValues = this.timeSeries.reduce((all, { values }) => all.concat(values), []);
|
|
axisXScale.domain(d3.extent(allValues, d => d.time));
|
|
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
|
|
.axisBottom()
|
|
.scale(axisXScale)
|
|
.ticks(this.graphWidth / 120)
|
|
.tickFormat(timeScaleFormat);
|
|
|
|
const yAxis = d3
|
|
.axisLeft()
|
|
.scale(axisYScale)
|
|
.ticks(measurements.yTicks);
|
|
|
|
d3
|
|
.select(this.$refs.baseSvg)
|
|
.select('.x-axis')
|
|
.call(xAxis);
|
|
|
|
const width = this.graphWidth;
|
|
d3
|
|
.select(this.$refs.baseSvg)
|
|
.select('.y-axis')
|
|
.call(yAxis)
|
|
.selectAll('.tick')
|
|
.each(function createTickLines(d, i) {
|
|
if (i > 0) {
|
|
d3
|
|
.select(this)
|
|
.select('line')
|
|
.attr('x2', width)
|
|
.attr('class', 'axis-tick');
|
|
} // Avoid adding the class to the first tick, to prevent coloring
|
|
}); // This will select all of the ticks once they're rendered
|
|
},
|
|
},
|
|
};
|
|
</script>
|
|
|
|
<template>
|
|
<div
|
|
class="prometheus-graph"
|
|
@mouseover="showFlagContent = true"
|
|
@mouseleave="showFlagContent = false"
|
|
>
|
|
<div class="prometheus-graph-header">
|
|
<h5 class="prometheus-graph-title">
|
|
{{ graphData.title }}
|
|
</h5>
|
|
<div class="prometheus-graph-widgets">
|
|
<slot></slot>
|
|
</div>
|
|
</div>
|
|
<div
|
|
:style="paddingBottomRootSvg"
|
|
class="prometheus-svg-container"
|
|
>
|
|
<svg
|
|
ref="baseSvg"
|
|
:viewBox="outerViewBox"
|
|
>
|
|
<g
|
|
:transform="axisTransform"
|
|
class="x-axis"
|
|
/>
|
|
<g
|
|
class="y-axis"
|
|
transform="translate(70, 20)"
|
|
/>
|
|
<graph-axis
|
|
:graph-width="graphWidth"
|
|
:graph-height="graphHeight"
|
|
:margin="margin"
|
|
:measurements="measurements"
|
|
:y-axis-label="yAxisLabel"
|
|
:unit-of-display="unitOfDisplay"
|
|
/>
|
|
<svg
|
|
ref="graphData"
|
|
:viewBox="innerViewBox"
|
|
class="graph-data"
|
|
>
|
|
<slot
|
|
name="additionalSvgContent"
|
|
:graphDrawData="graphDrawData"
|
|
/>
|
|
<graph-path
|
|
v-for="(path, index) in timeSeries"
|
|
:key="index"
|
|
:generated-line-path="path.linePath"
|
|
:generated-area-path="path.areaPath"
|
|
:line-style="path.lineStyle"
|
|
:line-color="path.lineColor"
|
|
:area-color="path.areaColor"
|
|
:current-coordinates="currentCoordinates[path.metricTag]"
|
|
:show-dot="showDot(path)"
|
|
/>
|
|
<graph-deployment
|
|
:deployment-data="reducedDeploymentData"
|
|
:graph-height="graphHeight"
|
|
:graph-height-offset="graphHeightOffset"
|
|
/>
|
|
<rect
|
|
ref="graphOverlay"
|
|
:width="(graphWidth - 70)"
|
|
:height="(graphHeight - 100)"
|
|
class="prometheus-graph-overlay"
|
|
transform="translate(-5, 20)"
|
|
@mousemove="handleMouseOverGraph($event)"
|
|
/>
|
|
</svg>
|
|
</svg>
|
|
<graph-flag
|
|
:real-pixel-ratio="realPixelRatio"
|
|
:current-x-coordinate="currentXCoordinate"
|
|
:current-data="currentData"
|
|
:graph-height="graphHeight"
|
|
:graph-height-offset="graphHeightOffset"
|
|
:show-flag-content="showFlagContent"
|
|
:time-series="seriesUnderMouse"
|
|
:unit-of-display="unitOfDisplay"
|
|
:legend-title="legendTitle"
|
|
:deployment-flag-data="deploymentFlagData"
|
|
:current-coordinates="currentCoordinates"
|
|
/>
|
|
</div>
|
|
<graph-legend
|
|
v-if="showLegend"
|
|
:legend-title="legendTitle"
|
|
:time-series="timeSeries"
|
|
/>
|
|
</div>
|
|
</template>
|