render = (selection, props) => {
const {
data,
height,
width,
margin,
circleRadius,
focusCircleRadius,
circleOpacity,
focusCircleOpacity,
unfocusCircleOpacity,
onMouseEnterCircle,
onMouseLeaveCircle,
onMouseEnterLegendTick,
onMouseLeaveLegendTick,
xAxisLabel,
yAxisLabel,
legendOffsetX,
legendOffsetY,
uniqueKeyValue,
colorValue,
labelValue,
xValue,
yValue,
} = props
const innerWidth = width - margin.left - margin.right
const innerHeight = height - margin.top - margin.bottom
const g = selection.selectAll('.container').data([null])
const gEnter = g.enter()
.append('g')
.attr('class', 'container')
gEnter.merge(g)
.attr('transform', `translate(${margin.left},${margin.top})`)
const scaleX = d3.scaleLinear()
.domain(d3.extent(data, xValue))
.range([0, innerWidth])
.nice()
const scaleY = d3.scaleLinear()
.domain(d3.extent(data, yValue))
.range([innerHeight, 0])
.nice()
const scaleColor = d3.scaleOrdinal()
.domain(Object.keys(colors))
.range(Object.values(colors))
const xAxis = d3.axisBottom(scaleX)
.tickPadding(5)
const yAxis = d3.axisLeft(scaleY)
.tickPadding(5)
const xAxisG = g.select('.x-axis')
const xAxisGEnter = gEnter.append('g')
.attr('class', 'x-axis')
xAxisG.merge(xAxisGEnter)
.attr('transform', `translate(0,${innerHeight + 10})`)
.call(xAxis)
const yAxisG = g.select('.y-axis')
const yAxisGEnter = gEnter.append('g')
.attr('class', 'y-axis')
yAxisG.merge(yAxisGEnter)
.attr('transform', 'translate(-10,0)')
.call(yAxis)
const xAxisLabelEnter = xAxisGEnter.append('text')
.attr('class', 'axis-label')
.attr('fill', 'currentColor')
.attr('x', innerWidth / 2)
.attr('y', 45)
xAxisG.select('.axis-label').merge(xAxisLabelEnter)
.text(xAxisLabel)
const yAxisLabelEnter = yAxisGEnter.append('text')
.attr('class', 'axis-label')
.attr('fill', 'currentColor')
.attr('x', -innerHeight / 2)
.attr('y', -45)
.attr('transform', `rotate(-90)`)
yAxisG.select('.axis-label').merge(yAxisLabelEnter)
.text(yAxisLabel)
const translatePoint = d => `translate(${scaleX(xValue(d))},${scaleY(yValue(d))})`
const points = g.merge(gEnter).selectAll('.data-point')
.data(data, uniqueKeyValue)
const pointsEnter = points.enter()
.append('g')
.attr('class', 'data-point')
.attr('transform', translatePoint)
.on('mouseenter', onMouseEnterCircle)
.on('mouseleave', onMouseLeaveCircle)
pointsEnter.append('circle')
.attr('fill', d => scaleColor(colorValue(d)))
points.merge(pointsEnter).select('circle')
.transition().duration(125)
.attr('r', d => focus === d.name ? focusCircleRadius : circleRadius)
.attr('opacity', d => focusType
? d.type1 === focusType ? focusCircleOpacity : unfocusCircleOpacity
: circleOpacity
)
points
.transition().duration(1000)
.ease(d3.easeBackInOut)
.attr('transform', translatePoint)
// add tooltip
pointsEnter.append('title')
points.merge(pointsEnter).select('title')
.text(d => [
labelValue,
d => d.type1 + " type",
d => xAxisLabel + ": " + xValue(d),
d => yAxisLabel + ": " + yValue(d),
].map(fn => fn(d)).join('\n'))
// add legend
const legendGEnter = gEnter.append('g')
.attr('class', 'legend')
.attr('transform', `translate(${legendOffsetX},${innerHeight + margin.top + legendOffsetY})`)
g.select('.legend').merge(legendGEnter)
.call(renderLegend, {
innerWidth,
innerHeight,
margin,
scaleColor,
onMouseEnterLegendTick,
onMouseLeaveLegendTick,
tickOpacity: circleOpacity,
focusTickOpacity: focusCircleOpacity,
unfocusTickOpacity: unfocusCircleOpacity,
})
}