Skip to content

Instantly share code, notes, and snippets.

@bwswedberg
Last active March 11, 2022 05:40
Show Gist options
  • Save bwswedberg/f76d2634592585c4e0a9 to your computer and use it in GitHub Desktop.
Save bwswedberg/f76d2634592585c4e0a9 to your computer and use it in GitHub Desktop.
Time Wheel

This is a time wheel for periodicity selections. I needed one for a large scale app, and decided to post this simple version.

<!DOCTYPE html>
<meta charset="UTF-8">
<script src="https://d3js.org/d3.v3.min.js" charset="utf-8"></script>
<script src="ring.js"></script>
<script src="timewheel.js"></script>
<style>
#timewheel {
position: relative;
width: 100%;
height: 300px;
padding: 100px 0;
}
.time-wheel-label {
position: absolute;
bottom: 20px;
left: 20px;
}
.arc > text {
pointer-events: none;
}
.arc > path {
cursor: pointer;
fill: #FAFAFA;
}
.arc.enabled > path,
.arc > path:hover {
fill: #FEF1B5;
}
.arc > path:active {
fill: #FFEC8B;
}
</style>
<body>
<div id="timewheel"></div>
<script>
// Build the data set. Details outside of time wheel because sometimes we
// don't want to be tied to the gregorian calendar (e.g. Islamic calendar).
var getData = function () {
var getRange = function (max) {
var array = [];
for (var i = 1; i <= max; i += 1) {
array.push(i);
}
return array;
};
var monthsInYear = {
label: 'Month in Year',
data: [
{short: 'J', long: 'January'},
{short: 'F', long: 'February'},
{short: 'M', long: 'March'},
{short: 'A', long: 'April'},
{short: 'M', long: 'May'},
{short: 'J', long: 'June'},
{short: 'J', long: 'July'},
{short: 'A', long: 'August'},
{short: 'S', long: 'September'},
{short: 'O', long: 'October'},
{short: 'N', long: 'Novemeber'},
{short: 'D', long: 'December'}
]
};
var daysInWeek = {
label: 'Day in Week',
data: [
{short: 'M', long: 'Monday'},
{short: 'T', long: 'Tuesday'},
{short: 'W', long: 'Wednesday'},
{short: 'T', long: 'Thursday'},
{short: 'F', long: 'Friday'},
{short: 'S', long: 'Saturday'},
{short: 'S', long: 'Sunday'}
]
};
var daysInMonthData = getRange(31).map(function (num) {
return {short: num.toString(), long: num.toString()};
});
var daysInMonth = {
label: 'Day in Month',
data: daysInMonthData
};
return [daysInMonth, monthsInYear, daysInWeek];
};
// Make the wheel happen
var tw = new timewheel.TimeWheel(document.getElementById('timewheel'));
tw.build(getData());
</script>
</body>
</html>
(function (timewheel, d3) {
timewheel.Ring = function (svg, label, centerX, centerY) {
this.svg = svg;
this.label = label;
this.position = {centerX: centerX, centerY: centerY};
this.data = undefined;
this.ring = undefined;
this.listeners = [];
};
timewheel.Ring.prototype.build = function (ringData, innerRadius, outerRadius) {
var pie = d3.layout.pie()
.sort(null) // preserve init order
.value(function () {return 1; }), // equal arcs
arcGroup,
that = this;
this.data = pie(ringData).map(function (v) {
v.innerRadius = innerRadius;
v.outerRadius = outerRadius;
v.data.enabled = false;
return v;
});
this.ring = this.svg.append('g')
.attr('transform', 'translate(' + this.position.centerX + ',' + this.position.centerY + ')')
.classed('ring', true);
arcGroup = this.ring.selectAll('g')
.data(this.data)
.enter().append('g')
.classed({'enabled': false, 'arc': true})
.on('mouseover', function (d) {
that.notifyListeners('onMouseOver', {
context: that,
ring: that,
data: d.data
});
})
.on('click', function (d) {
that.notifyListeners('onMouseClick', {
context: that,
ring: that,
data: d.data
});
});
arcGroup.append('path')
.attr('stroke', '#000')
.attr('fill', '#FFF');
arcGroup.append('text')
.text(function (d) { return d.data.short; })
.style('text-anchor', 'middle')
.attr('font-size', 0);
this.updateRing();
};
timewheel.Ring.prototype.updateRing = function (isBigOne) {
var arc = d3.svg.arc(),
g = this.ring.selectAll('g')
.data(this.data),
textFactor = this.data[0].outerRadius - this.data[0].innerRadius,
textFactor2 = isBigOne ? 0.015 : 0.03;
g.select('path')
.transition()
.delay(200)
.duration(200)
.attr('d', arc);
g.select('text')
.transition()
.delay(200)
.duration(200)
.attr('transform', function (d) {
return 'translate(' + arc.centroid(d) + ')';
})
.attr('font-size', function (d) {
return textFactor * textFactor2 + 'em';
})
.attr('dy', '.35em');
};
timewheel.Ring.prototype.getRadius = function () {
return {
inner: this.data[0].innerRadius,
outer: this.data[0].outerRadius
};
};
timewheel.Ring.prototype.updateSize = function (innerRadius, outerRadius, isBigOne) {
// update the data first
this.data.forEach(function (item, index) {
this.data[index].innerRadius = innerRadius;
this.data[index].outerRadius = outerRadius;
}, this);
// update the actual ring
this.updateRing(isBigOne);
};
timewheel.Ring.prototype.setIsEnabledArc = function (filterFunction, isEnabled) {
var item = this.ring.selectAll('g')
.filter(filterFunction)
.datum(function (d) {
d.data.enabled = isEnabled;
return d;
})
.classed('enabled', isEnabled);
};
timewheel.Ring.prototype.registerListener = function (listenerObj) {
this.listeners.push(listenerObj);
};
timewheel.Ring.prototype.notifyListeners = function (callbackStr, event) {
this.listeners.forEach(function (listenerObj) {
listenerObj[callbackStr].call(listenerObj.context, event);
});
};
}(timewheel = window.timewheel || {}, d3));
(function (timewheel, d3) {
timewheel.TimeWheel = function (container) {
this.wrapper = container;
this.svg = undefined;
this.viewBox = {width: 100, height: 100};
this.radius = {
max: this.viewBox.width / 2.0,
min: (this.viewBox.width / 2.0) * 0.2
};
this.labelDiv = undefined;
this.rings = [];
this.bigRing = null;
};
timewheel.TimeWheel.prototype.build = function (data) {
var that = this,
scale = 0.98;
this.svg = d3.select(this.wrapper).append('svg')
.attr('width', '100%')
.attr('height', '100%')
.attr('viewBox', [0, 0, this.viewBox.width, this.viewBox.height].join(' '))
.on('mouseout', function () {
// A more sophisticated function to reliably check if mouse outside of or inside
// of the wheel/annulus
var xy = d3.mouse(this),
centerX = that.viewBox.width / 2.0,
centerY = that.viewBox.height / 2.0,
distance = Math.sqrt(Math.pow(centerX - xy[0], 2) + Math.pow(centerY - xy[1], 2));
if (distance > that.radius.max * scale || distance < that.radius.min * scale) {
that.changeRingSize(null);
that.updateLabel(null, null);
}
})
.append('g')
.attr('transform', 'translate(1,1) scale(' + scale + ')');
var ringRadius = (this.radius.max - this.radius.min) / data.length;
data.forEach(function (item, index) {
var twr = new timewheel.Ring(this.svg, item.label, this.viewBox.width / 2.0, this.viewBox.height / 2.0),
endRadius = this.radius.max - (ringRadius * index),
startRadius = endRadius - ringRadius;
twr.registerListener(this.createRingListener());
twr.build(item.data, startRadius, endRadius);
this.rings.push(twr);
}, this);
this.labelDiv = document.createElement('div');
this.labelDiv.className = 'time-wheel-label';
this.labelDiv.appendChild(document.createElement('span'));
this.labelDiv.appendChild(document.createElement('span'));
this.wrapper.appendChild(this.labelDiv);
};
timewheel.TimeWheel.prototype.changeRingSize = function (enlargeRing) {
var scaleFactor = enlargeRing ? 2 : 0,
ringRadius = (this.radius.max - this.radius.min) / (this.rings.length + scaleFactor),
endRadius = this.radius.max,
startRadius;
if (enlargeRing !== this.bigRing) {
this.bigRing = enlargeRing;
this.rings.forEach(function (someRing, index) {
if (someRing === enlargeRing) {
startRadius = endRadius - ((scaleFactor + 1) * ringRadius);
someRing.updateSize(startRadius, endRadius, true);
} else {
startRadius = endRadius - ringRadius;
someRing.updateSize(startRadius, endRadius, false);
}
endRadius = startRadius;
}, this);
}
};
timewheel.TimeWheel.prototype.createRingListener = function () {
return {
context: this,
onMouseOver: function (event) {
this.changeRingSize(event.ring);
this.updateLabel(event.ring.label, event.data.long);
},
onMouseClick: function (event) {
event.ring.setIsEnabledArc(function (d) {
return d.data.long === event.data.long;
}, !event.data.enabled);
console.log('clicked', event);
}
};
};
timewheel.TimeWheel.prototype.updateLabel = function (ringLabel, arcLabel) {
if (ringLabel && arcLabel) {
this.labelDiv.children[0].innerHTML = ringLabel + ': ';
this.labelDiv.children[1].innerHTML = arcLabel;
} else {
this.labelDiv.children[0].innerHTML = '';
this.labelDiv.children[1].innerHTML = '';
}
};
}(timewheel = window.timewheel || {}, d3));
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment