UNPKG

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