Movatterモバイル変換


[0]ホーム

URL:


Skip to content
DEV Community
Log in Create account

DEV Community

benjaminadk
benjaminadk

Posted on

     

Basketball Stats Through D3 & React

Make aDonut Chart to visualize the scoring totals of the 2018-19Los Angeles Lakers basketball team.

Data

The first thing we need to create our data visualization is, not coincidentally, data.This well written article explains some of the legal and ethical ramifications of web scraping.This repository offers links to free public data.Dev.to itself has many articles on data, web scrapers, and visualizations. My two cents is that for simple data visualizations projects, good oldChrome Devtools on its own is more than enough to gather and shape data. Check out this over-simplified example.

NameAge
LeBron James34
Zion Williamson18
Micheal Jordan56

Given the above table here are the steps to massage the data:

  1. OpenChrome Devtools
  2. Isolate all table rows
  3. Convert results from aNodeList to anArray and ditch the title row
  4. Extract text from each table data cell and map the results to a new array of objects
  5. Typec (the variable name) and pressEnter and your new array will be displayed in the console
  6. Right click the array and choseStore as Global Variable. You will seetemp1 appear in the console.
  7. Use the built incopy function to copy the temporary variable to the clipboard -copy(temp1)
  8. Paste your data into aJavaScript orJSON file.
  9. 🤯
vara=document.querySelectorAll('tr')// 2varb=Array.from(a).slice(1)// 3varc=b.map(el=>{// 4varname=el.children[0].innerTextvarage=el.children[1].innerTextreturn{name,age}})c// 5// right click arraycopy(temp1)// 7
Enter fullscreen modeExit fullscreen mode

Note that every scenario is different and this example is simplified to help explain the process. Also, all of the logic above can be put into a single function to streamline the process. Remember you can create multi-line functions in the console by usingShift+Enter to create new lines. With this method we have what amounts to manual web scraping withJavaScript 101. Be sure to read a website'sTerms of Service before goingwilly-nilly and harvesting data where you aren't supposed to.

Create a Donut Chart

GettingD3 andReact to work together is not really that complicated. Generally, all that is needed is an entry point to the DOM and some logic that initializes the visualization when the page loads. To get started with our example project we want to havecreate-react-app installed. The first step is to create a new project. The first thing I like to do is clear out thesrc directory, leaving onlyApp.js andindex.js. Don't forget to remove any oldimport statements. Before we write any code we need to snag a couple dependencies.

1- DownloadD3 andStyled Components.

npm i d3 styled-components
Enter fullscreen modeExit fullscreen mode

2- Create a new filewhatever-you-want.js, or evendata.js in thesrc directory. The data used in the example is availablein this gist.

3- Create some basic boilerplate that can be used for a variety of projects with this configuration - akaD3 +React +Styled Components. I encourage you to tweak whatever you see fit as like most developers I have my own quircks and patterns. Case in point, I am bothered by#000000 black so I use#333333, I like the fontRaleway, etc. If you haven't usedHooks before, theuseEffect hook with an empty[] dependency array is similar tocomponentDidMount in aReact class component. The numbered comments correspond to upcoming steps and are the place to insert the code from those steps.

importReact,{useRef,useEffect,useState}from'react'import*asd3from'd3'importstyled,{createGlobalStyle}from'styled-components'importdatafrom'./data'constwidth=1000constheight=600constblack='#333333'consttitle='My Data Visualization'// 4// 7exportconstGlobalStyle=createGlobalStyle`@import url('https://fonts.googleapis.com/css?family=Raleway:400,600&display=swap');body {  font-family: 'Raleway', Arial, Helvetica, sans-serif;  color:${black};  padding: 0;  margin: 0;}`exportconstContainer=styled.div`  display: grid;  grid-template-rows: 30px 1fr;  align-items: center;  .title {    font-size: 25px;    font-weight: 600;    padding-left: 20px;  }`exportconstVisualization=styled.div`  justify-self: center;  width:${width}px;  height:${height}px;// 6`exportdefault()=>{constvisualization=useRef(null)useEffect(()=>{varsvg=d3.select(visualization.current).append('svg').attr('width',width).attr('height',height)// 5// 8},[])return(<><GlobalStyle/><Container><divclassName='title'>{title}</div><Visualizationref={visualization}/>{/*10*/}</Container><>  )}
Enter fullscreen modeExit fullscreen mode

4- We need to establish a color scheme and some dimensions for ourDonut Chart.

The radius of our pastry.

constradius=Math.min(width,height)/2
Enter fullscreen modeExit fullscreen mode

It only makes sense to use aLakers color theme.

varlakersColors=d3.scaleLinear().domain([0,1,2,3]).range(['#7E1DAF','#C08BDA','#FEEBBD','#FDBB21'])
Enter fullscreen modeExit fullscreen mode

TheD3pie function will map our data into pie slices. It does this by adding fields such asstartAngle andendAngle behind the scenes. We are using an optionalsort function just to shuffle the order of the slices. Play around with this, pass itnull or even leave it out to get different arrangements. Finally, we use thevalue function to tellD3 to use thepoints property to divide up the pie. Log thepie variable to the console to help conceptualize what theD3 pie function did to our data.

varpie=d3.pie().sort((a,b)=>{returna.name.length-b.name.length}).value(d=>d.points)(data)
Enter fullscreen modeExit fullscreen mode

Now we need to create circular layouts using thearc function. The variablearc is for ourDonut Chart and theouterArc will be used as a guide for labels later.getMidAngle is a helper function to be used at a later time also.

vararc=d3.arc().outerRadius(radius*0.7).innerRadius(radius*0.4)varouterArc=d3.arc().outerRadius(radius*0.9).innerRadius(radius*0.9)functiongetMidAngle(d){returnd.startAngle+(d.endAngle-d.startAngle)/2}
Enter fullscreen modeExit fullscreen mode

5- With a structure in place are almost to the point of seeing something on the screen.

Chain the following to our originalsvg variable declaration.

.append('g').attr('transform',`translate(${width/2},${height/2})`)
Enter fullscreen modeExit fullscreen mode

Now the magic happens when we feed ourpie back toD3.

svg.selectAll('slices').data(pie).enter().append('path').attr('d',arc).attr('fill',(d,i)=>lakersColors(i%4)).attr('stroke',black).attr('stroke-width',1)
Enter fullscreen modeExit fullscreen mode

Next we need to draw lines from each slice that will eventually point to a label. The well namedcentroid function returns an array with[x,y] coordinates to the center point of thepie slice (in this cased) within thearc. In the end we are returning an array of three coordinate arrays that correspond to the origin point, bend point, and termination point of each line the now appears on the screen. ThemidAngle helps determine which direction to point the tail of our line.

svg.selectAll('lines').data(pie).enter().append('polyline').attr('stroke',black).attr('stroke-width',1).style('fill','none').attr('points',d=>{varposA=arc.centroid(d)varposB=outerArc.centroid(d)varposC=outerArc.centroid(d)varmidAngle=getMidAngle(d)posC[0]=radius*0.95*(midAngle<Math.PI?1:-1)return[posA,posB,posC]})
Enter fullscreen modeExit fullscreen mode

Now our lines are ready for labels. The label seems to look better by adding some symmetry by flip-flopping the order ofname andpoints based on which side of the chart it appears on. Notice that thepie function moved our originaldata into a key nameddata. The top level keys ofpie objects contain the angle measurements used in thegetMidAngle function.

svg.selectAll('labels').data(pie).enter().append('text').text(d=>{varmidAngle=getMidAngle(d)returnmidAngle<Math.PI?`${d.data.name} -${d.data.points}`:`${d.data.points} -${d.data.name}`}).attr('class','label').attr('transform',d=>{varpos=outerArc.centroid(d)varmidAngle=getMidAngle(d)pos[0]=radius*0.99*(midAngle<Math.PI?1:-1)return`translate(${pos})`}).style('text-anchor',d=>{varmidAngle=getMidAngle(d)returnmidAngle<Math.PI?'start':'end'})
Enter fullscreen modeExit fullscreen mode

6- To polish off our labels with some style we just need to add a couple lines of code to theVisualization styled component. Having usedD3 to add aclass attribute inside aReactuseEffect hook and then defining that class usingStyled Components seems to check the boxes on integrating the libraries.

.label{font-size:12px;font-weight:600;}
Enter fullscreen modeExit fullscreen mode

7- We are looking good but why not add a little more flavor to give the user an interactive feel. We can quickly grab the total amount of points scored using thesum function fromD3.

vartotal=d3.sum(data,d=>d.points)
Enter fullscreen modeExit fullscreen mode

8- TheshowTotal function will simply tack on atext node displaying our total. Thetext-anchor style property ofmiddle should center the text within ourDonut hole. ThehideTotal function will come into play in a bit. Notice we are calling theshowTotal function to make sure the text is showing when the page loads.

functionshowTotal(){svg.append('text').text(`Total:${total}`).attr('class','total').style('text-anchor','middle')}functionhideTotal(){svg.selectAll('.total').remove()}showTotal()
Enter fullscreen modeExit fullscreen mode

We should tack on another class fortotal right next to ourlabel class from step 6.

.total{font-size:20px;font-weight:600;}
Enter fullscreen modeExit fullscreen mode

9- The numbered comment system is a getting a little gnarly at this point, but if you have made it this far you are smart enough to follow along. These next functions can go belowhideTotal. These are listeners we will apply to each slice.

functiononMouseOver(d,i){hideTotal()setPlayer(d.data)d3.select(this).attr('fill',d3.rgb(lakersColors(i%4)).brighter(0.5)).attr('stroke-width',2).attr('transform','scale(1.1)')}functiononMouseOut(d,i){setPlayer(null)showTotal()d3.select(this).attr('fill',lakersColors(i%4)).attr('stroke-width',1).attr('transform','scale(1)')}
Enter fullscreen modeExit fullscreen mode

When a slice is hovered the stroke and fill will be emphasized and a slight scale up will add a cool effect. The total points text will also be toggled so we can stick a tooltip with a little more information smack dab in the hole. First we need to create a piece ofstate, what would aReact app be without it.

const[player,setPlayer]=useState(null)
Enter fullscreen modeExit fullscreen mode

A keen observer may have noticed the reference tothis and wondered what was happening. The following listeners need to be tacked on to the end of theslicesD3 chain.

.attr('class','slice').on('mouseover',onMouseOver).on('mouseout',onMouseOut)
Enter fullscreen modeExit fullscreen mode

Since we are using atransform on theslice class let's control it through another couple lines in theVisualization styled component.

.slice{transition:transform0.5sease-in;}
Enter fullscreen modeExit fullscreen mode

10- We can now create the tooltip to display theplayer state that changes as individual slices are moused over.

{player?(<Tooltip><div><spanclassName='label'>Name:</span><span>{player.name}</span><br/><spanclassName='label'>Points:</span><span>{player.points}</span><br/><spanclassName='label'>Percent:</span><span>{Math.round((player.points/total)*1000)/10}%</span></div></Tooltip>):null}
Enter fullscreen modeExit fullscreen mode

In terms of new information the user is only getting the percentage of the team's points the current player scored. However, with the centralized position combined with the movement a nice effect and a nice feeling of interactivity is created. A similar pattern could be used more effectively if there was more information to show or I was smarter. It seems the last thing needed is theTooltip component, which goes with the other styled components.

exportconstTooltip=styled.div`  position: absolute;  top: 50%;  left: 50%;  transform: translate(-50%, -50%);  width:${radius*0.7}px;  height:${radius*0.7}px;  display: grid;  align-items: center;  justify-items: center;  border-radius: 50%;  margin-top: 10px;  font-size: 12px;  background: #ffffff;  .label {    font-weight: 600;  }`
Enter fullscreen modeExit fullscreen mode

Alas, our final code should look something like the following.

importReact,{useRef,useEffect,useState}from'react'import*asd3from'd3'importdatafrom'./data'importstyled,{createGlobalStyle}from'styled-components'/** * Constants */constwidth=1000constheight=600constradius=Math.min(width,height)/2constblack='#333333'consttitle='Los Angeles Lakers Scoring 2018-19'/** * D3 Helpers */// total pointsvartotal=d3.sum(data,d=>d.points)// lakers colorsvarlakersColors=d3.scaleLinear().domain([0,1,2,3]).range(['#7E1DAF','#C08BDA','#FEEBBD','#FDBB21'])// pie transformationvarpie=d3.pie().sort((a,b)=>{returna.name.length-b.name.length}).value(d=>d.points)(data)// inner arc used for pie chartvararc=d3.arc().outerRadius(radius*0.7).innerRadius(radius*0.4)// outer arc used for labelsvarouterArc=d3.arc().outerRadius(radius*0.9).innerRadius(radius*0.9)// midAngle helper functionfunctiongetMidAngle(d){returnd.startAngle+(d.endAngle-d.startAngle)/2}/** * Global Style Sheet */exportconstGlobalStyle=createGlobalStyle`@import url('https://fonts.googleapis.com/css?family=Raleway:400,600&display=swap');body {  font-family: 'Raleway', Arial, Helvetica, sans-serif;  color:${black};  padding: 0;  margin: 0;}`/** * Styled Components */exportconstContainer=styled.div`  display: grid;  grid-template-rows: 30px 1fr;  align-items: center;  user-select: none;  .title {    font-size: 25px;    font-weight: 600;    padding-left: 20px;  }`exportconstVisualization=styled.div`  justify-self: center;  width:${width}px;  height:${height}px;  .slice {    transition: transform 0.5s ease-in;  }  .label {    font-size: 12px;    font-weight: 600;  }  .total {    font-size: 20px;    font-weight: 600;  }`exportconstTooltip=styled.div`  position: absolute;  top: 50%;  left: 50%;  transform: translate(-50%, -50%);  width:${radius*0.7}px;  height:${radius*0.7}px;  display: grid;  align-items: center;  justify-items: center;  border-radius: 50%;  margin-top: 10px;  font-size: 12px;  background: #ffffff;  .label {    font-weight: 600;  }`exportdefault()=>{const[player,setPlayer]=useState(null)constvisualization=useRef(null)useEffect(()=>{varsvg=d3.select(visualization.current).append('svg').attr('width',width).attr('height',height).append('g').attr('transform',`translate(${width/2},${height/2})`)svg.selectAll('slices').data(pie).enter().append('path').attr('d',arc).attr('fill',(d,i)=>lakersColors(i%4)).attr('stroke',black).attr('stroke-width',1).attr('class','slice').on('mouseover',onMouseOver).on('mouseout',onMouseOut)svg.selectAll('lines').data(pie).enter().append('polyline').attr('stroke',black).attr('stroke-width',1).style('fill','none').attr('points',d=>{varposA=arc.centroid(d)varposB=outerArc.centroid(d)varposC=outerArc.centroid(d)varmidAngle=getMidAngle(d)posC[0]=radius*0.95*(midAngle<Math.PI?1:-1)return[posA,posB,posC]})svg.selectAll('labels').data(pie).enter().append('text').text(d=>{varmidAngle=getMidAngle(d)returnmidAngle<Math.PI?`${d.data.name} -${d.data.points}`:`${d.data.points} -${d.data.name}`}).attr('class','label').attr('transform',d=>{varpos=outerArc.centroid(d)varmidAngle=getMidAngle(d)pos[0]=radius*0.99*(midAngle<Math.PI?1:-1)return`translate(${pos})`}).style('text-anchor',d=>{varmidAngle=getMidAngle(d)returnmidAngle<Math.PI?'start':'end'})functionshowTotal(){svg.append('text').text(`Total:${total}`).attr('class','total').style('text-anchor','middle')}functionhideTotal(){svg.selectAll('.total').remove()}functiononMouseOver(d,i){hideTotal()setPlayer(d.data)d3.select(this).attr('fill',d3.rgb(lakersColors(i%4)).brighter(0.5)).attr('stroke-width',2).attr('transform','scale(1.1)')}functiononMouseOut(d,i){setPlayer(null)showTotal()d3.select(this).attr('fill',lakersColors(i%4)).attr('stroke-width',1).attr('transform','scale(1)')}showTotal()},[])return(<><GlobalStyle/><Container><divclassName='title'>{title}</div><Visualizationref={visualization}/>{player?(<Tooltip><div><spanclassName='label'>Name:</span><span>{player.name}</span><br/><spanclassName='label'>Points:</span><span>{player.points}</span><br/><spanclassName='label'>Percent:</span><span>{Math.round((player.points/total)*1000)/10}%</span></div></Tooltip>):null}</Container></>)}
Enter fullscreen modeExit fullscreen mode

NBA Player Salaries & Performance 2018-19 (Bubble Chart)

Inspiration for example Donut Chart

Top comments(6)

Subscribe
pic
Create template

Templates let you quickly answer FAQs or store snippets for re-use.

Dismiss
CollapseExpand
 
jmdembe profile image
Jessica Dembe
Front-end engineer evolving into the next phase of my career and mentoring others while doing so.
  • Location
    Washington, D.C
  • Education
    University of Maryland, College Park, The Iron Yard, Life
  • Joined

I have wanted to do something like this for a while, but it seems so daunting! Nice work!

CollapseExpand
 
benjaminadk profile image
benjaminadk
Software Developer working with PHP, JavaScript and Linux for an eCommerce website. Freelance for small businesses and others in my free time.
  • Location
    California
  • Education
    BS Software Development @ WGU
  • Work
    Web Developer at L.A. Design Concepts
  • Joined

Thanks. Trying to follow D3 examples is the worst part. Many are out of date and looking up what each thing does is a pain. I'm looking into a map for international player origin countries and its driving me insane right now.

CollapseExpand
 
the_riz profile image
Rich Winter
  • Joined

Recently found this which I thought was a nice collection:d3-graph-gallery.com/index.html

Otherwise....
How to DataViz w/d3:

  1. Go toobservablehq.com/collection/@obser... find something similar to what you want.
  2. Copy/Paste the code.
  3. Futz with it until it works or you until give up.
Thread Thread
 
benjaminadk profile image
benjaminadk
Software Developer working with PHP, JavaScript and Linux for an eCommerce website. Freelance for small businesses and others in my free time.
  • Location
    California
  • Education
    BS Software Development @ WGU
  • Work
    Web Developer at L.A. Design Concepts
  • Joined
• Edited on• Edited

Observable is pretty crazy. Yet another thing to learn, but after a couple days its fun.observablehq.com/@benjaminadk/rank...

Thanks for the heads up.

CollapseExpand
 
orisa profile image
orisa
  • Work
    Front end developer
  • Joined

That amount of code for a pie chart. I think it's an overkill. Anyway thanks for sharing your knowledge.

CollapseExpand
 
the_riz profile image
Rich Winter
  • Joined

That's d3.

Are you sure you want to hide this comment? It will become hidden in your post, but will still be visible via the comment'spermalink.

For further actions, you may consider blocking this person and/orreporting abuse

Software Developer working with PHP, JavaScript and Linux for an eCommerce website. Freelance for small businesses and others in my free time.
  • Location
    California
  • Education
    BS Software Development @ WGU
  • Work
    Web Developer at L.A. Design Concepts
  • Joined

More frombenjaminadk

DEV Community

We're a place where coders share, stay up-to-date and grow their careers.

Log in Create account

[8]ページ先頭

©2009-2025 Movatter.jp