UNPKG

7.7 kBJavaScriptView Raw
1'use strict'
2
3const d3 = require('./d3.js')
4const icons = require('./icons.js')
5const EventEmitter = require('events')
6const HoverBox = require('./hover-box')
7
8const margin = { top: 20, right: 20, bottom: 30, left: 50 }
9const headerHeight = 18
10
11function has (object, property) {
12 return Object.prototype.hasOwnProperty.call(object, property)
13}
14
15// https://bl.ocks.org/d3noob/402dd382a51a4f6eea487f9a35566de0
16class SubGraph extends EventEmitter {
17 constructor (container, setup) {
18 super()
19
20 this.setup = setup
21
22 // setup graph container
23 this.container = container.append('div')
24 .attr('id', `graph-${setup.className}`)
25 .classed('sub-graph', true)
26 .classed(setup.className, true)
27
28 // add headline
29 this.header = this.container.append('div')
30 .classed('header', true)
31
32 this.title = this.header.append('div')
33 .classed('title', true)
34
35 this.title.append('span')
36 .classed('name', true)
37 .text(this.setup.name)
38
39 this.title.append('span')
40 .classed('unit', true)
41 .text(this.setup.unit)
42
43 this.alert = this.title.append('svg')
44 .classed('alert', true)
45 .on('click', () => this.emit('alert-click'))
46 .call(icons.insertIcon('warning'))
47
48 // add legned
49 this.legendItems = []
50 if (setup.showLegend) {
51 const legend = this.header.append('div')
52 .classed('legend', true)
53
54 for (let i = 0; i < this.setup.numLines; i++) {
55 const legendItem = legend.append('div')
56 .classed('legend-item', true)
57
58 legendItem.append('svg')
59 .attr('width', 30)
60 .attr('height', 18)
61 .append('line')
62 .attr('stroke-dasharray', this.setup.lineStyle[i])
63 .attr('x1', 0)
64 .attr('x2', 30)
65 .attr('y1', 9)
66 .attr('y2', 9)
67
68 legendItem.append('span')
69 .classed('long-legend', true)
70 .attr('title', this.setup.longLegend[i])
71 .text(this.setup.longLegend[i])
72 legendItem.append('span')
73 .classed('short-legend', true)
74 .text(this.setup.shortLegend[i])
75
76 this.legendItems.push(legendItem)
77 }
78 }
79
80 // add hover box
81 this.hover = new HoverBox(this.container, this.setup)
82
83 // setup graph area
84 this.svg = this.container.append('svg')
85 .classed('chart', true)
86 this.graph = this.svg.append('g')
87 .attr('transform',
88 'translate(' + margin.left + ',' + margin.top + ')')
89
90 // setup hover events
91 this.hoverArea = this.container.append('div')
92 .classed('hover-area', true)
93 .style('left', margin.left + 'px')
94 .style('top', (margin.top + headerHeight) + 'px')
95 .on('mousemove', () => {
96 const positionX = d3.mouse(this.graph.node())[0]
97 if (positionX >= 0) {
98 const unitX = this.xScale.invert(positionX)
99 this.emit('hover-update', unitX)
100 }
101 })
102 .on('mouseleave', () => this.emit('hover-hide'))
103 .on('mouseenter', () => this.emit('hover-show'))
104
105 // add background node
106 this.background = this.graph.append('rect')
107 .classed('background', true)
108 .attr('x', 0)
109 .attr('y', 0)
110
111 this.interval = this.graph.append('rect')
112 .classed('interval', true)
113 .attr('x', 0)
114 .attr('y', 0)
115
116 // define scales
117 this.xScale = d3.scaleTime()
118 this.yScale = d3.scaleLinear()
119
120 // define axis
121 this.xAxis = d3.axisBottom(this.xScale).ticks(10)
122 this.xAxisElement = this.graph.append('g')
123
124 this.yAxis = d3.axisLeft(this.yScale).ticks(4)
125 this.yAxisElement = this.graph.append('g')
126
127 // Define drawer functions and line elements
128 this.lineDrawers = []
129 this.lineElements = []
130 for (let i = 0; i < this.setup.numLines; i++) {
131 const lineDrawer = d3.line()
132 .x((d) => this.xScale(d.x))
133 .y((d) => this.yScale(d.y[i]))
134 .curve(d3[this.setup.interpolation || 'curveLinear'])
135
136 this.lineDrawers.push(lineDrawer)
137
138 const lineElement = this.graph.append('path')
139 .attr('class', 'line')
140 .attr('stroke-dasharray', this.setup.lineStyle[i])
141
142 this.lineElements.push(lineElement)
143 }
144 }
145
146 getGraphSize () {
147 const outerSize = this.svg.node().getBoundingClientRect()
148 return {
149 width: outerSize.width - margin.left - margin.right,
150 height: outerSize.height - margin.top - margin.bottom
151 }
152 }
153
154 setData (data, interval, issues) {
155 // Update domain of scales
156 this.xScale.domain(d3.extent(data, function (d) { return d.x }))
157
158 // For the y-axis, ymin and ymax is supported, however they will
159 // never truncate the data.
160 let ymin = d3.min(data, function (d) { return Math.min(...d.y) })
161 if (has(this.setup, 'ymin')) {
162 ymin = Math.min(ymin, this.setup.ymin)
163 }
164 let ymax = d3.max(data, function (d) { return Math.max(...d.y) })
165 if (has(this.setup, 'ymax')) {
166 ymax = Math.max(ymax, this.setup.ymax)
167 }
168 this.yScale.domain([ymin, ymax])
169
170 // Save interval
171 this.interval.data([interval])
172
173 // Attach data
174 let foundIssue = false
175 for (let i = 0; i < this.setup.numLines; i++) {
176 this.lineElements[i].data([data])
177
178 // Modify css classes for lines, title icon
179 this.lineElements[i]
180 .classed('performance-issue', issues[i] === 'performance')
181 .classed('data-issue', issues[i] === 'data')
182 if (this.setup.showLegend) {
183 this.legendItems[i]
184 .classed('performance-issue', issues[i] === 'performance')
185 .classed('data-issue', issues[i] === 'data')
186 }
187
188 if (issues[i] !== 'none') foundIssue = true
189 }
190 this.alert.classed('visible', foundIssue)
191 }
192
193 draw () {
194 const { width, height } = this.getGraphSize()
195
196 // set hover area size
197 this.hoverArea
198 .style('width', width + 'px')
199 .style('height', height + 'px')
200
201 // set background size
202 this.background
203 .attr('width', width)
204 .attr('height', height)
205
206 // set the ranges
207 this.xScale.range([0, width])
208 this.yScale.range([height, 0])
209
210 // set interval size
211 this.interval
212 .attr('x', (d) => this.xScale(d[0]))
213 .attr('width', (d) => this.xScale(d[1]) - this.xScale(d[0]))
214 .attr('height', height)
215
216 // update axis
217 this.xAxisElement
218 .attr('transform', 'translate(0,' + height + ')')
219 .call(this.xAxis)
220 this.yAxisElement
221 .call(this.yAxis)
222
223 // update lines
224 for (let i = 0; i < this.setup.numLines; i++) {
225 this.lineElements[i].attr('d', this.lineDrawers[i])
226 }
227
228 // since the xScale was changed, update the hover box
229 if (this.hover.showen) {
230 this.hoverUpdate(this.hover.point)
231 }
232 }
233
234 hoverShow () {
235 this.hover.show()
236 }
237
238 hoverHide () {
239 this.hover.hide()
240 }
241
242 hoverUpdate (point) {
243 if (!this.hover.showen) return
244
245 // get position of curve there is at the top
246 const xInGraphPositon = this.xScale(point.x)
247 let yMetric = Math.max(...point.y)
248 if (this.setup.className === 'memory') {
249 // by default, hover box picks the highest value
250 // in case of memory subgraph, we always want to point at heap usage
251 yMetric = point.y[2]
252 }
253 const yInGraphPositon = this.yScale(yMetric)
254
255 // calculate graph position relative to `this.container`.
256 // The `this.container` has `position:relative`, which is why that is
257 // the origin.
258 const xPosition = xInGraphPositon + margin.left
259 const yPosition = yInGraphPositon + margin.top + headerHeight
260
261 this.hover.setPoint(point)
262 this.hover.setPosition(xPosition, yPosition)
263 this.hover.setDate(point.x)
264 this.hover.setData(point.y.map((v) => this.yScale.tickFormat()(v)))
265 }
266}
267
268module.exports = SubGraph