Comparing version 0.6.13 to 0.7.0
@@ -0,1 +1,49 @@ | ||
## v0.7.0 (October 4, 2015) | ||
D3Funnel v0.7 is a **backwards-incompatible** release that resolves some | ||
outstanding bugs, standardizes several option names and formats, and introduces | ||
a few new features. | ||
No new features will be added to the v0.6 series, but minor patches will be | ||
available for a few months. | ||
### Behavior Changes | ||
* [#29]: Dynamic block heights are no longer determined by their weighted area, but by their weighted height | ||
* Heights determined by weighted area: http://jsfiddle.net/zq4L82kv/2/ (legacy v0.6.x) | ||
* Heights determined by weighted height: http://jsfiddle.net/bawv6m0j/1/ (v0.7+) | ||
### New Features | ||
* [#9]: Block can now have their color scale specified in addition to data points | ||
* [#34]: Default options are now statically available and overridable | ||
### Bug Fixes | ||
* [#25]: Fix issues with `isInverted` and `dynamicArea` producing odd pyramids | ||
* [#32]: Fix issue where pinched blocks were not having the same width as `bottomWidth` | ||
### Upgrading from v0.6.x | ||
Several options have been renamed for standardization. Please refer to the table | ||
below for the new equivalent option: | ||
| Old option | New option | Notes | | ||
| -------------- | --------------------- | --------------- | | ||
| `animation` | `chart.animate` | | | ||
| `bottomPinch` | `chart.bottomPinch` | | | ||
| `bottomWidth` | `chart.bottomWidth` | | | ||
| `curveHeight | `chart.curve.height` | | | ||
| `dynamicArea` | `block.dynamicHeight` | See change #29. | | ||
| `fillType` | `block.fill.type` | | | ||
| `height` | `chart.height` | | | ||
| `hoverEffects` | `block.hightlight` | | | ||
| `isCurved` | `chart.curve.enabled` | | | ||
| `isInverted` | `chart.inverted` | | | ||
| `onItemClick` | `events.click.block` | | | ||
| `minHeight` | `block.minHeight` | | | ||
| `width` | `chart.width` | | | ||
In addition, please refer to change #29. | ||
## v0.6.13 (October 2, 2015) | ||
@@ -2,0 +50,0 @@ |
@@ -12,3 +12,3 @@ | ||
/* global d3, LabelFormatter, Navigator, Utils */ | ||
/* global d3, Colorizer, LabelFormatter, Navigator, Utils */ | ||
/* exported D3Funnel */ | ||
@@ -23,8 +23,45 @@ | ||
var D3Funnel = (function () { | ||
_createClass(D3Funnel, null, [{ | ||
key: 'defaults', | ||
value: { | ||
chart: { | ||
width: 350, | ||
height: 400, | ||
bottomWidth: 1 / 3, | ||
bottomPinch: 0, | ||
inverted: false, | ||
animate: false, | ||
curve: { | ||
enabled: false, | ||
height: 20 | ||
} | ||
}, | ||
block: { | ||
dynamicHeight: false, | ||
fill: { | ||
scale: d3.scale.category10().domain(d3.range(0, 10)), | ||
type: 'solid' | ||
}, | ||
minHeight: false, | ||
highlight: false | ||
}, | ||
label: { | ||
fontSize: '14px', | ||
fill: '#fff', | ||
format: '{l}: {f}' | ||
}, | ||
events: { | ||
click: { | ||
block: null | ||
} | ||
} | ||
}, | ||
/** | ||
* @param {string} selector A selector for the container element. | ||
* | ||
* @return {void} | ||
*/ | ||
/** | ||
* @param {string} selector A selector for the container element. | ||
* | ||
* @return {void} | ||
*/ | ||
enumerable: true | ||
}]); | ||
@@ -36,23 +73,3 @@ function D3Funnel(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.colorizer = new Colorizer(); | ||
@@ -64,3 +81,4 @@ this.labelFormatter = new LabelFormatter(); | ||
/* exported LabelFormatter */ | ||
/* exported Colorizer */ | ||
/* jshint bitwise: false */ | ||
@@ -117,4 +135,2 @@ /** | ||
this._setData(data); | ||
var settings = this._getSettings(options); | ||
@@ -126,16 +142,25 @@ | ||
// Set color scales | ||
this.colorizer.setLabelFill(settings.label.fill); | ||
this.colorizer.setScale(settings.block.fill.scale); | ||
// 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; | ||
this.width = settings.chart.width; | ||
this.height = settings.chart.height; | ||
this.bottomWidth = settings.chart.width * settings.chart.bottomWidth; | ||
this.bottomPinch = settings.chart.bottomPinch; | ||
this.isInverted = settings.chart.inverted; | ||
this.isCurved = settings.chart.curve.enabled; | ||
this.curveHeight = settings.chart.curve.height; | ||
this.fillType = settings.block.fill.type; | ||
this.hoverEffects = settings.block.highlight; | ||
this.dynamicHeight = settings.block.dynamicHeight; | ||
this.minHeight = settings.block.minHeight; | ||
this.animation = settings.chart.animate; | ||
// Support for events | ||
this.onBlockClick = settings.events.click.block; | ||
this._setBlocks(data); | ||
// Calculate the bottom left x position | ||
@@ -149,5 +174,2 @@ this.bottomLeftX = (this.width - this.bottomWidth) / 2; | ||
this.dy = this._getDy(); | ||
// Support for events | ||
this.onItemClick = settings.onItemClick; | ||
} | ||
@@ -169,2 +191,34 @@ | ||
/** | ||
* @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({}, D3Funnel.defaults); | ||
settings.chart.width = parseInt(d3.select(this.selector).style('width'), 10); | ||
settings.chart.height = parseInt(d3.select(this.selector).style('height'), 10); | ||
// 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.chart.width <= 0) { | ||
settings.chart.width = D3Funnel.defaults.chart.width; | ||
} | ||
if (settings.chart.height <= 0) { | ||
settings.chart.height = D3Funnel.defaults.chart.height; | ||
} | ||
return settings; | ||
} | ||
/** | ||
* Register the raw data into a standard block format and pre-calculate | ||
* some values. | ||
* | ||
* @param {Array} data | ||
@@ -175,63 +229,87 @@ * | ||
}, { | ||
key: '_setData', | ||
value: function _setData(data) { | ||
this.data = data; | ||
key: '_setBlocks', | ||
value: function _setBlocks(data) { | ||
var totalCount = this._getTotalCount(data); | ||
this._setColors(); | ||
this.blocks = this._standardizeData(data, totalCount); | ||
} | ||
/** | ||
* Set the colors for each block. | ||
* Return the total count of all blocks. | ||
* | ||
* @return {void} | ||
* @return {Number} | ||
*/ | ||
}, { | ||
key: '_setColors', | ||
value: function _setColors() { | ||
key: '_getTotalCount', | ||
value: function _getTotalCount(data) { | ||
var _this = this; | ||
var colorScale = d3.scale.category10(); | ||
var hexExpression = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i; | ||
var total = 0; | ||
// 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); | ||
} | ||
data.forEach(function (block) { | ||
total += _this._getRawBlockCount(block); | ||
}); | ||
return total; | ||
} | ||
/** | ||
* @param {Object} options | ||
* Convert the raw data into a standardized format. | ||
* | ||
* @returns {Object} | ||
* @param {Array} data | ||
* @param {Number} totalCount | ||
* | ||
* @return {Array} | ||
*/ | ||
}, { | ||
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); | ||
key: '_standardizeData', | ||
value: function _standardizeData(data, totalCount) { | ||
var _this2 = this; | ||
// Overwrite default settings with user options | ||
settings = Utils.extend(settings, options); | ||
var standardized = []; | ||
// 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; | ||
} | ||
var count = undefined; | ||
var ratio = undefined; | ||
var label = undefined; | ||
return settings; | ||
data.forEach(function (block, index) { | ||
count = _this2._getRawBlockCount(block); | ||
ratio = count / totalCount; | ||
label = block[0]; | ||
standardized.push({ | ||
index: index, | ||
value: count, | ||
ratio: ratio, | ||
height: _this2.height * ratio, | ||
formatted: _this2.labelFormatter.format(label, count), | ||
fill: _this2.colorizer.getBlockFill(block, index), | ||
label: { | ||
raw: label, | ||
formatted: _this2.labelFormatter.format(label, count), | ||
color: _this2.colorizer.getLabelFill(block, index) | ||
} | ||
}); | ||
}); | ||
return standardized; | ||
} | ||
/** | ||
* Given a raw data block, return its count. | ||
* | ||
* @param {Array} block | ||
* | ||
* @return {Number} | ||
*/ | ||
}, { | ||
key: '_getRawBlockCount', | ||
value: function _getRawBlockCount(block) { | ||
return Array.isArray(block[1]) ? block[1][0] : block[1]; | ||
} | ||
/** | ||
* @return {Number} | ||
*/ | ||
}, { | ||
key: '_getDx', | ||
@@ -241,6 +319,6 @@ value: function _getDx() { | ||
if (this.bottomPinch > 0) { | ||
return this.bottomLeftX / (this.data.length - this.bottomPinch); | ||
return this.bottomLeftX / (this.blocks.length - this.bottomPinch); | ||
} | ||
return this.bottomLeftX / this.data.length; | ||
return this.bottomLeftX / this.blocks.length; | ||
} | ||
@@ -256,6 +334,6 @@ | ||
if (this.isCurved) { | ||
return (this.height - this.curveHeight) / this.data.length; | ||
return (this.height - this.curveHeight) / this.blocks.length; | ||
} | ||
return this.height / this.data.length; | ||
return this.height / this.blocks.length; | ||
} | ||
@@ -299,3 +377,3 @@ | ||
value: function _makePaths() { | ||
var _this2 = this; | ||
var _this3 = this; | ||
@@ -331,8 +409,4 @@ var paths = []; | ||
var topBase = this.width; | ||
var bottomBase = 0; | ||
var totalHeight = this.height; | ||
var totalArea = this.height * (this.width + this.bottomWidth) / 2; | ||
var slope = 2 * this.height / (this.width - this.bottomWidth); | ||
// This is greedy in that the block will have a guaranteed height | ||
@@ -342,55 +416,84 @@ // and the remaining is shared among the ratio, instead of being | ||
if (this.minHeight !== false) { | ||
totalArea = (this.height - this.minHeight * this.data.length) * (this.width + this.bottomWidth) / 2; | ||
totalHeight = this.height - this.minHeight * this.blocks.length; | ||
} | ||
var totalCount = 0; | ||
var count = 0; | ||
var slopeHeight = this.height; | ||
// Harvest total count | ||
this.data.forEach(function (block) { | ||
totalCount += Array.isArray(block[1]) ? block[1][0] : block[1]; | ||
// Correct slope height if there are blocks being pinched (and thus | ||
// requiring a sharper curve) | ||
this.blocks.forEach(function (block, i) { | ||
if (_this3.bottomPinch > 0) { | ||
if (_this3.isInverted) { | ||
if (i < _this3.bottomPinch) { | ||
slopeHeight -= block.height; | ||
} | ||
} else if (i >= _this3.blocks.length - _this3.bottomPinch) { | ||
slopeHeight -= block.height; | ||
} | ||
} | ||
}); | ||
// The slope will determine the where the x points on each block | ||
// iteration | ||
var slope = 2 * slopeHeight / (this.width - this.bottomWidth); | ||
// 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]; | ||
this.blocks.forEach(function (block, i) { | ||
// Make heights proportional to block weight | ||
if (_this3.dynamicHeight) { | ||
// Slice off the height proportional to this block | ||
dy = totalHeight * block.ratio; | ||
// Calculate dynamic shapes based on area | ||
if (_this2.dynamicArea) { | ||
var ratio = count / totalCount; | ||
var area = ratio * totalArea; | ||
// Add greedy minimum height | ||
if (_this3.minHeight !== false) { | ||
dy += _this3.minHeight; | ||
} | ||
if (_this2.minHeight !== false) { | ||
area += _this2.minHeight * (_this2.width + _this2.bottomWidth) / 2; | ||
// Account for any curvature | ||
if (_this3.isCurved) { | ||
dy = dy - _this3.curveHeight / _this3.blocks.length; | ||
} | ||
bottomBase = Math.sqrt((slope * topBase * topBase - 4 * area) / slope); | ||
// Given: y = mx + b | ||
// Given: b = 0 (when funnel), b = this.height (when pyramid) | ||
// For funnel, x_i = y_i / slope | ||
nextLeftX = (prevHeight + dy) / slope; | ||
// Prevent bottm points from becomming NaN | ||
if (_this2.bottomWidth === 0 && i === _this2.data.length - 1) { | ||
bottomBase = 0; | ||
// For pyramid, x_i = y_i - this.height / -slope | ||
if (_this3.isInverted) { | ||
nextLeftX = (prevHeight + dy - _this3.height) / (-1 * slope); | ||
} | ||
// Prevent NaN slope | ||
if (_this2.bottomWidth === _this2.width) { | ||
bottomBase = topBase; | ||
// If bottomWidth is 0, adjust last x position (to circumvent | ||
// errors associated with rounding) | ||
if (_this3.bottomWidth === 0 && i === _this3.blocks.length - 1) { | ||
// For funnel, last position is the center | ||
nextLeftX = _this3.width / 2; | ||
// For pyramid, last position is the origin | ||
if (_this3.isInverted) { | ||
nextLeftX = 0; | ||
} | ||
} | ||
dx = topBase / 2 - bottomBase / 2; | ||
dy = area * 2 / (topBase + bottomBase); | ||
// If bottomWidth is same as width, stop x velocity | ||
if (_this3.bottomWidth === _this3.width) { | ||
nextLeftX = prevLeftX; | ||
} | ||
if (_this2.isCurved) { | ||
dy = dy - _this2.curveHeight / _this2.data.length; | ||
// Calculate the shift necessary for both x points | ||
dx = nextLeftX - prevLeftX; | ||
if (_this3.isInverted) { | ||
dx = prevLeftX - nextLeftX; | ||
} | ||
topBase = bottomBase; | ||
} | ||
// Stop velocity for pinched blocks | ||
if (_this2.bottomPinch > 0) { | ||
if (_this3.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) { | ||
if (!_this3.isInverted) { | ||
if (i >= _this3.blocks.length - _this3.bottomPinch) { | ||
dx = 0; | ||
@@ -403,9 +506,9 @@ } | ||
// static area's (prevents zero velocity if isInverted | ||
// and bottomPinch are non trivial and dynamicArea is | ||
// and bottomPinch are non trivial and dynamicHeight is | ||
// false) | ||
if (!_this2.dynamicArea) { | ||
dx = _this2.dx; | ||
if (!_this3.dynamicHeight) { | ||
dx = _this3.dx; | ||
} | ||
dx = i < _this2.bottomPinch ? 0 : dx; | ||
dx = i < _this3.bottomPinch ? 0 : dx; | ||
} | ||
@@ -420,3 +523,3 @@ } | ||
// Expand outward if inverted | ||
if (_this2.isInverted) { | ||
if (_this3.isInverted) { | ||
nextLeftX = prevLeftX - dx; | ||
@@ -427,10 +530,10 @@ nextRightX = prevRightX + dx; | ||
// Plot curved lines | ||
if (_this2.isCurved) { | ||
if (_this3.isCurved) { | ||
paths.push([ | ||
// Top Bezier curve | ||
[prevLeftX, prevHeight, 'M'], [middle, prevHeight + (_this2.curveHeight - 10), 'Q'], [prevRightX, prevHeight, ''], | ||
[prevLeftX, prevHeight, 'M'], [middle, prevHeight + (_this3.curveHeight - 10), 'Q'], [prevRightX, prevHeight, ''], | ||
// Right line | ||
[nextRightX, nextHeight, 'L'], | ||
// Bottom Bezier curve | ||
[nextRightX, nextHeight, 'M'], [middle, nextHeight + _this2.curveHeight, 'Q'], [nextLeftX, nextHeight, ''], | ||
[nextRightX, nextHeight, 'M'], [middle, nextHeight + _this3.curveHeight, 'Q'], [nextLeftX, nextHeight, ''], | ||
// Left line | ||
@@ -475,5 +578,5 @@ [prevLeftX, prevHeight, 'L']]); | ||
// Create a gradient for each block | ||
this.data.forEach(function (block, index) { | ||
var color = block[2]; | ||
var shade = Utils.shadeColor(color, -0.25); | ||
this.blocks.forEach(function (block, index) { | ||
var color = block.fill; | ||
var shade = Colorizer.shade(color, -0.25); | ||
@@ -525,3 +628,3 @@ // Create linear gradient | ||
// Draw top oval | ||
svg.append('path').attr('fill', Utils.shadeColor(this.data[0][2], -0.4)).attr('d', path); | ||
svg.append('path').attr('fill', Colorizer.shade(this.blocks[0].fill, -0.4)).attr('d', path); | ||
} | ||
@@ -539,5 +642,5 @@ | ||
value: function _drawBlock(index) { | ||
var _this3 = this; | ||
var _this4 = this; | ||
if (index === this.data.length) { | ||
if (index === this.blocks.length) { | ||
return; | ||
@@ -551,11 +654,11 @@ } | ||
var path = this._getBlockPath(group, index); | ||
path.data(this._getBlockData(index)); | ||
path.data(this._getD3Data(index)); | ||
// 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); | ||
path.transition().duration(this.animation).ease('linear').attr('fill', this._getFillColor(index)).attr('d', this._getPathDefinition(index)).each('end', function () { | ||
_this4._drawBlock(index + 1); | ||
}); | ||
} else { | ||
path.attr('fill', this._getColor(index)).attr('d', this._getPathDefinition(index)); | ||
path.attr('fill', this._getFillColor(index)).attr('d', this._getPathDefinition(index)); | ||
this._drawBlock(index + 1); | ||
@@ -570,4 +673,4 @@ } | ||
// ItemClick event | ||
if (this.onItemClick !== null) { | ||
path.on('click', this.onItemClick); | ||
if (this.onBlockClick !== null) { | ||
path.on('click', this.onBlockClick); | ||
} | ||
@@ -621,7 +724,7 @@ | ||
// 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) | ||
if (this.fillType === 'solid' && index > 0) { | ||
beforeFill = this._getFillColor(index - 1); | ||
// Otherwise use current background | ||
} else { | ||
beforeFill = this._getColor(index); | ||
beforeFill = this._getFillColor(index); | ||
} | ||
@@ -633,2 +736,4 @@ | ||
/** | ||
* Return d3 formatted data for the given block. | ||
* | ||
* @param {int} index | ||
@@ -639,19 +744,9 @@ * | ||
}, { | ||
key: '_getBlockData', | ||
value: function _getBlockData(index) { | ||
var label = this.data[index][0]; | ||
var 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) | ||
}]; | ||
key: '_getD3Data', | ||
value: function _getD3Data(index) { | ||
return [this.blocks[index]]; | ||
} | ||
/** | ||
* Return the color for the given index. | ||
* Return the block fill color for the given index. | ||
* | ||
@@ -663,9 +758,9 @@ * @param {int} index | ||
}, { | ||
key: '_getColor', | ||
value: function _getColor(index) { | ||
key: '_getFillColor', | ||
value: function _getFillColor(index) { | ||
if (this.fillType === 'solid') { | ||
return this.data[index][2]; | ||
} else { | ||
return 'url(#gradient-' + index + ')'; | ||
return this.blocks[index].fill; | ||
} | ||
return 'url(#gradient-' + index + ')'; | ||
} | ||
@@ -698,3 +793,3 @@ | ||
value: function _onMouseOver(data) { | ||
d3.select(this).attr('fill', Utils.shadeColor(data.baseColor, -0.2)); | ||
d3.select(this).attr('fill', Colorizer.shade(data.fill, -0.2)); | ||
} | ||
@@ -724,4 +819,4 @@ | ||
var label = this._getBlockData(index)[0].formatted; | ||
var fill = this.data[index][3] || this.label.fill; | ||
var text = this.blocks[index].label.formatted; | ||
var fill = this.blocks[index].label.color; | ||
@@ -731,3 +826,3 @@ var x = this.width / 2; // Center the text | ||
group.append('text').text(label).attr({ | ||
group.append('text').text(text).attr({ | ||
'x': x, | ||
@@ -754,3 +849,3 @@ 'y': y, | ||
if (this.isCurved) { | ||
return (paths[2][1] + paths[3][1]) / 2 + this.curveHeight / this.data.length; | ||
return (paths[2][1] + paths[3][1]) / 2 + this.curveHeight / this.blocks.length; | ||
} | ||
@@ -765,2 +860,126 @@ | ||
var Colorizer = (function () { | ||
function Colorizer() { | ||
_classCallCheck(this, Colorizer); | ||
this.hexExpression = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i; | ||
this.labelFill = null; | ||
this.scale = null; | ||
} | ||
/* exported LabelFormatter */ | ||
/** | ||
* @param {string} fill | ||
* | ||
* @return {void} | ||
*/ | ||
_createClass(Colorizer, [{ | ||
key: 'setLabelFill', | ||
value: function setLabelFill(fill) { | ||
this.labelFill = fill; | ||
} | ||
/** | ||
* @param {function|Array} scale | ||
* | ||
* @return {void} | ||
*/ | ||
}, { | ||
key: 'setScale', | ||
value: function setScale(scale) { | ||
this.scale = scale; | ||
} | ||
/** | ||
* Given a raw data block, return an appropriate color for the block. | ||
* | ||
* @param {Array} block | ||
* @param {Number} index | ||
* | ||
* @return {string} | ||
*/ | ||
}, { | ||
key: 'getBlockFill', | ||
value: function getBlockFill(block, index) { | ||
// Use the block's color, if set and valid | ||
if (block.length > 2 && this.hexExpression.test(block[2])) { | ||
return block[2]; | ||
} | ||
if (Array.isArray(this.scale)) { | ||
return this.scale[index]; | ||
} | ||
return this.scale(index); | ||
} | ||
/** | ||
* Given a raw data block, return an appropriate label color. | ||
* | ||
* @param {Array} block | ||
* | ||
* @return {string} | ||
*/ | ||
}, { | ||
key: 'getLabelFill', | ||
value: function getLabelFill(block) { | ||
// Use the label's color, if set and valid | ||
if (block.length > 3 && this.hexExpression.test(block[3])) { | ||
return block[3]; | ||
} | ||
return this.labelFill; | ||
} | ||
/** | ||
* 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: 'shade', | ||
value: function shade(color, _shade) { | ||
var hex = color.slice(1); | ||
if (hex.length === 3) { | ||
hex = Colorizer.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 Colorizer; | ||
})(); | ||
var LabelFormatter = (function () { | ||
@@ -816,5 +1035,5 @@ | ||
return this.formatter(label, value[0], value[1]); | ||
} else { | ||
return this.formatter(label, value, null); | ||
} | ||
return this.formatter(label, value, null); | ||
} | ||
@@ -840,9 +1059,11 @@ | ||
var formatted = fValue; | ||
// Attempt to use supplied formatted value | ||
// Otherwise, use the default | ||
if (fValue === null) { | ||
fValue = this.getDefaultFormattedValue(value); | ||
formatted = this.getDefaultFormattedValue(value); | ||
} | ||
return this.expression.split('{l}').join(label).split('{v}').join(value).split('{f}').join(fValue); | ||
return this.expression.split('{l}').join(label).split('{v}').join(value).split('{f}').join(formatted); | ||
} | ||
@@ -871,3 +1092,2 @@ | ||
/* exported Utils */ | ||
/* jshint bitwise: false */ | ||
@@ -923,4 +1143,8 @@ /** | ||
if (b.hasOwnProperty(prop)) { | ||
if (typeof a[prop] === 'object' && typeof b[prop] === 'object') { | ||
a[prop] = Utils.extend(a[prop], b[prop]); | ||
if (typeof b[prop] === 'object' && !Array.isArray(b[prop])) { | ||
if (typeof a[prop] === 'object' && !Array.isArray(a[prop])) { | ||
a[prop] = Utils.extend(a[prop], b[prop]); | ||
} else { | ||
a[prop] = Utils.extend({}, b[prop]); | ||
} | ||
} else { | ||
@@ -934,45 +1158,2 @@ a[prop] = b[prop]; | ||
} | ||
/** | ||
* 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]; | ||
} | ||
}]); | ||
@@ -979,0 +1160,0 @@ |
@@ -1,2 +0,2 @@ | ||
/*! d3-funnel - v0.6.13 | 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}); | ||
/*! d3-funnel - v0.7.0 | 2015 */ | ||
!function(t,e){"function"==typeof define&&define.amd?define(["d3"],e):"object"==typeof exports?module.exports=e(require("d3")):t.D3Funnel=e(t.d3)}(this,function(t){"use strict";function e(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}var i=function(){function t(t,e){for(var i=0;i<e.length;i++){var n=e[i];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(t,n.key,n)}}return function(e,i,n){return i&&t(e.prototype,i),n&&t(e,n),e}}(),n=function(){function n(t){e(this,n),this.selector=t,this.colorizer=new h,this.labelFormatter=new o,this.navigator=new a}return i(n,null,[{key:"defaults",value:{chart:{width:350,height:400,bottomWidth:1/3,bottomPinch:0,inverted:!1,animate:!1,curve:{enabled:!1,height:20}},block:{dynamicHeight:!1,fill:{scale:t.scale.category10().domain(t.range(0,10)),type:"solid"},minHeight:!1,highlight:!1},label:{fontSize:"14px",fill:"#fff",format:"{l}: {f}"},events:{click:{block:null}}},enumerable:!0}]),i(n,[{key:"destroy",value:function(){t.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._draw()}},{key:"_initialize",value:function(t,e){this._validateData(t);var i=this._getSettings(e);this.label=i.label,this.labelFormatter.setFormat(this.label.format),this.colorizer.setLabelFill(i.label.fill),this.colorizer.setScale(i.block.fill.scale),this.width=i.chart.width,this.height=i.chart.height,this.bottomWidth=i.chart.width*i.chart.bottomWidth,this.bottomPinch=i.chart.bottomPinch,this.isInverted=i.chart.inverted,this.isCurved=i.chart.curve.enabled,this.curveHeight=i.chart.curve.height,this.fillType=i.block.fill.type,this.hoverEffects=i.block.highlight,this.dynamicHeight=i.block.dynamicHeight,this.minHeight=i.block.minHeight,this.animation=i.chart.animate,this.onBlockClick=i.events.click.block,this._setBlocks(t),this.bottomLeftX=(this.width-this.bottomWidth)/2,this.dx=this._getDx(),this.dy=this._getDy()}},{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:"_getSettings",value:function(e){var i=l.extend({},n.defaults);return i.chart.width=parseInt(t.select(this.selector).style("width"),10),i.chart.height=parseInt(t.select(this.selector).style("height"),10),i=l.extend(i,e),i.chart.width<=0&&(i.chart.width=n.defaults.chart.width),i.chart.height<=0&&(i.chart.height=n.defaults.chart.height),i}},{key:"_setBlocks",value:function(t){var e=this._getTotalCount(t);this.blocks=this._standardizeData(t,e)}},{key:"_getTotalCount",value:function(t){var e=this,i=0;return t.forEach(function(t){i+=e._getRawBlockCount(t)}),i}},{key:"_standardizeData",value:function(t,e){var i=this,n=[],h=void 0,o=void 0,a=void 0;return t.forEach(function(t,l){h=i._getRawBlockCount(t),o=h/e,a=t[0],n.push({index:l,value:h,ratio:o,height:i.height*o,formatted:i.labelFormatter.format(a,h),fill:i.colorizer.getBlockFill(t,l),label:{raw:a,formatted:i.labelFormatter.format(a,h),color:i.colorizer.getLabelFill(t,l)}})}),n}},{key:"_getRawBlockCount",value:function(t){return Array.isArray(t[1])?t[1][0]:t[1]}},{key:"_getDx",value:function(){return this.bottomPinch>0?this.bottomLeftX/(this.blocks.length-this.bottomPinch):this.bottomLeftX/this.blocks.length}},{key:"_getDy",value:function(){return this.isCurved?(this.height-this.curveHeight)/this.blocks.length:this.height/this.blocks.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,e=[],i=this.dx,n=this.dy,h=0,o=this.width,a=0;this.isInverted&&(h=this.bottomLeftX,o=this.width-this.bottomLeftX);var l=0,s=0,r=0,c=this.width/2;this.isCurved&&(a=10);var u=this.height;this.minHeight!==!1&&(u=this.height-this.minHeight*this.blocks.length);var d=this.height;this.blocks.forEach(function(e,i){t.bottomPinch>0&&(t.isInverted?i<t.bottomPinch&&(d-=e.height):i>=t.blocks.length-t.bottomPinch&&(d-=e.height))});var f=2*d/(this.width-this.bottomWidth);return this.blocks.forEach(function(d,v){t.dynamicHeight&&(n=u*d.ratio,t.minHeight!==!1&&(n+=t.minHeight),t.isCurved&&(n-=t.curveHeight/t.blocks.length),l=(a+n)/f,t.isInverted&&(l=(a+n-t.height)/(-1*f)),0===t.bottomWidth&&v===t.blocks.length-1&&(l=t.width/2,t.isInverted&&(l=0)),t.bottomWidth===t.width&&(l=h),i=l-h,t.isInverted&&(i=h-l)),t.bottomPinch>0&&(t.isInverted?(t.dynamicHeight||(i=t.dx),i=v<t.bottomPinch?0:i):v>=t.blocks.length-t.bottomPinch&&(i=0)),l=h+i,s=o-i,r=a+n,t.isInverted&&(l=h-i,s=o+i),t.isCurved?e.push([[h,a,"M"],[c,a+(t.curveHeight-10),"Q"],[o,a,""],[s,r,"L"],[s,r,"M"],[c,r+t.curveHeight,"Q"],[l,r,""],[h,a,"L"]]):e.push([[h,a,"M"],[o,a,"L"],[s,r,"L"],[l,r,"L"],[h,a,"L"]]),h=l,o=s,a=r}),e}},{key:"_defineColorGradients",value:function(t){var e=t.append("defs");this.blocks.forEach(function(t,i){var n=t.fill,o=h.shade(n,-.25),a=e.append("linearGradient").attr({id:"gradient-"+i}),l=[[0,o],[40,n],[60,n],[100,o]];l.forEach(function(t){a.append("stop").attr({offset:t[0]+"%",style:"stop-color:"+t[1]})})})}},{key:"_drawTopOval",value:function(t,e){var i=0,n=this.width,o=this.width/2;this.isInverted&&(i=this.bottomLeftX,n=this.width-this.bottomLeftX);var a=e[0],l=a[1][1]+this.curveHeight-10,s=this.navigator.plot([["M",i,a[0][1]],["Q",o,l],[" ",n,a[2][1]],["M",n,10],["Q",o,0],[" ",i,10]]);t.append("path").attr("fill",h.shade(this.blocks[0].fill,-.4)).attr("d",s)}},{key:"_drawBlock",value:function(t){var e=this;if(t!==this.blocks.length){var i=this.svg.append("g"),n=this._getBlockPath(i,t);n.data(this._getD3Data(t)),this.animation!==!1?n.transition().duration(this.animation).ease("linear").attr("fill",this._getFillColor(t)).attr("d",this._getPathDefinition(t)).each("end",function(){e._drawBlock(t+1)}):(n.attr("fill",this._getFillColor(t)).attr("d",this._getPathDefinition(t)),this._drawBlock(t+1)),this.hoverEffects&&n.on("mouseover",this._onMouseOver).on("mouseout",this._onMouseOut),null!==this.onBlockClick&&n.on("click",this.onBlockClick),this._addBlockLabel(i,t)}}},{key:"_getBlockPath",value:function(t,e){var i=t.append("path");return this.animation!==!1&&this._addBeforeTransition(i,e),i}},{key:"_addBeforeTransition",value:function(t,e){var i=this.blockPaths[e],n="",h="";n=this.isCurved?this.navigator.plot([["M",i[0][0],i[0][1]],["Q",i[1][0],i[1][1]],[" ",i[2][0],i[2][1]],["L",i[2][0],i[2][1]],["M",i[2][0],i[2][1]],["Q",i[1][0],i[1][1]],[" ",i[0][0],i[0][1]]]):this.navigator.plot([["M",i[0][0],i[0][1]],["L",i[1][0],i[1][1]],["L",i[1][0],i[1][1]],["L",i[0][0],i[0][1]]]),h="solid"===this.fillType&&e>0?this._getFillColor(e-1):this._getFillColor(e),t.attr("d",n).attr("fill",h)}},{key:"_getD3Data",value:function(t){return[this.blocks[t]]}},{key:"_getFillColor",value:function(t){return"solid"===this.fillType?this.blocks[t].fill:"url(#gradient-"+t+")"}},{key:"_getPathDefinition",value:function(t){var e=[];return this.blockPaths[t].forEach(function(t){e.push([t[2],t[0],t[1]])}),this.navigator.plot(e)}},{key:"_onMouseOver",value:function(e){t.select(this).attr("fill",h.shade(e.fill,-.2))}},{key:"_onMouseOut",value:function(e){t.select(this).attr("fill",e.fill)}},{key:"_addBlockLabel",value:function(t,e){var i=this.blockPaths[e],n=this.blocks[e].label.formatted,h=this.blocks[e].label.color,o=this.width/2,a=this._getTextY(i);t.append("text").text(n).attr({x:o,y:a,"text-anchor":"middle","dominant-baseline":"middle",fill:h,"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.blocks.length:(t[1][1]+t[2][1])/2}}]),n}(),h=function(){function t(){e(this,t),this.hexExpression=/^#([0-9a-f]{3}|[0-9a-f]{6})$/i,this.labelFill=null,this.scale=null}return i(t,[{key:"setLabelFill",value:function(t){this.labelFill=t}},{key:"setScale",value:function(t){this.scale=t}},{key:"getBlockFill",value:function(t,e){return t.length>2&&this.hexExpression.test(t[2])?t[2]:Array.isArray(this.scale)?this.scale[e]:this.scale(e)}},{key:"getLabelFill",value:function(t){return t.length>3&&this.hexExpression.test(t[3])?t[3]:this.labelFill}}],[{key:"shade",value:function(e,i){var n=e.slice(1);3===n.length&&(n=t.expandHex(n));var h=parseInt(n,16),o=0>i?0:255,a=0>i?-1*i:i,l=h>>16,s=h>>8&255,r=255&h,c=16777216+65536*(Math.round((o-l)*a)+l)+256*(Math.round((o-s)*a)+s)+(Math.round((o-r)*a)+r);return"#"+c.toString(16).slice(1)}},{key:"expandHex",value:function(t){return t[0]+t[0]+t[1]+t[1]+t[2]+t[2]}}]),t}(),o=function(){function t(){e(this,t),this.expression=null}return i(t,[{key:"setFormat",value:function(t){"function"==typeof t?this.formatter=t:(this.expression=t,this.formatter=this.stringFormatter)}},{key:"format",value:function(t,e){return Array.isArray(e)?this.formatter(t,e[0],e[1]):this.formatter(t,e,null)}},{key:"stringFormatter",value:function(t,e){var i=arguments.length<=2||void 0===arguments[2]?null:arguments[2],n=i;return null===i&&(n=this.getDefaultFormattedValue(e)),this.expression.split("{l}").join(t).split("{v}").join(e).split("{f}").join(n)}},{key:"getDefaultFormattedValue",value:function(t){return t.toLocaleString()}}]),t}(),a=function(){function t(){e(this,t)}return i(t,[{key:"plot",value:function(t){var e="";return t.forEach(function(t){e+=t[0]+t[1]+","+t[2]+" "}),e.replace(/ +/g," ").trim()}}]),t}(),l=function(){function t(){e(this,t)}return i(t,null,[{key:"extend",value:function(e,i){var n=void 0;for(n in i)i.hasOwnProperty(n)&&("object"!=typeof i[n]||Array.isArray(i[n])?e[n]=i[n]:"object"!=typeof e[n]||Array.isArray(e[n])?e[n]=t.extend({},i[n]):e[n]=t.extend(e[n],i[n]));return e}}]),t}();return n}); |
var gulp = require('gulp'); | ||
var umd = require('gulp-wrap-umd'); | ||
var concat = require('gulp-concat'); | ||
var jshint = require('gulp-jshint'); | ||
var jscs = require('gulp-jscs'); | ||
var eslint = require('gulp-eslint'); | ||
var babel = require('gulp-babel'); | ||
@@ -17,2 +16,3 @@ var mocha = require('gulp-mocha-phantomjs'); | ||
'./src/d3-funnel/d3-funnel.js', | ||
'./src/d3-funnel/colorizer.js', | ||
'./src/d3-funnel/label-formatter.js', | ||
@@ -34,8 +34,5 @@ './src/d3-funnel/navigator.js', | ||
return gulp.src(src) | ||
.pipe(jshint()) | ||
.pipe(jshint.reporter('default')) | ||
.pipe(jshint.reporter('fail')) | ||
.pipe(jscs({ | ||
configPath: './.jscsrc', | ||
})); | ||
.pipe(eslint()) | ||
.pipe(eslint.format()) | ||
.pipe(eslint.failOnError()); | ||
}); | ||
@@ -46,3 +43,5 @@ | ||
.pipe(concat('d3-funnel.js')) | ||
.pipe(babel()) | ||
.pipe(babel({ | ||
stage: 0, | ||
})) | ||
.pipe(umd(umdOptions)) | ||
@@ -49,0 +48,0 @@ .pipe(gulp.dest('./compiled/')); |
{ | ||
"name": "d3-funnel", | ||
"version": "0.6.13", | ||
"version": "0.7.0", | ||
"description": "A library for rendering SVG funnel charts using D3.js", | ||
@@ -14,10 +14,12 @@ "author": "Jake Zatecky", | ||
"devDependencies": { | ||
"babel-eslint": "^4.1.3", | ||
"chai": "^3.2.0", | ||
"chai-spies": "^0.7.0", | ||
"eslint": "^1.6.0", | ||
"eslint-config-airbnb": "^0.1.0", | ||
"gulp": "^3.9.0", | ||
"gulp-babel": "^5.2.1", | ||
"gulp-concat": "^2.6.0", | ||
"gulp-eslint": "^1.0.0", | ||
"gulp-header": "^1.7.1", | ||
"gulp-jscs": "^2.0.0", | ||
"gulp-jshint": "^1.11.2", | ||
"gulp-mocha-phantomjs": "^0.10.1", | ||
@@ -27,2 +29,3 @@ "gulp-rename": "^1.2.2", | ||
"gulp-wrap-umd": "^0.2.1", | ||
"lodash": "^3.10.1", | ||
"mocha": "^2.3.2" | ||
@@ -29,0 +32,0 @@ }, |
@@ -58,20 +58,21 @@ # D3 Funnel | ||
| 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` | | ||
| Option | Description | Type | Default | | ||
| --------------------- | --------------------------------------------------------------------------- | -------------- | ----------------------- | | ||
| `chart.width` | The pixel width of the chart. | int | Container's width | | ||
| `chart.height` | The pixel height of the chart. | int | Container's height | | ||
| `chart.bottomWidth` | The percent of total width the bottom should be. | float | `1 / 3` | | ||
| `chart.bottomPinch` | How many blocks to pinch on the bottom to create a "neck". | int | `0` | | ||
| `chart.inverted` | Whether the funnel is inverted (like a pyramid). | bool | `false` | | ||
| `chart.animate` | The load animation speed in milliseconds. | int/bool | `false` | | ||
| `chart.curve.enabled` | Whether the funnel is curved. | bool | `false` | | ||
| `chart.curve.height` | The curvature amount. | int | `20` | | ||
| `block.dynamicHeight` | Whether the block heights are proportional to its weight. | bool | `false` | | ||
| `block.fill.scale` | The block background color scale. Expects an index and returns a color. | function/array | `d3.scale.category10()` | | ||
| `block.fill.type` | Either `'solid'` or `'gradient'`. | string | `'solid'` | | ||
| `block.minHeight` | The minimum pixel height of a block. | int/bool | `false` | | ||
| `block.highlight` | Whether the blocks are highlighted on hover. | 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}'` | | ||
| `events.click.block` | Callback for when a block is clicked. | function | `null` | | ||
@@ -89,2 +90,27 @@ ### Label Format | ||
### Overriding Defaults | ||
You may wish to override the default chart options. For example, you may wish | ||
for every funnel to have proportional heights. To do this, simplify modify the | ||
`D3Funnel.defaults` property: | ||
``` javascript | ||
D3Funnel.defaults.block.dynamicHeight = true; | ||
``` | ||
Should you wish to override multiple properties at a time, you may consider | ||
using [lodash's][lodash-merge] `_.merge` or [jQuery's][jquery-extend] `$.extend`: | ||
``` javascript | ||
D3Funnel.defaults = _.merge(D3Funnel.defaults, { | ||
chart: { | ||
dynamicHeight: true, | ||
animate: 200, | ||
}, | ||
label: { | ||
format: '{l}: ${f}', | ||
}, | ||
}); | ||
``` | ||
## API | ||
@@ -100,3 +126,4 @@ | ||
You can specify overriding colors for any data point (hex only): | ||
You can specify colors to override `block.fill.scale` and `label.fill` for any | ||
data point (hex only): | ||
@@ -131,1 +158,3 @@ ``` javascript | ||
[examples]: http://jakezatecky.github.io/d3-funnel/ | ||
[jQuery-extend]: https://api.jquery.com/jquery.extend/ | ||
[lodash-merge]: https://lodash.com/docs#merge |
@@ -1,2 +0,2 @@ | ||
/* global d3, LabelFormatter, Navigator, Utils */ | ||
/* global d3, Colorizer, LabelFormatter, Navigator, Utils */ | ||
/* exported D3Funnel */ | ||
@@ -6,2 +6,36 @@ | ||
static defaults = { | ||
chart: { | ||
width: 350, | ||
height: 400, | ||
bottomWidth: 1 / 3, | ||
bottomPinch: 0, | ||
inverted: false, | ||
animate: false, | ||
curve: { | ||
enabled: false, | ||
height: 20, | ||
}, | ||
}, | ||
block: { | ||
dynamicHeight: false, | ||
fill: { | ||
scale: d3.scale.category10().domain(d3.range(0, 10)), | ||
type: 'solid', | ||
}, | ||
minHeight: false, | ||
highlight: false, | ||
}, | ||
label: { | ||
fontSize: '14px', | ||
fill: '#fff', | ||
format: '{l}: {f}', | ||
}, | ||
events: { | ||
click: { | ||
block: null, | ||
}, | ||
}, | ||
}; | ||
/** | ||
@@ -15,23 +49,3 @@ * @param {string} selector A selector for the container element. | ||
// 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.colorizer = new Colorizer(); | ||
@@ -84,4 +98,2 @@ this.labelFormatter = new LabelFormatter(); | ||
this._setData(data); | ||
let settings = this._getSettings(options); | ||
@@ -93,16 +105,25 @@ | ||
// Set color scales | ||
this.colorizer.setLabelFill(settings.label.fill); | ||
this.colorizer.setScale(settings.block.fill.scale); | ||
// 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; | ||
this.width = settings.chart.width; | ||
this.height = settings.chart.height; | ||
this.bottomWidth = settings.chart.width * settings.chart.bottomWidth; | ||
this.bottomPinch = settings.chart.bottomPinch; | ||
this.isInverted = settings.chart.inverted; | ||
this.isCurved = settings.chart.curve.enabled; | ||
this.curveHeight = settings.chart.curve.height; | ||
this.fillType = settings.block.fill.type; | ||
this.hoverEffects = settings.block.highlight; | ||
this.dynamicHeight = settings.block.dynamicHeight; | ||
this.minHeight = settings.block.minHeight; | ||
this.animation = settings.chart.animate; | ||
// Support for events | ||
this.onBlockClick = settings.events.click.block; | ||
this._setBlocks(data); | ||
// Calculate the bottom left x position | ||
@@ -116,5 +137,2 @@ this.bottomLeftX = (this.width - this.bottomWidth) / 2; | ||
this.dy = this._getDy(); | ||
// Support for events | ||
this.onItemClick = settings.onItemClick; | ||
} | ||
@@ -137,2 +155,32 @@ | ||
/** | ||
* @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({}, D3Funnel.defaults); | ||
settings.chart.width = parseInt(d3.select(this.selector).style('width'), 10); | ||
settings.chart.height = parseInt(d3.select(this.selector).style('height'), 10); | ||
// 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.chart.width <= 0) { | ||
settings.chart.width = D3Funnel.defaults.chart.width; | ||
} | ||
if (settings.chart.height <= 0) { | ||
settings.chart.height = D3Funnel.defaults.chart.height; | ||
} | ||
return settings; | ||
} | ||
/** | ||
* Register the raw data into a standard block format and pre-calculate | ||
* some values. | ||
* | ||
* @param {Array} data | ||
@@ -142,62 +190,82 @@ * | ||
*/ | ||
_setData(data) { | ||
this.data = data; | ||
_setBlocks(data) { | ||
let totalCount = this._getTotalCount(data); | ||
this._setColors(); | ||
this.blocks = this._standardizeData(data, totalCount); | ||
} | ||
/** | ||
* Set the colors for each block. | ||
* Return the total count of all blocks. | ||
* | ||
* @return {void} | ||
* @return {Number} | ||
*/ | ||
_setColors() { | ||
let colorScale = d3.scale.category10(); | ||
let hexExpression = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i; | ||
_getTotalCount(data) { | ||
let total = 0; | ||
// 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); | ||
} | ||
data.forEach((block) => { | ||
total += this._getRawBlockCount(block); | ||
}); | ||
return total; | ||
} | ||
/** | ||
* @param {Object} options | ||
* Convert the raw data into a standardized format. | ||
* | ||
* @returns {Object} | ||
* @param {Array} data | ||
* @param {Number} totalCount | ||
* | ||
* @return {Array} | ||
*/ | ||
_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); | ||
_standardizeData(data, totalCount) { | ||
let standardized = []; | ||
// Overwrite default settings with user options | ||
settings = Utils.extend(settings, options); | ||
let count; | ||
let ratio; | ||
let label; | ||
// 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; | ||
} | ||
data.forEach((block, index) => { | ||
count = this._getRawBlockCount(block); | ||
ratio = count / totalCount; | ||
label = block[0]; | ||
return settings; | ||
standardized.push({ | ||
index: index, | ||
value: count, | ||
ratio: ratio, | ||
height: this.height * ratio, | ||
formatted: this.labelFormatter.format(label, count), | ||
fill: this.colorizer.getBlockFill(block, index), | ||
label: { | ||
raw: label, | ||
formatted: this.labelFormatter.format(label, count), | ||
color: this.colorizer.getLabelFill(block, index), | ||
}, | ||
}); | ||
}); | ||
return standardized; | ||
} | ||
/** | ||
* Given a raw data block, return its count. | ||
* | ||
* @param {Array} block | ||
* | ||
* @return {Number} | ||
*/ | ||
_getRawBlockCount(block) { | ||
return Array.isArray(block[1]) ? block[1][0] : block[1]; | ||
} | ||
/** | ||
* @return {Number} | ||
*/ | ||
_getDx() { | ||
// Will be sharper if there is a pinch | ||
if (this.bottomPinch > 0) { | ||
return this.bottomLeftX / (this.data.length - this.bottomPinch); | ||
return this.bottomLeftX / (this.blocks.length - this.bottomPinch); | ||
} | ||
return this.bottomLeftX / this.data.length; | ||
return this.bottomLeftX / this.blocks.length; | ||
} | ||
@@ -211,6 +279,6 @@ | ||
if (this.isCurved) { | ||
return (this.height - this.curveHeight) / this.data.length; | ||
return (this.height - this.curveHeight) / this.blocks.length; | ||
} | ||
return this.height / this.data.length; | ||
return this.height / this.blocks.length; | ||
} | ||
@@ -282,8 +350,4 @@ | ||
let topBase = this.width; | ||
let bottomBase = 0; | ||
let totalHeight = this.height; | ||
let totalArea = this.height * (this.width + this.bottomWidth) / 2; | ||
let slope = 2 * this.height / (this.width - this.bottomWidth); | ||
// This is greedy in that the block will have a guaranteed height | ||
@@ -293,47 +357,76 @@ // and the remaining is shared among the ratio, instead of being | ||
if (this.minHeight !== false) { | ||
totalArea = (this.height - this.minHeight * this.data.length) * (this.width + this.bottomWidth) / 2; | ||
totalHeight = this.height - this.minHeight * this.blocks.length; | ||
} | ||
let totalCount = 0; | ||
let count = 0; | ||
let slopeHeight = this.height; | ||
// Harvest total count | ||
this.data.forEach((block) => { | ||
totalCount += Array.isArray(block[1]) ? block[1][0] : block[1]; | ||
// Correct slope height if there are blocks being pinched (and thus | ||
// requiring a sharper curve) | ||
this.blocks.forEach((block, i) => { | ||
if (this.bottomPinch > 0) { | ||
if (this.isInverted) { | ||
if (i < this.bottomPinch) { | ||
slopeHeight -= block.height; | ||
} | ||
} else if (i >= this.blocks.length - this.bottomPinch) { | ||
slopeHeight -= block.height; | ||
} | ||
} | ||
}); | ||
// The slope will determine the where the x points on each block | ||
// iteration | ||
let slope = 2 * slopeHeight / (this.width - this.bottomWidth); | ||
// 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]; | ||
this.blocks.forEach((block, i) => { | ||
// Make heights proportional to block weight | ||
if (this.dynamicHeight) { | ||
// Slice off the height proportional to this block | ||
dy = totalHeight * block.ratio; | ||
// Calculate dynamic shapes based on area | ||
if (this.dynamicArea) { | ||
let ratio = count / totalCount; | ||
let area = ratio * totalArea; | ||
// Add greedy minimum height | ||
if (this.minHeight !== false) { | ||
area += this.minHeight * (this.width + this.bottomWidth) / 2; | ||
dy += this.minHeight; | ||
} | ||
bottomBase = Math.sqrt((slope * topBase * topBase - (4 * area)) / slope); | ||
// Account for any curvature | ||
if (this.isCurved) { | ||
dy = dy - (this.curveHeight / this.blocks.length); | ||
} | ||
// Prevent bottm points from becomming NaN | ||
if (this.bottomWidth === 0 && i === this.data.length - 1) { | ||
bottomBase = 0; | ||
// Given: y = mx + b | ||
// Given: b = 0 (when funnel), b = this.height (when pyramid) | ||
// For funnel, x_i = y_i / slope | ||
nextLeftX = (prevHeight + dy) / slope; | ||
// For pyramid, x_i = y_i - this.height / -slope | ||
if (this.isInverted) { | ||
nextLeftX = (prevHeight + dy - this.height) / (-1 * slope); | ||
} | ||
// Prevent NaN slope | ||
// If bottomWidth is 0, adjust last x position (to circumvent | ||
// errors associated with rounding) | ||
if (this.bottomWidth === 0 && i === this.blocks.length - 1) { | ||
// For funnel, last position is the center | ||
nextLeftX = this.width / 2; | ||
// For pyramid, last position is the origin | ||
if (this.isInverted) { | ||
nextLeftX = 0; | ||
} | ||
} | ||
// If bottomWidth is same as width, stop x velocity | ||
if (this.bottomWidth === this.width) { | ||
bottomBase = topBase; | ||
nextLeftX = prevLeftX; | ||
} | ||
dx = (topBase / 2) - (bottomBase / 2); | ||
dy = (area * 2) / (topBase + bottomBase); | ||
// Calculate the shift necessary for both x points | ||
dx = nextLeftX - prevLeftX; | ||
if (this.isCurved) { | ||
dy = dy - (this.curveHeight / this.data.length); | ||
if (this.isInverted) { | ||
dx = prevLeftX - nextLeftX; | ||
} | ||
topBase = bottomBase; | ||
} | ||
@@ -346,3 +439,3 @@ | ||
if (!this.isInverted) { | ||
if (i >= this.data.length - this.bottomPinch) { | ||
if (i >= this.blocks.length - this.bottomPinch) { | ||
dx = 0; | ||
@@ -355,5 +448,5 @@ } | ||
// static area's (prevents zero velocity if isInverted | ||
// and bottomPinch are non trivial and dynamicArea is | ||
// and bottomPinch are non trivial and dynamicHeight is | ||
// false) | ||
if (!this.dynamicArea) { | ||
if (!this.dynamicHeight) { | ||
dx = this.dx; | ||
@@ -429,5 +522,5 @@ } | ||
// Create a gradient for each block | ||
this.data.forEach((block, index) => { | ||
let color = block[2]; | ||
let shade = Utils.shadeColor(color, -0.25); | ||
this.blocks.forEach((block, index) => { | ||
let color = block.fill; | ||
let shade = Colorizer.shade(color, -0.25); | ||
@@ -491,3 +584,3 @@ // Create linear gradient | ||
svg.append('path') | ||
.attr('fill', Utils.shadeColor(this.data[0][2], -0.4)) | ||
.attr('fill', Colorizer.shade(this.blocks[0].fill, -0.4)) | ||
.attr('d', path); | ||
@@ -504,3 +597,3 @@ } | ||
_drawBlock(index) { | ||
if (index === this.data.length) { | ||
if (index === this.blocks.length) { | ||
return; | ||
@@ -514,3 +607,3 @@ } | ||
let path = this._getBlockPath(group, index); | ||
path.data(this._getBlockData(index)); | ||
path.data(this._getD3Data(index)); | ||
@@ -522,3 +615,3 @@ // Add animation components | ||
.ease('linear') | ||
.attr('fill', this._getColor(index)) | ||
.attr('fill', this._getFillColor(index)) | ||
.attr('d', this._getPathDefinition(index)) | ||
@@ -529,3 +622,3 @@ .each('end', () => { | ||
} else { | ||
path.attr('fill', this._getColor(index)) | ||
path.attr('fill', this._getFillColor(index)) | ||
.attr('d', this._getPathDefinition(index)); | ||
@@ -542,4 +635,4 @@ this._drawBlock(index + 1); | ||
// ItemClick event | ||
if (this.onItemClick !== null) { | ||
path.on('click', this.onItemClick); | ||
if (this.onBlockClick !== null) { | ||
path.on('click', this.onBlockClick); | ||
} | ||
@@ -602,7 +695,7 @@ | ||
// 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) | ||
if (this.fillType === 'solid' && index > 0) { | ||
beforeFill = this._getFillColor(index - 1); | ||
// Otherwise use current background | ||
} else { | ||
beforeFill = this._getColor(index); | ||
beforeFill = this._getFillColor(index); | ||
} | ||
@@ -615,2 +708,4 @@ | ||
/** | ||
* Return d3 formatted data for the given block. | ||
* | ||
* @param {int} index | ||
@@ -620,18 +715,8 @@ * | ||
*/ | ||
_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), | ||
}]; | ||
_getD3Data(index) { | ||
return [this.blocks[index]]; | ||
} | ||
/** | ||
* Return the color for the given index. | ||
* Return the block fill color for the given index. | ||
* | ||
@@ -642,8 +727,8 @@ * @param {int} index | ||
*/ | ||
_getColor(index) { | ||
_getFillColor(index) { | ||
if (this.fillType === 'solid') { | ||
return this.data[index][2]; | ||
} else { | ||
return 'url(#gradient-' + index + ')'; | ||
return this.blocks[index].fill; | ||
} | ||
return 'url(#gradient-' + index + ')'; | ||
} | ||
@@ -672,3 +757,3 @@ | ||
_onMouseOver(data) { | ||
d3.select(this).attr('fill', Utils.shadeColor(data.baseColor, -0.2)); | ||
d3.select(this).attr('fill', Colorizer.shade(data.fill, -0.2)); | ||
} | ||
@@ -694,4 +779,4 @@ | ||
let label = this._getBlockData(index)[0].formatted; | ||
let fill = this.data[index][3] || this.label.fill; | ||
let text = this.blocks[index].label.formatted; | ||
let fill = this.blocks[index].label.color; | ||
@@ -702,3 +787,3 @@ let x = this.width / 2; // Center the text | ||
group.append('text') | ||
.text(label) | ||
.text(text) | ||
.attr({ | ||
@@ -725,3 +810,3 @@ 'x': x, | ||
if (this.isCurved) { | ||
return (paths[2][1] + paths[3][1]) / 2 + (this.curveHeight / this.data.length); | ||
return (paths[2][1] + paths[3][1]) / 2 + (this.curveHeight / this.blocks.length); | ||
} | ||
@@ -728,0 +813,0 @@ |
@@ -43,5 +43,5 @@ /* exported LabelFormatter */ | ||
return this.formatter(label, value[0], value[1]); | ||
} else { | ||
return this.formatter(label, value, null); | ||
} | ||
return this.formatter(label, value, null); | ||
} | ||
@@ -63,6 +63,8 @@ | ||
stringFormatter(label, value, fValue = null) { | ||
let formatted = fValue; | ||
// Attempt to use supplied formatted value | ||
// Otherwise, use the default | ||
if (fValue === null) { | ||
fValue = this.getDefaultFormattedValue(value); | ||
formatted = this.getDefaultFormattedValue(value); | ||
} | ||
@@ -73,3 +75,3 @@ | ||
.split('{v}').join(value) | ||
.split('{f}').join(fValue); | ||
.split('{f}').join(formatted); | ||
} | ||
@@ -76,0 +78,0 @@ |
@@ -13,3 +13,3 @@ /* exported Navigator */ | ||
plot(commands) { | ||
var path = ''; | ||
let path = ''; | ||
@@ -16,0 +16,0 @@ commands.forEach((command) => { |
/* exported Utils */ | ||
/* jshint bitwise: false */ | ||
@@ -22,4 +21,8 @@ /** | ||
if (b.hasOwnProperty(prop)) { | ||
if (typeof a[prop] === 'object' && typeof b[prop] === 'object') { | ||
a[prop] = Utils.extend(a[prop], b[prop]); | ||
if (typeof b[prop] === 'object' && !Array.isArray(b[prop])) { | ||
if (typeof a[prop] === 'object' && !Array.isArray(a[prop])) { | ||
a[prop] = Utils.extend(a[prop], b[prop]); | ||
} else { | ||
a[prop] = Utils.extend({}, b[prop]); | ||
} | ||
} else { | ||
@@ -34,44 +37,2 @@ a[prop] = b[prop]; | ||
/** | ||
* 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} | ||
*/ | ||
static shadeColor(color, shade) { | ||
let hex = color.slice(1); | ||
if (hex.length === 3) { | ||
hex = Utils.expandHex(hex); | ||
} | ||
let f = parseInt(hex, 16); | ||
let t = shade < 0 ? 0 : 255; | ||
let p = shade < 0 ? shade * -1 : shade; | ||
let R = f >> 16; | ||
let G = f >> 8 & 0x00FF; | ||
let B = f & 0x0000FF; | ||
let 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} | ||
*/ | ||
static expandHex(hex) { | ||
return hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2]; | ||
} | ||
} |
/* global d3, assert, chai, D3Funnel */ | ||
function getFunnel() { | ||
return new D3Funnel('#funnel'); | ||
} | ||
function getSvg() { | ||
return d3.select('#funnel').selectAll('svg'); | ||
} | ||
function getBasicData() { | ||
return [['Node', 1000]]; | ||
} | ||
function getPathHeight(path) { | ||
var commands = path.attr('d').split(' '); | ||
return getCommandHeight(commands[2]) - getCommandHeight(commands[0]); | ||
} | ||
function getCommandHeight(command) { | ||
return parseFloat(command.split(',')[1]); | ||
} | ||
var defaults = _.clone(D3Funnel.defaults, true); | ||
describe('D3Funnel', function () { | ||
var getFunnel, getSvg, getBasicData, getPathHeight, getCommandHeight; | ||
beforeEach(function (done) { | ||
getFunnel = function () { | ||
return new D3Funnel('#funnel'); | ||
}; | ||
getSvg = function () { | ||
return d3.select('#funnel').selectAll('svg'); | ||
}; | ||
getBasicData = function () { | ||
return [['Node', 1000]]; | ||
}; | ||
getPathHeight = function (path) { | ||
var commands = path.attr('d').split(' '); | ||
d3.select('#funnel').attr('style', null); | ||
return getCommandHeight(commands[2]) - getCommandHeight(commands[0]); | ||
}; | ||
getCommandHeight = function (command) { | ||
return parseFloat(command.split(',')[1]); | ||
}; | ||
D3Funnel.defaults = _.clone(defaults, true); | ||
@@ -80,3 +88,3 @@ done(); | ||
colorScale = d3.scale.category10(); | ||
colorScale = d3.scale.category10().domain(d3.range(0, 10)); | ||
@@ -120,7 +128,27 @@ assert.equal('#111', d3.select(paths[0]).attr('fill')); | ||
describe('defaults', function () { | ||
it('should affect all default options', function () { | ||
D3Funnel.defaults.label.fill = '#777'; | ||
getFunnel().draw(getBasicData(), {}); | ||
assert.isTrue(d3.select('#funnel text').attr('fill').indexOf('#777') > -1); | ||
}); | ||
}); | ||
describe('options', function () { | ||
describe('width', function () { | ||
describe('chart.width', function () { | ||
it ('should default to the container\'s width', function () { | ||
d3.select('#funnel').style('width', '250px'); | ||
getFunnel().draw(getBasicData(), {}); | ||
assert.equal(250, getSvg().node().getBBox().width); | ||
}); | ||
it('should set the funnel\'s width to the specified amount', function () { | ||
getFunnel().draw(getBasicData(), { | ||
width: 200, | ||
chart: { | ||
width: 200, | ||
}, | ||
}); | ||
@@ -132,6 +160,16 @@ | ||
describe('height', function () { | ||
describe('chart.height', function () { | ||
it ('should default to the container\'s height', function () { | ||
d3.select('#funnel').style('height', '250px'); | ||
getFunnel().draw(getBasicData(), {}); | ||
assert.equal(250, getSvg().node().getBBox().height); | ||
}); | ||
it('should set the funnel\'s height to the specified amount', function () { | ||
getFunnel().draw(getBasicData(), { | ||
height: 200, | ||
chart: { | ||
height: 200, | ||
}, | ||
}); | ||
@@ -143,6 +181,10 @@ | ||
describe('isCurved', function () { | ||
describe('chart.curve.enabled', function () { | ||
it('should create an additional path on top of the trapezoids', function () { | ||
getFunnel().draw(getBasicData(), { | ||
isCurved: true, | ||
chart: { | ||
curve: { | ||
enabled: true, | ||
}, | ||
}, | ||
}); | ||
@@ -155,3 +197,7 @@ | ||
getFunnel().draw(getBasicData(), { | ||
isCurved: true, | ||
chart: { | ||
curve: { | ||
enabled: true, | ||
}, | ||
}, | ||
}); | ||
@@ -169,46 +215,3 @@ | ||
describe('fillType', function () { | ||
it('should create gradients when set to \'gradient\'', function () { | ||
getFunnel().draw(getBasicData(), { | ||
fillType: 'gradient', | ||
}); | ||
// Cannot try to re-select the camelCased linearGradient element | ||
// due to a Webkit bug in the current PhantomJS; workaround is | ||
// to select the known ID of the linearGradient element | ||
// https://bugs.webkit.org/show_bug.cgi?id=83438 | ||
assert.equal(1, d3.selectAll('#funnel defs #gradient-0')[0].length); | ||
assert.equal('url(#gradient-0)', d3.select('#funnel path').attr('fill')); | ||
}); | ||
it('should use solid fill when not set to \'gradient\'', function () { | ||
getFunnel().draw(getBasicData(), {}); | ||
// Check for valid hex string | ||
assert.isTrue(/(^#[0-9A-F]{6}$)|(^#[0-9A-F]{3}$)/i.test( | ||
d3.select('#funnel path').attr('fill') | ||
)); | ||
}); | ||
}); | ||
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 () { | ||
describe('block.dynamicHeight', function () { | ||
it('should use equal heights when false', function () { | ||
@@ -221,3 +224,5 @@ var paths; | ||
], { | ||
height: 300, | ||
chart: { | ||
height: 300, | ||
}, | ||
}); | ||
@@ -238,4 +243,8 @@ | ||
], { | ||
height: 300, | ||
dynamicArea: true, | ||
chart: { | ||
height: 300, | ||
}, | ||
block: { | ||
dynamicHeight: true, | ||
}, | ||
}); | ||
@@ -245,4 +254,4 @@ | ||
assert.equal(72, parseInt(getPathHeight(d3.select(paths[0])), 10)); | ||
assert.equal(227, parseInt(getPathHeight(d3.select(paths[1])), 10)); | ||
assert.equal(100, parseInt(getPathHeight(d3.select(paths[0])), 10)); | ||
assert.equal(200, parseInt(getPathHeight(d3.select(paths[1])), 10)); | ||
}); | ||
@@ -260,5 +269,9 @@ | ||
], { | ||
height: 300, | ||
dynamicArea: true, | ||
bottomWidth: 0, | ||
chart: { | ||
height: 300, | ||
bottomWidth: 0, | ||
}, | ||
block: { | ||
dynamicHeight: true, | ||
}, | ||
}); | ||
@@ -278,5 +291,9 @@ | ||
], { | ||
height: 300, | ||
dynamicArea: true, | ||
bottomWidth: 1, | ||
chart: { | ||
height: 300, | ||
bottomWidth: 1, | ||
}, | ||
block: { | ||
dynamicHeight: true, | ||
}, | ||
}); | ||
@@ -289,2 +306,141 @@ | ||
describe('block.fill.scale', function () { | ||
it('should use a function\'s return value', function () { | ||
getFunnel().draw([ | ||
['A', 1], | ||
['B', 2], | ||
], { | ||
block: { | ||
fill: { | ||
scale: function (index) { | ||
if (index === 0) { | ||
return '#111'; | ||
} | ||
return '#222'; | ||
}, | ||
}, | ||
}, | ||
}); | ||
paths = getSvg().selectAll('path')[0]; | ||
assert.equal('#111', d3.select(paths[0]).attr('fill')); | ||
assert.equal('#222', d3.select(paths[1]).attr('fill')); | ||
}); | ||
it('should use an array\'s return value', function () { | ||
getFunnel().draw([ | ||
['A', 1], | ||
['B', 2], | ||
], { | ||
block: { | ||
fill: { | ||
scale: ['#111', '#222'], | ||
}, | ||
}, | ||
}); | ||
paths = getSvg().selectAll('path')[0]; | ||
assert.equal('#111', d3.select(paths[0]).attr('fill')); | ||
assert.equal('#222', d3.select(paths[1]).attr('fill')); | ||
}); | ||
}); | ||
describe('block.fill.type', function () { | ||
it('should create gradients when set to \'gradient\'', function () { | ||
getFunnel().draw(getBasicData(), { | ||
block: { | ||
fill: { | ||
type: 'gradient', | ||
}, | ||
}, | ||
}); | ||
// Cannot try to re-select the camelCased linearGradient element | ||
// due to a Webkit bug in the current PhantomJS; workaround is | ||
// to select the known ID of the linearGradient element | ||
// https://bugs.webkit.org/show_bug.cgi?id=83438 | ||
assert.equal(1, d3.selectAll('#funnel defs #gradient-0')[0].length); | ||
assert.equal('url(#gradient-0)', d3.select('#funnel path').attr('fill')); | ||
}); | ||
it('should use solid fill when not set to \'gradient\'', function () { | ||
getFunnel().draw(getBasicData(), {}); | ||
// Check for valid hex string | ||
assert.isTrue(/(^#[0-9A-F]{6}$)|(^#[0-9A-F]{3}$)/i.test( | ||
d3.select('#funnel path').attr('fill') | ||
)); | ||
}); | ||
}); | ||
describe('block.minHeight', function () { | ||
it('should give each block the minimum height specified', function () { | ||
var paths; | ||
getFunnel().draw([ | ||
['A', 299], | ||
['B', 1], | ||
], { | ||
chart: { | ||
height: 300, | ||
}, | ||
block: { | ||
dynamicHeight: true, | ||
minHeight: 10, | ||
}, | ||
}); | ||
paths = d3.selectAll('#funnel path')[0]; | ||
assert.isAbove(parseFloat(getPathHeight(d3.select(paths[0]))), 10); | ||
assert.isAbove(parseFloat(getPathHeight(d3.select(paths[1]))), 10); | ||
}); | ||
it('should decrease the height of blocks above the minimum', function () { | ||
var paths; | ||
getFunnel().draw([ | ||
['A', 299], | ||
['B', 1], | ||
], { | ||
chart: { | ||
height: 300, | ||
}, | ||
block: { | ||
dynamicHeight: true, | ||
minHeight: 10, | ||
}, | ||
}); | ||
paths = d3.selectAll('#funnel path')[0]; | ||
assert.isBelow(parseFloat(getPathHeight(d3.select(paths[0]))), 290); | ||
}); | ||
}); | ||
describe('block.highlight', 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'], | ||
], { | ||
block: { | ||
highlight: true, | ||
}, | ||
}); | ||
d3.select('#funnel path').node().dispatchEvent(event); | ||
// #fff * -1/5 => #cccccc | ||
assert.equal('#cccccc', d3.select('#funnel path').attr('fill')); | ||
}); | ||
}); | ||
describe('label.fontSize', function () { | ||
@@ -319,3 +475,3 @@ it('should set the label\'s font size to the specified amount', function () { | ||
format: '{l} {v} {f}', | ||
} | ||
}, | ||
}); | ||
@@ -342,3 +498,3 @@ | ||
describe('onItemClick', function () { | ||
describe('events.click.block', function () { | ||
it('should invoke the callback function with the correct data', function () { | ||
@@ -351,8 +507,12 @@ var event = document.createEvent('CustomEvent'); | ||
getFunnel().draw(getBasicData(), { | ||
onItemClick: function (d, i) { | ||
spy({ | ||
index: d.index, | ||
label: d.label, | ||
value: d.value, | ||
}, i); | ||
events: { | ||
click: { | ||
block: function (d, i) { | ||
spy({ | ||
index: d.index, | ||
label: d.label.raw, | ||
value: d.value, | ||
}, i); | ||
}, | ||
}, | ||
}, | ||
@@ -359,0 +519,0 @@ }); |
@@ -0,5 +1,5 @@ | ||
* Make test case for container's default dimensions | ||
* Document the block object available on click (and hover) | ||
* Document ability to set block color scale | ||
* Standardize option names | ||
* Fix bug with hover overriding gradient | ||
* Add tests for EACH option | ||
* Allow user to specify defaults |
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
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
91519
2331
157
16