Skip to content

Instantly share code, notes, and snippets.

@mohdali
Forked from msqr/LICENSE
Last active July 25, 2019 10:45
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save mohdali/65f66c357d91cd7f2455d64052b56c75 to your computer and use it in GitHub Desktop.
Save mohdali/65f66c357d91cd7f2455d64052b56c75 to your computer and use it in GitHub Desktop.
d3 gauge
license: mit

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
// 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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment