Skip to content

Instantly share code, notes, and snippets.

@volodalexey
Forked from mbostock/.block
Last active February 21, 2021 00:00
Show Gist options
  • Save volodalexey/fe2d395c9dd60f6fcab069946fb49b90 to your computer and use it in GitHub Desktop.
Save volodalexey/fe2d395c9dd60f6fcab069946fb49b90 to your computer and use it in GitHub Desktop.
D3 v5 ES6 Box Plot (optimized for React or Vue)

Box Plot Forked from https://gist.github.com/mbostock/4061502

optimized for React or Vue

It means you can invoke renderChart() as many times as you wish.

E.g. in React:

import React, { Component } from 'react'
import { renderChart, destroyChart } from '...'

class Chart extends Component {
  ...
  render () {
    return <div ref={(el) => this.$wrapper = el}/>
  }
  
  componentDidUpdate () {
    renderChart(this.$wrapper, this.props.data)
  }
  
  componentWillUnmount () {
    destroyChart(this.$wrapper)
  }
  ...
}

E.g. in Vue:

<template>
  <div ref="$wrapper"></div>
</template>

<script>
import { renderChart, destroyChart } from '...'

export default {
  name: 'chart',
  ...
  watch: {
    data () {
      renderChart(this.$refs.$wrapper, this.data)
    }
  },
  beforeDestroy () {
    destroyChart(this.$refs.$wrapper)
  }
  ...
}
</script>
<!DOCTYPE html>
<html lang='en'>
<head>
<meta charset='UTF-8'>
<title>D3 v5 ES6 Box Plot (optimized for React or Vue)</title>
<script src='//d3js.org/d3.v5.min.js'></script>
</head>
<body>
<div id='wrapper'></div>
<script>
function renderChart (wrapper, curData) {
if (!wrapper) {
return
}
const {
select: d3Select, scaleBand: d3ScaleBand,
scaleLinear: d3ScaleLinear,
quantile: d3Quantile, ascending: d3Ascending,
min: d3Min, max: d3Max, map: d3Map,
axisBottom: d3AxisBottom, axisLeft: d3AxisLeft,
transition: d3Transition,
} = d3
function boxQuartiles(d) {
return {
q_25: d3Quantile(d, .25),
q_50: d3Quantile(d, .5),
q_75: d3Quantile(d, .75),
}
}
const dataKeys = Object.keys(curData)
dataKeys.forEach(key => {
curData[key] = curData[key].sort(d3Ascending)
})
const dataEntries = Object.entries(curData)
const dataValues = dataEntries.reduce((memo, item) => {
return memo.concat(item[1])
}, [])
const margin = { top: 50, right: 0, bottom: 50, left: 30 }
const width = 800 - margin.left - margin.right
const height = 500 - margin.top - margin.bottom
const gridSize = Math.floor(width / dataKeys.length)
const barWidth = gridSize / 2
const boxElementStroke = '#82cef5'
const boxElementStrokeWidth = '2'
// Prepare the data for the box plots
let boxPlotData = [];
for (const [key, values] of dataEntries) { // http://bl.ocks.org/jensgrubert/7789216
let record = {}
record["key"] = key
record["counts"] = values
record['quartiles'] = boxQuartiles(values)
const q1 = record.quartiles.q_25
const q3 = record.quartiles.q_75
const iqr = (q3 - q1) * 1.5
const irq1 = q1 - iqr
const irq3 = q3 + iqr
let outliers = []
let normalValues = []
for (const value of values) {
if (value < irq1) {
outliers.push(value)
} else if (value > irq3) {
outliers.push(value)
} else {
normalValues.push(value)
}
}
record['outliers'] = outliers
const minNormalValue = d3Min(normalValues)
const maxNormalValue = d3Max(normalValues)
record['whiskers'] = {
min: minNormalValue ? minNormalValue : d3Min([record.quartiles.q_25, record.quartiles.q_50, record.quartiles.q_75]),
max: maxNormalValue ? maxNormalValue : d3Max([record.quartiles.q_25, record.quartiles.q_50, record.quartiles.q_75]),
}
record["color"] = '#ffffff'
boxPlotData.push(record)
}
// Setup the svg and group we will draw the box plot in
const svgData = d3Select(wrapper).selectAll('svg').data(['dummy data'])
const svgEnter = svgData.enter().append('svg')
svgEnter.attr("width", width + margin.left + margin.right)
svgEnter.attr("height", height + margin.top + margin.bottom)
svgEnter.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")")
.attr('class', 'box-plot')
const svgMerge = svgData.merge(svgEnter)
const gMerge = svgMerge.selectAll('g.box-plot')
// Compute an ordinal xScale for the keys in boxPlotData
const xScale = d3ScaleBand()
.domain(dataKeys)
.rangeRound([0, dataKeys.length * gridSize])
const xAxis = d3AxisBottom(xScale)
const t = d3Transition().duration(1000)
const horAxisData = gMerge.selectAll('.x').data(['dummy data'])
const horAxisEnter = horAxisData.enter().append("g").attr("class", "x axis")
const horAxis = horAxisData.merge(horAxisEnter)
horAxis
.attr('transform', `translate(0, ${height})`)
.transition(t).call(xAxis)
// Compute a global y scale based on the global counts
const min = d3Min(dataValues)
const max = d3Max(dataValues)
const yScale = d3ScaleLinear()
.domain([max, min])
.range([0, height])
const yAxis = d3AxisLeft(yScale)
.tickSizeInner(-dataKeys.length * gridSize)
.tickSizeOuter(0)
const leftAxisData = gMerge.selectAll('.y').data(['dummy data'])
const leftAxisEnter = leftAxisData.enter().append("g")
.attr("class", "y axis")
const leftAxis = leftAxisData.merge(leftAxisEnter).transition(t).call(yAxis)
leftAxis.selectAll('.tick').attr("stroke", "#777").attr("stroke-dasharray", "2,2")
const tooltipData = d3Select(wrapper)
.selectAll('div').data(['dummy data'])
const tooltipEnter = tooltipData.enter()
.append('div')
.attr('class', 'tooltip')
.style('background', '#ffffff')
.style('color', '#000000')
.style('display', 'none')
.style('top', 0)
.style('left', 0)
.style('padding', '10px')
.style('position', 'absolute')
const tooltipMerge = tooltipData.merge(tooltipEnter)
const boxElementData = gMerge.selectAll('.box-element').data(boxPlotData)
boxElementData.exit().remove()
const boxElementEnter = boxElementData.enter().append('g')
.attr('class', 'box-element')
const boxElementMerge = boxElementData.merge(boxElementEnter)
boxElementMerge.on('mouseover', (d) => {
tooltipMerge
.html(`<div>
<span>Minimum: ${d.whiskers.min}</span><br />
<span>Lower quartile: ${d.quartiles.q_25}</span><br />
<span>Median: ${d.quartiles.q_50}</span><br />
<span>Upper quartile: ${d.quartiles.q_75}</span><br />
<span>Maximum: ${d.whiskers.max}</span><br />
${d.outliers.map(ov => `<span>Outlier: ${ov}</span>`).join('<br />')}
</div>`)
.style('box-shadow', `0 0 5px #82cef5`)
.style('border', `1px solid #82cef5`)
.style('display', 'block')
})
.on('mousemove', () => {
tooltipMerge
.style('top', (d3.event.layerY + 10) + 'px') // always 10px below the cursor
.style('left', (d3.event.layerX + 10) + 'px'); // always 10px to the right of the mouse
})
.on('mouseout', () => {
tooltipMerge
.style('display', 'none')
})
// horizontal line
boxElementEnter.append("line")
.attr("stroke", boxElementStroke)
.attr("stroke-width", boxElementStrokeWidth)
.attr('stroke-dasharray', '3, 3')
.attr("fill", "none")
.attr('class', 'vertical-line')
.attr("x1", (d) => xScale(d.key) + gridSize/2)
.attr("x2", (d) => xScale(d.key) + gridSize/2)
boxElementMerge.select('.vertical-line').transition(t)
.attr("y1", (d) => yScale(d.whiskers.min))
.attr("y2", (d) => yScale(d.whiskers.max))
const xCor = gridSize / 2 - barWidth / 2
// rect
boxElementEnter.append("rect")
.attr("width", barWidth)
.attr("fill", (d) => d.color)
.attr("stroke", boxElementStroke)
.attr("stroke-width", boxElementStrokeWidth)
.attr('class', 'box-quartiles')
.attr("x", (d) => xScale(d.key) + xCor)
boxElementMerge.select('.box-quartiles').transition(t)
.attr("height", (d) => Math.abs(yScale(d.quartiles.q_75) - yScale(d.quartiles.q_25)))
.attr("y", (d) => yScale(Math.max(d.quartiles.q_75, d.quartiles.q_25)))
// Top whisker
boxElementEnter.append("line")
.attr("stroke", boxElementStroke)
.attr("stroke-width", boxElementStrokeWidth)
.attr("fill", "none")
.attr('class', 'top-whisker')
.attr("x1", (d) => xScale(d.key) + xCor + barWidth/4)
.attr("x2", (d) => xScale(d.key) + xCor + barWidth - barWidth/4)
boxElementMerge.select('.top-whisker').transition(t)
.attr("y1", (d) => yScale(d.whiskers.min))
.attr("y2", (d) => yScale(d.whiskers.min))
// Median line
boxElementEnter.append("line")
.attr("stroke", boxElementStroke)
.attr("stroke-width", boxElementStrokeWidth)
.attr("fill", "none")
.attr('class', 'median')
.attr("x1", (d) => xScale(d.key) + xCor)
.attr("x2", (d) => xScale(d.key) + xCor + barWidth)
boxElementMerge.select('.median').transition(t)
.attr("y1", (d) => yScale(d.quartiles.q_50))
.attr("y2", (d) => yScale(d.quartiles.q_50))
// Bottom whisker
boxElementEnter.append("line")
.attr("stroke", boxElementStroke)
.attr("stroke-width", boxElementStrokeWidth)
.attr("fill", "none")
.attr('class', 'bottom-whisker')
.attr("x1", (d) => xScale(d.key) + xCor + barWidth/4)
.attr("x2", (d) => xScale(d.key) + xCor + barWidth - barWidth/4)
boxElementMerge.select('.bottom-whisker').transition(t)
.attr("y1", (d) => yScale(d.whiskers.max))
.attr("y2", (d) => yScale(d.whiskers.max))
const circleData = boxElementMerge.selectAll("circle.outlier")
.data(d => d.outliers.map(v => {
return {
min: d.whiskers.min,
max: d.whiskers.max,
key: d.key,
value: v,
}
}))
circleData.exit().remove()
const circleDataEnter = circleData.enter()
.append('circle')
.attr("class", "outlier")
.attr("r", barWidth/6)
.attr('fill', 'transparent')
.attr('stroke', boxElementStroke)
.attr("cx", (d) => xScale(d.key) + gridSize/2)
circleData.merge(circleDataEnter).transition(t)
.attr("cy", (d) => yScale(d.value))
}
function destroyChart (wrapper) {
const {select: d3Select} = d3
d3Select(wrapper).selectAll('*').remove()
}
document.addEventListener('DOMContentLoaded', () => {
function getDate () {
const date = new Date(1532000000000 + Math.round(Math.random()*999999999))
return `${date.getFullYear()}-${date.getMonth() + 1}-${date.getDate()}`
}
setInterval(() => {
renderChart(document.querySelector('#wrapper'), {
"Monday": [ Math.random()*100, Math.random()*100, Math.random()*100, Math.random()*100, Math.random()*100, Math.random()*100, Math.random()*100, Math.random()*100, Math.random()*100, Math.random()*100, ],
"Tuesday": [ Math.random()*100, Math.random()*100, Math.random()*100, Math.random()*100, Math.random()*100, Math.random()*100, Math.random()*100, Math.random()*100, Math.random()*100, Math.random()*100, ],
"Wednesday": [ Math.random()*100, Math.random()*100, Math.random()*100, Math.random()*100, Math.random()*100, Math.random()*100, Math.random()*100, Math.random()*100, Math.random()*100, Math.random()*100, ],
"Thursday": [ Math.random()*100, Math.random()*100, Math.random()*100, Math.random()*100, Math.random()*100, Math.random()*100, Math.random()*100, Math.random()*100, Math.random()*100, Math.random()*100, ],
"Friday": [ Math.random()*100, Math.random()*100, Math.random()*100, Math.random()*100, Math.random()*100, Math.random()*100, Math.random()*100, Math.random()*100, Math.random()*100, Math.random()*100, ],
"Saturday": [ Math.random()*100, Math.random()*100, Math.random()*100, Math.random()*100, Math.random()*100, Math.random()*100, Math.random()*100, Math.random()*100, Math.random()*100, Math.random()*100, ],
"Sunday": [ Math.random()*100, Math.random()*100, Math.random()*100, Math.random()*100, Math.random()*100, Math.random()*100, Math.random()*100, Math.random()*100, Math.random()*100, Math.random()*100, ],
})
}, 5000)
renderChart(document.querySelector('#wrapper'), {
"Monday": [ Math.random()*100, Math.random()*100, Math.random()*100, Math.random()*100, Math.random()*100, Math.random()*100, Math.random()*100, Math.random()*100, Math.random()*100, Math.random()*100, ],
"Tuesday": [ Math.random()*100, Math.random()*100, Math.random()*100, Math.random()*100, Math.random()*100, Math.random()*100, Math.random()*100, Math.random()*100, Math.random()*100, Math.random()*100, ],
"Wednesday": [ Math.random()*100, Math.random()*100, Math.random()*100, Math.random()*100, Math.random()*100, Math.random()*100, Math.random()*100, Math.random()*100, Math.random()*100, Math.random()*100, ],
"Thursday": [ Math.random()*100, Math.random()*100, Math.random()*100, Math.random()*100, Math.random()*100, Math.random()*100, Math.random()*100, Math.random()*100, Math.random()*100, Math.random()*100, ],
"Friday": [ Math.random()*100, Math.random()*100, Math.random()*100, Math.random()*100, Math.random()*100, Math.random()*100, Math.random()*100, Math.random()*100, Math.random()*100, Math.random()*100, ],
"Saturday": [ Math.random()*100, Math.random()*100, Math.random()*100, Math.random()*100, Math.random()*100, Math.random()*100, Math.random()*100, Math.random()*100, Math.random()*100, Math.random()*100, ],
"Sunday": [ Math.random()*100, Math.random()*100, Math.random()*100, Math.random()*100, Math.random()*100, Math.random()*100, Math.random()*100, Math.random()*100, Math.random()*100, Math.random()*100, ],
})
})
</script>
</body>
</html>
@ravikumarpitla
Copy link

Hi Could you please provide the data(data format) that you are passing it as props.data
Thanks,
Ravi.

@volodalexey
Copy link
Author

volodalexey commented Mar 23, 2020

Hi Ravi,
this should work:

this.props.data = {
        "Monday": [ Math.random()*100, Math.random()*100, Math.random()*100, Math.random()*100, Math.random()*100, Math.random()*100, Math.random()*100, Math.random()*100, Math.random()*100, Math.random()*100,  ],
        "Tuesday": [ Math.random()*100, Math.random()*100, Math.random()*100, Math.random()*100, Math.random()*100, Math.random()*100, Math.random()*100, Math.random()*100, Math.random()*100, Math.random()*100,  ],
        "Wednesday": [ Math.random()*100, Math.random()*100, Math.random()*100, Math.random()*100, Math.random()*100, Math.random()*100, Math.random()*100, Math.random()*100, Math.random()*100, Math.random()*100,  ],
        "Thursday": [ Math.random()*100, Math.random()*100, Math.random()*100, Math.random()*100, Math.random()*100, Math.random()*100, Math.random()*100, Math.random()*100, Math.random()*100, Math.random()*100,  ],
        "Friday": [ Math.random()*100, Math.random()*100, Math.random()*100, Math.random()*100, Math.random()*100, Math.random()*100, Math.random()*100, Math.random()*100, Math.random()*100, Math.random()*100,  ],
        "Saturday": [ Math.random()*100, Math.random()*100, Math.random()*100, Math.random()*100, Math.random()*100, Math.random()*100, Math.random()*100, Math.random()*100, Math.random()*100, Math.random()*100,  ],
        "Sunday": [ Math.random()*100, Math.random()*100, Math.random()*100, Math.random()*100, Math.random()*100, Math.random()*100, Math.random()*100, Math.random()*100, Math.random()*100, Math.random()*100,  ],
    };

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment