Comparing version 0.6.11 to 0.6.12
@@ -0,1 +1,15 @@ | ||
(function(root, factory) { | ||
if (typeof define === 'function' && define.amd) { | ||
define(["d3"], factory); | ||
} else if (typeof exports === 'object') { | ||
module.exports = factory(require('d3')); | ||
} else { | ||
root.D3Funnel = factory(root.d3); | ||
} | ||
}(this, function(d3) { | ||
/* global d3, LabelFormatter, Navigator, Utils */ | ||
/* exported D3Funnel */ | ||
'use strict'; | ||
@@ -7,672 +21,929 @@ | ||
(function (global, d3) { | ||
var D3Funnel = (function () { | ||
/* global d3 */ | ||
/* jshint bitwise: false */ | ||
'use strict'; | ||
/** | ||
* @param {string} selector A selector for the container element. | ||
* | ||
* @return {void} | ||
*/ | ||
var D3Funnel = (function () { | ||
function D3Funnel(selector) { | ||
_classCallCheck(this, D3Funnel); | ||
this.selector = selector; | ||
// Default configuration values | ||
this.defaults = { | ||
width: 350, | ||
height: 400, | ||
bottomWidth: 1 / 3, | ||
bottomPinch: 0, | ||
isCurved: false, | ||
curveHeight: 20, | ||
fillType: 'solid', | ||
isInverted: false, | ||
hoverEffects: false, | ||
dynamicArea: false, | ||
minHeight: false, | ||
animation: false, | ||
label: { | ||
fontSize: '14px', | ||
fill: '#fff', | ||
format: '{l}: {f}' | ||
}, | ||
onItemClick: null | ||
}; | ||
this.labelFormatter = new LabelFormatter(); | ||
this.navigator = new Navigator(); | ||
} | ||
/* exported LabelFormatter */ | ||
/** | ||
* Remove the funnel and its events from the DOM. | ||
* | ||
* @return {void} | ||
*/ | ||
_createClass(D3Funnel, [{ | ||
key: 'destroy', | ||
value: function destroy() { | ||
// D3's remove method appears to be sufficient for removing the events | ||
d3.select(this.selector).selectAll('svg').remove(); | ||
} | ||
/** | ||
* @param {string} selector A selector for the container element. | ||
* Draw the chart inside the container with the data and configuration | ||
* specified. This will remove any previous SVG elements in the container | ||
* and draw a new funnel chart on top of it. | ||
* | ||
* @param {Array} data A list of rows containing a category, a count, | ||
* and optionally a color (in hex). | ||
* @param {Object} options An optional configuration object to override | ||
* defaults. See the docs. | ||
* | ||
* @return {void} | ||
*/ | ||
}, { | ||
key: 'draw', | ||
value: function draw(data) { | ||
var options = arguments.length <= 1 || arguments[1] === undefined ? {} : arguments[1]; | ||
function D3Funnel(selector) { | ||
_classCallCheck(this, D3Funnel); | ||
this.destroy(); | ||
this.selector = selector; | ||
this._initialize(data, options); | ||
// Default configuration values | ||
this.defaults = { | ||
width: 350, | ||
height: 400, | ||
bottomWidth: 1 / 3, | ||
bottomPinch: 0, | ||
isCurved: false, | ||
curveHeight: 20, | ||
fillType: 'solid', | ||
isInverted: false, | ||
hoverEffects: false, | ||
dynamicArea: false, | ||
minHeight: false, | ||
animation: false, | ||
label: { | ||
fontSize: '14px', | ||
fill: '#fff' | ||
} | ||
}; | ||
this._draw(); | ||
} | ||
/** | ||
* Check if the supplied value is an array. | ||
* Initialize and calculate important variables for drawing the chart. | ||
* | ||
* @param {*} value | ||
* @param {Array} data | ||
* @param {Object} options | ||
* | ||
* @return {bool} | ||
*/ | ||
/** | ||
* Remove the funnel and its events from the DOM. | ||
* | ||
* @return {void} | ||
*/ | ||
}, { | ||
key: '_initialize', | ||
value: function _initialize(data, options) { | ||
this._validateData(data); | ||
_createClass(D3Funnel, [{ | ||
key: 'destroy', | ||
value: function destroy() { | ||
// D3's remove method appears to be sufficient for removing the events | ||
d3.select(this.selector).selectAll('svg').remove(); | ||
} | ||
this._setData(data); | ||
/** | ||
* Draw the chart inside the container with the data and configuration | ||
* specified. This will remove any previous SVG elements in the container | ||
* and draw a new funnel chart on top of it. | ||
* | ||
* @param {Array} data A list of rows containing a category, a count, | ||
* and optionally a color (in hex). | ||
* @param {Object} options An optional configuration object to override | ||
* defaults. See the docs. | ||
* | ||
* @return {void} | ||
*/ | ||
}, { | ||
key: 'draw', | ||
value: function draw(data) { | ||
var options = arguments.length <= 1 || arguments[1] === undefined ? {} : arguments[1]; | ||
var settings = this._getSettings(options); | ||
// Remove any previous drawings | ||
this.destroy(); | ||
// Set labels | ||
this.label = settings.label; | ||
this.labelFormatter.setFormat(this.label.format); | ||
// Initialize chart options | ||
this._initialize(data, options); | ||
// Initialize funnel chart settings | ||
this.width = settings.width; | ||
this.height = settings.height; | ||
this.bottomWidth = settings.width * settings.bottomWidth; | ||
this.bottomPinch = settings.bottomPinch; | ||
this.isCurved = settings.isCurved; | ||
this.curveHeight = settings.curveHeight; | ||
this.fillType = settings.fillType; | ||
this.isInverted = settings.isInverted; | ||
this.hoverEffects = settings.hoverEffects; | ||
this.dynamicArea = settings.dynamicArea; | ||
this.minHeight = settings.minHeight; | ||
this.animation = settings.animation; | ||
// Add the SVG | ||
this.svg = d3.select(this.selector).append('svg').attr('width', this.width).attr('height', this.height); | ||
// Calculate the bottom left x position | ||
this.bottomLeftX = (this.width - this.bottomWidth) / 2; | ||
this.blockPaths = this._makePaths(); | ||
// Change in x direction | ||
this.dx = this._getDx(); | ||
// Define color gradients | ||
if (this.fillType === 'gradient') { | ||
this._defineColorGradients(this.svg); | ||
} | ||
// Change in y direction | ||
this.dy = this._getDy(); | ||
// Add top oval if curved | ||
if (this.isCurved) { | ||
this._drawTopOval(this.svg, this.blockPaths); | ||
} | ||
// Support for events | ||
this.onItemClick = settings.onItemClick; | ||
} | ||
// Add each block | ||
this._drawBlock(0); | ||
/** | ||
* @param {Array} data | ||
* | ||
* @return void | ||
*/ | ||
}, { | ||
key: '_validateData', | ||
value: function _validateData(data) { | ||
if (Array.isArray(data) === false || data.length === 0 || Array.isArray(data[0]) === false || data[0].length < 2) { | ||
throw new Error('Funnel data is not valid.'); | ||
} | ||
} | ||
/** | ||
* Initialize and calculate important variables for drawing the chart. | ||
* | ||
* @param {Array} data | ||
* @param {Object} options | ||
* | ||
* @return {void} | ||
*/ | ||
}, { | ||
key: '_initialize', | ||
value: function _initialize(data, options) { | ||
if (!isArray(data) || data.length === 0 || !isArray(data[0]) || data[0].length < 2) { | ||
throw new Error('Funnel data is not valid.'); | ||
} | ||
/** | ||
* @param {Array} data | ||
* | ||
* @return void | ||
*/ | ||
}, { | ||
key: '_setData', | ||
value: function _setData(data) { | ||
this.data = data; | ||
this.data = data; | ||
this._setColors(); | ||
} | ||
// Counter | ||
var i = undefined; | ||
/** | ||
* Set the colors for each block. | ||
* | ||
* @return {void} | ||
*/ | ||
}, { | ||
key: '_setColors', | ||
value: function _setColors() { | ||
var _this = this; | ||
// Prepare the configuration settings based on the defaults | ||
// Set the default width and height based on the container | ||
var settings = extend({}, this.defaults); | ||
settings.width = parseInt(d3.select(this.selector).style('width'), 10); | ||
settings.height = parseInt(d3.select(this.selector).style('height'), 10); | ||
var colorScale = d3.scale.category10(); | ||
var hexExpression = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i; | ||
// Overwrite default settings with user options | ||
var keys = Object.keys(options); | ||
for (i = 0; i < keys.length; i++) { | ||
if (keys[i] !== 'label') { | ||
settings[keys[i]] = options[keys[i]]; | ||
} | ||
// Add a color for for each block without one | ||
this.data.forEach(function (block, index) { | ||
if (block.length < 3 || !hexExpression.test(block[2])) { | ||
_this.data[index][2] = colorScale(index); | ||
} | ||
}); | ||
} | ||
// Label settings | ||
if (options.hasOwnProperty('label')) { | ||
(function () { | ||
var validLabelOptions = /fontSize|fill/; | ||
/** | ||
* @param {Object} options | ||
* | ||
* @returns {Object} | ||
*/ | ||
}, { | ||
key: '_getSettings', | ||
value: function _getSettings(options) { | ||
// Prepare the configuration settings based on the defaults | ||
// Set the default width and height based on the container | ||
var settings = Utils.extend({}, this.defaults); | ||
settings.width = parseInt(d3.select(this.selector).style('width'), 10); | ||
settings.height = parseInt(d3.select(this.selector).style('height'), 10); | ||
Object.keys(options.label).forEach(function (labelOption) { | ||
if (labelOption.match(validLabelOptions)) { | ||
settings.label[labelOption] = options.label[labelOption]; | ||
} | ||
}); | ||
})(); | ||
} | ||
this.label = settings.label; | ||
// Overwrite default settings with user options | ||
settings = Utils.extend(settings, options); | ||
// In the case that the width or height is not valid, set | ||
// the width/height as its default hard-coded value | ||
if (settings.width <= 0) { | ||
settings.width = this.defaults.width; | ||
} | ||
if (settings.height <= 0) { | ||
settings.height = this.defaults.height; | ||
} | ||
// In the case that the width or height is not valid, set | ||
// the width/height as its default hard-coded value | ||
if (settings.width <= 0) { | ||
settings.width = this.defaults.width; | ||
} | ||
if (settings.height <= 0) { | ||
settings.height = this.defaults.height; | ||
} | ||
// Initialize the colors for each block | ||
var colorScale = d3.scale.category10(); | ||
for (i = 0; i < this.data.length; i++) { | ||
var hexExpression = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i; | ||
return settings; | ||
} | ||
// If a color is not set for the record, add one | ||
if (!('2' in this.data[i]) || !hexExpression.test(this.data[i][2])) { | ||
this.data[i][2] = colorScale(i); | ||
} | ||
} | ||
/** | ||
* @return {Number} | ||
*/ | ||
}, { | ||
key: '_getDx', | ||
value: function _getDx() { | ||
// Will be sharper if there is a pinch | ||
if (this.bottomPinch > 0) { | ||
return this.bottomLeftX / (this.data.length - this.bottomPinch); | ||
} | ||
// Initialize funnel chart settings | ||
this.width = settings.width; | ||
this.height = settings.height; | ||
this.bottomWidth = settings.width * settings.bottomWidth; | ||
this.bottomPinch = settings.bottomPinch; | ||
this.isCurved = settings.isCurved; | ||
this.curveHeight = settings.curveHeight; | ||
this.fillType = settings.fillType; | ||
this.isInverted = settings.isInverted; | ||
this.hoverEffects = settings.hoverEffects; | ||
this.dynamicArea = settings.dynamicArea; | ||
this.minHeight = settings.minHeight; | ||
this.animation = settings.animation; | ||
return this.bottomLeftX / this.data.length; | ||
} | ||
// Calculate the bottom left x position | ||
this.bottomLeftX = (this.width - this.bottomWidth) / 2; | ||
/** | ||
* @return {Number} | ||
*/ | ||
}, { | ||
key: '_getDy', | ||
value: function _getDy() { | ||
// Curved chart needs reserved pixels to account for curvature | ||
if (this.isCurved) { | ||
return (this.height - this.curveHeight) / this.data.length; | ||
} | ||
// Change in x direction | ||
// Will be sharper if there is a pinch | ||
this.dx = this.bottomPinch > 0 ? this.bottomLeftX / (data.length - this.bottomPinch) : this.bottomLeftX / data.length; | ||
// Change in y direction | ||
// Curved chart needs reserved pixels to account for curvature | ||
this.dy = this.isCurved ? (this.height - this.curveHeight) / data.length : this.height / data.length; | ||
return this.height / this.data.length; | ||
} | ||
// Support for events | ||
this.onItemClick = settings.onItemClick; | ||
/** | ||
* Draw the chart onto the DOM. | ||
* | ||
* @return {void} | ||
*/ | ||
}, { | ||
key: '_draw', | ||
value: function _draw() { | ||
// Add the SVG | ||
this.svg = d3.select(this.selector).append('svg').attr('width', this.width).attr('height', this.height); | ||
this.blockPaths = this._makePaths(); | ||
// Define color gradients | ||
if (this.fillType === 'gradient') { | ||
this._defineColorGradients(this.svg); | ||
} | ||
/** | ||
* Create the paths to be used to define the discrete funnel blocks and | ||
* returns the results in an array. | ||
* | ||
* @return {Array} | ||
*/ | ||
}, { | ||
key: '_makePaths', | ||
value: function _makePaths() { | ||
var paths = []; | ||
// Add top oval if curved | ||
if (this.isCurved) { | ||
this._drawTopOval(this.svg, this.blockPaths); | ||
} | ||
// Initialize velocity | ||
var dx = this.dx; | ||
var dy = this.dy; | ||
// Add each block | ||
this._drawBlock(0); | ||
} | ||
// Initialize starting positions | ||
var prevLeftX = 0; | ||
var prevRightX = this.width; | ||
var prevHeight = 0; | ||
/** | ||
* Create the paths to be used to define the discrete funnel blocks and | ||
* returns the results in an array. | ||
* | ||
* @return {Array} | ||
*/ | ||
}, { | ||
key: '_makePaths', | ||
value: function _makePaths() { | ||
var _this2 = this; | ||
// Start from the bottom for inverted | ||
if (this.isInverted) { | ||
prevLeftX = this.bottomLeftX; | ||
prevRightX = this.width - this.bottomLeftX; | ||
} | ||
var paths = []; | ||
// Initialize next positions | ||
var nextLeftX = 0; | ||
var nextRightX = 0; | ||
var nextHeight = 0; | ||
// Initialize velocity | ||
var dx = this.dx; | ||
var dy = this.dy; | ||
var middle = this.width / 2; | ||
// Initialize starting positions | ||
var prevLeftX = 0; | ||
var prevRightX = this.width; | ||
var prevHeight = 0; | ||
// Move down if there is an initial curve | ||
if (this.isCurved) { | ||
prevHeight = 10; | ||
} | ||
// Start from the bottom for inverted | ||
if (this.isInverted) { | ||
prevLeftX = this.bottomLeftX; | ||
prevRightX = this.width - this.bottomLeftX; | ||
} | ||
var topBase = this.width; | ||
var bottomBase = 0; | ||
// Initialize next positions | ||
var nextLeftX = 0; | ||
var nextRightX = 0; | ||
var nextHeight = 0; | ||
var totalArea = this.height * (this.width + this.bottomWidth) / 2; | ||
var slope = 2 * this.height / (this.width - this.bottomWidth); | ||
var middle = this.width / 2; | ||
// This is greedy in that the block will have a guaranteed height | ||
// and the remaining is shared among the ratio, instead of being | ||
// shared according to the remaining minus the guaranteed | ||
if (this.minHeight !== false) { | ||
var height = this.height - this.minHeight * this.data.length; | ||
totalArea = height * (this.width + this.bottomWidth) / 2; | ||
} | ||
// Move down if there is an initial curve | ||
if (this.isCurved) { | ||
prevHeight = 10; | ||
} | ||
var totalCount = 0; | ||
var count = 0; | ||
var topBase = this.width; | ||
var bottomBase = 0; | ||
// Harvest total count | ||
for (var i = 0; i < this.data.length; i++) { | ||
totalCount += isArray(this.data[i][1]) ? this.data[i][1][0] : this.data[i][1]; | ||
} | ||
var totalArea = this.height * (this.width + this.bottomWidth) / 2; | ||
var slope = 2 * this.height / (this.width - this.bottomWidth); | ||
// Create the path definition for each funnel block | ||
// Remember to loop back to the beginning point for a closed path | ||
for (var i = 0; i < this.data.length; i++) { | ||
count = isArray(this.data[i][1]) ? this.data[i][1][0] : this.data[i][1]; | ||
// This is greedy in that the block will have a guaranteed height | ||
// and the remaining is shared among the ratio, instead of being | ||
// shared according to the remaining minus the guaranteed | ||
if (this.minHeight !== false) { | ||
totalArea = (this.height - this.minHeight * this.data.length) * (this.width + this.bottomWidth) / 2; | ||
} | ||
// Calculate dynamic shapes based on area | ||
if (this.dynamicArea) { | ||
var ratio = count / totalCount; | ||
var area = ratio * totalArea; | ||
var totalCount = 0; | ||
var count = 0; | ||
if (this.minHeight !== false) { | ||
area += this.minHeight * (this.width + this.bottomWidth) / 2; | ||
} | ||
// Harvest total count | ||
this.data.forEach(function (block) { | ||
totalCount += Array.isArray(block[1]) ? block[1][0] : block[1]; | ||
}); | ||
bottomBase = Math.sqrt((slope * topBase * topBase - 4 * area) / slope); | ||
dx = topBase / 2 - bottomBase / 2; | ||
dy = area * 2 / (topBase + bottomBase); | ||
// Create the path definition for each funnel block | ||
// Remember to loop back to the beginning point for a closed path | ||
this.data.forEach(function (block, i) { | ||
count = Array.isArray(block[1]) ? block[0] : block[1]; | ||
if (this.isCurved) { | ||
dy = dy - this.curveHeight / this.data.length; | ||
} | ||
// Calculate dynamic shapes based on area | ||
if (_this2.dynamicArea) { | ||
var ratio = count / totalCount; | ||
var area = ratio * totalArea; | ||
topBase = bottomBase; | ||
if (_this2.minHeight !== false) { | ||
area += _this2.minHeight * (_this2.width + _this2.bottomWidth) / 2; | ||
} | ||
// Stop velocity for pinched blocks | ||
if (this.bottomPinch > 0) { | ||
// Check if we've reached the bottom of the pinch | ||
// If so, stop changing on x | ||
if (!this.isInverted) { | ||
if (i >= this.data.length - this.bottomPinch) { | ||
dx = 0; | ||
} | ||
// Pinch at the first blocks relating to the bottom pinch | ||
// Revert back to normal velocity after pinch | ||
} else { | ||
// Revert velocity back to the initial if we are using | ||
// static area's (prevents zero velocity if isInverted | ||
// and bottomPinch are non trivial and dynamicArea is | ||
// false) | ||
if (!this.dynamicArea) { | ||
dx = this.dx; | ||
} | ||
bottomBase = Math.sqrt((slope * topBase * topBase - 4 * area) / slope); | ||
dx = i < this.bottomPinch ? 0 : dx; | ||
} | ||
// Prevent bottm points from becomming NaN | ||
if (_this2.bottomWidth === 0 && i === _this2.data.length - 1) { | ||
bottomBase = 0; | ||
} | ||
// Calculate the position of next block | ||
nextLeftX = prevLeftX + dx; | ||
nextRightX = prevRightX - dx; | ||
nextHeight = prevHeight + dy; | ||
// Prevent NaN slope | ||
if (_this2.bottomWidth === _this2.width) { | ||
bottomBase = topBase; | ||
} | ||
// Expand outward if inverted | ||
if (this.isInverted) { | ||
nextLeftX = prevLeftX - dx; | ||
nextRightX = prevRightX + dx; | ||
dx = topBase / 2 - bottomBase / 2; | ||
dy = area * 2 / (topBase + bottomBase); | ||
if (_this2.isCurved) { | ||
dy = dy - _this2.curveHeight / _this2.data.length; | ||
} | ||
// Plot curved lines | ||
if (this.isCurved) { | ||
paths.push([ | ||
// Top Bezier curve | ||
[prevLeftX, prevHeight, 'M'], [middle, prevHeight + (this.curveHeight - 10), 'Q'], [prevRightX, prevHeight, ''], | ||
// Right line | ||
[nextRightX, nextHeight, 'L'], | ||
// Bottom Bezier curve | ||
[nextRightX, nextHeight, 'M'], [middle, nextHeight + this.curveHeight, 'Q'], [nextLeftX, nextHeight, ''], | ||
// Left line | ||
[prevLeftX, prevHeight, 'L']]); | ||
// Plot straight lines | ||
topBase = bottomBase; | ||
} | ||
// Stop velocity for pinched blocks | ||
if (_this2.bottomPinch > 0) { | ||
// Check if we've reached the bottom of the pinch | ||
// If so, stop changing on x | ||
if (!_this2.isInverted) { | ||
if (i >= _this2.data.length - _this2.bottomPinch) { | ||
dx = 0; | ||
} | ||
// Pinch at the first blocks relating to the bottom pinch | ||
// Revert back to normal velocity after pinch | ||
} else { | ||
paths.push([ | ||
// Start position | ||
[prevLeftX, prevHeight, 'M'], | ||
// Move to right | ||
[prevRightX, prevHeight, 'L'], | ||
// Move down | ||
[nextRightX, nextHeight, 'L'], | ||
// Move to left | ||
[nextLeftX, nextHeight, 'L'], | ||
// Wrap back to top | ||
[prevLeftX, prevHeight, 'L']]); | ||
// Revert velocity back to the initial if we are using | ||
// static area's (prevents zero velocity if isInverted | ||
// and bottomPinch are non trivial and dynamicArea is | ||
// false) | ||
if (!_this2.dynamicArea) { | ||
dx = _this2.dx; | ||
} | ||
dx = i < _this2.bottomPinch ? 0 : dx; | ||
} | ||
} | ||
// Set the next block's previous position | ||
prevLeftX = nextLeftX; | ||
prevRightX = nextRightX; | ||
prevHeight = nextHeight; | ||
// Calculate the position of next block | ||
nextLeftX = prevLeftX + dx; | ||
nextRightX = prevRightX - dx; | ||
nextHeight = prevHeight + dy; | ||
// Expand outward if inverted | ||
if (_this2.isInverted) { | ||
nextLeftX = prevLeftX - dx; | ||
nextRightX = prevRightX + dx; | ||
} | ||
return paths; | ||
} | ||
// Plot curved lines | ||
if (_this2.isCurved) { | ||
paths.push([ | ||
// Top Bezier curve | ||
[prevLeftX, prevHeight, 'M'], [middle, prevHeight + (_this2.curveHeight - 10), 'Q'], [prevRightX, prevHeight, ''], | ||
// Right line | ||
[nextRightX, nextHeight, 'L'], | ||
// Bottom Bezier curve | ||
[nextRightX, nextHeight, 'M'], [middle, nextHeight + _this2.curveHeight, 'Q'], [nextLeftX, nextHeight, ''], | ||
// Left line | ||
[prevLeftX, prevHeight, 'L']]); | ||
// Plot straight lines | ||
} else { | ||
paths.push([ | ||
// Start position | ||
[prevLeftX, prevHeight, 'M'], | ||
// Move to right | ||
[prevRightX, prevHeight, 'L'], | ||
// Move down | ||
[nextRightX, nextHeight, 'L'], | ||
// Move to left | ||
[nextLeftX, nextHeight, 'L'], | ||
// Wrap back to top | ||
[prevLeftX, prevHeight, 'L']]); | ||
} | ||
/** | ||
* Define the linear color gradients. | ||
* | ||
* @param {Object} svg | ||
* | ||
* @return {void} | ||
*/ | ||
}, { | ||
key: '_defineColorGradients', | ||
value: function _defineColorGradients(svg) { | ||
var defs = svg.append('defs'); | ||
// Set the next block's previous position | ||
prevLeftX = nextLeftX; | ||
prevRightX = nextRightX; | ||
prevHeight = nextHeight; | ||
}); | ||
// Create a gradient for each block | ||
for (var i = 0; i < this.data.length; i++) { | ||
var color = this.data[i][2]; | ||
var shade = shadeColor(color, -0.25); | ||
return paths; | ||
} | ||
// Create linear gradient | ||
var gradient = defs.append('linearGradient').attr({ | ||
id: 'gradient-' + i | ||
/** | ||
* Define the linear color gradients. | ||
* | ||
* @param {Object} svg | ||
* | ||
* @return {void} | ||
*/ | ||
}, { | ||
key: '_defineColorGradients', | ||
value: function _defineColorGradients(svg) { | ||
var defs = svg.append('defs'); | ||
// Create a gradient for each block | ||
this.data.forEach(function (block, index) { | ||
var color = block[2]; | ||
var shade = Utils.shadeColor(color, -0.25); | ||
// Create linear gradient | ||
var gradient = defs.append('linearGradient').attr({ | ||
id: 'gradient-' + index | ||
}); | ||
// Define the gradient stops | ||
var stops = [[0, shade], [40, color], [60, color], [100, shade]]; | ||
// Add the gradient stops | ||
stops.forEach(function (stop) { | ||
gradient.append('stop').attr({ | ||
offset: stop[0] + '%', | ||
style: 'stop-color:' + stop[1] | ||
}); | ||
}); | ||
}); | ||
} | ||
// Define the gradient stops | ||
var stops = [[0, shade], [40, color], [60, color], [100, shade]]; | ||
/** | ||
* Draw the top oval of a curved funnel. | ||
* | ||
* @param {Object} svg | ||
* @param {Array} blockPaths | ||
* | ||
* @return {void} | ||
*/ | ||
}, { | ||
key: '_drawTopOval', | ||
value: function _drawTopOval(svg, blockPaths) { | ||
var leftX = 0; | ||
var rightX = this.width; | ||
var centerX = this.width / 2; | ||
// Add the gradient stops | ||
for (var j = 0; j < stops.length; j++) { | ||
var _stop = stops[j]; | ||
gradient.append('stop').attr({ | ||
offset: _stop[0] + '%', | ||
style: 'stop-color:' + _stop[1] | ||
}); | ||
} | ||
} | ||
if (this.isInverted) { | ||
leftX = this.bottomLeftX; | ||
rightX = this.width - this.bottomLeftX; | ||
} | ||
/** | ||
* Draw the top oval of a curved funnel. | ||
* | ||
* @param {Object} svg | ||
* @param {Array} blockPaths | ||
* | ||
* @return {void} | ||
*/ | ||
}, { | ||
key: '_drawTopOval', | ||
value: function _drawTopOval(svg, blockPaths) { | ||
var leftX = 0; | ||
var rightX = this.width; | ||
var centerX = this.width / 2; | ||
// Create path from top-most block | ||
var paths = blockPaths[0]; | ||
var topCurve = paths[1][1] + this.curveHeight - 10; | ||
if (this.isInverted) { | ||
leftX = this.bottomLeftX; | ||
rightX = this.width - this.bottomLeftX; | ||
} | ||
var path = this.navigator.plot([['M', leftX, paths[0][1]], ['Q', centerX, topCurve], [' ', rightX, paths[2][1]], ['M', rightX, 10], ['Q', centerX, 0], [' ', leftX, 10]]); | ||
// Create path from top-most block | ||
var paths = blockPaths[0]; | ||
var path = 'M' + leftX + ',' + paths[0][1] + ' Q' + centerX + ',' + (paths[1][1] + this.curveHeight - 10) + ' ' + rightX + ',' + paths[2][1] + ' M' + rightX + ',10' + ' Q' + centerX + ',0' + ' ' + leftX + ',10'; | ||
// Draw top oval | ||
svg.append('path').attr('fill', Utils.shadeColor(this.data[0][2], -0.4)).attr('d', path); | ||
} | ||
// Draw top oval | ||
svg.append('path').attr('fill', shadeColor(this.data[0][2], -0.4)).attr('d', path); | ||
/** | ||
* Draw the next block in the iteration. | ||
* | ||
* @param {int} index | ||
* | ||
* @return {void} | ||
*/ | ||
}, { | ||
key: '_drawBlock', | ||
value: function _drawBlock(index) { | ||
var _this3 = this; | ||
if (index === this.data.length) { | ||
return; | ||
} | ||
/** | ||
* Draw the next block in the iteration. | ||
* | ||
* @param {int} index | ||
* | ||
* @return {void} | ||
*/ | ||
}, { | ||
key: '_drawBlock', | ||
value: function _drawBlock(index) { | ||
var _this = this; | ||
// Create a group just for this block | ||
var group = this.svg.append('g'); | ||
if (index === this.data.length) { | ||
return; | ||
} | ||
// Fetch path element | ||
var path = this._getBlockPath(group, index); | ||
path.data(this._getBlockData(index)); | ||
// Create a group just for this block | ||
var group = this.svg.append('g'); | ||
// Add animation components | ||
if (this.animation !== false) { | ||
path.transition().duration(this.animation).ease('linear').attr('fill', this._getColor(index)).attr('d', this._getPathDefinition(index)).each('end', function () { | ||
_this3._drawBlock(index + 1); | ||
}); | ||
} else { | ||
path.attr('fill', this._getColor(index)).attr('d', this._getPathDefinition(index)); | ||
this._drawBlock(index + 1); | ||
} | ||
// Fetch path element | ||
var path = this._getBlockPath(group, index); | ||
path.data(this._getBlockData(index)); | ||
// Add the hover events | ||
if (this.hoverEffects) { | ||
path.on('mouseover', this._onMouseOver).on('mouseout', this._onMouseOut); | ||
} | ||
// Add animation components | ||
if (this.animation !== false) { | ||
path.transition().duration(this.animation).ease('linear').attr('fill', this._getColor(index)).attr('d', this._getPathDefinition(index)).each('end', function () { | ||
_this._drawBlock(index + 1); | ||
}); | ||
} else { | ||
path.attr('fill', this._getColor(index)).attr('d', this._getPathDefinition(index)); | ||
this._drawBlock(index + 1); | ||
} | ||
// ItemClick event | ||
if (this.onItemClick !== null) { | ||
path.on('click', this.onItemClick); | ||
} | ||
// Add the hover events | ||
if (this.hoverEffects) { | ||
path.on('mouseover', this._onMouseOver).on('mouseout', this._onMouseOut); | ||
} | ||
this._addBlockLabel(group, index); | ||
} | ||
// ItemClick event | ||
if (this.onItemClick) { | ||
path.on('click', this.onItemClick); | ||
} | ||
/** | ||
* @param {Object} group | ||
* @param {int} index | ||
* | ||
* @return {Object} | ||
*/ | ||
}, { | ||
key: '_getBlockPath', | ||
value: function _getBlockPath(group, index) { | ||
var path = group.append('path'); | ||
this._addBlockLabel(group, index); | ||
if (this.animation !== false) { | ||
this._addBeforeTransition(path, index); | ||
} | ||
/** | ||
* @param {Object} group | ||
* @param {int} index | ||
* | ||
* @return {Object} | ||
*/ | ||
}, { | ||
key: '_getBlockPath', | ||
value: function _getBlockPath(group, index) { | ||
var path = group.append('path'); | ||
return path; | ||
} | ||
if (this.animation !== false) { | ||
this._addBeforeTransition(path, index); | ||
} | ||
/** | ||
* Set the attributes of a path element before its animation. | ||
* | ||
* @param {Object} path | ||
* @param {int} index | ||
* | ||
* @return {void} | ||
*/ | ||
}, { | ||
key: '_addBeforeTransition', | ||
value: function _addBeforeTransition(path, index) { | ||
var paths = this.blockPaths[index]; | ||
return path; | ||
var beforePath = ''; | ||
var beforeFill = ''; | ||
// Construct the top of the trapezoid and leave the other elements | ||
// hovering around to expand downward on animation | ||
if (!this.isCurved) { | ||
beforePath = this.navigator.plot([['M', paths[0][0], paths[0][1]], ['L', paths[1][0], paths[1][1]], ['L', paths[1][0], paths[1][1]], ['L', paths[0][0], paths[0][1]]]); | ||
} else { | ||
beforePath = this.navigator.plot([['M', paths[0][0], paths[0][1]], ['Q', paths[1][0], paths[1][1]], [' ', paths[2][0], paths[2][1]], ['L', paths[2][0], paths[2][1]], ['M', paths[2][0], paths[2][1]], ['Q', paths[1][0], paths[1][1]], [' ', paths[0][0], paths[0][1]]]); | ||
} | ||
/** | ||
* Set the attributes of a path element before its animation. | ||
* | ||
* @param {Object} path | ||
* @param {int} index | ||
* | ||
* @return {void} | ||
*/ | ||
}, { | ||
key: '_addBeforeTransition', | ||
value: function _addBeforeTransition(path, index) { | ||
var paths = this.blockPaths[index]; | ||
// Use previous fill color, if available | ||
if (this.fillType === 'solid') { | ||
beforeFill = index > 0 ? this._getColor(index - 1) : this._getColor(index); | ||
// Use current background if gradient (gradients do not transition) | ||
} else { | ||
beforeFill = this._getColor(index); | ||
} | ||
var beforePath = ''; | ||
var beforeFill = ''; | ||
path.attr('d', beforePath).attr('fill', beforeFill); | ||
} | ||
// Construct the top of the trapezoid and leave the other elements | ||
// hovering around to expand downward on animation | ||
if (!this.isCurved) { | ||
beforePath = 'M' + paths[0][0] + ',' + paths[0][1] + ' L' + paths[1][0] + ',' + paths[1][1] + ' L' + paths[1][0] + ',' + paths[1][1] + ' L' + paths[0][0] + ',' + paths[0][1]; | ||
} else { | ||
beforePath = 'M' + paths[0][0] + ',' + paths[0][1] + ' Q' + paths[1][0] + ',' + paths[1][1] + ' ' + paths[2][0] + ',' + paths[2][1] + ' L' + paths[2][0] + ',' + paths[2][1] + ' M' + paths[2][0] + ',' + paths[2][1] + ' Q' + paths[1][0] + ',' + paths[1][1] + ' ' + paths[0][0] + ',' + paths[0][1]; | ||
} | ||
/** | ||
* @param {int} index | ||
* | ||
* @return {Array} | ||
*/ | ||
}, { | ||
key: '_getBlockData', | ||
value: function _getBlockData(index) { | ||
var label = this.data[index][0]; | ||
var value = this.data[index][1]; | ||
// Use previous fill color, if available | ||
if (this.fillType === 'solid') { | ||
beforeFill = index > 0 ? this._getColor(index - 1) : this._getColor(index); | ||
// Use current background if gradient (gradients do not transition) | ||
} else { | ||
beforeFill = this._getColor(index); | ||
} | ||
return [{ | ||
index: index, | ||
label: label, | ||
value: value, | ||
formatted: this.labelFormatter.format(label, value), | ||
baseColor: this.data[index][2], | ||
fill: this._getColor(index) | ||
}]; | ||
} | ||
path.attr('d', beforePath).attr('fill', beforeFill); | ||
/** | ||
* Return the color for the given index. | ||
* | ||
* @param {int} index | ||
* | ||
* @return {string} | ||
*/ | ||
}, { | ||
key: '_getColor', | ||
value: function _getColor(index) { | ||
if (this.fillType === 'solid') { | ||
return this.data[index][2]; | ||
} else { | ||
return 'url(#gradient-' + index + ')'; | ||
} | ||
} | ||
/** | ||
* @param {int} index | ||
* | ||
* @return {Array} | ||
*/ | ||
}, { | ||
key: '_getBlockData', | ||
value: function _getBlockData(index) { | ||
return [{ | ||
index: index, | ||
label: this.data[index][0], | ||
value: isArray(this.data[index][1]) ? this.data[index][1][0] : this.data[index][1], | ||
formattedValue: isArray(this.data[index][1]) ? this.data[index][1][1] : this.data[index][1].toLocaleString(), | ||
baseColor: this.data[index][2], | ||
fill: this._getColor(index) | ||
}]; | ||
} | ||
/** | ||
* @param {int} index | ||
* | ||
* @return {string} | ||
*/ | ||
}, { | ||
key: '_getPathDefinition', | ||
value: function _getPathDefinition(index) { | ||
var commands = []; | ||
/** | ||
* Return the color for the given index. | ||
* | ||
* @param {int} index | ||
* | ||
* @return {string} | ||
*/ | ||
}, { | ||
key: '_getColor', | ||
value: function _getColor(index) { | ||
if (this.fillType === 'solid') { | ||
return this.data[index][2]; | ||
} else { | ||
return 'url(#gradient-' + index + ')'; | ||
} | ||
} | ||
this.blockPaths[index].forEach(function (command) { | ||
commands.push([command[2], command[0], command[1]]); | ||
}); | ||
/** | ||
* @param {int} index | ||
* | ||
* @return {string} | ||
*/ | ||
}, { | ||
key: '_getPathDefinition', | ||
value: function _getPathDefinition(index) { | ||
var pathStr = ''; | ||
var point = []; | ||
var paths = this.blockPaths[index]; | ||
return this.navigator.plot(commands); | ||
} | ||
for (var j = 0; j < paths.length; j++) { | ||
point = paths[j]; | ||
pathStr += point[2] + point[0] + ',' + point[1] + ' '; | ||
} | ||
/** | ||
* @param {Object} data | ||
* | ||
* @return {void} | ||
*/ | ||
}, { | ||
key: '_onMouseOver', | ||
value: function _onMouseOver(data) { | ||
d3.select(this).attr('fill', Utils.shadeColor(data.baseColor, -0.2)); | ||
} | ||
return pathStr; | ||
} | ||
/** | ||
* @param {Object} data | ||
* | ||
* @return {void} | ||
*/ | ||
}, { | ||
key: '_onMouseOut', | ||
value: function _onMouseOut(data) { | ||
d3.select(this).attr('fill', data.fill); | ||
} | ||
/** | ||
* @param {Object} data | ||
* | ||
* @return {void} | ||
*/ | ||
}, { | ||
key: '_onMouseOver', | ||
value: function _onMouseOver(data) { | ||
d3.select(this).attr('fill', shadeColor(data.baseColor, -0.2)); | ||
} | ||
/** | ||
* @param {Object} group | ||
* @param {int} index | ||
* | ||
* @return {void} | ||
*/ | ||
}, { | ||
key: '_addBlockLabel', | ||
value: function _addBlockLabel(group, index) { | ||
var paths = this.blockPaths[index]; | ||
/** | ||
* @param {Object} data | ||
* | ||
* @return {void} | ||
*/ | ||
}, { | ||
key: '_onMouseOut', | ||
value: function _onMouseOut(data) { | ||
d3.select(this).attr('fill', data.fill); | ||
} | ||
var label = this._getBlockData(index)[0].formatted; | ||
var fill = this.data[index][3] || this.label.fill; | ||
/** | ||
* @param {Object} group | ||
* @param {int} index | ||
* | ||
* @return {void} | ||
*/ | ||
}, { | ||
key: '_addBlockLabel', | ||
value: function _addBlockLabel(group, index) { | ||
var i = index; | ||
var paths = this.blockPaths[index]; | ||
var blockData = this._getBlockData(index)[0]; | ||
var textStr = blockData.label + ': ' + blockData.formattedValue; | ||
var textFill = this.data[i][3] || this.label.fill; | ||
var x = this.width / 2; // Center the text | ||
var y = this._getTextY(paths); | ||
var textX = this.width / 2; // Center the text | ||
var textY = !this.isCurved ? // Average height of bases | ||
(paths[1][1] + paths[2][1]) / 2 : (paths[2][1] + paths[3][1]) / 2 + this.curveHeight / this.data.length; | ||
group.append('text').text(label).attr({ | ||
'x': x, | ||
'y': y, | ||
'text-anchor': 'middle', | ||
'dominant-baseline': 'middle', | ||
'fill': fill, | ||
'pointer-events': 'none' | ||
}).style('font-size', this.label.fontSize); | ||
} | ||
group.append('text').text(textStr).attr({ | ||
'x': textX, | ||
'y': textY, | ||
'text-anchor': 'middle', | ||
'dominant-baseline': 'middle', | ||
'fill': textFill, | ||
'pointer-events': 'none' | ||
}).style('font-size', this.label.fontSize); | ||
/** | ||
* Returns the y position of the given label's text. This is determined by | ||
* taking the mean of the bases. | ||
* | ||
* @param {Array} paths | ||
* | ||
* @return {Number} | ||
*/ | ||
}, { | ||
key: '_getTextY', | ||
value: function _getTextY(paths) { | ||
if (this.isCurved) { | ||
return (paths[2][1] + paths[3][1]) / 2 + this.curveHeight / this.data.length; | ||
} | ||
}]); | ||
return D3Funnel; | ||
})(); | ||
return (paths[1][1] + paths[2][1]) / 2; | ||
} | ||
}]); | ||
function isArray(value) { | ||
return Object.prototype.toString.call(value) === '[object Array]'; | ||
return D3Funnel; | ||
})(); | ||
var LabelFormatter = (function () { | ||
/** | ||
* Initial the formatter. | ||
* | ||
* @return {void} | ||
*/ | ||
function LabelFormatter() { | ||
_classCallCheck(this, LabelFormatter); | ||
this.expression = null; | ||
} | ||
/* exported Navigator */ | ||
/** | ||
* Extends an object with the members of another. | ||
* Register the format function. | ||
* | ||
* @param {Object} a The object to be extended. | ||
* @param {Object} b The object to clone from. | ||
* @param {string|function} format | ||
* | ||
* @return {Object} | ||
* @return {void} | ||
*/ | ||
function extend(a, b) { | ||
var prop = undefined; | ||
for (prop in b) { | ||
if (b.hasOwnProperty(prop)) { | ||
a[prop] = b[prop]; | ||
_createClass(LabelFormatter, [{ | ||
key: 'setFormat', | ||
value: function setFormat(format) { | ||
if (typeof format === 'function') { | ||
this.formatter = format; | ||
} else { | ||
this.expression = format; | ||
this.formatter = this.stringFormatter; | ||
} | ||
} | ||
return a; | ||
/** | ||
* Format the given value according to the data point or the format. | ||
* | ||
* @param {string} label | ||
* @param {number} value | ||
* | ||
* @return string | ||
*/ | ||
}, { | ||
key: 'format', | ||
value: function format(label, value) { | ||
// Try to use any formatted value specified through the data | ||
// Otherwise, attempt to use the format function | ||
if (Array.isArray(value)) { | ||
return this.formatter(label, value[0], value[1]); | ||
} else { | ||
return this.formatter(label, value, null); | ||
} | ||
} | ||
/** | ||
* Format the string according to a simple expression. | ||
* | ||
* {l}: label | ||
* {v}: raw value | ||
* {f}: formatted value | ||
* | ||
* @param {string} label | ||
* @param {number} value | ||
* @param {*} fValue | ||
* | ||
* @return {string} | ||
*/ | ||
}, { | ||
key: 'stringFormatter', | ||
value: function stringFormatter(label, value) { | ||
var fValue = arguments.length <= 2 || arguments[2] === undefined ? null : arguments[2]; | ||
// Attempt to use supplied formatted value | ||
// Otherwise, use the default | ||
if (fValue === null) { | ||
fValue = this.getDefaultFormattedValue(value); | ||
} | ||
return this.expression.split('{l}').join(label).split('{v}').join(value).split('{f}').join(fValue); | ||
} | ||
/** | ||
* @param {number} value | ||
* | ||
* @return {string} | ||
*/ | ||
}, { | ||
key: 'getDefaultFormattedValue', | ||
value: function getDefaultFormattedValue(value) { | ||
return value.toLocaleString(); | ||
} | ||
}]); | ||
return LabelFormatter; | ||
})(); | ||
var Navigator = (function () { | ||
function Navigator() { | ||
_classCallCheck(this, Navigator); | ||
} | ||
/* exported Utils */ | ||
/* jshint bitwise: false */ | ||
/** | ||
* Shade a color to the given percentage. | ||
* | ||
* @param {string} color A hex color. | ||
* @param {number} shade The shade adjustment. Can be positive or negative. | ||
* | ||
* @return {string} | ||
* Simple utility class. | ||
*/ | ||
function shadeColor(color, shade) { | ||
var f = parseInt(color.slice(1), 16); | ||
var t = shade < 0 ? 0 : 255; | ||
var p = shade < 0 ? shade * -1 : shade; | ||
var R = f >> 16, | ||
G = f >> 8 & 0x00FF; | ||
var B = f & 0x0000FF; | ||
var converted = 0x1000000 + (Math.round((t - R) * p) + R) * 0x10000 + (Math.round((t - G) * p) + G) * 0x100 + (Math.round((t - B) * p) + B); | ||
_createClass(Navigator, [{ | ||
key: 'plot', | ||
return '#' + converted.toString(16).slice(1); | ||
/** | ||
* Given a list of path commands, returns the compiled description. | ||
* | ||
* @param {Array} commands | ||
* | ||
* @returns {string} | ||
*/ | ||
value: function plot(commands) { | ||
var path = ''; | ||
commands.forEach(function (command) { | ||
path += command[0] + command[1] + ',' + command[2] + ' '; | ||
}); | ||
return path.replace(/ +/g, ' ').trim(); | ||
} | ||
}]); | ||
return Navigator; | ||
})(); | ||
var Utils = (function () { | ||
function Utils() { | ||
_classCallCheck(this, Utils); | ||
} | ||
global.D3Funnel = D3Funnel; | ||
})(window, d3); | ||
_createClass(Utils, null, [{ | ||
key: 'extend', | ||
/** | ||
* Extends an object with the members of another. | ||
* | ||
* @param {Object} a The object to be extended. | ||
* @param {Object} b The object to clone from. | ||
* | ||
* @return {Object} | ||
*/ | ||
value: function extend(a, b) { | ||
var prop = undefined; | ||
for (prop in b) { | ||
if (b.hasOwnProperty(prop)) { | ||
if (typeof a[prop] === 'object' && typeof b[prop] === 'object') { | ||
a[prop] = Utils.extend(a[prop], b[prop]); | ||
} else { | ||
a[prop] = b[prop]; | ||
} | ||
} | ||
} | ||
return a; | ||
} | ||
/** | ||
* Shade a color to the given percentage. | ||
* | ||
* @param {string} color A hex color. | ||
* @param {number} shade The shade adjustment. Can be positive or negative. | ||
* | ||
* @return {string} | ||
*/ | ||
}, { | ||
key: 'shadeColor', | ||
value: function shadeColor(color, shade) { | ||
var hex = color.slice(1); | ||
if (hex.length === 3) { | ||
hex = Utils.expandHex(hex); | ||
} | ||
var f = parseInt(hex, 16); | ||
var t = shade < 0 ? 0 : 255; | ||
var p = shade < 0 ? shade * -1 : shade; | ||
var R = f >> 16; | ||
var G = f >> 8 & 0x00FF; | ||
var B = f & 0x0000FF; | ||
var converted = 0x1000000 + (Math.round((t - R) * p) + R) * 0x10000 + (Math.round((t - G) * p) + G) * 0x100 + (Math.round((t - B) * p) + B); | ||
return '#' + converted.toString(16).slice(1); | ||
} | ||
/** | ||
* Expands a three character hex code to six characters. | ||
* | ||
* @param {string} hex | ||
* | ||
* @return {string} | ||
*/ | ||
}, { | ||
key: 'expandHex', | ||
value: function expandHex(hex) { | ||
return hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2]; | ||
} | ||
}]); | ||
return Utils; | ||
})(); | ||
return D3Funnel; | ||
})); |
@@ -1,2 +0,2 @@ | ||
/*! d3-funnel - v0.6.11 | 2015 */ | ||
"use strict";function _classCallCheck(t,i){if(!(t instanceof i))throw new TypeError("Cannot call a class as a function")}var _createClass=function(){function t(t,i){for(var e=0;e<i.length;e++){var h=i[e];h.enumerable=h.enumerable||!1,h.configurable=!0,"value"in h&&(h.writable=!0),Object.defineProperty(t,h.key,h)}}return function(i,e,h){return e&&t(i.prototype,e),h&&t(i,h),i}}();!function(t,i){function e(t){return"[object Array]"===Object.prototype.toString.call(t)}function h(t,i){var e=void 0;for(e in i)i.hasOwnProperty(e)&&(t[e]=i[e]);return t}function a(t,i){var e=parseInt(t.slice(1),16),h=0>i?0:255,a=0>i?-1*i:i,s=e>>16,n=e>>8&255,o=255&e,r=16777216+65536*(Math.round((h-s)*a)+s)+256*(Math.round((h-n)*a)+n)+(Math.round((h-o)*a)+o);return"#"+r.toString(16).slice(1)}var s=function(){function t(i){_classCallCheck(this,t),this.selector=i,this.defaults={width:350,height:400,bottomWidth:1/3,bottomPinch:0,isCurved:!1,curveHeight:20,fillType:"solid",isInverted:!1,hoverEffects:!1,dynamicArea:!1,minHeight:!1,animation:!1,label:{fontSize:"14px",fill:"#fff"}}}return _createClass(t,[{key:"destroy",value:function(){i.select(this.selector).selectAll("svg").remove()}},{key:"draw",value:function(t){var e=arguments.length<=1||void 0===arguments[1]?{}:arguments[1];this.destroy(),this._initialize(t,e),this.svg=i.select(this.selector).append("svg").attr("width",this.width).attr("height",this.height),this.blockPaths=this._makePaths(),"gradient"===this.fillType&&this._defineColorGradients(this.svg),this.isCurved&&this._drawTopOval(this.svg,this.blockPaths),this._drawBlock(0)}},{key:"_initialize",value:function(t,a){if(!e(t)||0===t.length||!e(t[0])||t[0].length<2)throw new Error("Funnel data is not valid.");this.data=t;var s=void 0,n=h({},this.defaults);n.width=parseInt(i.select(this.selector).style("width"),10),n.height=parseInt(i.select(this.selector).style("height"),10);var o=Object.keys(a);for(s=0;s<o.length;s++)"label"!==o[s]&&(n[o[s]]=a[o[s]]);a.hasOwnProperty("label")&&!function(){var t=/fontSize|fill/;Object.keys(a.label).forEach(function(i){i.match(t)&&(n.label[i]=a.label[i])})}(),this.label=n.label,n.width<=0&&(n.width=this.defaults.width),n.height<=0&&(n.height=this.defaults.height);var r=i.scale.category10();for(s=0;s<this.data.length;s++){var l=/^#([0-9a-f]{3}|[0-9a-f]{6})$/i;"2"in this.data[s]&&l.test(this.data[s][2])||(this.data[s][2]=r(s))}this.width=n.width,this.height=n.height,this.bottomWidth=n.width*n.bottomWidth,this.bottomPinch=n.bottomPinch,this.isCurved=n.isCurved,this.curveHeight=n.curveHeight,this.fillType=n.fillType,this.isInverted=n.isInverted,this.hoverEffects=n.hoverEffects,this.dynamicArea=n.dynamicArea,this.minHeight=n.minHeight,this.animation=n.animation,this.bottomLeftX=(this.width-this.bottomWidth)/2,this.dx=this.bottomPinch>0?this.bottomLeftX/(t.length-this.bottomPinch):this.bottomLeftX/t.length,this.dy=this.isCurved?(this.height-this.curveHeight)/t.length:this.height/t.length,this.onItemClick=n.onItemClick}},{key:"_makePaths",value:function(){var t=[],i=this.dx,h=this.dy,a=0,s=this.width,n=0;this.isInverted&&(a=this.bottomLeftX,s=this.width-this.bottomLeftX);var o=0,r=0,l=0,d=this.width/2;this.isCurved&&(n=10);var c=this.width,u=0,f=this.height*(this.width+this.bottomWidth)/2,v=2*this.height/(this.width-this.bottomWidth);if(this.minHeight!==!1){var g=this.height-this.minHeight*this.data.length;f=g*(this.width+this.bottomWidth)/2}for(var m=0,b=0,y=0;y<this.data.length;y++)m+=e(this.data[y][1])?this.data[y][1][0]:this.data[y][1];for(var y=0;y<this.data.length;y++){if(b=e(this.data[y][1])?this.data[y][1][0]:this.data[y][1],this.dynamicArea){var k=b/m,p=k*f;this.minHeight!==!1&&(p+=this.minHeight*(this.width+this.bottomWidth)/2),u=Math.sqrt((v*c*c-4*p)/v),i=c/2-u/2,h=2*p/(c+u),this.isCurved&&(h-=this.curveHeight/this.data.length),c=u}this.bottomPinch>0&&(this.isInverted?(this.dynamicArea||(i=this.dx),i=y<this.bottomPinch?0:i):y>=this.data.length-this.bottomPinch&&(i=0)),o=a+i,r=s-i,l=n+h,this.isInverted&&(o=a-i,r=s+i),t.push(this.isCurved?[[a,n,"M"],[d,n+(this.curveHeight-10),"Q"],[s,n,""],[r,l,"L"],[r,l,"M"],[d,l+this.curveHeight,"Q"],[o,l,""],[a,n,"L"]]:[[a,n,"M"],[s,n,"L"],[r,l,"L"],[o,l,"L"],[a,n,"L"]]),a=o,s=r,n=l}return t}},{key:"_defineColorGradients",value:function(t){for(var i=t.append("defs"),e=0;e<this.data.length;e++)for(var h=this.data[e][2],s=a(h,-.25),n=i.append("linearGradient").attr({id:"gradient-"+e}),o=[[0,s],[40,h],[60,h],[100,s]],r=0;r<o.length;r++){var l=o[r];n.append("stop").attr({offset:l[0]+"%",style:"stop-color:"+l[1]})}}},{key:"_drawTopOval",value:function(t,i){var e=0,h=this.width,s=this.width/2;this.isInverted&&(e=this.bottomLeftX,h=this.width-this.bottomLeftX);var n=i[0],o="M"+e+","+n[0][1]+" Q"+s+","+(n[1][1]+this.curveHeight-10)+" "+h+","+n[2][1]+" M"+h+",10 Q"+s+",0 "+e+",10";t.append("path").attr("fill",a(this.data[0][2],-.4)).attr("d",o)}},{key:"_drawBlock",value:function(t){var i=this;if(t!==this.data.length){var e=this.svg.append("g"),h=this._getBlockPath(e,t);h.data(this._getBlockData(t)),this.animation!==!1?h.transition().duration(this.animation).ease("linear").attr("fill",this._getColor(t)).attr("d",this._getPathDefinition(t)).each("end",function(){i._drawBlock(t+1)}):(h.attr("fill",this._getColor(t)).attr("d",this._getPathDefinition(t)),this._drawBlock(t+1)),this.hoverEffects&&h.on("mouseover",this._onMouseOver).on("mouseout",this._onMouseOut),this.onItemClick&&h.on("click",this.onItemClick),this._addBlockLabel(e,t)}}},{key:"_getBlockPath",value:function(t,i){var e=t.append("path");return this.animation!==!1&&this._addBeforeTransition(e,i),e}},{key:"_addBeforeTransition",value:function(t,i){var e=this.blockPaths[i],h="",a="";h=this.isCurved?"M"+e[0][0]+","+e[0][1]+" Q"+e[1][0]+","+e[1][1]+" "+e[2][0]+","+e[2][1]+" L"+e[2][0]+","+e[2][1]+" M"+e[2][0]+","+e[2][1]+" Q"+e[1][0]+","+e[1][1]+" "+e[0][0]+","+e[0][1]:"M"+e[0][0]+","+e[0][1]+" L"+e[1][0]+","+e[1][1]+" L"+e[1][0]+","+e[1][1]+" L"+e[0][0]+","+e[0][1],a=this._getColor("solid"===this.fillType?i>0?i-1:i:i),t.attr("d",h).attr("fill",a)}},{key:"_getBlockData",value:function(t){return[{index:t,label:this.data[t][0],value:e(this.data[t][1])?this.data[t][1][0]:this.data[t][1],formattedValue:e(this.data[t][1])?this.data[t][1][1]:this.data[t][1].toLocaleString(),baseColor:this.data[t][2],fill:this._getColor(t)}]}},{key:"_getColor",value:function(t){return"solid"===this.fillType?this.data[t][2]:"url(#gradient-"+t+")"}},{key:"_getPathDefinition",value:function(t){for(var i="",e=[],h=this.blockPaths[t],a=0;a<h.length;a++)e=h[a],i+=e[2]+e[0]+","+e[1]+" ";return i}},{key:"_onMouseOver",value:function(t){i.select(this).attr("fill",a(t.baseColor,-.2))}},{key:"_onMouseOut",value:function(t){i.select(this).attr("fill",t.fill)}},{key:"_addBlockLabel",value:function(t,i){var e=i,h=this.blockPaths[i],a=this._getBlockData(i)[0],s=a.label+": "+a.formattedValue,n=this.data[e][3]||this.label.fill,o=this.width/2,r=this.isCurved?(h[2][1]+h[3][1])/2+this.curveHeight/this.data.length:(h[1][1]+h[2][1])/2;t.append("text").text(s).attr({x:o,y:r,"text-anchor":"middle","dominant-baseline":"middle",fill:n,"pointer-events":"none"}).style("font-size",this.label.fontSize)}}]),t}();t.D3Funnel=s}(window,d3); | ||
/*! d3-funnel - v0.6.12 | 2015 */ | ||
!function(t,i){"function"==typeof define&&define.amd?define(["d3"],i):"object"==typeof exports?module.exports=i(require("d3")):t.D3Funnel=i(t.d3)}(this,function(t){"use strict";function i(t,i){if(!(t instanceof i))throw new TypeError("Cannot call a class as a function")}var e=function(){function t(t,i){for(var e=0;e<i.length;e++){var a=i[e];a.enumerable=a.enumerable||!1,a.configurable=!0,"value"in a&&(a.writable=!0),Object.defineProperty(t,a.key,a)}}return function(i,e,a){return e&&t(i.prototype,e),a&&t(i,a),i}}(),a=function(){function a(t){i(this,a),this.selector=t,this.defaults={width:350,height:400,bottomWidth:1/3,bottomPinch:0,isCurved:!1,curveHeight:20,fillType:"solid",isInverted:!1,hoverEffects:!1,dynamicArea:!1,minHeight:!1,animation:!1,label:{fontSize:"14px",fill:"#fff",format:"{l}: {f}"},onItemClick:null},this.labelFormatter=new n,this.navigator=new h}return e(a,[{key:"destroy",value:function(){t.select(this.selector).selectAll("svg").remove()}},{key:"draw",value:function(t){var i=arguments.length<=1||void 0===arguments[1]?{}:arguments[1];this.destroy(),this._initialize(t,i),this._draw()}},{key:"_initialize",value:function(t,i){this._validateData(t),this._setData(t);var e=this._getSettings(i);this.label=e.label,this.labelFormatter.setFormat(this.label.format),this.width=e.width,this.height=e.height,this.bottomWidth=e.width*e.bottomWidth,this.bottomPinch=e.bottomPinch,this.isCurved=e.isCurved,this.curveHeight=e.curveHeight,this.fillType=e.fillType,this.isInverted=e.isInverted,this.hoverEffects=e.hoverEffects,this.dynamicArea=e.dynamicArea,this.minHeight=e.minHeight,this.animation=e.animation,this.bottomLeftX=(this.width-this.bottomWidth)/2,this.dx=this._getDx(),this.dy=this._getDy(),this.onItemClick=e.onItemClick}},{key:"_validateData",value:function(t){if(Array.isArray(t)===!1||0===t.length||Array.isArray(t[0])===!1||t[0].length<2)throw new Error("Funnel data is not valid.")}},{key:"_setData",value:function(t){this.data=t,this._setColors()}},{key:"_setColors",value:function(){var i=this,e=t.scale.category10(),a=/^#([0-9a-f]{3}|[0-9a-f]{6})$/i;this.data.forEach(function(t,n){(t.length<3||!a.test(t[2]))&&(i.data[n][2]=e(n))})}},{key:"_getSettings",value:function(i){var e=s.extend({},this.defaults);return e.width=parseInt(t.select(this.selector).style("width"),10),e.height=parseInt(t.select(this.selector).style("height"),10),e=s.extend(e,i),e.width<=0&&(e.width=this.defaults.width),e.height<=0&&(e.height=this.defaults.height),e}},{key:"_getDx",value:function(){return this.bottomPinch>0?this.bottomLeftX/(this.data.length-this.bottomPinch):this.bottomLeftX/this.data.length}},{key:"_getDy",value:function(){return this.isCurved?(this.height-this.curveHeight)/this.data.length:this.height/this.data.length}},{key:"_draw",value:function(){this.svg=t.select(this.selector).append("svg").attr("width",this.width).attr("height",this.height),this.blockPaths=this._makePaths(),"gradient"===this.fillType&&this._defineColorGradients(this.svg),this.isCurved&&this._drawTopOval(this.svg,this.blockPaths),this._drawBlock(0)}},{key:"_makePaths",value:function(){var t=this,i=[],e=this.dx,a=this.dy,n=0,h=this.width,s=0;this.isInverted&&(n=this.bottomLeftX,h=this.width-this.bottomLeftX);var o=0,r=0,l=0,u=this.width/2;this.isCurved&&(s=10);var d=this.width,f=0,c=this.height*(this.width+this.bottomWidth)/2,v=2*this.height/(this.width-this.bottomWidth);this.minHeight!==!1&&(c=(this.height-this.minHeight*this.data.length)*(this.width+this.bottomWidth)/2);var g=0,m=0;return this.data.forEach(function(t){g+=Array.isArray(t[1])?t[1][0]:t[1]}),this.data.forEach(function(y,p){if(m=Array.isArray(y[1])?y[0]:y[1],t.dynamicArea){var k=m/g,b=k*c;t.minHeight!==!1&&(b+=t.minHeight*(t.width+t.bottomWidth)/2),f=Math.sqrt((v*d*d-4*b)/v),0===t.bottomWidth&&p===t.data.length-1&&(f=0),t.bottomWidth===t.width&&(f=d),e=d/2-f/2,a=2*b/(d+f),t.isCurved&&(a-=t.curveHeight/t.data.length),d=f}t.bottomPinch>0&&(t.isInverted?(t.dynamicArea||(e=t.dx),e=p<t.bottomPinch?0:e):p>=t.data.length-t.bottomPinch&&(e=0)),o=n+e,r=h-e,l=s+a,t.isInverted&&(o=n-e,r=h+e),t.isCurved?i.push([[n,s,"M"],[u,s+(t.curveHeight-10),"Q"],[h,s,""],[r,l,"L"],[r,l,"M"],[u,l+t.curveHeight,"Q"],[o,l,""],[n,s,"L"]]):i.push([[n,s,"M"],[h,s,"L"],[r,l,"L"],[o,l,"L"],[n,s,"L"]]),n=o,h=r,s=l}),i}},{key:"_defineColorGradients",value:function(t){var i=t.append("defs");this.data.forEach(function(t,e){var a=t[2],n=s.shadeColor(a,-.25),h=i.append("linearGradient").attr({id:"gradient-"+e}),o=[[0,n],[40,a],[60,a],[100,n]];o.forEach(function(t){h.append("stop").attr({offset:t[0]+"%",style:"stop-color:"+t[1]})})})}},{key:"_drawTopOval",value:function(t,i){var e=0,a=this.width,n=this.width/2;this.isInverted&&(e=this.bottomLeftX,a=this.width-this.bottomLeftX);var h=i[0],o=h[1][1]+this.curveHeight-10,r=this.navigator.plot([["M",e,h[0][1]],["Q",n,o],[" ",a,h[2][1]],["M",a,10],["Q",n,0],[" ",e,10]]);t.append("path").attr("fill",s.shadeColor(this.data[0][2],-.4)).attr("d",r)}},{key:"_drawBlock",value:function(t){var i=this;if(t!==this.data.length){var e=this.svg.append("g"),a=this._getBlockPath(e,t);a.data(this._getBlockData(t)),this.animation!==!1?a.transition().duration(this.animation).ease("linear").attr("fill",this._getColor(t)).attr("d",this._getPathDefinition(t)).each("end",function(){i._drawBlock(t+1)}):(a.attr("fill",this._getColor(t)).attr("d",this._getPathDefinition(t)),this._drawBlock(t+1)),this.hoverEffects&&a.on("mouseover",this._onMouseOver).on("mouseout",this._onMouseOut),null!==this.onItemClick&&a.on("click",this.onItemClick),this._addBlockLabel(e,t)}}},{key:"_getBlockPath",value:function(t,i){var e=t.append("path");return this.animation!==!1&&this._addBeforeTransition(e,i),e}},{key:"_addBeforeTransition",value:function(t,i){var e=this.blockPaths[i],a="",n="";a=this.isCurved?this.navigator.plot([["M",e[0][0],e[0][1]],["Q",e[1][0],e[1][1]],[" ",e[2][0],e[2][1]],["L",e[2][0],e[2][1]],["M",e[2][0],e[2][1]],["Q",e[1][0],e[1][1]],[" ",e[0][0],e[0][1]]]):this.navigator.plot([["M",e[0][0],e[0][1]],["L",e[1][0],e[1][1]],["L",e[1][0],e[1][1]],["L",e[0][0],e[0][1]]]),n="solid"===this.fillType&&i>0?this._getColor(i-1):this._getColor(i),t.attr("d",a).attr("fill",n)}},{key:"_getBlockData",value:function(t){var i=this.data[t][0],e=this.data[t][1];return[{index:t,label:i,value:e,formatted:this.labelFormatter.format(i,e),baseColor:this.data[t][2],fill:this._getColor(t)}]}},{key:"_getColor",value:function(t){return"solid"===this.fillType?this.data[t][2]:"url(#gradient-"+t+")"}},{key:"_getPathDefinition",value:function(t){var i=[];return this.blockPaths[t].forEach(function(t){i.push([t[2],t[0],t[1]])}),this.navigator.plot(i)}},{key:"_onMouseOver",value:function(i){t.select(this).attr("fill",s.shadeColor(i.baseColor,-.2))}},{key:"_onMouseOut",value:function(i){t.select(this).attr("fill",i.fill)}},{key:"_addBlockLabel",value:function(t,i){var e=this.blockPaths[i],a=this._getBlockData(i)[0].formatted,n=this.data[i][3]||this.label.fill,h=this.width/2,s=this._getTextY(e);t.append("text").text(a).attr({x:h,y:s,"text-anchor":"middle","dominant-baseline":"middle",fill:n,"pointer-events":"none"}).style("font-size",this.label.fontSize)}},{key:"_getTextY",value:function(t){return this.isCurved?(t[2][1]+t[3][1])/2+this.curveHeight/this.data.length:(t[1][1]+t[2][1])/2}}]),a}(),n=function(){function t(){i(this,t),this.expression=null}return e(t,[{key:"setFormat",value:function(t){"function"==typeof t?this.formatter=t:(this.expression=t,this.formatter=this.stringFormatter)}},{key:"format",value:function(t,i){return Array.isArray(i)?this.formatter(t,i[0],i[1]):this.formatter(t,i,null)}},{key:"stringFormatter",value:function(t,i){var e=arguments.length<=2||void 0===arguments[2]?null:arguments[2];return null===e&&(e=this.getDefaultFormattedValue(i)),this.expression.split("{l}").join(t).split("{v}").join(i).split("{f}").join(e)}},{key:"getDefaultFormattedValue",value:function(t){return t.toLocaleString()}}]),t}(),h=function(){function t(){i(this,t)}return e(t,[{key:"plot",value:function(t){var i="";return t.forEach(function(t){i+=t[0]+t[1]+","+t[2]+" "}),i.replace(/ +/g," ").trim()}}]),t}(),s=function(){function t(){i(this,t)}return e(t,null,[{key:"extend",value:function(i,e){var a=void 0;for(a in e)e.hasOwnProperty(a)&&("object"==typeof i[a]&&"object"==typeof e[a]?i[a]=t.extend(i[a],e[a]):i[a]=e[a]);return i}},{key:"shadeColor",value:function(i,e){var a=i.slice(1);3===a.length&&(a=t.expandHex(a));var n=parseInt(a,16),h=0>e?0:255,s=0>e?-1*e:e,o=n>>16,r=n>>8&255,l=255&n,u=16777216+65536*(Math.round((h-o)*s)+o)+256*(Math.round((h-r)*s)+r)+(Math.round((h-l)*s)+l);return"#"+u.toString(16).slice(1)}},{key:"expandHex",value:function(t){return t[0]+t[0]+t[1]+t[1]+t[2]+t[2]}}]),t}();return a}); |
var gulp = require('gulp'); | ||
var umd = require('gulp-wrap-umd'); | ||
var concat = require('gulp-concat'); | ||
var jshint = require('gulp-jshint'); | ||
@@ -13,3 +15,17 @@ var jscs = require('gulp-jscs'); | ||
var src = ['./src/d3-funnel/d3-funnel.js']; | ||
var src = [ | ||
'./src/d3-funnel/d3-funnel.js', | ||
'./src/d3-funnel/label-formatter.js', | ||
'./src/d3-funnel/navigator.js', | ||
'./src/d3-funnel/utils.js', | ||
]; | ||
var umdOptions = { | ||
exports: 'D3Funnel', | ||
namespace: 'D3Funnel', | ||
deps: [{ | ||
name: 'd3', | ||
globalName: 'd3', | ||
paramName: 'd3', | ||
}], | ||
}; | ||
@@ -22,3 +38,3 @@ gulp.task('test-format', function () { | ||
.pipe(jscs({ | ||
configPath: './.jscsrc' | ||
configPath: './.jscsrc', | ||
})); | ||
@@ -29,3 +45,5 @@ }); | ||
return gulp.src(src) | ||
.pipe(concat('d3-funnel.js')) | ||
.pipe(babel()) | ||
.pipe(umd(umdOptions)) | ||
.pipe(gulp.dest('./compiled/')); | ||
@@ -39,11 +57,13 @@ }); | ||
gulp.task('build', ['test-format', 'test-mocha'], function () { | ||
gulp.task('test', ['test-format', 'test-mocha']); | ||
gulp.task('build', ['test'], function () { | ||
return gulp.src(['./compiled/d3-funnel.js']) | ||
.pipe(gulp.dest('./dist/')) | ||
.pipe(rename({ | ||
extname: '.min.js'} | ||
)) | ||
extname: '.min.js', | ||
})) | ||
.pipe(uglify()) | ||
.pipe(header(banner, { | ||
pkg: pkg | ||
pkg: pkg, | ||
})) | ||
@@ -53,2 +73,6 @@ .pipe(gulp.dest('./dist/')); | ||
gulp.task('watch', function () { | ||
gulp.watch(src, ['build']); | ||
}); | ||
gulp.task('default', ['build']); |
@@ -0,0 +0,0 @@ The MIT License (MIT) |
{ | ||
"name": "d3-funnel", | ||
"version": "0.6.11", | ||
"version": "0.6.12", | ||
"description": "A library for rendering SVG funnel charts using D3.js", | ||
@@ -15,11 +15,14 @@ "author": "Jake Zatecky", | ||
"chai": "^3.2.0", | ||
"chai-spies": "^0.7.0", | ||
"gulp": "^3.9.0", | ||
"gulp-babel": "^5.2.0", | ||
"gulp-header": "^1.2.2", | ||
"gulp-babel": "^5.2.1", | ||
"gulp-concat": "^2.6.0", | ||
"gulp-header": "^1.7.1", | ||
"gulp-jscs": "^2.0.0", | ||
"gulp-jshint": "^1.11.2", | ||
"gulp-mocha-phantomjs": "^0.8.1", | ||
"gulp-mocha-phantomjs": "^0.10.1", | ||
"gulp-rename": "^1.2.2", | ||
"gulp-uglify": "^1.2.0", | ||
"mocha": "^2.2.5" | ||
"gulp-uglify": "^1.4.1", | ||
"gulp-wrap-umd": "^0.2.1", | ||
"mocha": "^2.3.2" | ||
}, | ||
@@ -26,0 +29,0 @@ "dependencies": { |
# D3 Funnel | ||
[![npm](https://img.shields.io/npm/v/d3-funnel.svg)](https://www.npmjs.com/package/d3-funnel) | ||
[![Build Status](https://travis-ci.org/jakezatecky/d3-funnel.svg?branch=master)](https://travis-ci.org/jakezatecky/d3-funnel) | ||
[![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](https://raw.githubusercontent.com/jakezatecky/d3-funnel/master/LICENSE.txt) | ||
[![npm](https://img.shields.io/npm/v/d3-funnel.svg?style=flat-square)](https://www.npmjs.com/package/d3-funnel) | ||
[![Build Status](https://img.shields.io/travis/jakezatecky/d3-funnel/master.svg?style=flat-square)](https://travis-ci.org/jakezatecky/d3-funnel) | ||
[![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg?style=flat-square)](https://raw.githubusercontent.com/jakezatecky/d3-funnel/master/LICENSE.txt) | ||
@@ -34,3 +34,3 @@ **D3Funnel** is an extensible, open-source JavaScript library for rendering | ||
['Perennials', 200], | ||
['Roses', 50] | ||
['Roses', 50], | ||
]; | ||
@@ -46,20 +46,32 @@ var options = {}; | ||
| Option | Description | Type | Default | | ||
| ---------------- | --------------------------------------------------------------------------- | -------- | ------------------- | | ||
| `width` | The pixel width of the chart. | int | Container's width | | ||
| `height` | The pixel height of the chart. | int | Container's height | | ||
| `bottomWidth` | The percent of total width the bottom should be. | float | `1 / 3` | | ||
| `bottomPinch` | How many blocks to pinch on the bottom to create a "neck". | int | `0` | | ||
| `isCurved` | Whether the funnel is curved. | bool | `false` | | ||
| `curveHeight` | The curvature amount (if `isCurved` is `true`). | int | `20` | | ||
| `fillType` | Either `'solid'` or `'gradient'`. | string | `'solid'` | | ||
| `isInverted` | Whether the funnel is inverted (like a pyramid). | bool | `false` | | ||
| `hoverEffects` | Whether the funnel has effects on hover. | bool | `false` | | ||
| `dynamicArea` | Whether block areas are calculated by counts (as opposed to static height). | bool | `false` | | ||
| `minHeight` | The minimum pixel height of a block. | int/bool | `false` | | ||
| `animation` | The load animation speed in milliseconds. | int/bool | `false` | | ||
| `label.fontSize` | Any valid font size for the labels. | string | `'14px'` | | ||
| `label.fill` | Any valid hex color for the label color | string | `'#fff'` | | ||
| `onItemClick` | Event handler if one of the items is clicked. | function | `function(d, i) {}` | | ||
| Option | Description | Type | Default | | ||
| ---------------- | --------------------------------------------------------------------------- | -------- | ------------------ | | ||
| `width` | The pixel width of the chart. | int | Container's width | | ||
| `height` | The pixel height of the chart. | int | Container's height | | ||
| `bottomWidth` | The percent of total width the bottom should be. | float | `1 / 3` | | ||
| `bottomPinch` | How many blocks to pinch on the bottom to create a "neck". | int | `0` | | ||
| `isCurved` | Whether the funnel is curved. | bool | `false` | | ||
| `curveHeight` | The curvature amount (if `isCurved` is `true`). | int | `20` | | ||
| `fillType` | Either `'solid'` or `'gradient'`. | string | `'solid'` | | ||
| `isInverted` | Whether the funnel is inverted (like a pyramid). | bool | `false` | | ||
| `hoverEffects` | Whether the funnel has effects on hover. | bool | `false` | | ||
| `dynamicArea` | Whether block areas are calculated by counts (as opposed to static height). | bool | `false` | | ||
| `minHeight` | The minimum pixel height of a block. | int/bool | `false` | | ||
| `animation` | The load animation speed in milliseconds. | int/bool | `false` | | ||
| `label.fontSize` | Any valid font size for the labels. | string | `'14px'` | | ||
| `label.fill` | Any valid hex color for the label color | string | `'#fff'` | | ||
| `label.format` | Either `function(label, value)` or a format string. See below. | mixed | `'{l}: {f}'` | | ||
| `onItemClick` | Event handler if one of the items is clicked. | function | `null` | | ||
### Label Format | ||
The option `label.format` can either be a function or a string. The following | ||
keys will be substituted by the string formatter: | ||
| Key | Description | | ||
| ------- | ---------------------------- | | ||
| `'{l}'` | The block's supplied label. | | ||
| `'{v}'` | The block's raw value. | | ||
| `'{f}'` | The block's formatted value. | | ||
## API | ||
@@ -82,3 +94,3 @@ | ||
['Persimmon', 2500, '#ff634d' '#6f34fd'], | ||
['Azure', 1500, '#007fff' '#07fff0'] | ||
['Azure', 1500, '#007fff' '#07fff0'], | ||
// Background ---^ ^--- Label | ||
@@ -96,3 +108,3 @@ ]; | ||
['Persimmon', [2500, 'USD 2,500']], | ||
['Azure', [1500, 'USD 1,500']] | ||
['Azure', [1500, 'USD 1,500']], | ||
]; | ||
@@ -99,0 +111,0 @@ ``` |
@@ -1,701 +0,705 @@ | ||
((global, d3) => { | ||
/* global d3, LabelFormatter, Navigator, Utils */ | ||
/* exported D3Funnel */ | ||
/* global d3 */ | ||
/* jshint bitwise: false */ | ||
'use strict'; | ||
class D3Funnel { | ||
class D3Funnel | ||
{ | ||
/** | ||
* @param {string} selector A selector for the container element. | ||
* | ||
* @return {void} | ||
*/ | ||
constructor(selector) { | ||
this.selector = selector; | ||
/** | ||
* @param {string} selector A selector for the container element. | ||
* | ||
* @return {void} | ||
*/ | ||
constructor(selector) | ||
{ | ||
this.selector = selector; | ||
// Default configuration values | ||
this.defaults = { | ||
width: 350, | ||
height: 400, | ||
bottomWidth: 1 / 3, | ||
bottomPinch: 0, | ||
isCurved: false, | ||
curveHeight: 20, | ||
fillType: 'solid', | ||
isInverted: false, | ||
hoverEffects: false, | ||
dynamicArea: false, | ||
minHeight: false, | ||
animation: false, | ||
label: { | ||
fontSize: '14px', | ||
fill: '#fff', | ||
format: '{l}: {f}', | ||
}, | ||
onItemClick: null, | ||
}; | ||
// Default configuration values | ||
this.defaults = { | ||
width: 350, | ||
height: 400, | ||
bottomWidth: 1 / 3, | ||
bottomPinch: 0, | ||
isCurved: false, | ||
curveHeight: 20, | ||
fillType: 'solid', | ||
isInverted: false, | ||
hoverEffects: false, | ||
dynamicArea: false, | ||
minHeight: false, | ||
animation: false, | ||
label: { | ||
fontSize: '14px', | ||
fill: '#fff', | ||
}, | ||
}; | ||
} | ||
this.labelFormatter = new LabelFormatter(); | ||
/** | ||
* Remove the funnel and its events from the DOM. | ||
* | ||
* @return {void} | ||
*/ | ||
destroy() | ||
{ | ||
// D3's remove method appears to be sufficient for removing the events | ||
d3.select(this.selector).selectAll('svg').remove(); | ||
} | ||
this.navigator = new Navigator(); | ||
} | ||
/** | ||
* Draw the chart inside the container with the data and configuration | ||
* specified. This will remove any previous SVG elements in the container | ||
* and draw a new funnel chart on top of it. | ||
* | ||
* @param {Array} data A list of rows containing a category, a count, | ||
* and optionally a color (in hex). | ||
* @param {Object} options An optional configuration object to override | ||
* defaults. See the docs. | ||
* | ||
* @return {void} | ||
*/ | ||
draw(data, options = {}) | ||
{ | ||
// Remove any previous drawings | ||
this.destroy(); | ||
/** | ||
* Remove the funnel and its events from the DOM. | ||
* | ||
* @return {void} | ||
*/ | ||
destroy() { | ||
// D3's remove method appears to be sufficient for removing the events | ||
d3.select(this.selector).selectAll('svg').remove(); | ||
} | ||
// Initialize chart options | ||
this._initialize(data, options); | ||
/** | ||
* Draw the chart inside the container with the data and configuration | ||
* specified. This will remove any previous SVG elements in the container | ||
* and draw a new funnel chart on top of it. | ||
* | ||
* @param {Array} data A list of rows containing a category, a count, | ||
* and optionally a color (in hex). | ||
* @param {Object} options An optional configuration object to override | ||
* defaults. See the docs. | ||
* | ||
* @return {void} | ||
*/ | ||
draw(data, options = {}) { | ||
this.destroy(); | ||
// Add the SVG | ||
this.svg = d3.select(this.selector) | ||
.append('svg') | ||
.attr('width', this.width) | ||
.attr('height', this.height); | ||
this._initialize(data, options); | ||
this.blockPaths = this._makePaths(); | ||
this._draw(); | ||
} | ||
// Define color gradients | ||
if (this.fillType === 'gradient') { | ||
this._defineColorGradients(this.svg); | ||
} | ||
/** | ||
* Initialize and calculate important variables for drawing the chart. | ||
* | ||
* @param {Array} data | ||
* @param {Object} options | ||
* | ||
* @return {void} | ||
*/ | ||
_initialize(data, options) { | ||
this._validateData(data); | ||
// Add top oval if curved | ||
if (this.isCurved) { | ||
this._drawTopOval(this.svg, this.blockPaths); | ||
} | ||
this._setData(data); | ||
// Add each block | ||
this._drawBlock(0); | ||
} | ||
let settings = this._getSettings(options); | ||
/** | ||
* Initialize and calculate important variables for drawing the chart. | ||
* | ||
* @param {Array} data | ||
* @param {Object} options | ||
* | ||
* @return {void} | ||
*/ | ||
_initialize(data, options) | ||
{ | ||
if (!isArray(data) || data.length === 0 || !isArray(data[0]) || data[0].length < 2) { | ||
throw new Error('Funnel data is not valid.'); | ||
} | ||
// Set labels | ||
this.label = settings.label; | ||
this.labelFormatter.setFormat(this.label.format); | ||
this.data = data; | ||
// Initialize funnel chart settings | ||
this.width = settings.width; | ||
this.height = settings.height; | ||
this.bottomWidth = settings.width * settings.bottomWidth; | ||
this.bottomPinch = settings.bottomPinch; | ||
this.isCurved = settings.isCurved; | ||
this.curveHeight = settings.curveHeight; | ||
this.fillType = settings.fillType; | ||
this.isInverted = settings.isInverted; | ||
this.hoverEffects = settings.hoverEffects; | ||
this.dynamicArea = settings.dynamicArea; | ||
this.minHeight = settings.minHeight; | ||
this.animation = settings.animation; | ||
// Counter | ||
let i; | ||
// Calculate the bottom left x position | ||
this.bottomLeftX = (this.width - this.bottomWidth) / 2; | ||
// Prepare the configuration settings based on the defaults | ||
// Set the default width and height based on the container | ||
let settings = extend({}, this.defaults); | ||
settings.width = parseInt(d3.select(this.selector).style('width'), 10); | ||
settings.height = parseInt(d3.select(this.selector).style('height'), 10); | ||
// Change in x direction | ||
this.dx = this._getDx(); | ||
// Overwrite default settings with user options | ||
let keys = Object.keys(options); | ||
for (i = 0; i < keys.length; i++) { | ||
if (keys[i] !== 'label') { | ||
settings[keys[i]] = options[keys[i]]; | ||
} | ||
} | ||
// Change in y direction | ||
this.dy = this._getDy(); | ||
// Label settings | ||
if (options.hasOwnProperty('label')) { | ||
let validLabelOptions = /fontSize|fill/; | ||
// Support for events | ||
this.onItemClick = settings.onItemClick; | ||
} | ||
Object.keys(options.label).forEach((labelOption) => { | ||
if (labelOption.match(validLabelOptions)) { | ||
settings.label[labelOption] = options.label[labelOption]; | ||
} | ||
}); | ||
} | ||
this.label = settings.label; | ||
/** | ||
* @param {Array} data | ||
* | ||
* @return void | ||
*/ | ||
_validateData(data) { | ||
if (Array.isArray(data) === false || | ||
data.length === 0 || | ||
Array.isArray(data[0]) === false || | ||
data[0].length < 2) { | ||
throw new Error('Funnel data is not valid.'); | ||
} | ||
} | ||
// In the case that the width or height is not valid, set | ||
// the width/height as its default hard-coded value | ||
if (settings.width <= 0) { | ||
settings.width = this.defaults.width; | ||
} | ||
if (settings.height <= 0) { | ||
settings.height = this.defaults.height; | ||
} | ||
/** | ||
* @param {Array} data | ||
* | ||
* @return void | ||
*/ | ||
_setData(data) { | ||
this.data = data; | ||
// Initialize the colors for each block | ||
let colorScale = d3.scale.category10(); | ||
for (i = 0; i < this.data.length; i++) { | ||
let hexExpression = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i; | ||
this._setColors(); | ||
} | ||
// If a color is not set for the record, add one | ||
if (!('2' in this.data[i]) || !hexExpression.test(this.data[i][2])) { | ||
this.data[i][2] = colorScale(i); | ||
} | ||
/** | ||
* Set the colors for each block. | ||
* | ||
* @return {void} | ||
*/ | ||
_setColors() { | ||
let colorScale = d3.scale.category10(); | ||
let hexExpression = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i; | ||
// Add a color for for each block without one | ||
this.data.forEach((block, index) => { | ||
if (block.length < 3 || !hexExpression.test(block[2])) { | ||
this.data[index][2] = colorScale(index); | ||
} | ||
}); | ||
} | ||
// Initialize funnel chart settings | ||
this.width = settings.width; | ||
this.height = settings.height; | ||
this.bottomWidth = settings.width * settings.bottomWidth; | ||
this.bottomPinch = settings.bottomPinch; | ||
this.isCurved = settings.isCurved; | ||
this.curveHeight = settings.curveHeight; | ||
this.fillType = settings.fillType; | ||
this.isInverted = settings.isInverted; | ||
this.hoverEffects = settings.hoverEffects; | ||
this.dynamicArea = settings.dynamicArea; | ||
this.minHeight = settings.minHeight; | ||
this.animation = settings.animation; | ||
/** | ||
* @param {Object} options | ||
* | ||
* @returns {Object} | ||
*/ | ||
_getSettings(options) { | ||
// Prepare the configuration settings based on the defaults | ||
// Set the default width and height based on the container | ||
let settings = Utils.extend({}, this.defaults); | ||
settings.width = parseInt(d3.select(this.selector).style('width'), 10); | ||
settings.height = parseInt(d3.select(this.selector).style('height'), 10); | ||
// Calculate the bottom left x position | ||
this.bottomLeftX = (this.width - this.bottomWidth) / 2; | ||
// Overwrite default settings with user options | ||
settings = Utils.extend(settings, options); | ||
// Change in x direction | ||
// Will be sharper if there is a pinch | ||
this.dx = this.bottomPinch > 0 ? | ||
this.bottomLeftX / (data.length - this.bottomPinch) : | ||
this.bottomLeftX / data.length; | ||
// Change in y direction | ||
// Curved chart needs reserved pixels to account for curvature | ||
this.dy = this.isCurved ? | ||
(this.height - this.curveHeight) / data.length : | ||
this.height / data.length; | ||
// In the case that the width or height is not valid, set | ||
// the width/height as its default hard-coded value | ||
if (settings.width <= 0) { | ||
settings.width = this.defaults.width; | ||
} | ||
if (settings.height <= 0) { | ||
settings.height = this.defaults.height; | ||
} | ||
// Support for events | ||
this.onItemClick = settings.onItemClick; | ||
return settings; | ||
} | ||
/** | ||
* @return {Number} | ||
*/ | ||
_getDx() { | ||
// Will be sharper if there is a pinch | ||
if (this.bottomPinch > 0) { | ||
return this.bottomLeftX / (this.data.length - this.bottomPinch); | ||
} | ||
/** | ||
* Create the paths to be used to define the discrete funnel blocks and | ||
* returns the results in an array. | ||
* | ||
* @return {Array} | ||
*/ | ||
_makePaths() | ||
{ | ||
let paths = []; | ||
return this.bottomLeftX / this.data.length; | ||
} | ||
// Initialize velocity | ||
let dx = this.dx; | ||
let dy = this.dy; | ||
/** | ||
* @return {Number} | ||
*/ | ||
_getDy() { | ||
// Curved chart needs reserved pixels to account for curvature | ||
if (this.isCurved) { | ||
return (this.height - this.curveHeight) / this.data.length; | ||
} | ||
// Initialize starting positions | ||
let prevLeftX = 0; | ||
let prevRightX = this.width; | ||
let prevHeight = 0; | ||
return this.height / this.data.length; | ||
} | ||
// Start from the bottom for inverted | ||
if (this.isInverted) { | ||
prevLeftX = this.bottomLeftX; | ||
prevRightX = this.width - this.bottomLeftX; | ||
} | ||
/** | ||
* Draw the chart onto the DOM. | ||
* | ||
* @return {void} | ||
*/ | ||
_draw() { | ||
// Add the SVG | ||
this.svg = d3.select(this.selector) | ||
.append('svg') | ||
.attr('width', this.width) | ||
.attr('height', this.height); | ||
// Initialize next positions | ||
let nextLeftX = 0; | ||
let nextRightX = 0; | ||
let nextHeight = 0; | ||
this.blockPaths = this._makePaths(); | ||
let middle = this.width / 2; | ||
// Define color gradients | ||
if (this.fillType === 'gradient') { | ||
this._defineColorGradients(this.svg); | ||
} | ||
// Move down if there is an initial curve | ||
if (this.isCurved) { | ||
prevHeight = 10; | ||
} | ||
// Add top oval if curved | ||
if (this.isCurved) { | ||
this._drawTopOval(this.svg, this.blockPaths); | ||
} | ||
let topBase = this.width; | ||
let bottomBase = 0; | ||
// Add each block | ||
this._drawBlock(0); | ||
} | ||
let totalArea = this.height * (this.width + this.bottomWidth) / 2; | ||
let slope = 2 * this.height / (this.width - this.bottomWidth); | ||
/** | ||
* Create the paths to be used to define the discrete funnel blocks and | ||
* returns the results in an array. | ||
* | ||
* @return {Array} | ||
*/ | ||
_makePaths() { | ||
let paths = []; | ||
// This is greedy in that the block will have a guaranteed height | ||
// and the remaining is shared among the ratio, instead of being | ||
// shared according to the remaining minus the guaranteed | ||
if (this.minHeight !== false) { | ||
let height = (this.height - this.minHeight * this.data.length); | ||
totalArea = height * (this.width + this.bottomWidth) / 2; | ||
} | ||
// Initialize velocity | ||
let dx = this.dx; | ||
let dy = this.dy; | ||
let totalCount = 0; | ||
let count = 0; | ||
// Initialize starting positions | ||
let prevLeftX = 0; | ||
let prevRightX = this.width; | ||
let prevHeight = 0; | ||
// Harvest total count | ||
for (let i = 0; i < this.data.length; i++) { | ||
totalCount += isArray(this.data[i][1]) ? this.data[i][1][0] : this.data[i][1]; | ||
} | ||
// Start from the bottom for inverted | ||
if (this.isInverted) { | ||
prevLeftX = this.bottomLeftX; | ||
prevRightX = this.width - this.bottomLeftX; | ||
} | ||
// Create the path definition for each funnel block | ||
// Remember to loop back to the beginning point for a closed path | ||
for (let i = 0; i < this.data.length; i++) { | ||
count = isArray(this.data[i][1]) ? this.data[i][1][0] : this.data[i][1]; | ||
// Initialize next positions | ||
let nextLeftX = 0; | ||
let nextRightX = 0; | ||
let nextHeight = 0; | ||
// Calculate dynamic shapes based on area | ||
if (this.dynamicArea) { | ||
let ratio = count / totalCount; | ||
let area = ratio * totalArea; | ||
let middle = this.width / 2; | ||
if (this.minHeight !== false) { | ||
area += this.minHeight * (this.width + this.bottomWidth) / 2; | ||
} | ||
// Move down if there is an initial curve | ||
if (this.isCurved) { | ||
prevHeight = 10; | ||
} | ||
bottomBase = Math.sqrt((slope * topBase * topBase - (4 * area)) / slope); | ||
dx = (topBase / 2) - (bottomBase / 2); | ||
dy = (area * 2) / (topBase + bottomBase); | ||
let topBase = this.width; | ||
let bottomBase = 0; | ||
if (this.isCurved) { | ||
dy = dy - (this.curveHeight / this.data.length); | ||
} | ||
let totalArea = this.height * (this.width + this.bottomWidth) / 2; | ||
let slope = 2 * this.height / (this.width - this.bottomWidth); | ||
topBase = bottomBase; | ||
} | ||
// This is greedy in that the block will have a guaranteed height | ||
// and the remaining is shared among the ratio, instead of being | ||
// shared according to the remaining minus the guaranteed | ||
if (this.minHeight !== false) { | ||
totalArea = (this.height - this.minHeight * this.data.length) * (this.width + this.bottomWidth) / 2; | ||
} | ||
// Stop velocity for pinched blocks | ||
if (this.bottomPinch > 0) { | ||
// Check if we've reached the bottom of the pinch | ||
// If so, stop changing on x | ||
if (!this.isInverted) { | ||
if (i >= this.data.length - this.bottomPinch) { | ||
dx = 0; | ||
} | ||
// Pinch at the first blocks relating to the bottom pinch | ||
// Revert back to normal velocity after pinch | ||
} else { | ||
// Revert velocity back to the initial if we are using | ||
// static area's (prevents zero velocity if isInverted | ||
// and bottomPinch are non trivial and dynamicArea is | ||
// false) | ||
if (!this.dynamicArea) { | ||
dx = this.dx; | ||
} | ||
let totalCount = 0; | ||
let count = 0; | ||
dx = i < this.bottomPinch ? 0 : dx; | ||
} | ||
// Harvest total count | ||
this.data.forEach((block) => { | ||
totalCount += Array.isArray(block[1]) ? block[1][0] : block[1]; | ||
}); | ||
// Create the path definition for each funnel block | ||
// Remember to loop back to the beginning point for a closed path | ||
this.data.forEach((block, i) => { | ||
count = Array.isArray(block[1]) ? block[0] : block[1]; | ||
// Calculate dynamic shapes based on area | ||
if (this.dynamicArea) { | ||
let ratio = count / totalCount; | ||
let area = ratio * totalArea; | ||
if (this.minHeight !== false) { | ||
area += this.minHeight * (this.width + this.bottomWidth) / 2; | ||
} | ||
// Calculate the position of next block | ||
nextLeftX = prevLeftX + dx; | ||
nextRightX = prevRightX - dx; | ||
nextHeight = prevHeight + dy; | ||
bottomBase = Math.sqrt((slope * topBase * topBase - (4 * area)) / slope); | ||
// Expand outward if inverted | ||
if (this.isInverted) { | ||
nextLeftX = prevLeftX - dx; | ||
nextRightX = prevRightX + dx; | ||
// Prevent bottm points from becomming NaN | ||
if (this.bottomWidth === 0 && i === this.data.length - 1) { | ||
bottomBase = 0; | ||
} | ||
// Plot curved lines | ||
// Prevent NaN slope | ||
if (this.bottomWidth === this.width) { | ||
bottomBase = topBase; | ||
} | ||
dx = (topBase / 2) - (bottomBase / 2); | ||
dy = (area * 2) / (topBase + bottomBase); | ||
if (this.isCurved) { | ||
paths.push([ | ||
// Top Bezier curve | ||
[prevLeftX, prevHeight, 'M'], | ||
[middle, prevHeight + (this.curveHeight - 10), 'Q'], | ||
[prevRightX, prevHeight, ''], | ||
// Right line | ||
[nextRightX, nextHeight, 'L'], | ||
// Bottom Bezier curve | ||
[nextRightX, nextHeight, 'M'], | ||
[middle, nextHeight + this.curveHeight, 'Q'], | ||
[nextLeftX, nextHeight, ''], | ||
// Left line | ||
[prevLeftX, prevHeight, 'L'], | ||
]); | ||
// Plot straight lines | ||
} else { | ||
paths.push([ | ||
// Start position | ||
[prevLeftX, prevHeight, 'M'], | ||
// Move to right | ||
[prevRightX, prevHeight, 'L'], | ||
// Move down | ||
[nextRightX, nextHeight, 'L'], | ||
// Move to left | ||
[nextLeftX, nextHeight, 'L'], | ||
// Wrap back to top | ||
[prevLeftX, prevHeight, 'L'], | ||
]); | ||
dy = dy - (this.curveHeight / this.data.length); | ||
} | ||
// Set the next block's previous position | ||
prevLeftX = nextLeftX; | ||
prevRightX = nextRightX; | ||
prevHeight = nextHeight; | ||
topBase = bottomBase; | ||
} | ||
return paths; | ||
} | ||
// Stop velocity for pinched blocks | ||
if (this.bottomPinch > 0) { | ||
// Check if we've reached the bottom of the pinch | ||
// If so, stop changing on x | ||
if (!this.isInverted) { | ||
if (i >= this.data.length - this.bottomPinch) { | ||
dx = 0; | ||
} | ||
// Pinch at the first blocks relating to the bottom pinch | ||
// Revert back to normal velocity after pinch | ||
} else { | ||
// Revert velocity back to the initial if we are using | ||
// static area's (prevents zero velocity if isInverted | ||
// and bottomPinch are non trivial and dynamicArea is | ||
// false) | ||
if (!this.dynamicArea) { | ||
dx = this.dx; | ||
} | ||
/** | ||
* Define the linear color gradients. | ||
* | ||
* @param {Object} svg | ||
* | ||
* @return {void} | ||
*/ | ||
_defineColorGradients(svg) | ||
{ | ||
let defs = svg.append('defs'); | ||
// Create a gradient for each block | ||
for (let i = 0; i < this.data.length; i++) { | ||
let color = this.data[i][2]; | ||
let shade = shadeColor(color, -0.25); | ||
// Create linear gradient | ||
let gradient = defs.append('linearGradient') | ||
.attr({ | ||
id: 'gradient-' + i | ||
}); | ||
// Define the gradient stops | ||
let stops = [ | ||
[0, shade], | ||
[40, color], | ||
[60, color], | ||
[100, shade], | ||
]; | ||
// Add the gradient stops | ||
for (let j = 0; j < stops.length; j++) { | ||
let stop = stops[j]; | ||
gradient.append('stop').attr({ | ||
offset: stop[0] + '%', | ||
style: 'stop-color:' + stop[1], | ||
}); | ||
dx = i < this.bottomPinch ? 0 : dx; | ||
} | ||
} | ||
} | ||
/** | ||
* Draw the top oval of a curved funnel. | ||
* | ||
* @param {Object} svg | ||
* @param {Array} blockPaths | ||
* | ||
* @return {void} | ||
*/ | ||
_drawTopOval(svg, blockPaths) | ||
{ | ||
let leftX = 0; | ||
let rightX = this.width; | ||
let centerX = this.width / 2; | ||
// Calculate the position of next block | ||
nextLeftX = prevLeftX + dx; | ||
nextRightX = prevRightX - dx; | ||
nextHeight = prevHeight + dy; | ||
// Expand outward if inverted | ||
if (this.isInverted) { | ||
leftX = this.bottomLeftX; | ||
rightX = this.width - this.bottomLeftX; | ||
nextLeftX = prevLeftX - dx; | ||
nextRightX = prevRightX + dx; | ||
} | ||
// Create path from top-most block | ||
let paths = blockPaths[0]; | ||
let path = 'M' + leftX + ',' + paths[0][1] + | ||
' Q' + centerX + ',' + (paths[1][1] + this.curveHeight - 10) + | ||
' ' + rightX + ',' + paths[2][1] + | ||
' M' + rightX + ',10' + | ||
' Q' + centerX + ',0' + | ||
' ' + leftX + ',10'; | ||
// Plot curved lines | ||
if (this.isCurved) { | ||
paths.push([ | ||
// Top Bezier curve | ||
[prevLeftX, prevHeight, 'M'], | ||
[middle, prevHeight + (this.curveHeight - 10), 'Q'], | ||
[prevRightX, prevHeight, ''], | ||
// Right line | ||
[nextRightX, nextHeight, 'L'], | ||
// Bottom Bezier curve | ||
[nextRightX, nextHeight, 'M'], | ||
[middle, nextHeight + this.curveHeight, 'Q'], | ||
[nextLeftX, nextHeight, ''], | ||
// Left line | ||
[prevLeftX, prevHeight, 'L'], | ||
]); | ||
// Plot straight lines | ||
} else { | ||
paths.push([ | ||
// Start position | ||
[prevLeftX, prevHeight, 'M'], | ||
// Move to right | ||
[prevRightX, prevHeight, 'L'], | ||
// Move down | ||
[nextRightX, nextHeight, 'L'], | ||
// Move to left | ||
[nextLeftX, nextHeight, 'L'], | ||
// Wrap back to top | ||
[prevLeftX, prevHeight, 'L'], | ||
]); | ||
} | ||
// Draw top oval | ||
svg.append('path') | ||
.attr('fill', shadeColor(this.data[0][2], -0.4)) | ||
.attr('d', path); | ||
} | ||
// Set the next block's previous position | ||
prevLeftX = nextLeftX; | ||
prevRightX = nextRightX; | ||
prevHeight = nextHeight; | ||
}); | ||
/** | ||
* Draw the next block in the iteration. | ||
* | ||
* @param {int} index | ||
* | ||
* @return {void} | ||
*/ | ||
_drawBlock(index) | ||
{ | ||
if (index === this.data.length) { | ||
return; | ||
} | ||
return paths; | ||
} | ||
// Create a group just for this block | ||
let group = this.svg.append('g'); | ||
/** | ||
* Define the linear color gradients. | ||
* | ||
* @param {Object} svg | ||
* | ||
* @return {void} | ||
*/ | ||
_defineColorGradients(svg) { | ||
let defs = svg.append('defs'); | ||
// Fetch path element | ||
let path = this._getBlockPath(group, index); | ||
path.data(this._getBlockData(index)); | ||
// Create a gradient for each block | ||
this.data.forEach((block, index) => { | ||
let color = block[2]; | ||
let shade = Utils.shadeColor(color, -0.25); | ||
// Add animation components | ||
if (this.animation !== false) { | ||
path.transition() | ||
.duration(this.animation) | ||
.ease('linear') | ||
.attr('fill', this._getColor(index)) | ||
.attr('d', this._getPathDefinition(index)) | ||
.each('end', () => { | ||
this._drawBlock(index + 1); | ||
}); | ||
} else { | ||
path.attr('fill', this._getColor(index)) | ||
.attr('d', this._getPathDefinition(index)); | ||
this._drawBlock(index + 1); | ||
} | ||
// Create linear gradient | ||
let gradient = defs.append('linearGradient') | ||
.attr({ | ||
id: 'gradient-' + index, | ||
}); | ||
// Add the hover events | ||
if (this.hoverEffects) { | ||
path.on('mouseover', this._onMouseOver) | ||
.on('mouseout', this._onMouseOut); | ||
} | ||
// Define the gradient stops | ||
let stops = [ | ||
[0, shade], | ||
[40, color], | ||
[60, color], | ||
[100, shade], | ||
]; | ||
// ItemClick event | ||
if (this.onItemClick) { | ||
path.on('click', this.onItemClick); | ||
} | ||
// Add the gradient stops | ||
stops.forEach((stop) => { | ||
gradient.append('stop').attr({ | ||
offset: stop[0] + '%', | ||
style: 'stop-color:' + stop[1], | ||
}); | ||
}); | ||
}); | ||
} | ||
this._addBlockLabel(group, index); | ||
/** | ||
* Draw the top oval of a curved funnel. | ||
* | ||
* @param {Object} svg | ||
* @param {Array} blockPaths | ||
* | ||
* @return {void} | ||
*/ | ||
_drawTopOval(svg, blockPaths) { | ||
let leftX = 0; | ||
let rightX = this.width; | ||
let centerX = this.width / 2; | ||
if (this.isInverted) { | ||
leftX = this.bottomLeftX; | ||
rightX = this.width - this.bottomLeftX; | ||
} | ||
/** | ||
* @param {Object} group | ||
* @param {int} index | ||
* | ||
* @return {Object} | ||
*/ | ||
_getBlockPath(group, index) | ||
{ | ||
let path = group.append('path'); | ||
// Create path from top-most block | ||
let paths = blockPaths[0]; | ||
let topCurve = paths[1][1] + this.curveHeight - 10; | ||
if (this.animation !== false) { | ||
this._addBeforeTransition(path, index); | ||
} | ||
let path = this.navigator.plot([ | ||
['M', leftX, paths[0][1]], | ||
['Q', centerX, topCurve], | ||
[' ', rightX, paths[2][1]], | ||
['M', rightX, 10], | ||
['Q', centerX, 0], | ||
[' ', leftX, 10], | ||
]); | ||
return path; | ||
// Draw top oval | ||
svg.append('path') | ||
.attr('fill', Utils.shadeColor(this.data[0][2], -0.4)) | ||
.attr('d', path); | ||
} | ||
/** | ||
* Draw the next block in the iteration. | ||
* | ||
* @param {int} index | ||
* | ||
* @return {void} | ||
*/ | ||
_drawBlock(index) { | ||
if (index === this.data.length) { | ||
return; | ||
} | ||
/** | ||
* Set the attributes of a path element before its animation. | ||
* | ||
* @param {Object} path | ||
* @param {int} index | ||
* | ||
* @return {void} | ||
*/ | ||
_addBeforeTransition(path, index) | ||
{ | ||
let paths = this.blockPaths[index]; | ||
// Create a group just for this block | ||
let group = this.svg.append('g'); | ||
let beforePath = ''; | ||
let beforeFill = ''; | ||
// Fetch path element | ||
let path = this._getBlockPath(group, index); | ||
path.data(this._getBlockData(index)); | ||
// Construct the top of the trapezoid and leave the other elements | ||
// hovering around to expand downward on animation | ||
if (!this.isCurved) { | ||
beforePath = 'M' + paths[0][0] + ',' + paths[0][1] + | ||
' L' + paths[1][0] + ',' + paths[1][1] + | ||
' L' + paths[1][0] + ',' + paths[1][1] + | ||
' L' + paths[0][0] + ',' + paths[0][1]; | ||
} else { | ||
beforePath = 'M' + paths[0][0] + ',' + paths[0][1] + | ||
' Q' + paths[1][0] + ',' + paths[1][1] + | ||
' ' + paths[2][0] + ',' + paths[2][1] + | ||
' L' + paths[2][0] + ',' + paths[2][1] + | ||
' M' + paths[2][0] + ',' + paths[2][1] + | ||
' Q' + paths[1][0] + ',' + paths[1][1] + | ||
' ' + paths[0][0] + ',' + paths[0][1]; | ||
} | ||
// Use previous fill color, if available | ||
if (this.fillType === 'solid') { | ||
beforeFill = index > 0 ? this._getColor(index - 1) : this._getColor(index); | ||
// Use current background if gradient (gradients do not transition) | ||
} else { | ||
beforeFill = this._getColor(index); | ||
} | ||
path.attr('d', beforePath) | ||
.attr('fill', beforeFill); | ||
// Add animation components | ||
if (this.animation !== false) { | ||
path.transition() | ||
.duration(this.animation) | ||
.ease('linear') | ||
.attr('fill', this._getColor(index)) | ||
.attr('d', this._getPathDefinition(index)) | ||
.each('end', () => { | ||
this._drawBlock(index + 1); | ||
}); | ||
} else { | ||
path.attr('fill', this._getColor(index)) | ||
.attr('d', this._getPathDefinition(index)); | ||
this._drawBlock(index + 1); | ||
} | ||
/** | ||
* @param {int} index | ||
* | ||
* @return {Array} | ||
*/ | ||
_getBlockData(index) | ||
{ | ||
return [{ | ||
index: index, | ||
label: this.data[index][0], | ||
value: isArray(this.data[index][1]) ? | ||
this.data[index][1][0] : | ||
this.data[index][1], | ||
formattedValue: isArray(this.data[index][1]) ? | ||
this.data[index][1][1] : | ||
this.data[index][1].toLocaleString(), | ||
baseColor: this.data[index][2], | ||
fill: this._getColor(index), | ||
}]; | ||
// Add the hover events | ||
if (this.hoverEffects) { | ||
path.on('mouseover', this._onMouseOver) | ||
.on('mouseout', this._onMouseOut); | ||
} | ||
/** | ||
* Return the color for the given index. | ||
* | ||
* @param {int} index | ||
* | ||
* @return {string} | ||
*/ | ||
_getColor(index) | ||
{ | ||
if (this.fillType === 'solid') { | ||
return this.data[index][2]; | ||
} else { | ||
return 'url(#gradient-' + index + ')'; | ||
} | ||
// ItemClick event | ||
if (this.onItemClick !== null) { | ||
path.on('click', this.onItemClick); | ||
} | ||
/** | ||
* @param {int} index | ||
* | ||
* @return {string} | ||
*/ | ||
_getPathDefinition(index) | ||
{ | ||
let pathStr = ''; | ||
let point = []; | ||
let paths = this.blockPaths[index]; | ||
this._addBlockLabel(group, index); | ||
} | ||
for (let j = 0; j < paths.length; j++) { | ||
point = paths[j]; | ||
pathStr += point[2] + point[0] + ',' + point[1] + ' '; | ||
} | ||
/** | ||
* @param {Object} group | ||
* @param {int} index | ||
* | ||
* @return {Object} | ||
*/ | ||
_getBlockPath(group, index) { | ||
let path = group.append('path'); | ||
return pathStr; | ||
if (this.animation !== false) { | ||
this._addBeforeTransition(path, index); | ||
} | ||
/** | ||
* @param {Object} data | ||
* | ||
* @return {void} | ||
*/ | ||
_onMouseOver(data) | ||
{ | ||
d3.select(this).attr('fill', shadeColor(data.baseColor, -0.2)); | ||
} | ||
return path; | ||
} | ||
/** | ||
* @param {Object} data | ||
* | ||
* @return {void} | ||
*/ | ||
_onMouseOut(data) | ||
{ | ||
d3.select(this).attr('fill', data.fill); | ||
} | ||
/** | ||
* Set the attributes of a path element before its animation. | ||
* | ||
* @param {Object} path | ||
* @param {int} index | ||
* | ||
* @return {void} | ||
*/ | ||
_addBeforeTransition(path, index) { | ||
let paths = this.blockPaths[index]; | ||
/** | ||
* @param {Object} group | ||
* @param {int} index | ||
* | ||
* @return {void} | ||
*/ | ||
_addBlockLabel(group, index) | ||
{ | ||
let i = index; | ||
let paths = this.blockPaths[index]; | ||
let blockData = this._getBlockData(index)[0]; | ||
let textStr = blockData.label + ': ' + blockData.formattedValue; | ||
let textFill = this.data[i][3] || this.label.fill; | ||
let beforePath = ''; | ||
let beforeFill = ''; | ||
let textX = this.width / 2; // Center the text | ||
let textY = !this.isCurved ? // Average height of bases | ||
(paths[1][1] + paths[2][1]) / 2 : | ||
(paths[2][1] + paths[3][1]) / 2 + (this.curveHeight / this.data.length); | ||
// Construct the top of the trapezoid and leave the other elements | ||
// hovering around to expand downward on animation | ||
if (!this.isCurved) { | ||
beforePath = this.navigator.plot([ | ||
['M', paths[0][0], paths[0][1]], | ||
['L', paths[1][0], paths[1][1]], | ||
['L', paths[1][0], paths[1][1]], | ||
['L', paths[0][0], paths[0][1]], | ||
]); | ||
} else { | ||
beforePath = this.navigator.plot([ | ||
['M', paths[0][0], paths[0][1]], | ||
['Q', paths[1][0], paths[1][1]], | ||
[' ', paths[2][0], paths[2][1]], | ||
['L', paths[2][0], paths[2][1]], | ||
['M', paths[2][0], paths[2][1]], | ||
['Q', paths[1][0], paths[1][1]], | ||
[' ', paths[0][0], paths[0][1]], | ||
]); | ||
} | ||
group.append('text') | ||
.text(textStr) | ||
.attr({ | ||
'x': textX, | ||
'y': textY, | ||
'text-anchor': 'middle', | ||
'dominant-baseline': 'middle', | ||
'fill': textFill, | ||
'pointer-events': 'none', | ||
}) | ||
.style('font-size', this.label.fontSize); | ||
// Use previous fill color, if available | ||
if (this.fillType === 'solid') { | ||
beforeFill = index > 0 ? this._getColor(index - 1) : this._getColor(index); | ||
// Use current background if gradient (gradients do not transition) | ||
} else { | ||
beforeFill = this._getColor(index); | ||
} | ||
path.attr('d', beforePath) | ||
.attr('fill', beforeFill); | ||
} | ||
/** | ||
* Check if the supplied value is an array. | ||
* @param {int} index | ||
* | ||
* @param {*} value | ||
* | ||
* @return {bool} | ||
* @return {Array} | ||
*/ | ||
function isArray(value) | ||
{ | ||
return Object.prototype.toString.call(value) === '[object Array]'; | ||
_getBlockData(index) { | ||
let label = this.data[index][0]; | ||
let value = this.data[index][1]; | ||
return [{ | ||
index: index, | ||
label: label, | ||
value: value, | ||
formatted: this.labelFormatter.format(label, value), | ||
baseColor: this.data[index][2], | ||
fill: this._getColor(index), | ||
}]; | ||
} | ||
/** | ||
* Extends an object with the members of another. | ||
* Return the color for the given index. | ||
* | ||
* @param {Object} a The object to be extended. | ||
* @param {Object} b The object to clone from. | ||
* @param {int} index | ||
* | ||
* @return {Object} | ||
* @return {string} | ||
*/ | ||
function extend(a, b) | ||
{ | ||
let prop; | ||
for (prop in b) { | ||
if (b.hasOwnProperty(prop)) { | ||
a[prop] = b[prop]; | ||
} | ||
_getColor(index) { | ||
if (this.fillType === 'solid') { | ||
return this.data[index][2]; | ||
} else { | ||
return 'url(#gradient-' + index + ')'; | ||
} | ||
return a; | ||
} | ||
/** | ||
* Shade a color to the given percentage. | ||
* @param {int} index | ||
* | ||
* @param {string} color A hex color. | ||
* @param {number} shade The shade adjustment. Can be positive or negative. | ||
* | ||
* @return {string} | ||
*/ | ||
function shadeColor(color, shade) | ||
{ | ||
let f = parseInt(color.slice(1), 16); | ||
let t = shade < 0 ? 0 : 255; | ||
let p = shade < 0 ? shade * -1 : shade; | ||
let R = f >> 16, G = f >> 8 & 0x00FF; | ||
let B = f & 0x0000FF; | ||
_getPathDefinition(index) { | ||
let commands = []; | ||
let converted = (0x1000000 + (Math.round((t - R) * p) + R) * | ||
0x10000 + (Math.round((t - G) * p) + G) * | ||
0x100 + (Math.round((t - B) * p) + B)); | ||
this.blockPaths[index].forEach((command) => { | ||
commands.push([command[2], command[0], command[1]]); | ||
}); | ||
return '#' + converted.toString(16).slice(1); | ||
return this.navigator.plot(commands); | ||
} | ||
global.D3Funnel = D3Funnel; | ||
/** | ||
* @param {Object} data | ||
* | ||
* @return {void} | ||
*/ | ||
_onMouseOver(data) { | ||
d3.select(this).attr('fill', Utils.shadeColor(data.baseColor, -0.2)); | ||
} | ||
})(window, d3); | ||
/** | ||
* @param {Object} data | ||
* | ||
* @return {void} | ||
*/ | ||
_onMouseOut(data) { | ||
d3.select(this).attr('fill', data.fill); | ||
} | ||
/** | ||
* @param {Object} group | ||
* @param {int} index | ||
* | ||
* @return {void} | ||
*/ | ||
_addBlockLabel(group, index) { | ||
let paths = this.blockPaths[index]; | ||
let label = this._getBlockData(index)[0].formatted; | ||
let fill = this.data[index][3] || this.label.fill; | ||
let x = this.width / 2; // Center the text | ||
let y = this._getTextY(paths); | ||
group.append('text') | ||
.text(label) | ||
.attr({ | ||
'x': x, | ||
'y': y, | ||
'text-anchor': 'middle', | ||
'dominant-baseline': 'middle', | ||
'fill': fill, | ||
'pointer-events': 'none', | ||
}) | ||
.style('font-size', this.label.fontSize); | ||
} | ||
/** | ||
* Returns the y position of the given label's text. This is determined by | ||
* taking the mean of the bases. | ||
* | ||
* @param {Array} paths | ||
* | ||
* @return {Number} | ||
*/ | ||
_getTextY(paths) { | ||
if (this.isCurved) { | ||
return (paths[2][1] + paths[3][1]) / 2 + (this.curveHeight / this.data.length); | ||
} | ||
return (paths[1][1] + paths[2][1]) / 2; | ||
} | ||
} |
@@ -0,3 +1,5 @@ | ||
/* global d3, assert, chai, D3Funnel */ | ||
describe('D3Funnel', function () { | ||
var getFunnel, getSvg, getLength, getBasicData; | ||
var getFunnel, getSvg, getBasicData, getPathHeight, getCommandHeight; | ||
@@ -11,8 +13,13 @@ beforeEach(function (done) { | ||
}; | ||
getLength = function (selection) { | ||
return selection[0].length; | ||
getBasicData = function () { | ||
return [['Node', 1000]]; | ||
}; | ||
getBasicData = function() { | ||
return [['Node', 100]]; | ||
getPathHeight = function (path) { | ||
var commands = path.attr('d').split(' '); | ||
return getCommandHeight(commands[2]) - getCommandHeight(commands[0]); | ||
}; | ||
getCommandHeight = function (command) { | ||
return parseFloat(command.split(',')[1]); | ||
}; | ||
@@ -30,6 +37,6 @@ done(); | ||
describe('draw', function () { | ||
it('should draw simple chart', function () { | ||
it('should draw a chart on the identified target', function () { | ||
getFunnel().draw(getBasicData(), {}); | ||
assert.equal(1, getLength(getSvg())); | ||
assert.equal(1, getSvg()[0].length); | ||
}); | ||
@@ -40,3 +47,3 @@ | ||
assert.equal(1, getLength(getSvg())); | ||
assert.equal(1, getSvg()[0].length); | ||
}); | ||
@@ -51,2 +58,52 @@ | ||
}); | ||
it('should draw as many blocks as there are elements', function () { | ||
getFunnel().draw([ | ||
['Node A', 1], | ||
['Node B', 2], | ||
['Node C', 3], | ||
['Node D', 4], | ||
]); | ||
assert.equal(4, getSvg().selectAll('path')[0].length); | ||
}); | ||
it('should use colors assigned to a data element', function () { | ||
var paths; | ||
var colorScale; | ||
getFunnel().draw([ | ||
['A', 1, '#111'], | ||
['B', 2, '#222'], | ||
['C', 3], | ||
['D', 4, '#444'], | ||
]); | ||
paths = getSvg().selectAll('path')[0]; | ||
colorScale = d3.scale.category10(); | ||
assert.equal('#111', d3.select(paths[0]).attr('fill')); | ||
assert.equal('#222', d3.select(paths[1]).attr('fill')); | ||
assert.equal(colorScale(2), d3.select(paths[2]).attr('fill')); | ||
assert.equal('#444', d3.select(paths[3]).attr('fill')); | ||
}); | ||
it('should use label colors assigned to a data element', function () { | ||
var texts; | ||
getFunnel().draw([ | ||
['A', 1, null, '#111'], | ||
['B', 2, null, '#222'], | ||
['C', 3], | ||
['D', 4, null, '#444'], | ||
]); | ||
texts = getSvg().selectAll('text')[0]; | ||
assert.equal('#111', d3.select(texts[0]).attr('fill')); | ||
assert.equal('#222', d3.select(texts[1]).attr('fill')); | ||
assert.equal('#fff', d3.select(texts[2]).attr('fill')); | ||
assert.equal('#444', d3.select(texts[3]).attr('fill')); | ||
}); | ||
}); | ||
@@ -61,3 +118,3 @@ | ||
assert.equal(0, getLength(getSvg())); | ||
assert.equal(0, getSvg()[0].length); | ||
}); | ||
@@ -71,3 +128,3 @@ }); | ||
getFunnel().draw(getBasicData(), { | ||
width: 200 | ||
width: 200, | ||
}); | ||
@@ -82,3 +139,3 @@ | ||
getFunnel().draw(getBasicData(), { | ||
height: 200 | ||
height: 200, | ||
}); | ||
@@ -93,3 +150,3 @@ | ||
getFunnel().draw(getBasicData(), { | ||
isCurved: true | ||
isCurved: true, | ||
}); | ||
@@ -102,3 +159,3 @@ | ||
getFunnel().draw(getBasicData(), { | ||
isCurved: true | ||
isCurved: true, | ||
}); | ||
@@ -119,3 +176,3 @@ | ||
getFunnel().draw(getBasicData(), { | ||
fillType: 'gradient' | ||
fillType: 'gradient', | ||
}); | ||
@@ -142,2 +199,91 @@ | ||
describe('hoverEffects', function () { | ||
it('should change block color on hover', function () { | ||
var event = document.createEvent('CustomEvent'); | ||
event.initCustomEvent('mouseover', false, false, null); | ||
getFunnel().draw([ | ||
['A', 1, '#fff'], | ||
], { | ||
hoverEffects: true, | ||
}); | ||
d3.select('#funnel path').node().dispatchEvent(event); | ||
// #fff * -1/5 => #cccccc | ||
assert.equal('#cccccc', d3.select('#funnel path').attr('fill')); | ||
}); | ||
}); | ||
describe('dynamicArea', function () { | ||
it('should use equal heights when false', function () { | ||
var paths; | ||
getFunnel().draw([ | ||
['A', 1], | ||
['B', 2], | ||
], { | ||
height: 300, | ||
}); | ||
paths = d3.selectAll('#funnel path')[0]; | ||
assert.equal(150, getPathHeight(d3.select(paths[0]))); | ||
assert.equal(150, getPathHeight(d3.select(paths[1]))); | ||
}); | ||
it('should use proportional heights when true', function () { | ||
var paths; | ||
getFunnel().draw([ | ||
['A', 1], | ||
['B', 2], | ||
], { | ||
height: 300, | ||
dynamicArea: true, | ||
}); | ||
paths = d3.selectAll('#funnel path')[0]; | ||
assert.equal(72, parseInt(getPathHeight(d3.select(paths[0])), 10)); | ||
assert.equal(227, parseInt(getPathHeight(d3.select(paths[1])), 10)); | ||
}); | ||
it('should not have NaN in the last path when bottomWidth is equal to 0%', function () { | ||
var paths; | ||
// A very specific cooked-up example that could trigger NaN | ||
getFunnel().draw([ | ||
['A', 120], | ||
['B', 40], | ||
['C', 20], | ||
['D', 15], | ||
], { | ||
height: 300, | ||
dynamicArea: true, | ||
bottomWidth: 0, | ||
}); | ||
paths = d3.selectAll('#funnel path')[0]; | ||
assert.equal(-1, d3.select(paths[3]).attr('d').indexOf('NaN')) | ||
}); | ||
it('should not error when bottomWidth is equal to 100%', function () { | ||
var paths; | ||
getFunnel().draw([ | ||
['A', 1], | ||
['B', 2], | ||
], { | ||
height: 300, | ||
dynamicArea: true, | ||
bottomWidth: 1, | ||
}); | ||
paths = d3.selectAll('#funnel path')[0]; | ||
}); | ||
}); | ||
describe('label.fontSize', function () { | ||
@@ -147,4 +293,4 @@ it('should set the label\'s font size to the specified amount', function () { | ||
label: { | ||
fontSize: '16px' | ||
} | ||
fontSize: '16px', | ||
}, | ||
}); | ||
@@ -160,4 +306,4 @@ | ||
label: { | ||
fill: '#777' | ||
} | ||
fill: '#777', | ||
}, | ||
}); | ||
@@ -168,3 +314,57 @@ | ||
}); | ||
describe('label.format', function () { | ||
it('should parse a string template', function () { | ||
getFunnel().draw(getBasicData(), { | ||
label: { | ||
format: '{l} {v} {f}', | ||
} | ||
}); | ||
// Node.js does not have localization, so toLocaleString() will | ||
// leave the value untouched | ||
// https://github.com/joyent/node/issues/4689 | ||
assert.equal('Node 1000 1000', d3.select('#funnel text').text()); | ||
}); | ||
it('should pass values to a supplied function', function () { | ||
getFunnel().draw(getBasicData(), { | ||
label: { | ||
format: function (label, value, fValue) { | ||
return label + '/' + value + '/' + fValue; | ||
} | ||
} | ||
}); | ||
assert.equal('Node/1000/null', d3.select('#funnel text').text()); | ||
}); | ||
}); | ||
describe('onItemClick', function () { | ||
it('should invoke the callback function with the correct data', function () { | ||
var event = document.createEvent('CustomEvent'); | ||
event.initCustomEvent('click', false, false, null); | ||
var spy = chai.spy(); | ||
getFunnel().draw(getBasicData(), { | ||
onItemClick: function (d, i) { | ||
spy({ | ||
index: d.index, | ||
label: d.label, | ||
value: d.value, | ||
}, i); | ||
}, | ||
}); | ||
d3.select('#funnel path').node().dispatchEvent(event); | ||
chai.expect(spy).to.have.been.called.once.with({ | ||
index: 0, | ||
label: 'Node', | ||
value: 1000, | ||
}, 0); | ||
}); | ||
}); | ||
}); | ||
}); |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Minified code
QualityThis package contains minified code. This may be harmless in some cases where minified code is included in packaged libraries, however packages on npm should not minify code.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
81463
21
1913
115
13
3