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. |