@nearform/doctor
Version:
Programmable interface to clinic doctor
260 lines (210 loc) • 7.37 kB
JavaScript
'use strict'
const d3 = require('./d3.js')
const icons = require('./icons.js')
const EventEmitter = require('events')
const HoverBox = require('./hover-box')
const margin = { top: 20, right: 20, bottom: 30, left: 50 }
const headerHeight = 18
// https://bl.ocks.org/d3noob/402dd382a51a4f6eea487f9a35566de0
class SubGraph extends EventEmitter {
constructor (container, setup) {
super()
this.setup = setup
// setup graph container
this.container = container.append('div')
.attr('id', `graph-${setup.className}`)
.classed('sub-graph', true)
.classed(setup.className, true)
// add headline
this.header = this.container.append('div')
.classed('header', true)
this.title = this.header.append('div')
.classed('title', true)
this.title.append('span')
.classed('name', true)
.text(this.setup.name)
this.title.append('span')
.classed('unit', true)
.text(this.setup.unit)
this.alert = this.title.append('svg')
.classed('alert', true)
.on('click', () => this.emit('alert-click'))
.call(icons.insertIcon('warning'))
// add legned
this.legendItems = []
if (setup.showLegend) {
const legend = this.header.append('div')
.classed('legend', true)
for (let i = 0; i < this.setup.numLines; i++) {
const legendItem = legend.append('div')
.classed('legend-item', true)
legendItem.append('svg')
.attr('width', 30)
.attr('height', 18)
.append('line')
.attr('stroke-dasharray', this.setup.lineStyle[i])
.attr('x1', 0)
.attr('x2', 30)
.attr('y1', 9)
.attr('y2', 9)
legendItem.append('span')
.classed('long-legend', true)
.text(this.setup.longLegend[i])
legendItem.append('span')
.classed('short-legend', true)
.text(this.setup.shortLegend[i])
this.legendItems.push(legendItem)
}
}
// add hover box
this.hover = new HoverBox(this.container, this.setup)
// setup graph area
this.svg = this.container.append('svg')
.classed('chart', true)
this.graph = this.svg.append('g')
.attr('transform',
'translate(' + margin.left + ',' + margin.top + ')')
// setup hover events
this.hoverArea = this.container.append('div')
.classed('hover-area', true)
.style('left', margin.left + 'px')
.style('top', (margin.top + headerHeight) + 'px')
.on('mousemove', () => {
const positionX = d3.mouse(this.graph.node())[0]
if (positionX >= 0) {
const unitX = this.xScale.invert(positionX)
this.emit('hover-update', unitX)
}
})
.on('mouseleave', () => this.emit('hover-hide'))
.on('mouseenter', () => this.emit('hover-show'))
// add background node
this.background = this.graph.append('rect')
.classed('background', true)
.attr('x', 0)
.attr('y', 0)
this.interval = this.graph.append('rect')
.classed('interval', true)
.attr('x', 0)
.attr('y', 0)
// define scales
this.xScale = d3.scaleTime()
this.yScale = d3.scaleLinear()
// define axis
this.xAxis = d3.axisBottom(this.xScale).ticks(10)
this.xAxisElement = this.graph.append('g')
this.yAxis = d3.axisLeft(this.yScale).ticks(4)
this.yAxisElement = this.graph.append('g')
// Define drawer functions and line elements
this.lineDrawers = []
this.lineElements = []
for (let i = 0; i < this.setup.numLines; i++) {
const lineDrawer = d3.line()
.x((d) => this.xScale(d.x))
.y((d) => this.yScale(d.y[i]))
.curve(d3[this.setup.interpolation || 'curveLinear'])
this.lineDrawers.push(lineDrawer)
const lineElement = this.graph.append('path')
.attr('class', 'line')
.attr('stroke-dasharray', this.setup.lineStyle[i])
this.lineElements.push(lineElement)
}
}
getGraphSize () {
const outerSize = this.svg.node().getBoundingClientRect()
return {
width: outerSize.width - margin.left - margin.right,
height: outerSize.height - margin.top - margin.bottom
}
}
setData (data, interval, issues) {
// Update domain of scales
this.xScale.domain(d3.extent(data, function (d) { return d.x }))
// For the y-axis, ymin and ymax is supported, however they will
// never truncate the data.
let ymin = d3.min(data, function (d) { return Math.min(...d.y) })
if (this.setup.hasOwnProperty('ymin')) {
ymin = Math.min(ymin, this.setup.ymin)
}
let ymax = d3.max(data, function (d) { return Math.max(...d.y) })
if (this.setup.hasOwnProperty('ymax')) {
ymax = Math.max(ymax, this.setup.ymax)
}
this.yScale.domain([ymin, ymax])
// Save interval
this.interval.data([interval])
// Attach data
let foundIssue = false
for (let i = 0; i < this.setup.numLines; i++) {
this.lineElements[i].data([data])
// Modify css classes for lines, title icon
this.lineElements[i].classed('bad', issues[i])
if (this.setup.showLegend) {
this.legendItems[i].classed('bad', issues[i])
}
if (issues[i]) foundIssue = true
}
this.alert.classed('visible', foundIssue)
}
draw () {
const { width, height } = this.getGraphSize()
// set hover area size
this.hoverArea
.style('width', width + 'px')
.style('height', height + 'px')
// set background size
this.background
.attr('width', width)
.attr('height', height)
// set the ranges
this.xScale.range([0, width])
this.yScale.range([height, 0])
// set interval size
this.interval
.attr('x', (d) => this.xScale(d[0]))
.attr('width', (d) => this.xScale(d[1]) - this.xScale(d[0]))
.attr('height', height)
// update axis
this.xAxisElement
.attr('transform', 'translate(0,' + height + ')')
.call(this.xAxis)
this.yAxisElement
.call(this.yAxis)
// update lines
for (let i = 0; i < this.setup.numLines; i++) {
this.lineElements[i].attr('d', this.lineDrawers[i])
}
// since the xScale was changed, update the hover box
if (this.hover.showen) {
this.hoverUpdate(this.hover.point)
}
}
hoverShow () {
this.hover.show()
}
hoverHide () {
this.hover.hide()
}
hoverUpdate (point) {
if (!this.hover.showen) return
// get position of curve there is at the top
const xInGraphPositon = this.xScale(point.x)
let yMetric = Math.max(...point.y)
if (this.setup.className === 'memory') {
// by default, hover box picks the highest value
// in case of memory subgraph, we always want to point at heap usage
yMetric = point.y[2]
}
const yInGraphPositon = this.yScale(yMetric)
// calculate graph position relative to `this.container`.
// The `this.container` has `position:relative`, which is why that is
// the origin.
const xPosition = xInGraphPositon + margin.left
const yPosition = yInGraphPositon + margin.top + headerHeight
this.hover.setPoint(point)
this.hover.setPosition(xPosition, yPosition)
this.hover.setDate(point.x)
this.hover.setData(point.y.map((v) => this.yScale.tickFormat()(v)))
}
}
module.exports = SubGraph