This is a time wheel for periodicity selections. I needed one for a large scale app, and decided to post this simple version.
Last active
March 11, 2022 05:40
-
-
Save bwswedberg/f76d2634592585c4e0a9 to your computer and use it in GitHub Desktop.
Time Wheel
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
screenshots |
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"> | |
<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> |
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
(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)); |
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
(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