<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); }, shouldRenderData() { return this.graphData.queries.filter(s => s.result.length > 0).length > 0; }, }, watch: { hoverData() { this.positionFlag(); }, }, mounted() { this.draw(); }, methods: { showDot(path) { return this.showFlagContent && this.seriesUnderMouse.includes(path); }, draw() { const breakpointSize = bp.getBreakpointSize(); const svgWidth = this.$refs.baseSvg.getBoundingClientRect().width; 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.yAxisLabel = this.graphData.y_label || 'Values'; this.graphWidth = svgWidth - 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 = svgWidth / this.baseGraphWidth; // set the legends on the axes const [query] = this.graphData.queries; this.legendTitle = query ? query.label : 'Average'; this.unitOfDisplay = query ? query.unit : ''; if (this.shouldRenderData) { 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 v-if="shouldRenderData" 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 v-else :viewBox="innerViewBox" class="js-no-data-to-display"> <text x="50%" y="50%" alignment-baseline="middle" text-anchor="middle"> {{ s__('Metrics|No data to display') }} </text> </svg> </svg> <graph-flag v-if="shouldRenderData" :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>