React & D3: Adding A Bar Chart
By: Joe Keohan
Tags: Data Visualization, D3js, JavaScript, Dashboard, React

Image by author

So the last component to refactor in Streetball Mecca is the bar chart. In my last article, React & D3: Preparing The Data With D3.Nest, I discussed how the data for the chart was organized and formatted using d3.nest. This involved grouping the parks based on their neighborhood location and calculating the overall average of all parks associated with that neighborhood.

Here is the current working version used in this article and the CodeSandbox as well.

Thinking Outside The Box

As I started to contemplate how best to refactor the D3 bar chart code into the React ecosystem, I leveraged the lessons learned from all the previous refactoring. One of the most important changes was to allow React to manage state, render/update DOM elements, and then allow D3 to do what it does best, such as creating scales and nesting data.

For this refactor I decided to take it one step further and opted to take a non-svg approach to rendering the bar and circle elements. This decision was born initially from many previous frustrations having to manually position g elements. It was also influenced by the fact that I just taught a class on Flexbox and as part of that prep I came across a bookmark I saved over a year ago to the article “Making Data Vis Without SVG Using D3 & Flexbox” by Amber Thomas. I felt like the stars had aligned and all the signs pointed to Flexbox.

Planning It Out

Initially, I had ported over the D3 code and refactored it just slightly to work within a single React BarChart component. This D3 within React approach was meant as a first step to understanding how to coerce D3 to work within React. I also kept much of the D3 code intact in order to get something up and running. It however ended up being close to 200 lines of code and started to feel unmanageable.

Now that I was ready to move forward with the second refactor and take a “D3 for the math and React for everything else” approach, the first thing I needed to plan out was the component hierarchy. I used an app called excalidraw to draw out the initial mockup and help work through the though and design process.

Image by author

BarChart Component

The BarChart is the first component in this hierarchy so it makes sense to start there. Since both the XAxis and Neighborhoods components would require the same D3 scale, I decided to create it here and then pass it down to both as a prop.

const BarChart = (props) => { const margin = { right: 25 }; const xScale = d3.scaleLinear()

.domain([0, 100])

.range([0, 1100 - margin.right]); return (

<>

<div id="axis"><XAxis xScale={xScale} /></div>

<div id="bar-chart">

<Neighborhoods {...props} xScale={xScale} />

</div>

</>

)

}

D3 has several methods that can be used for creating scales. Scales take an input (usually a number, date, or category) and return a value (such as a coordinate, color, length, or radius). They come in several categories that range from continuous input/output (d3.scaleLinear()) to discrete input/output (d3.scaleOridinal()) and somewhere in-between (d3.scaleQuantize()). Scales themselves could be an entire lecture in their own right so if you’re interested in learning more I highly suggest you read d3indepth.com.

The scale I needed for this chart would be a dynamic, continuous input/output scale so I went with d3.scaleLinear(). All D3 scales require that you define a .domain() and .range(). The domain of values used for this scale is based on the overall rating of the parks. They could potentially be in total disrepair and assigned 0 (lowest value possible) or the creme de la creme of parks with a rating of 100 (highest value possible). Here, for example, is the 100% Playground park located in Canarsie Brooklyn which falls somewhere in between with an overall rating of 56.

Image by author

The average of those parks was also needed as this would be assigned to the parent neighborhood and I had used D3.nest() to rollup those values as seen in the example below.

Image by author

If you’re curious about working with d3.nest (which I hope you are), you can read more about how I nested the data in my previous article: Preparing The Data With D3.Nest.

The .range() is based on the pixel space provided to the .bar-group element which is assigned a width of 1100px. I did need to subtract 25px in order to provide a bit of margin between both, the last tick value(100) and the circle and the right border of the .bar-group element.

Image by author

As you continue to examine more and more D3 code in general, you will see a common pattern of defining a set of margins that are used to define this additional space which is then referenced by the scale but quite possibly throughout the code in key places.

const margin = { right: 25 };

const xScale = d3.scaleLinear()

.domain([0, 100])

.range([0, 1100 - margin.right]);

Almost any D3 tutorial on scales will provide a similar visual in order to convey the relationship between the domain and range so I thought I’d do the same as well.

Image by author

A Little Bit About Axes

If you have ever worked with D3 in the past, using it to create any one of the more standard chart types such as bar, line, or scatterplot, then it’s safe to say you have some familiarity with adding an axis. If, however, you are new to D3, then know that axes require the following steps:

Creating a scale (we did so using d3.scaleLinear())

Setting its orientation: the orientation of an axis is fixed, however, it can be changed using one of the four orientation methods — .axisBottom(), .axisTop(), .axisLeft(), and .axisRight().

Appending the Axis: once the axis has been created it is then appended using .call(xAxis).

The axis also has additional configurations for setting tick values, sizes, and format.

Being that I’ll be using React for the DOM and D3 for where it’s best suited, I decided that for this refactor I would only make use of d3.scaleLinear() and render the ticks using React.

XAxis Component

The only prop being passed to the XAxis component was the xScale so I’ll just object de-structure that value to make it clear that this is all that the component needs as it pertains to props.

const Axis = ({xScale}) => { }

For much of the XAxis refactor I had used as a reference a beautifully written tutorial on React & D3 by Amelia Wattenberger, She wrote the article in order to promote her recent book “Full Stack D3 and Data Visualization”.

I was, at one point, tempted to buy it and would have done so had I not already owned the following books on D3:

In Amelia’s article she looped over the array of individual ticks (0, 10, 20, etc.) using .ticks() and assigned each of them an xOffset value based on the returned value from the xScale.

const Axis = ({xScale}) => { const ticks = xScale.ticks().map((value) => ({

value,

xOffset: xScale(value)

})) }

It’s been a while since I’ve worked on such a low level with scales and forgot that they included a .ticks() method that returns an array of the ticks.

For the axis I decided to stay in line with Amelia’s article rendering an svg that uses a nested g element to position the axis to the right 200px. It then then maps over the ticks array and positions each one of those g elements which, itself, renders the tick value.

const Axis = ({xScale}) => {



const ticks = xScale.ticks().map((value) => ({

value,

xOffset: xScale(value)

})) return (

<svg style={{ height: '20px' }}>

<g transform={'translate(200,0)'}>

{ticks.map(({ value, xOffset }) => (

<g key={value} transform={`translate(${xOffset}, 0)`}>

<text

key={value}

style={{

fontSize: '14px',

textAnchor: 'middle',

transform: 'translateY(20px)'

}}>

{value}

</text>

</g>

))}

</g>

</svg>

)

}

Side Note: I was able to render the ticks as divs and flexbox, but that solution required creating an additional xScale and removing not only an additional 200px but also another 8px for half the font size. Since I felt only 1 scale should be used in this project to keep things consistent, I opted to put that solution on the shelf for now.

The Neighborhoods Component

This component is responsible for mapping over the nestedData array and rendering individual Neighborhood components. It passes down several props to the child component, including the dispatch function which will be passed as an action and which the useReducer uses to update state. If you’re curious about useReducer, take a look at this previous article I wrote: Managing Complex State Transitions With useReducer.

const Neighborhoods = ({

nestedData,

dispatch,

activeNeighborhood,

xScale}) => { const neighborhoods = nestedData.map((d, i) => {

return (

<Neighborhood

key={i}

title={d.key}

parks={d.value.parks}

width={xScale(d.value.avg)}

xScale={xScale}

neighborhood={d}

dispatch={dispatch}

activeNeighborhood={activeNeighborhood}

/>

);

}); return ( <>{neighborhoods}</> )

};

The Neighborhood Component

This component is responsible for rendering the following elements:

neighborhood title

bar — show the average of all parks

circles — one for every park

It also makes use of the following hooks:

useState — used to keep track of its active status which is used assign a background color the entire element when the user clicks and it becomes the active neighborhood

— used to keep track of its active status which is used assign a background color the entire element when the user clicks and it becomes the active neighborhood useEffect — used to update global state if it determines to set the activeNeighborhood value to this neighborhood

const Neighborhood = ({

parks,

width,

xScale,

title,

dispatch,

neighborhood,

activeNeighborhood

}) => { const [active, setActive] = useState(false); useEffect(() => {

activeNeighborhood === title ?

setActive(true) : setActive(false);

}, [activeNeighborhood, title]); // more code...

}

The next step is to map over the parks array and create a Circle component. One thing to note is that the element determines how many pixels it should position itself left using the xScale but it also needs to subtract 5px. These additional pixels were needed in order to move the circle left, to center it directly under the axis value.

const circles = parks.map((d, i) => {

return (

<Circle

key={i}

dispatch={dispatch}

color={d.color}

park={d}

left={xScale(+d.overall) - 5}

/>

);

});

Let’s break down the return statement in the map of the Neighborhood component as there are a few things happening here.

const neighborhoods = nestedData.map((d, i) => {

return (

<Neighborhood

key={i}

title={d.key}

parks={d.value.parks}

width={xScale(d.value.avg)}

xScale={xScale}

neighborhood={d}

dispatch={dispatch}

activeNeighborhood={activeNeighborhood}

/>

);

});

First, the neighborhood element will reference the activeNeighborhood value in state to determine if it needs to highlight its background color in order to indicate it is the active neighborhood as seen below.

Image by author

I opted to allow the user to click anywhere in on the element in order to trigger the onClick event which calls the dispatch function and passes it an object containing a type and payload as required by useReducer. You can read more about working with useReducer and actions in this article: React: Managing Complex State Transitions With useReducer.

return (

<div

className="neighborhood"

style={{ background: active ? 'lightyellow' : '' }}

onClick={() => dispatch({

type: 'FILTER_ACTIVE_NEIGHBORHOOD',

payload: { neighborhood }

})}> // more code...

</div>

)

Next, we have to render the neighborhood title and assign it a specific color based on which borough it belongs to.

return (

<div

className="neighborhood"

style={{ background: active ? 'lightyellow' : '' }}

onClick={() => dispatch({

type: 'FILTER_ACTIVE_NEIGHBORHOOD',

payload: { neighborhood }

})}>



<div

className="title"

style={{ color: parks[0].boroughColor }}

> {title} </div> // more code...

</div>

)

And finally, the Bars and Circles needed to be rendered. The Bar component requires the width value which was calculated using the xScale along with the entire neighborhood object as key data points are needed by the tooltip.

return (

<div

className="neighborhood"

style={{ background: active ? 'lightyellow' : '' }}

onClick={() => dispatch({

type: 'FILTER_ACTIVE_NEIGHBORHOOD',

payload: { neighborhood }

})}>



<div

className="title"

style={{ color: parks[0].boroughColor }}

> {title} </div>



<div className="bar-group">

<Bar width={width} neighborhood={neighborhood}/>

{circles}

</div> </div>

)

Flexbox was enabled on the

The Bar Component

This component would require the logic needed to render a tooltip when the user hovered over the colored bar. That logic involves adding both onMouseMove and onMouseOut event listeners and updating a local version of state in order to trigger the re-render.

The first step to take was create the following state values:

toolTipCoords — this stores the left and top values to position the tooltip

isActive — this determines if the tooltip is active

const Bar = ({ width, neighborhood }) => {

const [toolTipCoords, setToolTipCoords] = useState(null)

const [isActive, setIsActive] = useState(false) // more code... }

Now we add the handler functions for the event listeners. For this I had to do a bit of research as the standard event object’s e.pageY, e.clientY, and e.screenY were returning values that were much greater than the actual position of the mouse. I ended up using e.nativeEvent.offsetX and e.nativeEvent.offsetY and pulled some of that insight right from the React docs themselves:

If you find that you need the underlying browser event for some reason, simply use the nativeEvent attribute to get it. The synthetic events are different from, and do not map directly to, the browser’s native events. For example in onMouseLeave event.nativeEvent will point to a mouseout event.

const Bar = ({ width, neighborhood }) => {

const [toolTipCoords, setToolTipCoords] = useState(null)

const [isActive, setIsActive] = useState(false) const handleMouseOut = () => setIsActive(false) const handleMouseOver = (e) => {

const top = e.nativeEvent.offsetY - 60;

const left = e.nativeEvent.offsetX;

setToolTipCoords({ top, left });

setIsActive(true);

}; // more code... }

All that was left was to return the DOM elements configured, assign the event listeners and add some conditional logic to render the tooltip only when isActive was true.

const Bar = ({ width, neighborhood }) => {

const [toolTipCoords, setToolTipCoords] = useState(null)

const [isActive, setIsActive] = useState(false) const handleMouseOut = () => setIsActive(false) const handleMouseOver = (e) => {

const top = e.nativeEvent.offsetY - 60;

const left = e.nativeEvent.offsetX;

setToolTipCoords({ top, left });

setIsActive(true);

}; return (

<>

<div

className="bar"

style={{ width: width }}

onMouseMove={(e) => handleMouseOver(e)}

onMouseOut={() => handleMouseOut()}

></div>

{isActive && (

<ToolTip coords={toolTipCoords} neighborhood={neighborhood} />

)}

</>

);

}

The Circle Component

This component required a much of the same logic for the tooltip as was needed for the Bar component along with one additional onClick event listener that filtered the map for the selected park.

The handleClick function required the use of e.stopPropagation() in order to prevent the event from bubbling up to the Neighborhood component where an additional onClick event was configured. I found that without e.stopPropagation(), all parks, from that neighborhood, would be rendered within the map instead of just that single park.

const Circle = ({

color, left, park, dispatch}) => {



const [toolTipCoords, setToolTipCoords] = useState(null);

const [isActive, setIsActive] = useState(false); const handleClick = (e) => {

e.stopPropagation()

dispatch({ type: 'FILTER_ACTIVE_PARK', payload: { park } })

}



const handleMouseOut = () => {

setIsActive(false);

}; const handleMouseOver = (e) => {

const top = e.nativeEvent.offsetY - 90;

const left = e.clientX - 500;

setToolTipCoords({ top, left });

};

Flexbox CSS

I had looked forward to using fancy Flexbox properties for this particular layout but it turned out it was only needed enable it on the neighborhood and title elements.

Here are is the actual html layout of the .neighborhood element and its children.

Image by author

Those elements, along with their corresponding CSS rendered as follows:

Image by author

As for Flexbox I first enabled it on the .neighborhood element which positioned the title and bar-group horizontally to one another.

.neighborhood {

display: flex;

height: 28px;

margin-bottom: 5px;

}

The .title element was also assigned a display of flex and used align-items to to vertically center the title text

.title {

display: flex;

align-items: center;

width:200px;

}

Conclusion

This is the 7th article of my Streetball Mecca series, where I document refactoring the project from its humble beginnings as a D3 visualization over to a React build to manage state and the DOM and use D3 for its unique helper functions.

Thus far, I’ve replaced all instances of D3’s Enter/Update/Exit with either React’s useReducer or useState hooks. useReducer is being used to manage the global state and overall business logic of the app, and useState is used for more localized state as in the tooltips. Although React took the reins here, D3 was used for what it does best; creating scales (d3.scaleLinear), rendering geo-projections (d3.geoMercator/d3.geoPath), and nesting data (d3.nest).

This project has certainly helped validate my opinion that React is by far the better choice for managing state and the overall flow of the application logic. Besides all the native React hooks used, such as useReducer, useState, useRef, and useCallback, I also leveraged a few custom hooks such as useDataApi, useOnClickOutside, and useLocalStorage. My favorite part of the refactor has been working with the React-Spring animation library in order to replace all the D3 transitions. A close runner up was building a bar chart using DIV’s and Flexbox instead of SVGs and G elements, something I’ve wanted to try for some time.

I certainly have a new appreciation for both libraries and look forward to building more interactive dashboards.