<script> import { scaleLinear, scaleTime } from 'd3-scale'; import { axisLeft, axisBottom } from 'd3-axis'; import { max, extent } from 'd3-array'; import { select } from 'd3-selection'; 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: { GraphLegend, GraphFlag, GraphDeployment, GraphPath, }, mixins: [MonitoringMixin], props: { graphData: { type: Object, required: true, }, updateAspectRatio: { type: Boolean, required: true, }, deploymentData: { type: Array, required: true, }, hoverData: { type: Object, required: false, default: () => ({}), }, projectPath: { type: String, required: true, }, tagsPath: { type: String, required: true, }, }, 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, }, currentDataIndex: 0, currentXCoordinate: 0, currentFlagPosition: 0, showFlag: false, showFlagContent: false, timeSeries: [], realPixelRatio: 1, }; }, 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: { updateAspectRatio() { if (this.updateAspectRatio) { this.graphHeight = 450; this.graphWidth = 600; this.measurements = measurements.large; this.draw(); eventHub.$emit('toggleAspectRatio'); } }, hoverData() { this.positionFlag(); }, }, mounted() { this.draw(); }, methods: { draw() { const breakpointSize = bp.getBreakpointSize(); const query = this.graphData.queries[0]; this.margin = measurements.large.margin; if (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; 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 = point.x += 7; const firstTimeSeries = this.timeSeries[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() { this.timeSeries = createTimeSeries( this.graphData.queries, this.graphWidth, this.graphHeight, this.graphHeightOffset, ); if (this.timeSeries.length > 3) { this.baseGraphHeight = this.baseGraphHeight += (this.timeSeries.length - 3) * 20; } 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))]); const xAxis = d3.axisBottom() .scale(axisXScale) .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" > <h5 class="text-center graph-title"> {{ graphData.title }} </h5> <div class="prometheus-svg-container" :style="paddingBottomRootSvg" > <svg :viewBox="outerViewBox" ref="baseSvg" > <g class="x-axis" :transform="axisTransform" /> <g class="y-axis" transform="translate(70, 20)" /> <graph-legend :graph-width="graphWidth" :graph-height="graphHeight" :margin="margin" :measurements="measurements" :legend-title="legendTitle" :y-axis-label="yAxisLabel" :time-series="timeSeries" :unit-of-display="unitOfDisplay" :current-data-index="currentDataIndex" /> <svg class="graph-data" :viewBox="innerViewBox" ref="graphData" > <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" /> <graph-deployment :deployment-data="reducedDeploymentData" :graph-height="graphHeight" :graph-height-offset="graphHeightOffset" /> <rect class="prometheus-graph-overlay" :width="(graphWidth - 70)" :height="(graphHeight - 100)" transform="translate(-5, 20)" ref="graphOverlay" @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="timeSeries" :unit-of-display="unitOfDisplay" :current-data-index="currentDataIndex" :legend-title="legendTitle" :deployment-flag-data="deploymentFlagData" /> </div> </div> </template>