UNPKG

radial-progress-chart

Version:

A customizable Radial Progress Chart written on the top of D3.js.

472 lines (402 loc) 13.9 kB
'use strict'; var d3; // RadialProgressChart object function RadialProgressChart(query, options) { // verify d3 is loaded d3 = (typeof window !== 'undefined' && window.d3) ? window.d3 : typeof require !== 'undefined' ? require("d3") : undefined; if(!d3) throw new Error('d3 object is missing. D3.js library has to be loaded before.'); var self = this; self.options = RadialProgressChart.normalizeOptions(options); // internal variables var series = self.options.series , width = 15 + ((self.options.diameter / 2) + (self.options.stroke.width * self.options.series.length) + (self.options.stroke.gap * self.options.series.length - 1)) * 2 , height = width , dim = "0 0 " + height + " " + width , τ = 2 * Math.PI , inner = [] , outer = []; function innerRadius(item) { var radius = inner[item.index]; if (radius) return radius; // first ring based on diameter and the rest based on the previous outer radius plus gap radius = item.index === 0 ? self.options.diameter / 2 : outer[item.index - 1] + self.options.stroke.gap; inner[item.index] = radius; return radius; } function outerRadius(item) { var radius = outer[item.index]; if (radius) return radius; // based on the previous inner radius + stroke width radius = inner[item.index] + self.options.stroke.width; outer[item.index] = radius; return radius; } self.progress = d3.svg.arc() .startAngle(0) .endAngle(function (item) { return item.percentage / 100 * τ; }) .innerRadius(innerRadius) .outerRadius(outerRadius) .cornerRadius(function (d) { // Workaround for d3 bug https://github.com/mbostock/d3/issues/2249 // Reduce corner radius when corners are close each other var m = d.percentage >= 90 ? (100 - d.percentage) * 0.1 : 1; return (self.options.stroke.width / 2) * m; }); var background = d3.svg.arc() .startAngle(0) .endAngle(τ) .innerRadius(innerRadius) .outerRadius(outerRadius); // create svg self.svg = d3.select(query).append("svg") .attr("preserveAspectRatio","xMinYMin meet") .attr("viewBox", dim) .append("g") .attr("transform", "translate(" + width / 2 + "," + height / 2 + ")"); // add gradients defs var defs = self.svg.append("svg:defs"); series.forEach(function (item) { if (item.color.linearGradient || item.color.radialGradient) { var gradient = RadialProgressChart.Gradient.toSVGElement('gradient' + item.index, item.color); defs.node().appendChild(gradient); } }); // add shadows defs defs = self.svg.append("svg:defs"); var dropshadowId = "dropshadow-" + Math.random(); var filter = defs.append("filter").attr("id", dropshadowId); if(self.options.shadow.width > 0) { filter.append("feGaussianBlur") .attr("in", "SourceAlpha") .attr("stdDeviation", self.options.shadow.width) .attr("result", "blur"); filter.append("feOffset") .attr("in", "blur") .attr("dx", 1) .attr("dy", 1) .attr("result", "offsetBlur"); } var feMerge = filter.append("feMerge"); feMerge.append("feMergeNode").attr("in", "offsetBlur"); feMerge.append("feMergeNode").attr("in", "SourceGraphic"); // add inner text if (self.options.center) { self.svg.append("text") .attr('class', 'rbc-center-text') .attr("text-anchor", "middle") .attr('x', self.options.center.x + 'px') .attr('y', self.options.center.y + 'px') .selectAll('tspan') .data(self.options.center.content).enter() .append('tspan') .attr("dominant-baseline", function () { // Single lines can easily centered in the middle using dominant-baseline, multiline need to use y if (self.options.center.content.length === 1) { return 'central'; } }) .attr('class', function (d, i) { return 'rbc-center-text-line' + i; }) .attr('x', 0) .attr('dy', function (d, i) { if (i > 0) { return '1.1em'; } }) .each(function (d) { if (typeof d === 'function') { this.callback = d; } }) .text(function (d) { if (typeof d === 'string') { return d; } return ''; }); } // add ring structure self.field = self.svg.selectAll("g") .data(series) .enter().append("g"); self.field.append("path").attr("class", "progress").attr("filter", "url(#" + dropshadowId +")"); self.field.append("path").attr("class", "bg") .style("fill", function (item) { return item.color.background; }) .style("opacity", 0.2) .attr("d", background); self.field.append("text") .classed('rbc-label rbc-label-start', true) .attr("dominant-baseline", "central") .attr("x", "10") .attr("y", function (item) { return -( self.options.diameter / 2 + item.index * (self.options.stroke.gap + self.options.stroke.width) + self.options.stroke.width / 2 ); }) .text(function (item) { return item.labelStart; }); self.update(); } /** * Update data to be visualized in the chart. * * @param {Object|Array} data Optional data you'd like to set for the chart before it will update. If not specified the update method will use the data that is already configured with the chart. * @example update([70, 10, 45]) * @example update({series: [{value: 70}, 10, 45]}) * */ RadialProgressChart.prototype.update = function (data) { var self = this; // parse new data if (data) { if (typeof data === 'number') { data = [data]; } var series; if (Array.isArray(data)) { series = data; } else if (typeof data === 'object') { series = data.series || []; } for (var i = 0; i < series.length; i++) { this.options.series[i].previousValue = this.options.series[i].value; var item = series[i]; if (typeof item === 'number') { this.options.series[i].value = item; } else if (typeof item === 'object') { this.options.series[i].value = item.value; } } } // calculate from percentage and new percentage for the progress animation self.options.series.forEach(function (item) { item.fromPercentage = item.percentage ? item.percentage : 5; item.percentage = (item.value - self.options.min) * 100 / (self.options.max - self.options.min); }); var center = self.svg.select("text.rbc-center-text"); // progress self.field.select("path.progress") .interrupt() .transition() .duration(self.options.animation.duration) .delay(function (d, i) { // delay between each item return i * self.options.animation.delay; }) .ease("elastic") .attrTween("d", function (item) { var interpolator = d3.interpolateNumber(item.fromPercentage, item.percentage); return function (t) { item.percentage = interpolator(t); return self.progress(item); }; }) .tween("center", function (item) { // Execute callbacks on each line if (self.options.center) { var interpolate = self.options.round ? d3.interpolateRound : d3.interpolateNumber; var interpolator = interpolate(item.previousValue || 0, item.value); return function (t) { center .selectAll('tspan') .each(function () { if (this.callback) { d3.select(this).text(this.callback(interpolator(t), item.index, item)); } }); }; } }) .tween("interpolate-color", function (item) { if (item.color.interpolate && item.color.interpolate.length == 2) { var colorInterpolator = d3.interpolateHsl(item.color.interpolate[0], item.color.interpolate[1]); return function (t) { var color = colorInterpolator(item.percentage / 100); d3.select(this).style('fill', color); d3.select(this.parentNode).select('path.bg').style('fill', color); }; } }) .style("fill", function (item) { if (item.color.solid) { return item.color.solid; } if (item.color.linearGradient || item.color.radialGradient) { return "url(#gradient" + item.index + ')'; } }); }; /** * Remove svg and clean some references */ RadialProgressChart.prototype.destroy = function () { this.svg.remove(); delete this.svg; }; /** * Detach and normalize user's options input. */ RadialProgressChart.normalizeOptions = function (options) { if (!options || typeof options !== 'object') { options = {}; } var _options = { diameter: options.diameter || 100, stroke: { width: options.stroke && options.stroke.width || 40, gap: options.stroke && options.stroke.gap || 2 }, shadow: { width: (!options.shadow || options.shadow.width === null) ? 4 : options.shadow.width }, animation: { duration: options.animation && options.animation.duration || 1750, delay: options.animation && options.animation.delay || 200 }, min: options.min || 0, max: options.max || 100, round: options.round !== undefined ? !!options.round : true, series: options.series || [], center: RadialProgressChart.normalizeCenter(options.center) }; var defaultColorsIterator = new RadialProgressChart.ColorsIterator(); for (var i = 0, length = _options.series.length; i < length; i++) { var item = options.series[i]; // convert number to object if (typeof item === 'number') { item = {value: item}; } _options.series[i] = { index: i, value: item.value, labelStart: item.labelStart, color: RadialProgressChart.normalizeColor(item.color, defaultColorsIterator) }; } return _options; }; /** * Normalize different notations of color property * * @param {String|Array|Object} color * @example '#fe08b5' * @example { solid: '#fe08b5', background: '#000000' } * @example ['#000000', '#ff0000'] * @example { linearGradient: { x1: '0%', y1: '100%', x2: '50%', y2: '0%'}, stops: [ {offset: '0%', 'stop-color': '#fe08b5', 'stop-opacity': 1}, {offset: '100%', 'stop-color': '#ff1410', 'stop-opacity': 1} ] } * @example { radialGradient: {cx: '60', cy: '60', r: '50'}, stops: [ {offset: '0%', 'stop-color': '#fe08b5', 'stop-opacity': 1}, {offset: '100%', 'stop-color': '#ff1410', 'stop-opacity': 1} ] } * */ RadialProgressChart.normalizeColor = function (color, defaultColorsIterator) { if (!color) { color = {solid: defaultColorsIterator.next()}; } else if (typeof color === 'string') { color = {solid: color}; } else if (Array.isArray(color)) { color = {interpolate: color}; } else if (typeof color === 'object') { if (!color.solid && !color.interpolate && !color.linearGradient && !color.radialGradient) { color.solid = defaultColorsIterator.next(); } } // Validate interpolate syntax if (color.interpolate) { if (color.interpolate.length !== 2) { throw new Error('interpolate array should contain two colors'); } } // Validate gradient syntax if (color.linearGradient || color.radialGradient) { if (!color.stops || !Array.isArray(color.stops) || color.stops.length !== 2) { throw new Error('gradient syntax is malformed'); } } // Set background when is not provided if (!color.background) { if (color.solid) { color.background = color.solid; } else if (color.interpolate) { color.background = color.interpolate[0]; } else if (color.linearGradient || color.radialGradient) { color.background = color.stops[0]['stop-color']; } } return color; }; /** * Normalize different notations of center property * * @param {String|Array|Function|Object} center * @example 'foo bar' * @example { content: 'foo bar', x: 10, y: 4 } * @example function(value, index, item) {} * @example ['foo bar', function(value, index, item) {}] */ RadialProgressChart.normalizeCenter = function (center) { if (!center) return null; // Convert to object notation if (center.constructor !== Object) { center = {content: center}; } // Defaults center.content = center.content || []; center.x = center.x || 0; center.y = center.y || 0; // Convert content to array notation if (!Array.isArray(center.content)) { center.content = [center.content]; } return center; }; // Linear or Radial Gradient internal object RadialProgressChart.Gradient = (function () { function Gradient() { } Gradient.toSVGElement = function (id, options) { var gradientType = options.linearGradient ? 'linearGradient' : 'radialGradient'; var gradient = d3.select(document.createElementNS(d3.ns.prefix.svg, gradientType)) .attr(options[gradientType]) .attr('id', id); options.stops.forEach(function (stopAttrs) { gradient.append("svg:stop").attr(stopAttrs); }); this.background = options.stops[0]['stop-color']; return gradient.node(); }; return Gradient; })(); // Default colors iterator RadialProgressChart.ColorsIterator = (function () { ColorsIterator.DEFAULT_COLORS = ["#1ad5de", "#a0ff03", "#e90b3a", '#ff9500', '#007aff', '#ffcc00', '#5856d6', '#8e8e93']; function ColorsIterator() { this.index = 0; } ColorsIterator.prototype.next = function () { if (this.index === ColorsIterator.DEFAULT_COLORS.length) { this.index = 0; } return ColorsIterator.DEFAULT_COLORS[this.index++]; }; return ColorsIterator; })(); // Export RadialProgressChart object if (typeof module !== "undefined")module.exports = RadialProgressChart;