Skip to content

Instantly share code, notes, and snippets.

@mbostock
Last active May 19, 2023 19:28
Show Gist options
  • Save mbostock/c150b717e18d387e1b98 to your computer and use it in GitHub Desktop.
Save mbostock/c150b717e18d387e1b98 to your computer and use it in GitHub Desktop.
Polar Clock III
license: gpl-3.0
height: 960
<!DOCTYPE html>
<meta charset="utf-8">
<style>
body {
background: #222;
margin: auto;
width: 960px;
}
.field-track,
.field-arm {
fill: none;
stroke: #000;
stroke-width: 1.5px;
}
.field-tick {
transition: opacity 750ms linear;
}
.field-tick:not(.field-tick--active) circle,
.field-tick:not(.field-tick--active):first-of-type text {
fill: #222 !important;
}
.field-tick:not(.field-tick--active):first-of-type circle {
fill: #000 !important;
}
.field-tick--disabled {
opacity: 0;
}
.field-tick circle,
.field-tick text {
transition: fill 250ms linear;
transition-delay: 400ms;
}
.field-tick text {
font: 700 14px "Helvetica Neue";
text-anchor: middle;
}
</style>
<svg width="960" height="960"></svg>
<script src="//d3js.org/d3.v4.0.0-alpha.28.min.js"></script>
<script>
var svg = d3.select("svg"),
width = +svg.attr("width"),
height = +svg.attr("height"),
radius = Math.min(width, height) / 1.9,
armRadius = radius / 22,
dotRadius = armRadius - 6;
var duration = 750,
now = new Date(Date.now() + 2 * duration);
var pi = Math.PI,
tau = pi * 2;
var fields = [
{radius: 0.2 * radius, interval: d3.timeYear, subinterval: d3.timeMonth, format: d3.timeFormat("%b")},
{radius: 0.3 * radius, interval: d3.timeMonth, subinterval: d3.timeDay, format: d3.timeFormat("%d")},
{radius: 0.4 * radius, interval: d3.timeWeek, subinterval: d3.timeDay, format: d3.timeFormat("%a")},
{radius: 0.6 * radius, interval: d3.timeDay, subinterval: d3.timeHour, format: d3.timeFormat("%H")},
{radius: 0.7 * radius, interval: d3.timeHour, subinterval: d3.timeMinute, format: d3.timeFormat("%M")},
{radius: 0.8 * radius, interval: d3.timeMinute, subinterval: d3.timeSecond, format: d3.timeFormat("%S")}
];
var color = d3.scaleRainbow()
.domain([0, tau]);
var arcArm = d3.arc()
.startAngle(function(d) { return armRadius / d.radius; })
.endAngle(function(d) { return -pi - armRadius / d.radius; })
.innerRadius(function(d) { return d.radius - armRadius; })
.outerRadius(function(d) { return d.radius + armRadius; })
.cornerRadius(armRadius);
var field = svg.append("g")
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")")
.selectAll(".field")
.data(fields)
.enter().append("g")
.attr("class", "field");
field.append("circle")
.attr("class", "field-track")
.attr("r", function(d) { return d.radius; });
var fieldTick = field.selectAll(".field-tick")
.data(function(d) {
var date = d.interval(new Date(2000, 0, 1));
d.range = d.subinterval.range(date, d.interval.offset(date, 1));
return d.range.map(function(t) { return {time: t, field: d}; });
})
.enter().append("g")
.attr("class", "field-tick")
.attr("transform", function(d, i) {
var angle = i / d.field.range.length * tau - pi / 2;
return "translate(" + Math.cos(angle) * d.field.radius + "," + Math.sin(angle) * d.field.radius + ")";
});
fieldTick.append("circle")
.attr("r", dotRadius - 3)
.style("fill", function(d, i) { return color(i / d.field.range.length * tau); });
fieldTick.append("text")
.attr("dy", "0.35em")
.text(function(d) { return d.field.format(d.time).slice(0, 2); });
var fieldArm = field.append("path")
.attr("class", "field-arm")
.attr("transform", "rotate(0)")
.attr("d", function(d) {
return arcArm(d)
+ "M0," + (dotRadius - d.radius)
+ "a" + dotRadius + "," + dotRadius + " 0 0,1 0," + -dotRadius * 2
+ "a" + dotRadius + "," + dotRadius + " 0 0,1 0," + dotRadius * 2;
});
(function tick() {
var now = new Date,
then = new Date(+now + duration),
next = d3.timeSecond.offset(d3.timeSecond(then), 1),
delay = next - duration - now;
// Skip ahead a second if there’s not time for this transition.
if (delay < duration) delay += 1000, then = next;
fieldArm.transition()
.duration(duration)
.each(function(d) {
var start = d.interval(then);
d.activeLength = d.subinterval.count(start, d.interval.offset(start, 1));
d.activeIndex = d.subinterval.count(start, then);
d.angle = d.activeIndex / d.range.length * tau;
})
.attr("transform", function(d) { return "rotate(" + d.angle / pi * 180 + ")"; })
.style("fill", function(d) { return color(d.angle); });
fieldTick
.classed("field-tick--disabled", function(d, i) { return i >= d.field.activeLength; })
.classed("field-tick--active", function(d, i) { return i === d.field.activeIndex; });
setTimeout(tick, delay);
})();
</script>
@danse
Copy link

danse commented Feb 25, 2016

I am amazed by the expressiveness you reached with D3. Anyway, this is taking a lot of processor time on Firefox. Do you think that it is normal?

@danse
Copy link

danse commented Feb 25, 2016

From the point of view of conveying information, i think that filling half of the circle kind of defeats the idea of having a circle.

Better explained: what is cool in using a circle is an immediate communication of the status of progress within a cycle, for example i am close to the end of the month, the beginning of the year, the half of a day, etcetera. This was really nice in your first version. I like better the transition animation in this version, and the placeholders are great, but it does not communicate the current time coordinates as effectively as the first one

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