Skip to content

Instantly share code, notes, and snippets.

@ErikOnBike
Last active September 30, 2018 18:55
Show Gist options
  • Save ErikOnBike/4f73fe95a6041d625a96794bc0a094b2 to your computer and use it in GitHub Desktop.
Save ErikOnBike/4f73fe95a6041d625a96794bc0a094b2 to your computer and use it in GitHub Desktop.
Rewrite of Arpit Narechania's Condegram Spiral Plot gist using d3-template
license: gpl-3.0
height: 620
scrolling: yes

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.

<!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