This example is a rewrite of the Condegram Spiral Plot example from Arpit Narechania to show the usage of d3-template. An extra update with a transition is added as a small feature.
Last active
September 30, 2018 18:55
-
-
Save ErikOnBike/4f73fe95a6041d625a96794bc0a094b2 to your computer and use it in GitHub Desktop.
Rewrite of Arpit Narechania's Condegram Spiral Plot gist using d3-template
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
license: gpl-3.0 | |
height: 620 | |
scrolling: yes |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<meta charset="utf-8"> | |
<style> | |
body { | |
font-family: sans-serif; | |
} | |
button { | |
position: absolute; | |
right: 10px; | |
top: 10px; | |
} | |
#spiral { | |
fill: none; | |
stroke: steelblue; | |
} | |
text { | |
font-size: 10px; | |
} | |
rect { | |
stroke: none; | |
} | |
rect.selected { | |
fill: #fff; | |
stroke: #000; | |
stroke-width: 2px; | |
} | |
#tooltip { | |
background: #eee; | |
box-shadow: 0 0 5px #999; | |
color: #333; | |
font-size: 12px; | |
padding: 10px; | |
position: absolute; | |
text-align: center; | |
display: none; | |
} | |
</style> | |
<body> | |
<button>Update</button> | |
<div id="chart"> | |
<svg width="600" height="600"> | |
<g transform="translate(300,300)"> | |
<!-- Template for spiral --> | |
<g id="spiral-template"> | |
<path id="spiral" data-attr-d="{{d}}"></path> | |
</g> | |
<!-- Template for bars and text --> | |
<g id="data-template"> | |
<!-- First show text --> | |
<g data-repeat="{{firstOfMonth(d)}}"> | |
<text dy="10" text-anchor="start"> | |
<textPath xlink:href="#spiral" data-attr-startOffset='{{formatPercentage(d.textOffset)}}'>{{formatShortDate(d.date)}}</textPath> | |
</text> | |
</g> | |
<!-- Then 'bars' on top --> | |
<g data-repeat="{{d}}"> | |
<rect data-attr-x="{{d.x}}" | |
data-attr-y="{{d.y}}" | |
data-attr-width="{{barWidth}}" | |
data-attr-height="{{yScale(d.value)}}" | |
data-attr-fill="{{color(d.group)}}" | |
data-attr-transform="{{`rotate(${d.a},${d.x},${d.y})`}}"> | |
</rect> | |
</g> | |
</g> | |
</g> | |
</svg> | |
<!-- Template for tooltip --> | |
<div id="tooltip" data-style-display="{{d ? 'block' : 'none'}}"> | |
<div class="date">Date: <b>{{formatLongDate(d.date)}}</b></div> | |
<div class="value">Value: <b>{{formatTwoDecimals(d.value)}}</b></div> | |
</div> | |
</div> | |
</body> | |
<script src="https://d3js.org/d3.v4.min.js"></script> | |
<script src="https://unpkg.com/d3-template-plugin/build/d3-template.min.js"></script> | |
<script> | |
var width = 500, | |
height = 500, | |
start = 0, | |
end = 2.25, | |
numSpirals = 3, | |
margin = { top: 50, bottom: 50, left: 50, right: 50 }, | |
formatShortDate = d3.timeFormat("%b %Y"), | |
formatLongDate = d3.timeFormat("%B %d, %Y"), | |
formatPercentage = d3.format("%"), | |
formatTwoDecimals = d3.format(".2"); | |
// Calculate spiral (segments) | |
var theta = function(r) { | |
return numSpirals * Math.PI * r; | |
}; | |
var r = d3.min([ width, height ]) / 2 - 40; | |
var radius = d3.scaleLinear() | |
.domain([ start, end ]) | |
.range([ 40, r ]) | |
; | |
// Create helper function | |
function firstOfMonth(d) { | |
var timeFormat = d3.timeFormat("%b %Y"); | |
return d.reduce(function(result, item) { | |
var currentDate = timeFormat(item.date); | |
if(result.length > 0) { | |
var lastDate = timeFormat(result[result.length - 1].date); | |
if(currentDate !== lastDate) { | |
result.push(item); | |
} | |
} else { | |
result.push(item); | |
} | |
return result; | |
}, []); | |
} | |
// Render spiral onto template (using spiral path as data) | |
var points = d3.range(start, end + 0.001, (end - start) / 1000); | |
var spiral = d3.radialLine() | |
.curve(d3.curveCardinal) | |
.angle(theta) | |
.radius(radius) | |
; | |
d3.select("#spiral-template") | |
.template() | |
.render(spiral(points)) | |
; | |
var spiralPathNode = d3.select("#spiral").node(); | |
// Create data for bars | |
var spiralLength = spiralPathNode.getTotalLength(), | |
N = 365, | |
barWidth = (spiralLength / N) - 1, | |
data = [], | |
color = d3.scaleOrdinal(d3.schemeCategory10) | |
; | |
for(var i = 0; i < N; i++) { | |
var currentDate = new Date(); | |
currentDate.setDate(currentDate.getDate() + i); | |
data.push({ | |
date: currentDate, | |
value: Math.random(), | |
group: currentDate.getMonth() | |
}); | |
} | |
var timeScale = d3.scaleTime() | |
.domain(d3.extent(data, function(d) { | |
return d.date; | |
})) | |
.range([ 0, spiralLength]) | |
; | |
var yScale = d3.scaleLinear() | |
.domain([ 0, d3.max(data, function(d) { | |
return d.value; | |
})]) | |
.range([ 0, (r / numSpirals) - 30 ]) | |
; | |
// Extend the data | |
data.forEach(function(d) { | |
var linePer = timeScale(d.date), | |
posOnLine = spiralPathNode.getPointAtLength(linePer), | |
angleOnLine = spiralPathNode.getPointAtLength(linePer - barWidth) | |
; | |
d.linePer = linePer; // % distance are on the spiral | |
d.x = posOnLine.x; // x postion on the spiral | |
d.y = posOnLine.y; // y position on the spiral | |
d.a = (Math.atan2(angleOnLine.y, angleOnLine.x) * 180 / Math.PI) - 90; //angle at the spiral position | |
d.textOffset = d.linePer / spiralLength; | |
}); | |
// Create tooltip template | |
var tooltip = d3.select("#tooltip").template(); | |
// Add event handlers (before creating template) | |
d3.select("rect") | |
.on('mouseover', function(d) { | |
tooltip.render(d); | |
d3.select(this).classed("selected", true); | |
}) | |
.on('mousemove', function(d) { | |
tooltip | |
.style('top', (d3.event.layerY + 10) + 'px') | |
.style('left', (d3.event.layerX - 25) + 'px') | |
; | |
}) | |
.on('mouseout', function(d) { | |
tooltip.render(false); | |
d3.selectAll("rect").classed("selected", false); | |
}) | |
; | |
// Render the bars | |
var template = d3.select("#data-template").template(); | |
template.render(data); | |
// Add event handler for updating the bars | |
d3.select("button").on("click", function() { | |
// New random data | |
data.forEach(function(d) { | |
d.value = Math.random(); | |
}); | |
// Transition the data | |
template | |
.transition() | |
.duration(1000) | |
.render(data) | |
; | |
}); | |
</script> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment