PageRenderTime 55ms CodeModel.GetById 25ms RepoModel.GetById 1ms app.codeStats 0ms

/app/assets/javascripts/monitoring/components/graph.vue

https://gitlab.com/bugagazavr/gitlab-ce
Vue | 340 lines | 321 code | 18 blank | 1 comment | 13 complexity | 856d7e6f9ba3449ddefb1a1bf647fc4b MD5 | raw file
Possible License(s): Apache-2.0, CC0-1.0
  1. <script>
  2. import { scaleLinear, scaleTime } from 'd3-scale';
  3. import { axisLeft, axisBottom } from 'd3-axis';
  4. import _ from 'underscore';
  5. import { max, extent } from 'd3-array';
  6. import { select } from 'd3-selection';
  7. import GraphAxis from './graph/axis.vue';
  8. import GraphLegend from './graph/legend.vue';
  9. import GraphFlag from './graph/flag.vue';
  10. import GraphDeployment from './graph/deployment.vue';
  11. import GraphPath from './graph/path.vue';
  12. import MonitoringMixin from '../mixins/monitoring_mixins';
  13. import eventHub from '../event_hub';
  14. import measurements from '../utils/measurements';
  15. import { bisectDate, timeScaleFormat } from '../utils/date_time_formatters';
  16. import createTimeSeries from '../utils/multiple_time_series';
  17. import bp from '../../breakpoints';
  18. const d3 = { scaleLinear, scaleTime, axisLeft, axisBottom, max, extent, select };
  19. export default {
  20. components: {
  21. GraphAxis,
  22. GraphFlag,
  23. GraphDeployment,
  24. GraphPath,
  25. GraphLegend,
  26. },
  27. mixins: [MonitoringMixin],
  28. props: {
  29. graphData: {
  30. type: Object,
  31. required: true,
  32. },
  33. deploymentData: {
  34. type: Array,
  35. required: true,
  36. },
  37. hoverData: {
  38. type: Object,
  39. required: false,
  40. default: () => ({}),
  41. },
  42. projectPath: {
  43. type: String,
  44. required: true,
  45. },
  46. tagsPath: {
  47. type: String,
  48. required: true,
  49. },
  50. showLegend: {
  51. type: Boolean,
  52. required: false,
  53. default: true,
  54. },
  55. smallGraph: {
  56. type: Boolean,
  57. required: false,
  58. default: false,
  59. },
  60. },
  61. data() {
  62. return {
  63. baseGraphHeight: 450,
  64. baseGraphWidth: 600,
  65. graphHeight: 450,
  66. graphWidth: 600,
  67. graphHeightOffset: 120,
  68. margin: {},
  69. unitOfDisplay: '',
  70. yAxisLabel: '',
  71. legendTitle: '',
  72. reducedDeploymentData: [],
  73. measurements: measurements.large,
  74. currentData: {
  75. time: new Date(),
  76. value: 0,
  77. },
  78. currentXCoordinate: 0,
  79. currentCoordinates: {},
  80. showFlag: false,
  81. showFlagContent: false,
  82. timeSeries: [],
  83. graphDrawData: {},
  84. realPixelRatio: 1,
  85. seriesUnderMouse: [],
  86. };
  87. },
  88. computed: {
  89. outerViewBox() {
  90. return `0 0 ${this.baseGraphWidth} ${this.baseGraphHeight}`;
  91. },
  92. innerViewBox() {
  93. return `0 0 ${this.baseGraphWidth - 150} ${this.baseGraphHeight}`;
  94. },
  95. axisTransform() {
  96. return `translate(70, ${this.graphHeight - 100})`;
  97. },
  98. paddingBottomRootSvg() {
  99. return {
  100. paddingBottom: `${Math.ceil(this.baseGraphHeight * 100) / this.baseGraphWidth || 0}%`,
  101. };
  102. },
  103. deploymentFlagData() {
  104. return this.reducedDeploymentData.find(deployment => deployment.showDeploymentFlag);
  105. },
  106. },
  107. watch: {
  108. hoverData() {
  109. this.positionFlag();
  110. },
  111. },
  112. mounted() {
  113. this.draw();
  114. },
  115. methods: {
  116. showDot(path) {
  117. return this.showFlagContent && this.seriesUnderMouse.includes(path);
  118. },
  119. draw() {
  120. const breakpointSize = bp.getBreakpointSize();
  121. const query = this.graphData.queries[0];
  122. const svgWidth = this.$refs.baseSvg.getBoundingClientRect().width;
  123. this.margin = measurements.large.margin;
  124. if (this.smallGraph || breakpointSize === 'xs' || breakpointSize === 'sm') {
  125. this.graphHeight = 300;
  126. this.margin = measurements.small.margin;
  127. this.measurements = measurements.small;
  128. }
  129. this.unitOfDisplay = query.unit || '';
  130. this.yAxisLabel = this.graphData.y_label || 'Values';
  131. this.legendTitle = query.label || 'Average';
  132. this.graphWidth = svgWidth - this.margin.left - this.margin.right;
  133. this.graphHeight = this.graphHeight - this.margin.top - this.margin.bottom;
  134. this.baseGraphHeight = this.graphHeight - 50;
  135. this.baseGraphWidth = this.graphWidth;
  136. // pixel offsets inside the svg and outside are not 1:1
  137. this.realPixelRatio = svgWidth / this.baseGraphWidth;
  138. this.renderAxesPaths();
  139. this.formatDeployments();
  140. },
  141. handleMouseOverGraph(e) {
  142. let point = this.$refs.graphData.createSVGPoint();
  143. point.x = e.clientX;
  144. point.y = e.clientY;
  145. point = point.matrixTransform(this.$refs.graphData.getScreenCTM().inverse());
  146. point.x += 7;
  147. this.seriesUnderMouse = this.timeSeries.filter(series => {
  148. const mouseX = series.timeSeriesScaleX.invert(point.x);
  149. let minDistance = Infinity;
  150. const closestTickMark = Object.keys(this.allXAxisValues).reduce((closest, x) => {
  151. const distance = Math.abs(Number(new Date(x)) - Number(mouseX));
  152. if (distance < minDistance) {
  153. minDistance = distance;
  154. return x;
  155. }
  156. return closest;
  157. });
  158. return series.values.find(v => v.time.toString() === closestTickMark);
  159. });
  160. const firstTimeSeries = this.seriesUnderMouse[0];
  161. const timeValueOverlay = firstTimeSeries.timeSeriesScaleX.invert(point.x);
  162. const overlayIndex = bisectDate(firstTimeSeries.values, timeValueOverlay, 1);
  163. const d0 = firstTimeSeries.values[overlayIndex - 1];
  164. const d1 = firstTimeSeries.values[overlayIndex];
  165. if (d0 === undefined || d1 === undefined) return;
  166. const evalTime = timeValueOverlay - d0[0] > d1[0] - timeValueOverlay;
  167. const hoveredDataIndex = evalTime ? overlayIndex : overlayIndex - 1;
  168. const hoveredDate = firstTimeSeries.values[hoveredDataIndex].time;
  169. const currentDeployXPos = this.mouseOverDeployInfo(point.x);
  170. eventHub.$emit('hoverChanged', {
  171. hoveredDate,
  172. currentDeployXPos,
  173. });
  174. },
  175. renderAxesPaths() {
  176. ({ timeSeries: this.timeSeries, graphDrawData: this.graphDrawData } = createTimeSeries(
  177. this.graphData.queries,
  178. this.graphWidth,
  179. this.graphHeight,
  180. this.graphHeightOffset,
  181. ));
  182. if (_.findWhere(this.timeSeries, { renderCanary: true })) {
  183. this.timeSeries = this.timeSeries.map(series => ({ ...series, renderCanary: true }));
  184. }
  185. const axisXScale = d3.scaleTime().range([0, this.graphWidth - 70]);
  186. const axisYScale = d3.scaleLinear().range([this.graphHeight - this.graphHeightOffset, 0]);
  187. const allValues = this.timeSeries.reduce((all, { values }) => all.concat(values), []);
  188. axisXScale.domain(d3.extent(allValues, d => d.time));
  189. axisYScale.domain([0, d3.max(allValues.map(d => d.value))]);
  190. this.allXAxisValues = this.timeSeries.reduce((obj, series) => {
  191. const seriesKeys = {};
  192. series.values.forEach(v => {
  193. seriesKeys[v.time] = true;
  194. });
  195. return {
  196. ...obj,
  197. ...seriesKeys,
  198. };
  199. }, {});
  200. const xAxis = d3
  201. .axisBottom()
  202. .scale(axisXScale)
  203. .ticks(this.graphWidth / 120)
  204. .tickFormat(timeScaleFormat);
  205. const yAxis = d3
  206. .axisLeft()
  207. .scale(axisYScale)
  208. .ticks(measurements.yTicks);
  209. d3.select(this.$refs.baseSvg)
  210. .select('.x-axis')
  211. .call(xAxis);
  212. const width = this.graphWidth;
  213. d3.select(this.$refs.baseSvg)
  214. .select('.y-axis')
  215. .call(yAxis)
  216. .selectAll('.tick')
  217. .each(function createTickLines(d, i) {
  218. if (i > 0) {
  219. d3.select(this)
  220. .select('line')
  221. .attr('x2', width)
  222. .attr('class', 'axis-tick');
  223. } // Avoid adding the class to the first tick, to prevent coloring
  224. }); // This will select all of the ticks once they're rendered
  225. },
  226. },
  227. };
  228. </script>
  229. <template>
  230. <div
  231. class="prometheus-graph"
  232. @mouseover="showFlagContent = true"
  233. @mouseleave="showFlagContent = false"
  234. >
  235. <div class="prometheus-graph-header">
  236. <h5 class="prometheus-graph-title">
  237. {{ graphData.title }}
  238. </h5>
  239. <div class="prometheus-graph-widgets">
  240. <slot></slot>
  241. </div>
  242. </div>
  243. <div
  244. :style="paddingBottomRootSvg"
  245. class="prometheus-svg-container"
  246. >
  247. <svg
  248. ref="baseSvg"
  249. :viewBox="outerViewBox"
  250. >
  251. <g
  252. :transform="axisTransform"
  253. class="x-axis"
  254. />
  255. <g
  256. class="y-axis"
  257. transform="translate(70, 20)"
  258. />
  259. <graph-axis
  260. :graph-width="graphWidth"
  261. :graph-height="graphHeight"
  262. :margin="margin"
  263. :measurements="measurements"
  264. :y-axis-label="yAxisLabel"
  265. :unit-of-display="unitOfDisplay"
  266. />
  267. <svg
  268. ref="graphData"
  269. :viewBox="innerViewBox"
  270. class="graph-data"
  271. >
  272. <slot
  273. name="additionalSvgContent"
  274. :graphDrawData="graphDrawData"
  275. />
  276. <graph-path
  277. v-for="(path, index) in timeSeries"
  278. :key="index"
  279. :generated-line-path="path.linePath"
  280. :generated-area-path="path.areaPath"
  281. :line-style="path.lineStyle"
  282. :line-color="path.lineColor"
  283. :area-color="path.areaColor"
  284. :current-coordinates="currentCoordinates[path.metricTag]"
  285. :show-dot="showDot(path)"
  286. />
  287. <graph-deployment
  288. :deployment-data="reducedDeploymentData"
  289. :graph-height="graphHeight"
  290. :graph-height-offset="graphHeightOffset"
  291. />
  292. <rect
  293. ref="graphOverlay"
  294. :width="(graphWidth - 70)"
  295. :height="(graphHeight - 100)"
  296. class="prometheus-graph-overlay"
  297. transform="translate(-5, 20)"
  298. @mousemove="handleMouseOverGraph($event)"
  299. />
  300. </svg>
  301. </svg>
  302. <graph-flag
  303. :real-pixel-ratio="realPixelRatio"
  304. :current-x-coordinate="currentXCoordinate"
  305. :current-data="currentData"
  306. :graph-height="graphHeight"
  307. :graph-height-offset="graphHeightOffset"
  308. :show-flag-content="showFlagContent"
  309. :time-series="seriesUnderMouse"
  310. :unit-of-display="unitOfDisplay"
  311. :legend-title="legendTitle"
  312. :deployment-flag-data="deploymentFlagData"
  313. :current-coordinates="currentCoordinates"
  314. />
  315. </div>
  316. <graph-legend
  317. v-if="showLegend"
  318. :legend-title="legendTitle"
  319. :time-series="timeSeries"
  320. />
  321. </div>
  322. </template>