class ScatterPlotRenderer extends EventTarget {
constructor(height) {
super()
this.height = height
this.container = DOM.element('div')
this.svg = d3.select(DOM.svg(width, height))
this.offsetContainer = this.svg.append('g')
this.scaleContainer = this.offsetContainer.append('g')
this.container.append(this.svg.node())
this.root = this.scaleContainer.append('g')
this.lines = this.scaleContainer.append('g')
this.tooltip = DOM.element('div')
this.container.append(this.tooltip)
this._offset = { x: width / 2, y: height / 2 }
this._scale = 1
const node = this.svg.node()
let mousedown = false
this.userPosition = false
this.svg.on('mousedown', () => {
if (!this.selecting) mousedown = true
})
window.addEventListener('mouseup', e => {
mousedown = false
this.focused = null
})
window.addEventListener('mousemove', e => {
if (mousedown && this.focused == null) {
this.userPosition = true
this.offset = {
x: this.offset.x + e.movementX,
y: this.offset.y + e.movementY
}
}
if (mousedown && this.focused != null && this._enterPoint) {
const { point, data } = this.focused
data[0] += e.movementX / this.scale / 2
data[1] -= e.movementY / this.scale / 2
const [x, y] = data
const scale = 2
point.style('transform', `translate(${scale*x}px,${-scale*y}px)`)
}
if (mousedown && this.focused != null && this._editBox) {
const { box, data, p1, p2, move } = this.focused
if (move[0]) {
data[0][0] += e.movementX / this.scale / 2
data[0][1] -= e.movementY / this.scale / 2
}
if (move[1]) {
data[1][0] += e.movementX / this.scale / 2
data[1][1] -= e.movementY / this.scale / 2
}
applyBox(box, data)
p1.style('transform', `translate(${2*data[0][0]}px,${-2*data[0][1]}px)`)
p2.style('transform', `translate(${2*data[1][0]}px,${-2*data[1][1]}px)`)
}
e.preventDefault()
})
node.addEventListener('mousewheel', e => {
this.scale -= Math.sign(e.deltaY) / 3
e.preventDefault()
})
node.addEventListener('dblclick', e => {
if (this._enterPoint) {
const that = this
const scale =2
const {x, y} = this.eventToLocalCoords(e)
const point = [x / scale, y / scale]
this.data.points.push(point)
this.points
.append('ellipse')
.attr('rx', scale)
.attr('ry', scale)
.attr('stroke', '#1f1f1f')
.attr('fill', '#f1f1f1')
.style('transform', `translate(${x}px,${-y}px)`)
.on('mousedown', function() {
if (that._enterPoint) {
that.focused = {
data: point,
point: d3.select(this)
}
}
})
.on('click', function(node) {
that.tooltip.innerText = point
})
}
else this.moveHomeAnimation()
})
}
eventToLocalCoords(e) {
const x = (e.offsetX - this.offset.x) / this.scale
const y = -(e.offsetY - this.offset.y) / this.scale
return { x, y }
}
render(points, lines = [], boxes = []) {
const oldScale = this.scale
const offset = this.offset
this.scale = 1
this.root.remove()
this.lines.remove()
this.data = {points,lines,boxes}
this.lines = this.scaleContainer.append('g')
this.root = this.scaleContainer.append('g')
this.offset = offset
this.scale = oldScale
this.points = this.root.append('g')
const scale = 2
const that = this
for (let point of points) {
const [x, y] = point
this.points
.append('ellipse')
.attr('rx', scale)
.attr('ry', scale)
.attr('stroke', point.stroke || '#1f1f1f')
.attr('fill', point.fillStyle || '#f1f1f1')
.style('transform', `translate(${scale*x}px,${-scale*y}px)`)
.on('mousedown', function() {
if (that._enterPoint) {
that.focused = {
data: point,
point: d3.select(this)
}
}
})
.on('click', function() {
that.tooltip.innerText = point
})
}
for (let box of boxes) {
appendRect(this.lines, box)
}
for (let line of lines) {
var [[x1, y1],[x2,y2]] = line
this.lines
.append('line')
.attr('x1', x1 * scale)
.attr('y1', -y1 * scale)
.attr('x2', x2 * scale)
.attr('y2', -y2 * scale)
.attr('stroke', line.stroke || '#000')
}
if (!this.userPosition) setTimeout(() => this.moveHome(), 100)
return this.container
}
updateTransforms() {
this.root.attr(
'transform',
`translate(${this._offset.x}, ${this._offset.y}), scale(${this.scale})`
);
this.lines.attr(
'transform',
`translate(${this._offset.x}, ${this._offset.y}), scale(${this.scale})`
)
}
moveHome() {
this.scale = 1
this.userPosition = false
const {width: bbWidth,height} = this.root.node().getBoundingClientRect()
const dw = (width - bbWidth)
const dh = (this.height - height)
if (dh < dw) this.scale = (this.height - 60) / (height)
else this.scale = (width - 60) / (bbWidth)
this.offset = { x: (width - bbWidth * this.scale)/ 2, y: height * this.scale + 30 }
}
moveHomeAnimation() {
const oldScale = this.scale
this.scale = 1
this.userPosition = false
const {width: bbWidth,height} = this.root.node().getBoundingClientRect()
const dw = (width - bbWidth)
const dh = (this.height - height)
var scale
if (dh < dw) scale = (this.height - 60) / (height)
else scale = (width - 60) / (bbWidth)
this.scale = oldScale
this.offsetContainer.style('transition-duration', '0.3s')
this.scale = scale
this.offset = { x: (width - bbWidth * scale)/ 2, y: height * scale + 30 }
setTimeout(() => {
this.offsetContainer.style('transition-duration', '')
}, 350)
}
set offset(value) {
this._offset = value
this.offsetContainer.attr('transform', `translate(${this._offset.x}, ${this._offset.y})`)
}
get offset() {
return this._offset
}
set scale(value) {
const scale = Math.max(0, value)
this._scale = scale
this.scaleContainer.style('transform', `scale(${scale})`)
}
get scale() {
return this._scale || 1
}
enterPoints(x) {
this._enterPoint = x
}
editBox(box) {
const that = this
this.selection = box
this._editBox = appendRect(this.lines, box)
const move = [true, true]
const p1 = this.p1 = this.lines
.append('ellipse').attr('rx', 2).attr('ry', 2)
.style('transform', `translate(${2*box[0][0]}px,${-2*box[0][1]}px)`)
.on('mousedown', e => {
move[1] = false
that.focused = { data: box, box: this._editBox, p1, p2, move }
})
.on('mouseup', e => move[1] = true)
const p2 = this.p2 = this.lines
.append('ellipse').attr('rx', 2).attr('ry', 2)
.style('transform', `translate(${2*box[1][0]}px,${-2*box[1][1]}px)`)
.on('mousedown', e => {
move[0] = false
that.focused = { data: box, box: this._editBox, p1, p2, move }
})
.on('mouseup', e => move[0] = true)
this._editBox.on('mousedown', function() {
if (that._editBox) {
that.focused = { data: box, box: d3.select(this), p1, p2, move }
}
})
}
selectBox() {
if (this._editBox) {
this.p1.remove()
this.p2.remove()
this._editBox.remove()
}
return this.selection
}
}