An update to Matt Magoffin’s Block 3202712 with some changes:
- Update to D3 v4
- Use data attributes to specify values
- Added Target Value Indicator
- Added tooltips using d3-tip
- Added a span with text value
license: mit |
An update to Matt Magoffin’s Block 3202712 with some changes:
// d3.tip | |
// Copyright (c) 2013 Justin Palmer | |
// ES6 / D3 v4 Adaption Copyright (c) 2016 Constantin Gavrilete | |
// Removal of ES6 for D3 v4 Adaption Copyright (c) 2016 David Gotz | |
// | |
// Tooltips for d3.js SVG visualizations | |
d3.functor = function functor(v) { | |
return typeof v === "function" ? v : function() { | |
return v; | |
}; | |
}; | |
d3.tip = function() { | |
var direction = d3_tip_direction, | |
offset = d3_tip_offset, | |
html = d3_tip_html, | |
node = initNode(), | |
svg = null, | |
point = null, | |
target = null | |
function tip(vis) { | |
svg = getSVGNode(vis) | |
point = svg.createSVGPoint() | |
document.body.appendChild(node) | |
} | |
// Public - show the tooltip on the screen | |
// | |
// Returns a tip | |
tip.show = function() { | |
var args = Array.prototype.slice.call(arguments) | |
if(args[args.length - 1] instanceof SVGElement) target = args.pop() | |
var content = html.apply(this, args), | |
poffset = offset.apply(this, args), | |
dir = direction.apply(this, args), | |
nodel = getNodeEl(), | |
i = directions.length, | |
coords, | |
scrollTop = document.documentElement.scrollTop || document.body.scrollTop, | |
scrollLeft = document.documentElement.scrollLeft || document.body.scrollLeft | |
nodel.html(content) | |
.style('position', 'absolute') | |
.style('opacity', 1) | |
.style('pointer-events', 'all') | |
while(i--) nodel.classed(directions[i], false) | |
coords = direction_callbacks[dir].apply(this) | |
nodel.classed(dir, true) | |
.style('top', (coords.top + poffset[0]) + scrollTop + 'px') | |
.style('left', (coords.left + poffset[1]) + scrollLeft + 'px') | |
return tip | |
} | |
// Public - hide the tooltip | |
// | |
// Returns a tip | |
tip.hide = function() { | |
var nodel = getNodeEl() | |
nodel | |
.style('opacity', 0) | |
.style('pointer-events', 'none') | |
return tip | |
} | |
// Public: Proxy attr calls to the d3 tip container. Sets or gets attribute value. | |
// | |
// n - name of the attribute | |
// v - value of the attribute | |
// | |
// Returns tip or attribute value | |
tip.attr = function(n, v) { | |
if (arguments.length < 2 && typeof n === 'string') { | |
return getNodeEl().attr(n) | |
} else { | |
var args = Array.prototype.slice.call(arguments) | |
d3.selection.prototype.attr.apply(getNodeEl(), args) | |
} | |
return tip | |
} | |
// Public: Proxy style calls to the d3 tip container. Sets or gets a style value. | |
// | |
// n - name of the property | |
// v - value of the property | |
// | |
// Returns tip or style property value | |
tip.style = function(n, v) { | |
// debugger; | |
if (arguments.length < 2 && typeof n === 'string') { | |
return getNodeEl().style(n) | |
} else { | |
var args = Array.prototype.slice.call(arguments); | |
if (args.length === 1) { | |
var styles = args[0]; | |
Object.keys(styles).forEach(function(key) { | |
return d3.selection.prototype.style.apply(getNodeEl(), [key, styles[key]]); | |
}); | |
} | |
} | |
return tip | |
} | |
// Public: Set or get the direction of the tooltip | |
// | |
// v - One of n(north), s(south), e(east), or w(west), nw(northwest), | |
// sw(southwest), ne(northeast) or se(southeast) | |
// | |
// Returns tip or direction | |
tip.direction = function(v) { | |
if (!arguments.length) return direction | |
direction = v == null ? v : d3.functor(v) | |
return tip | |
} | |
// Public: Sets or gets the offset of the tip | |
// | |
// v - Array of [x, y] offset | |
// | |
// Returns offset or | |
tip.offset = function(v) { | |
if (!arguments.length) return offset | |
offset = v == null ? v : d3.functor(v) | |
return tip | |
} | |
// Public: sets or gets the html value of the tooltip | |
// | |
// v - String value of the tip | |
// | |
// Returns html value or tip | |
tip.html = function(v) { | |
if (!arguments.length) return html | |
html = v == null ? v : d3.functor(v) | |
return tip | |
} | |
// Public: destroys the tooltip and removes it from the DOM | |
// | |
// Returns a tip | |
tip.destroy = function() { | |
if(node) { | |
getNodeEl().remove(); | |
node = null; | |
} | |
return tip; | |
} | |
function d3_tip_direction() { return 'n' } | |
function d3_tip_offset() { return [0, 0] } | |
function d3_tip_html() { return ' ' } | |
var direction_callbacks = { | |
n: direction_n, | |
s: direction_s, | |
e: direction_e, | |
w: direction_w, | |
nw: direction_nw, | |
ne: direction_ne, | |
sw: direction_sw, | |
se: direction_se | |
}; | |
var directions = Object.keys(direction_callbacks); | |
function direction_n() { | |
var bbox = getScreenBBox() | |
return { | |
top: bbox.n.y - node.offsetHeight, | |
left: bbox.n.x - node.offsetWidth / 2 | |
} | |
} | |
function direction_s() { | |
var bbox = getScreenBBox() | |
return { | |
top: bbox.s.y, | |
left: bbox.s.x - node.offsetWidth / 2 | |
} | |
} | |
function direction_e() { | |
var bbox = getScreenBBox() | |
return { | |
top: bbox.e.y - node.offsetHeight / 2, | |
left: bbox.e.x | |
} | |
} | |
function direction_w() { | |
var bbox = getScreenBBox() | |
return { | |
top: bbox.w.y - node.offsetHeight / 2, | |
left: bbox.w.x - node.offsetWidth | |
} | |
} | |
function direction_nw() { | |
var bbox = getScreenBBox() | |
return { | |
top: bbox.nw.y - node.offsetHeight, | |
left: bbox.nw.x - node.offsetWidth | |
} | |
} | |
function direction_ne() { | |
var bbox = getScreenBBox() | |
return { | |
top: bbox.ne.y - node.offsetHeight, | |
left: bbox.ne.x | |
} | |
} | |
function direction_sw() { | |
var bbox = getScreenBBox() | |
return { | |
top: bbox.sw.y, | |
left: bbox.sw.x - node.offsetWidth | |
} | |
} | |
function direction_se() { | |
var bbox = getScreenBBox() | |
return { | |
top: bbox.se.y, | |
left: bbox.e.x | |
} | |
} | |
function initNode() { | |
var node = d3.select(document.createElement('div')) | |
node | |
.style('position', 'absolute') | |
.style('top', 0) | |
.style('opacity', 0) | |
.style('pointer-events', 'none') | |
.style('box-sizing', 'border-box') | |
return node.node() | |
} | |
function getSVGNode(el) { | |
el = el.node() | |
if(el.tagName.toLowerCase() === 'svg') | |
return el | |
return el.ownerSVGElement | |
} | |
function getNodeEl() { | |
if(node === null) { | |
node = initNode(); | |
// re-add node to DOM | |
document.body.appendChild(node); | |
}; | |
return d3.select(node); | |
} | |
// Private - gets the screen coordinates of a shape | |
// | |
// Given a shape on the screen, will return an SVGPoint for the directions | |
// n(north), s(south), e(east), w(west), ne(northeast), se(southeast), nw(northwest), | |
// sw(southwest). | |
// | |
// +-+-+ | |
// | | | |
// + + | |
// | | | |
// +-+-+ | |
// | |
// Returns an Object {n, s, e, w, nw, sw, ne, se} | |
function getScreenBBox() { | |
var targetel = target || d3.event.target; | |
while ('undefined' === typeof targetel.getScreenCTM && 'undefined' === targetel.parentNode) { | |
targetel = targetel.parentNode; | |
} | |
var bbox = {}, | |
matrix = targetel.getScreenCTM(), | |
tbbox = targetel.getBBox(), | |
width = tbbox.width, | |
height = tbbox.height, | |
x = tbbox.x, | |
y = tbbox.y | |
point.x = x | |
point.y = y | |
bbox.nw = point.matrixTransform(matrix) | |
point.x += width | |
bbox.ne = point.matrixTransform(matrix) | |
point.y += height | |
bbox.se = point.matrixTransform(matrix) | |
point.x -= width | |
bbox.sw = point.matrixTransform(matrix) | |
point.y -= height / 2 | |
bbox.w = point.matrixTransform(matrix) | |
point.x += width | |
bbox.e = point.matrixTransform(matrix) | |
point.x -= width / 2 | |
point.y -= height / 2 | |
bbox.n = point.matrixTransform(matrix) | |
point.y += height | |
bbox.s = point.matrixTransform(matrix) | |
return bbox | |
} | |
return tip | |
}; |
<!DOCTYPE html> | |
<html> | |
<head> | |
<meta charset="utf-8"> | |
<meta name="viewport" content="width=320" /> | |
<meta name="apple-mobile-web-app-capable" content="yes" /> | |
<title>Gauge</title> | |
<script src="https://d3js.org/d3.v4.min.js" type="text/javascript"></script> | |
<script src="d3-tip.js"></script> | |
<script src="https://code.jquery.com/jquery-2.2.4.min.js" type="text/javascript"></script> | |
<style> | |
body { | |
font-family: Helvetica, Arial, sans-serif; | |
} | |
.gauge-container { | |
margin: 60px; | |
text-align: center; | |
float: left; | |
} | |
.power-gauge g.arc { | |
fill: steelblue; | |
fill-opacity: 0.6; | |
} | |
.power-gauge g.pointer { | |
fill: #666; | |
stroke: #fff; | |
} | |
.power-gauge g.label text { | |
text-anchor: middle; | |
font-size: 14px; | |
font-weight: bold; | |
fill: #666; | |
} | |
.d3-tip { | |
line-height: 1; | |
font-weight: bold; | |
padding: 12px; | |
background: rgba(0, 0, 0, 0.8); | |
color: #fff; | |
border-radius: 2px; | |
text-align: center; | |
} | |
/* Creates a small triangle extender for the tooltip */ | |
.d3-tip:after { | |
box-sizing: border-box; | |
display: inline; | |
font-size: 10px; | |
width: 100%; | |
line-height: 1; | |
color: rgba(0, 0, 0, 0.8); | |
content: "\25BC"; | |
position: absolute; | |
text-align: center; | |
} | |
/* Style northward tooltips differently */ | |
.d3-tip.n:after { | |
margin: -1px 0 0 0; | |
top: 100%; | |
left: 0; | |
} | |
</style> | |
</head> | |
<body> | |
<div class="gauge-container"> | |
<div class="power-gauge" data-value="20" data-ticks="[30, 70]" data-target="50"></div> | |
<span class="text-muted"></span> | |
</div> | |
<div class="gauge-container"> | |
<div class="power-gauge" data-value="30" data-ticks="[30, 60]" data-target="45"></div> | |
<span class="text-muted"></span> | |
</div> | |
<script> | |
var gauge = function (container, configuration) { | |
var that = {}; | |
var config = { | |
size: 200, | |
clipWidth: 200, | |
clipHeight: 110, | |
ringInset: 20, | |
ringWidth: 20, | |
pointerWidth: 10, | |
pointerTailLength: 5, | |
pointerHeadLengthPercent: 0.9, | |
minValue: 0, | |
maxValue: 10, | |
minAngle: -90, | |
maxAngle: 90, | |
transitionMs: 750, | |
majorTicks: 5, | |
labelFormat: d3.format('.2'), | |
labelInset: 10, | |
arcColorFn: d3.interpolateHsl(d3.rgb('#e8e2ca'), d3.rgb('#3e6c0a')) | |
}; | |
var range = undefined; | |
var r = undefined; | |
var pointerHeadLength = undefined; | |
var value = 0; | |
var svg = undefined; | |
var arc = undefined; | |
var scale = undefined; | |
var ticks = undefined; | |
var tickData = undefined; | |
var pointer = undefined; | |
var gagues = undefined; | |
var tip = d3.tip() | |
.attr('class', 'd3-tip') | |
.offset([-10, 0]) | |
.html(function (d) { | |
return "<strong>Value:</strong> <span style='color:red'>" + d.value + "</span><br \><strong>Target:</strong> <span style='color:red'>" + d.target + "</span>"; | |
}); | |
var donut = d3.pie(); | |
function deg2rad(deg) { | |
return deg * Math.PI / 180; | |
} | |
function newAngle(d) { | |
var ratio = scale(d); | |
var newAngle = config.minAngle + (ratio * range); | |
return newAngle; | |
} | |
function configure(configuration) { | |
var prop = undefined; | |
for (prop in configuration) { | |
config[prop] = configuration[prop]; | |
} | |
range = config.maxAngle - config.minAngle; | |
r = config.size / 2; | |
pointerHeadLength = Math.round(r * config.pointerHeadLengthPercent); | |
// a linear scale that maps domain values to a percent from 0..1 | |
scale = d3.scaleLinear() | |
.range([0, 1]) | |
.domain([config.minValue, config.maxValue]); | |
arc = d3.arc() | |
.innerRadius(r - config.ringWidth - config.ringInset) | |
.outerRadius(r - config.ringInset) | |
.startAngle(function (d, i) { | |
var ratio = d[0] * range / (config.maxValue - config.minValue); | |
return deg2rad(config.minAngle + ratio); | |
}) | |
.endAngle(function (d, i) { | |
var ratio = d[1] * range / (config.maxValue - config.minValue); | |
return deg2rad(config.minAngle + ratio); | |
}); | |
} | |
that.configure = configure; | |
function centerTranslation() { | |
return 'translate(' + r + ',' + r + ')'; | |
} | |
function isRendered() { | |
return (svg !== undefined); | |
} | |
that.isRendered = isRendered; | |
function render(newValue) { | |
gauges = d3.selectAll(container).datum(function () { return this.dataset; }); | |
svg = gauges | |
.append('svg:svg') | |
.attr('class', 'gauge') | |
.attr('width', config.clipWidth) | |
.attr('height', config.clipHeight); | |
svg.call(tip); | |
var centerTx = centerTranslation(); | |
var arcs = svg.append('g') | |
.attr('class', 'arc') | |
.attr('transform', centerTx); | |
arcs.selectAll('path') | |
.data(function (d) { | |
var ticks = JSON.parse(d.ticks); | |
return d3.pairs(ticks.concat(config.minValue, config.maxValue).sort(d3.ascending)); | |
}) | |
.enter().append('path') | |
.attr('fill', function (d, i) { | |
return config.arcColorFn(i); | |
}) | |
.attr('d', arc); | |
var lg = svg.append('g') | |
.attr('class', 'label') | |
.attr('transform', centerTx); | |
lg.selectAll('text') | |
.data(function (d) { | |
return JSON.parse(d.ticks); | |
}) | |
.enter().append('text') | |
.attr('transform', function (d) { | |
var ratio = scale(d); | |
var newAngle = config.minAngle + (ratio * range); | |
return 'rotate(' + newAngle + ') translate(0,' + (config.labelInset - r) + ')'; | |
}) | |
.text(config.labelFormat); | |
var lineData = [[config.pointerWidth / 2, 0], | |
[0, -pointerHeadLength], | |
[-(config.pointerWidth / 2), 0], | |
[0, config.pointerTailLength], | |
[config.pointerWidth / 2, 0]]; | |
var targetData = [[0, config.labelInset - r], [-config.pointerWidth / 2, | |
config.labelInset - r - (config.pointerWidth / 2)], [config.pointerWidth / 2, config.labelInset - r - (config.pointerWidth / 2)]]; | |
var pointerLine = d3.line().curve(d3.curveMonotoneX); | |
var pg = svg.append('g') | |
.attr('class', 'pointer') | |
.attr('transform', centerTx); | |
pointer = pg.append('path') | |
.attr('d', pointerLine(lineData)/*function(d) { return pointerLine(d) +'Z';}*/) | |
.attr('transform', 'rotate(' + config.minAngle + ')') | |
.on('mouseover', tip.show) | |
.on('mouseout', tip.hide); | |
pg.append('path') | |
.attr("class", "target") | |
.attr('d', pointerLine(targetData)/*function(d) { return pointerLine(d) +'Z';}*/) | |
.attr('transform', 'rotate(' + config.minAngle + ')') | |
.on('mouseover', tip.show) | |
.on('mouseout', tip.hide); | |
update(); | |
} | |
that.render = render; | |
function update(newConfiguration) { | |
if (newConfiguration !== undefined) { | |
configure(newConfiguration); | |
} | |
var pointers = gauges | |
.select(".pointer path"); | |
pointers | |
.transition() | |
.duration(config.transitionMs) | |
.ease(d3.easeElastic) | |
.attr('transform', function (d) { | |
var ratio = scale(d.value); | |
var newAngle = config.minAngle + (ratio * range); | |
return 'rotate(' + newAngle + ')'; | |
}); | |
gauges | |
.select(".pointer .target") | |
.attr('transform', function (d) { | |
var ratio = scale(d.target); | |
var newAngle = config.minAngle + (ratio * range); | |
return 'rotate(' + newAngle + ')'; | |
});; | |
} | |
that.update = update; | |
configure(configuration); | |
return that; | |
}; | |
</script> | |
<script> | |
function onDocumentReady() { | |
var powerGauge = gauge('.power-gauge', { | |
size: 300, | |
clipWidth: 300, | |
clipHeight: 180, | |
ringWidth: 60, | |
maxValue: 100, | |
transitionMs: 2000, | |
arcColorFn: d3.scaleOrdinal().range(['#3182bd', '#9ecae1', '#3182bd']) | |
}); | |
powerGauge.render(20); | |
updateReadings(); | |
function updateReadings() { | |
// just pump in random data here... | |
$('.power-gauge').each(function () { | |
this.dataset.value = parseInt(parseInt(this.dataset.value) + (Math.floor(Math.random() * 3) - 1)); | |
$(this).siblings("span").text(this.dataset.value); | |
}); | |
powerGauge.update(); | |
} | |
setInterval(function () { | |
updateReadings(); | |
}, 3 * 1000); | |
} | |
if (!window.isLoaded) { | |
window.addEventListener("load", function () { | |
onDocumentReady(); | |
}, false); | |
} else { | |
onDocumentReady(); | |
} | |
</script> | |
</body> | |
</html> |
This code is released under the MIT license. | |
Copyright (C) 2012 Matt Magoffin | |
Permission is hereby granted, free of charge, to any person obtaining a | |
copy of this software and associated documentation files (the | |
"Software"), to deal in the Software without restriction, including | |
without limitation the rights to use, copy, modify, merge, publish, | |
distribute, sublicense, and/or sell copies of the Software, and to | |
permit persons to whom the Software is furnished to do so, subject to | |
the following conditions: | |
The above copyright notice and this permission notice shall be included | |
in all copies or substantial portions of the Software. | |
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS | |
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF | |
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. | |
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY | |
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, | |
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE | |
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. |