Edit: if there's a better way to make this reproducible, please let me know. I can create some dummy data and put this in a codepen, but that wouldn't really reflect the interaction with React that's required.
I am going to need to build several React d3 components for a web application that I am building - I'd like to make sure the first one is done correctly before moving onto the next.
It is difficult to get a fully-working reproducable example, however here's the code for the full component (with my thoughts below):
Note: The component may seem long, but a lot of the code is simply getting the axis and grid drawn. In total i think it's a short component..
import React, { Component } from 'react';
import * as d3 from "d3";
class D3Scatter extends Component {
// When component mounts, get
componentDidMount() {
const context = this.setContext();
this.drawScatter(context);
}
componentDidUpdate() {
var context = d3.select(`#${this.props.svgID}`);
context.remove();
context = this.setContext();
this.drawScatter(context);
}
// This adds the SVG to the <div ref="scatter"> with some attrs
setContext() {
const { height, width, svgID } = this.props;
return d3.select(this.refs.scatter).append('svg')
.attr('id', svgID)
.attr('width', '100%')
.attr('height', '100%')
.attr('viewBox', "0 0 " + width + " " + height)
.attr('preserveAspectRatio', "xMaxYMax")
.append('g')
.attr('transform', `translate(25, 25)`);
}
// drawScatter called each time new props are passed
drawScatter(context) {
const {
// data
playerData,
colorData,
// formatting vals
padding,
margin,
height,
width,
// chart parameters
chartType,
position,
teamName,
playerName,
xColname,
yColname
} = this.props;
// Fit The Data to the SVG size and width
var xScale = d3.scaleLinear()
.domain(d3.extent(playerData, d => d[xColname]))
.range([padding, width - padding])
var yScale = d3.scaleLinear()
.domain(d3.extent(playerData, d => d[yColname]))
.range([height - padding, padding])
var xAxis = d3.axisBottom(xScale)
.tickSize(2*padding-height)
.tickSizeOuter(0);
var yAxis = d3.axisLeft(yScale)
.tickSize(-width - 2*padding)
.tickSizeOuter(0);
// Size Scale for the markers
// if a certain playerName or teamName is passed, change marker size
var sizeScale = (player) => {
// console.log("playerName:", playerName)
// console.log("teamName:", teamName)
if(player.playerFullName === playerName) {
return "24px";
} else if(player.teamName === teamName) {
return "12px";
} else {
return "6px";
}
}
// also change color for certain teamName or playerName passed
var colorScale = (player) => {
if(player.playerFullName === playerName) {
var playersTeam = playerData
.filter(d => d.playerFullName === playerName)
.map(d => d.teamName)
var thisColor = colorData
.filter(d => d.teamname === playersTeam)
.map(d => d.colorhex1)
return thisColor;
}
else if(player.teamName === teamName) {
var teamColor = colorData
.filter(d => d.teamname === teamName)
.map(d => d.colorhex1);
return teamColor;
} else {
return '#DDD';
}
}
// append the circles onto the page
context
.append('g')
.selectAll("circle")
.data(playerData)
.enter()
.append("circle")
.attr("cx", d => xScale(d[xColname]))
.attr("cy", d => yScale(d[yColname]))
.attr("r", d => sizeScale(d))
.attr("fill", d => colorScale(d))
.attr("stroke", "#FFF")
// Title, Y-Axis Name, X-Axis Name, Y-Axis Lines, X-Axis Lines
context
.append("text")
.attr("x", width/2)
.attr("y", padding)
.attr("dy", "-1.5em")
.style("text-anchor", "middle")
.style("font-size", "2.5em")
.text(`Highlighting ${teamName} and ${playerName}!`)
context
.append("text")
.attr("transform", "rotate(-90)")
.attr("x", -height/2) //
.attr("y", padding)
.attr("dy", "-2.5em") // gap from axis numbers
.style("font-size", "1.5em") // axis name size
.style("text-anchor", "middle")
.text(`Player ${yColname}`)
context
.append("text")
.attr("x", width/2)
.attr("y", height - padding)
.attr("dy", "2.5em")
.style("font-size", "1.5em")
.style("text-anchor", "middle")
.text(`Player ${xColname}`)
context
.append("g")
.attr("transform", "translate(0," + (height-padding) + ")")
.call(xAxis);
context
.append("g")
.attr("transform", "translate(" + padding + ",0)")
.call(yAxis);
return context
}
render() {
const { gridID } = this.props;
return (
<div id={gridID} ref="scatter"></div>
)
}
}
export default D3Scatter;
There are a number of props passed to this component, which I've grouped into 3 different buckets:
data props
playerData
: has all of the data that gets plottedcolorData
: has colors for each team (mapsteamName
to color)
formatting props
padding
,margin
,height
,width
all used for the SVG
parent component state props (for filtering
playerData
)chartType
: (not used here yet)position
: (not used here yet)- `teamName: used for setting colors / sizes
playerName
: used for setting colors / sizesxColname
: changes which key inplayerData
is used for x axis valuesyColname
: changes which key inplayerData
is used for y axis values
My container app (the one that calls D3Scatter
) has chartType
, position
, teamName
, playerName
, xColname
and yColname
in its state, and passes those down to D3Scatter
depending on which buttons / select widgets are selected.
Eventually, I want to add transitions / animations so that when xColname
and yColname
are updated, the points slide to their new spots. I also plan on adding a tooltip and potentially other onClick
effects, but I haven't done those yet. Currently I'm worried about whether I'm updating the chart right (if I'm using lifecycle components correctly), and other general best practices.
Last, an example of how this is called in its parent component:
<D3Scatter
gridID="graph1"
svgID={'d3-scatterplot'}
playerData={playerSeasonData}
colorData={mlbLogos}
height={window.innerWidth*0.425}
width={window.innerWidth*0.85}
padding={50}
margins={{top: 80, right: 35, bottom: 80, left: 25}}
chartType={null}
xColname={statNameX}
yColname={statNameY}
position={position}
teamName={selectedTeam && selectedTeam.label}
playerName={"Jay Bruce"} />