|
<!DOCTYPE html> |
|
<html lang='en'> |
|
<head> |
|
<meta charset='UTF-8'> |
|
<title>D3 v5 ES6 Bars Chart (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, min: d3Min, max: d3Max, |
|
scaleBand: d3ScaleBand, scaleLinear: d3ScaleLinear, |
|
axisBottom: d3AxisBottom, axisLeft: d3AxisLeft, |
|
} = d3 |
|
|
|
const width = 500 |
|
const height = 500 |
|
const upperFill = '#f05757' |
|
const lowerFill = '#8abe6e' |
|
const leftAxisStroke = '#777777' |
|
const DURATION = 1000 |
|
const {baseLine, data} = curData |
|
const minValue = Math.min(d3Min(data, d => d.value), baseLine) |
|
const maxValue = Math.max(d3Max(data, d => d.value), baseLine) |
|
const margin = { top: 20, right: 20, bottom: 80, left: 60 } |
|
const innerWidth = width - margin.left - margin.right |
|
const innerHeight = height - margin.top - margin.bottom |
|
// https://groups.google.com/forum/#!topic/d3-js/Rlv0O8xsFhs |
|
const svgData = d3Select(wrapper).selectAll('svg').data(['dummy data']) |
|
const svgEnter = svgData.enter().append('svg') |
|
svgEnter.attr('width', width) |
|
svgEnter.attr('height', height) |
|
const gEnter = svgEnter.append("g") |
|
.attr("transform", "translate(" + margin.left + "," + margin.top + ")") |
|
.attr('class', 'bars-plot') |
|
|
|
const svgMerge = svgData.merge(svgEnter) |
|
const gMerge = svgMerge.selectAll('g.bars-plot') |
|
|
|
const x = d3ScaleBand().range([0, innerWidth]) |
|
const y = d3ScaleLinear().range([innerHeight, 0]) |
|
|
|
// Scale the range of the data in the domains |
|
x.domain(data.map(d => d.label)); |
|
y.domain([minValue, maxValue]); |
|
|
|
// append the rectangles for the bar chart |
|
const barWidth = x.bandwidth() / 2 |
|
const barXCorrect = barWidth / 2 |
|
|
|
const lowerBarData = gMerge.selectAll('rect.bar-lower').data(data) |
|
const lowerBarEnter = lowerBarData.enter().append("rect") |
|
.attr("class", "bar-lower") |
|
.attr("fill", lowerFill) |
|
const lowerBarMerge = lowerBarData.merge(lowerBarEnter) |
|
|
|
lowerBarMerge |
|
.attr("x", d => x(d.label) + barXCorrect) |
|
.attr("width", barWidth) |
|
.transition() |
|
.duration(DURATION) |
|
.attr('y', d => y(d.value > baseLine ? baseLine : d.value)) |
|
.attr('height', d => { |
|
const height = innerHeight - y(d.value > baseLine ? baseLine : d.value) |
|
return height < 0 ? 0 : height |
|
}) |
|
|
|
const upperBarData = gMerge.selectAll('rect.bar-upper').data(data.filter(d => d.value > baseLine)) |
|
const upperBarEnter = upperBarData.enter().append("rect") |
|
.attr("class", "bar-upper") |
|
.attr("fill", upperFill) |
|
const upperBarMerge = upperBarData.merge(upperBarEnter) |
|
upperBarData.exit().remove() |
|
|
|
upperBarMerge |
|
.attr("x", d => x(d.label) + barXCorrect) |
|
.attr("width", barWidth) |
|
.transition() |
|
.duration(DURATION) |
|
.attr('y', d => y(d.value)) |
|
.attr("height", d => { |
|
const height = (innerHeight - y(d.value)) - (innerHeight - y(baseLine)) |
|
return height < 0 ? 0 : height |
|
}) |
|
|
|
// add the x Axis |
|
gEnter.append('g') |
|
.attr('class', 'x') |
|
gMerge.select('g.x') |
|
.attr("transform", "translate(0," + innerHeight + ")") |
|
.transition() |
|
.duration(DURATION) |
|
.call(d3AxisBottom(x)) |
|
gMerge.selectAll('g.x text') |
|
.attr('y', 0) |
|
.attr('x', -9) |
|
.attr('dy', '-.35em') |
|
.attr('transform', 'rotate(-45)') |
|
.style('text-anchor', 'end'); |
|
|
|
// add the y Axis |
|
gEnter.append("g") |
|
.attr('class', 'y') |
|
gMerge |
|
.select('g.y') |
|
.transition() |
|
.duration(DURATION) |
|
.call(d3AxisLeft(y) |
|
.tickSizeInner(-innerWidth) |
|
.tickValues([minValue, baseLine, maxValue])) |
|
.selectAll('text') |
|
.attr('x', -10); |
|
gMerge.selectAll('g.y .tick') |
|
.attr("stroke", leftAxisStroke) |
|
.attr("stroke-dasharray", "2,2"); |
|
} |
|
|
|
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'), { |
|
data: [ |
|
{ value: Math.random()*1000, label: getDate() }, |
|
{ value: Math.random()*1000, label: getDate() }, |
|
{ value: Math.random()*1000, label: getDate() }, |
|
{ value: Math.random()*1000, label: getDate() }, |
|
{ value: Math.random()*1000, label: getDate() }, |
|
{ value: Math.random()*1000, label: getDate() }, |
|
{ value: Math.random()*1000, label: getDate() }, |
|
{ value: Math.random()*1000, label: getDate() }, |
|
{ value: Math.random()*1000, label: getDate() }, |
|
{ value: Math.random()*1000, label: getDate() }, |
|
], |
|
baseLine: Math.random()*1000, |
|
}) |
|
}, 5000) |
|
|
|
|
|
renderChart(document.querySelector('#wrapper'), { |
|
data: [ |
|
{ value: Math.random()*1000, label: getDate() }, |
|
{ value: Math.random()*1000, label: getDate() }, |
|
{ value: Math.random()*1000, label: getDate() }, |
|
{ value: Math.random()*1000, label: getDate() }, |
|
{ value: Math.random()*1000, label: getDate() }, |
|
{ value: Math.random()*1000, label: getDate() }, |
|
{ value: Math.random()*1000, label: getDate() }, |
|
{ value: Math.random()*1000, label: getDate() }, |
|
{ value: Math.random()*1000, label: getDate() }, |
|
{ value: Math.random()*1000, label: getDate() }, |
|
], |
|
baseLine: Math.random()*1000, |
|
}) |
|
}) |
|
</script> |
|
</body> |
|
</html> |