d3-funnel
Version:
A library for rendering SVG funnel charts using D3.js
1,076 lines (870 loc) • 37.8 kB
JavaScript
import { cloneDeep } from 'lodash';
import {
range,
select,
selectAll,
scaleOrdinal,
schemeCategory10,
} from 'd3';
import chai from 'chai';
import sinon from 'sinon';
import D3Funnel from '../../src/d3-funnel/D3Funnel';
const { assert } = chai;
function getFunnel() {
return new D3Funnel('#funnel');
}
function getSvg() {
return select('#funnel').selectAll('svg');
}
function getSvgId() {
return document.querySelector('#funnel svg').id;
}
function getBasicData() {
return [{ label: 'Node', value: 1000 }];
}
function isLetter(str) {
return str.length === 1 && str.match(/[a-z]/i);
}
function getCommandPoint(command) {
const points = command.split(',');
const y = points[1];
let x = points[0];
// Strip any letter in front of number
if (isLetter(x[0])) {
x = x.substr(1);
}
return {
x: parseFloat(x),
y: parseFloat(y),
};
}
function getPathTopWidth(path) {
const commands = path.attr('d').split(' ');
return getCommandPoint(commands[1]).x - getCommandPoint(commands[0]).x;
}
function getPathBottomWidth(path) {
const commands = path.attr('d').split(' ');
return getCommandPoint(commands[2]).x - getCommandPoint(commands[3]).x;
}
function getPathHeight(path) {
const commands = path.attr('d').split(' ');
return getCommandPoint(commands[2]).y - getCommandPoint(commands[0]).y;
}
const defaults = cloneDeep(D3Funnel.defaults);
describe('D3Funnel', () => {
beforeEach((done) => {
// Reset any styles
select('#funnel').attr('style', null);
// Reset defaults
D3Funnel.defaults = cloneDeep(defaults);
// Clear out sandbox
document.getElementById('sandbox').innerHTML = '';
done();
});
describe('constructor', () => {
it('should instantiate without error when a query string is provided', () => {
new D3Funnel('#funnel'); // eslint-disable-line no-new
});
it('should instantiate without error when a DOM node is provided', () => {
new D3Funnel(document.querySelector('#funnel')); // eslint-disable-line no-new
});
});
describe('methods', () => {
describe('draw', () => {
it('should draw a chart on the identified target', () => {
getFunnel().draw(getBasicData());
assert.equal(1, getSvg().nodes().length);
});
it('should draw when no options are specified', () => {
getFunnel().draw(getBasicData());
assert.equal(1, getSvg().nodes().length);
});
it('should throw an error when the data is not an array', () => {
const funnel = getFunnel();
assert.throws(() => {
funnel.draw('Not array');
}, Error, 'Data must be an array.');
});
it('should throw an error when the data array does not have an element', () => {
const funnel = getFunnel();
assert.throws(() => {
funnel.draw([]);
}, Error, 'Data array must contain at least one element.');
});
it('should throw an error when the first data array element is not an object', () => {
const funnel = getFunnel();
assert.throws(() => {
funnel.draw(['Not array']);
}, Error, 'Data array elements must be an object.');
});
it('should throw an error when the first data array element does not have a value', () => {
const funnel = getFunnel();
assert.throws(() => {
funnel.draw([{ label: 'Only Label' }]);
}, Error, 'Data array elements must contain a label and value.');
});
it('should draw as many blocks as there are elements', () => {
getFunnel().draw([
{ label: 'Node A', value: 1 },
{ label: 'Node B', value: 2 },
{ label: 'Node C', value: 3 },
{ label: 'Node D', value: 4 },
]);
assert.equal(4, getSvg().selectAll('path').nodes().length);
});
it('should pass any row-specified formatted values to the label formatter', () => {
getFunnel().draw([
{ label: 'Node A', value: 1, formattedValue: 'One' },
{ label: 'Node B', value: 2 },
{ label: 'Node C', value: 1, formattedValue: 'Three' },
]);
const texts = getSvg().selectAll('text').nodes();
assert.equal('Node A: One', select(texts[0]).text());
assert.equal('Node B: 2', select(texts[1]).text());
assert.equal('Node C: Three', select(texts[2]).text());
});
it('should hide the labels of any row specified', () => {
getFunnel().draw([
{ label: 'Node A', value: 1, hideLabel: true },
{ label: 'Node B', value: 2 },
{ label: 'Node C', value: 3, hideLabel: true },
]);
const texts = getSvg().selectAll('text').nodes();
assert.equal('Node B: 2', select(texts[0]).text());
assert.equal(undefined, texts[1]);
});
it('should use colors assigned to a data element', () => {
getFunnel().draw([
{ label: 'Node A', value: 1, backgroundColor: '#111' },
{ label: 'Node B', value: 2, backgroundColor: '#222' },
{ label: 'Node C', value: 3 },
{ label: 'Node D', value: 4, backgroundColor: '#444' },
]);
const paths = getSvg().selectAll('path').nodes();
const colorScale = scaleOrdinal(schemeCategory10).domain(range(0, 10));
assert.equal('#111', select(paths[0]).attr('fill'));
assert.equal('#222', select(paths[1]).attr('fill'));
assert.equal(colorScale(2), select(paths[2]).attr('fill'));
assert.equal('#444', select(paths[3]).attr('fill'));
});
it('should use label colors assigned to a data element', () => {
getFunnel().draw([
{ label: 'A', value: 1, labelColor: '#111' },
{ label: 'B', value: 2, labelColor: '#222' },
{ label: 'C', value: 3 },
{ label: 'D', value: 4, labelColor: '#444' },
]);
const texts = getSvg().selectAll('text').nodes();
assert.equal('#111', select(texts[0]).attr('fill'));
assert.equal('#222', select(texts[1]).attr('fill'));
assert.equal('#fff', select(texts[2]).attr('fill'));
assert.equal('#444', select(texts[3]).attr('fill'));
});
it('should remove other elements from container', () => {
const container = select('#funnel');
const funnel = getFunnel();
// Make sure the container has no children
container.selectAll('*').remove();
container.append('p');
funnel.draw(getBasicData());
// Expect funnel children count plus funnel itself
const expected = getSvg().selectAll('*').size() + 1;
const actual = container.selectAll('*').size();
assert.equal(expected, actual);
});
it('should remove inner text from container', () => {
const container = select('#funnel');
const funnel = getFunnel();
// Make sure the container has no text
container.text();
container.text('to be removed');
funnel.draw(getBasicData());
// Make sure the only text in container comes from the funnel
assert.equal(getSvg().text(), container.text());
});
it('should assign a unique ID upon draw', () => {
getFunnel().draw(getBasicData());
const id = getSvgId();
assert.isTrue(document.querySelectorAll(`#${id}`).length === 1);
});
});
describe('destroy', () => {
it('should remove a drawn SVG element', () => {
const funnel = getFunnel();
funnel.draw(getBasicData());
funnel.destroy();
assert.equal(0, getSvg().nodes().length);
});
});
});
describe('defaults', () => {
it('should affect all default options', () => {
D3Funnel.defaults.label.fill = '#777';
getFunnel().draw(getBasicData());
assert.isTrue(select('#funnel text').attr('fill').indexOf('#777') > -1);
});
});
describe('options', () => {
describe('chart.width/height', () => {
it('should default to the container\'s dimensions', () => {
['width', 'height'].forEach((direction) => {
select('#funnel').style(direction, '250px');
getFunnel().draw(getBasicData());
assert.equal(250, getSvg().node().getBBox()[direction]);
});
});
it('should default to the library defaults if the container dimensions are zero', () => {
document.querySelector('#funnel').style.width = '0px';
document.querySelector('#funnel').style.height = '0px';
getFunnel().draw(getBasicData());
assert.equal(350, getSvg().node().getBBox().width);
assert.equal(400, getSvg().node().getBBox().height);
});
it('should set the funnel\'s width/height to the specified amount', () => {
['width', 'height'].forEach((direction) => {
getFunnel().draw(getBasicData(), {
chart: {
[direction]: 200,
},
});
assert.equal(200, getSvg().node().getBBox()[direction]);
});
});
it('should set the funnel\'s percent width/height to the specified amount', () => {
['width', 'height'].forEach((direction) => {
select('#funnel').style(direction, '200px');
getFunnel().draw(getBasicData(), {
chart: {
[direction]: '75%',
},
});
assert.equal(150, getSvg().node().getBBox()[direction]);
});
});
});
describe('chart.height', () => {
it('should default to the container\'s height', () => {
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', () => {
getFunnel().draw(getBasicData(), {
chart: {
height: 200,
},
});
assert.equal(200, getSvg().node().getBBox().height);
});
it('should set the funnel\'s percentage height to the specified amount', () => {
select('#funnel').style('height', '300px');
getFunnel().draw(getBasicData(), {
chart: {
height: '50%',
},
});
assert.equal(150, getSvg().node().getBBox().height);
});
});
describe('chart.bottomWidth', () => {
it('should set the bottom tip width to the specified percentage', () => {
getFunnel().draw(getBasicData(), {
chart: {
width: 200,
bottomWidth: 1 / 2,
},
});
assert.equal(100, getPathBottomWidth(select('path')));
});
});
describe('chart.bottomPinch', () => {
it('should set the last n number of blocks to have the width of chart.bottomWidth', () => {
getFunnel().draw([
{ label: 'A', value: 1 },
{ label: 'B', value: 2 },
{ label: 'C', value: 3 },
], {
chart: {
width: 450,
bottomWidth: 1 / 3,
bottomPinch: 2,
},
});
const paths = selectAll('path').nodes();
assert.equal(150, paths[1].getBBox().width);
assert.equal(150, paths[2].getBBox().width);
});
it('should maintain chart.bottomWidth when combined with block.minHeight', () => {
getFunnel().draw([
{ label: 'A', value: 1 },
{ label: 'B', value: 2 },
{ label: 'C', value: 3 },
], {
chart: {
width: 450,
height: 100,
bottomWidth: 1 / 3,
bottomPinch: 1,
},
block: {
dynamicHeight: true,
minHeight: 20,
},
});
const paths = selectAll('path').nodes();
assert.equal(150, paths[2].getBBox().width);
});
it('should maintain chart.bottomWidth when combined with block.dynamicHeight and curve.enabled', () => {
getFunnel().draw([
{ label: 'A', value: 1 },
{ label: 'B', value: 2 },
{ label: 'C', value: 3 },
{ label: 'D', value: 4 },
], {
chart: {
width: 320,
height: 400,
bottomWidth: 3 / 8,
bottomPinch: 1,
curve: {
enabled: true,
},
},
block: {
dynamicHeight: true,
},
});
const paths = selectAll('path').nodes();
assert.equal(120, paths[4].getBBox().width);
});
});
describe('chart.inverted', () => {
it('should draw the chart in a top-to-bottom arrangement by default', () => {
getFunnel().draw([
{ label: 'A', value: 1 },
{ label: 'B', value: 2 },
], {
chart: {
width: 200,
bottomWidth: 1 / 2,
},
});
const paths = selectAll('path').nodes();
assert.equal(200, getPathTopWidth(select(paths[0])));
assert.equal(100, getPathBottomWidth(select(paths[1])));
});
it('should draw the chart in a bottom-to-top arrangement when true', () => {
getFunnel().draw([
{ label: 'A', value: 1 },
{ label: 'B', value: 2 },
], {
chart: {
width: 200,
bottomWidth: 1 / 2,
inverted: true,
},
});
const paths = selectAll('path').nodes();
assert.equal(100, getPathTopWidth(select(paths[0])));
assert.equal(200, getPathBottomWidth(select(paths[1])));
});
});
describe('chart.curve.enabled', () => {
it('should create an additional path on top of the trapezoids', () => {
getFunnel().draw(getBasicData(), {
chart: {
curve: {
enabled: true,
},
},
});
assert.equal(2, selectAll('#funnel path').nodes().length);
});
it('should create a quadratic Bezier curve on each path', () => {
getFunnel().draw(getBasicData(), {
chart: {
curve: {
enabled: true,
},
},
});
const paths = selectAll('#funnel path').nodes();
const quadraticPaths = paths.filter((path) => select(path).attr('d').indexOf('Q') > -1);
assert.equal(paths.length, quadraticPaths.length);
});
});
describe('block.dynamicHeight', () => {
it('should use equal heights when false', () => {
getFunnel().draw([
{ label: 'A', value: 1 },
{ label: 'B', value: 2 },
], {
chart: {
height: 300,
},
});
const paths = selectAll('#funnel path').nodes();
assert.equal(150, getPathHeight(select(paths[0])));
assert.equal(150, getPathHeight(select(paths[1])));
});
it('should use proportional heights when true', () => {
getFunnel().draw([
{ label: 'A', value: 1 },
{ label: 'B', value: 2 },
], {
chart: {
height: 300,
},
block: {
dynamicHeight: true,
},
});
const paths = selectAll('#funnel path').nodes();
assert.equal(100, parseInt(getPathHeight(select(paths[0])), 10));
assert.equal(200, parseInt(getPathHeight(select(paths[1])), 10));
});
it('should not have NaN in the last path when bottomWidth is equal to 0%', () => {
// A very specific cooked-up example that could trigger NaN
getFunnel().draw([
{ label: 'A', value: 120 },
{ label: 'B', value: 40 },
{ label: 'C', value: 20 },
{ label: 'D', value: 15 },
], {
chart: {
height: 300,
bottomWidth: 0,
},
block: {
dynamicHeight: true,
},
});
const paths = selectAll('#funnel path').nodes();
assert.equal(-1, select(paths[3]).attr('d').indexOf('NaN'));
});
it('should not error when bottomWidth is equal to 100%', () => {
getFunnel().draw([
{ label: 'A', value: 1 },
{ label: 'B', value: 2 },
], {
chart: {
height: 300,
bottomWidth: 1,
},
block: {
dynamicHeight: true,
},
});
});
it('should not generate NaN or Infinite values when zero', () => {
getFunnel().draw(getBasicData(), {
chart: {
height: 0,
},
block: {
dynamicHeight: true,
},
});
selectAll('path').nodes().forEach((node) => {
const definition = String(select(node).attr('d'));
assert.equal(false, definition.indexOf('NaN') > -1 || definition.indexOf('Infinity') > -1);
});
});
it('should give all blocks equal height if the sum of values is zero', () => {
getFunnel().draw([
{ label: 'A', value: 0 },
{ label: 'B', value: 0 },
], {
chart: {
height: 300,
},
block: {
dynamicHeight: true,
},
});
const paths = selectAll('#funnel path').nodes();
assert.equal(150, getPathHeight(select(paths[0])));
assert.equal(150, getPathHeight(select(paths[1])));
});
});
describe('block.dynamicSlope', () => {
it('should give each block top width relative to its value', () => {
getFunnel().draw([
{ label: 'A', value: 100 },
{ label: 'B', value: 55 },
{ label: 'C', value: 42 },
{ label: 'D', value: 74 },
], {
chart: {
width: 100,
},
block: {
dynamicSlope: true,
},
});
const paths = selectAll('#funnel path').nodes();
assert.equal(parseFloat(getPathTopWidth(select(paths[0]))), 100);
assert.equal(parseFloat(getPathTopWidth(select(paths[1]))), 55);
assert.equal(parseFloat(getPathTopWidth(select(paths[2]))), 42);
assert.equal(parseFloat(getPathTopWidth(select(paths[3]))), 74);
});
it('should make the last block top width equal to bottom width', () => {
getFunnel().draw([
{ label: 'A', value: 100 },
{ label: 'B', value: 52 },
{ label: 'C', value: 42 },
{ label: 'D', value: 74 },
], {
chart: {
width: 100,
},
block: {
dynamicSlope: true,
},
});
const paths = selectAll('#funnel path').nodes();
assert.equal(parseFloat(getPathTopWidth(select(paths[3]))), 74);
assert.equal(parseFloat(getPathBottomWidth(select(paths[3]))), 74);
});
it('should use bottomWidth value when false', () => {
getFunnel().draw([
{ label: 'A', value: 100 },
{ label: 'B', value: 90 },
], {
chart: {
width: 100,
bottomWidth: 0.4,
},
});
const paths = selectAll('#funnel path').nodes();
assert.equal(parseFloat(getPathTopWidth(select(paths[0]))), 100);
assert.equal(parseFloat(getPathBottomWidth(select(paths[1]))), 40);
});
});
describe('block.barOverlay', () => {
it('should draw value overlay within each path', () => {
getFunnel().draw([
{ label: 'A', value: 10 },
{ label: 'B', value: 20 },
], {
block: {
barOverlay: true,
},
});
// draw 2 path for each data point
assert.equal(4, selectAll('#funnel path').nodes().length);
});
it('should draw value overlay with overridden total count', () => {
getFunnel().draw([
{ label: 'A', value: 10 },
{ label: 'B', value: 20 },
], {
chart: {
totalCount: 100,
},
block: {
barOverlay: true,
},
});
const paths = selectAll('path').nodes();
const APathFullWidth = getPathTopWidth(select(paths[0]));
const APathOverlayWidth = getPathTopWidth(select(paths[1]));
const BPathFullWidth = getPathTopWidth(select(paths[2]));
const BPathOverlayWidth = getPathTopWidth(select(paths[3]));
assert.equal(10, Math.round((APathOverlayWidth / APathFullWidth) * 100));
assert.equal(20, Math.round((BPathOverlayWidth / BPathFullWidth) * 100));
});
});
describe('block.fill.scale', () => {
it('should use a function\'s return value', () => {
getFunnel().draw([
{ label: 'A', value: 1 },
{ label: 'B', value: 2 },
], {
block: {
fill: {
scale: (index) => {
if (index === 0) {
return '#111';
}
return '#222';
},
},
},
});
const paths = getSvg().selectAll('path').nodes();
assert.equal('#111', select(paths[0]).attr('fill'));
assert.equal('#222', select(paths[1]).attr('fill'));
});
it('should use an array\'s return value', () => {
getFunnel().draw([
{ label: 'A', value: 1 },
{ label: 'B', value: 2 },
], {
block: {
fill: {
scale: ['#111', '#222'],
},
},
});
const paths = getSvg().selectAll('path').nodes();
assert.equal('#111', select(paths[0]).attr('fill'));
assert.equal('#222', select(paths[1]).attr('fill'));
});
});
describe('block.fill.type', () => {
it('should create gradients when set to \'gradient\'', () => {
getFunnel().draw(getBasicData(), {
block: {
fill: {
type: 'gradient',
},
},
});
const id = getSvgId();
// 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, selectAll(`#funnel defs #${id}-gradient-0`).nodes().length);
assert.equal(`url(#${id}-gradient-0)`, select('#funnel path').attr('fill'));
});
it('should use solid fill when not set to \'gradient\'', () => {
getFunnel().draw(getBasicData());
// Check for valid hex string
assert.isTrue(/(^#[0-9A-F]{6}$)|(^#[0-9A-F]{3}$)/i.test(
select('#funnel path').attr('fill'),
));
});
});
describe('block.minHeight', () => {
it('should give each block the minimum height specified', () => {
getFunnel().draw([
{ label: 'A', value: 299 },
{ label: 'B', value: 1 },
], {
chart: {
height: 300,
},
block: {
dynamicHeight: true,
minHeight: 10,
},
});
const paths = selectAll('#funnel path').nodes();
assert.isAbove(parseFloat(getPathHeight(select(paths[0]))), 10);
assert.isAbove(parseFloat(getPathHeight(select(paths[1]))), 10);
});
it('should decrease the height of blocks above the minimum', () => {
getFunnel().draw([
{ label: 'A', value: 299 },
{ label: 'B', value: 1 },
], {
chart: {
height: 300,
},
block: {
dynamicHeight: true,
minHeight: 10,
},
});
const paths = selectAll('#funnel path').nodes();
assert.isBelow(parseFloat(getPathHeight(select(paths[0]))), 290);
});
});
describe('block.highlight', () => {
it('should change block color on hover', () => {
const event = document.createEvent('CustomEvent');
event.initCustomEvent('mouseover', false, false, null);
getFunnel().draw([
{ label: 'A', value: 1, backgroundColor: '#fff' },
], {
block: {
highlight: true,
},
});
select('#funnel path').node().dispatchEvent(event);
// #fff * -1/5 => #cccccc
assert.equal('#cccccc', select('#funnel path').attr('fill'));
});
});
describe('label.enabled', () => {
it('should render block labels when set to true', () => {
getFunnel().draw(getBasicData(), {
label: { enabled: true },
});
assert.equal(1, selectAll('#funnel text').size());
});
it('should not render block labels when set to false', () => {
getFunnel().draw(getBasicData(), {
label: { enabled: false },
});
assert.equal(0, selectAll('#funnel text').size());
});
});
describe('label.fontFamily', () => {
it('should set the label\'s font size to the specified amount', () => {
getFunnel().draw(getBasicData(), {
label: {
fontFamily: 'Open Sans',
},
});
assert.equal('Open Sans', select('#funnel text').attr('font-family'));
});
});
describe('label.fontSize', () => {
it('should set the label\'s font size to the specified amount', () => {
getFunnel().draw(getBasicData(), {
label: {
fontSize: '16px',
},
});
assert.equal('16px', select('#funnel text').attr('font-size'));
});
});
describe('label.fill', () => {
it('should set the label\'s fill color to the specified color', () => {
getFunnel().draw(getBasicData(), {
label: {
fill: '#777',
},
});
assert.isTrue(select('#funnel text').attr('fill').indexOf('#777') > -1);
});
});
describe('label.format', () => {
it('should parse a string template', () => {
getFunnel().draw(getBasicData(), {
label: {
format: '{l} {v} {f}',
},
});
assert.equal('Node 1000 1,000', select('#funnel text').text());
});
it('should create split multiple lines into multiple tspans', () => {
getFunnel().draw(getBasicData(), {
label: {
format: '{l}\n{v}',
},
});
const tspans = selectAll('#funnel text tspan').nodes();
assert.equal('Node', select(tspans[0]).text());
assert.equal('1000', select(tspans[1]).text());
});
it('should create position multiple lines in a vertically-centered manner', () => {
getFunnel().draw(getBasicData(), {
chart: {
height: 200,
},
label: {
format: '{l}\n{v}\n{f}',
},
});
const tspans = selectAll('#funnel text tspan').nodes();
assert.equal(-20, select(tspans[0]).attr('dy'));
assert.equal(20, select(tspans[1]).attr('dy'));
assert.equal(20, select(tspans[2]).attr('dy'));
});
it('should pass values to a supplied function', () => {
getFunnel().draw(getBasicData(), {
label: {
format: (label, value, formattedValue) => `${label}/${value}/${formattedValue}`,
},
});
assert.equal('Node/1000/null', select('#funnel text').text());
});
});
describe('tooltip.enabled', () => {
it('should render a simple tooltip box when hovering over a block', () => {
const event = document.createEvent('CustomEvent');
event.initCustomEvent('mousemove', false, false, null);
getFunnel().draw(getBasicData(), {
tooltip: {
enabled: true,
},
});
select('#funnel path').node().dispatchEvent(event);
assert.notEqual(null, select('#funnel .d3-funnel-tooltip').node());
});
it('should hide the tooltip on mouseout', () => {
const mouseMove = document.createEvent('CustomEvent');
const mouseOut = document.createEvent('CustomEvent');
mouseMove.initCustomEvent('mousemove', false, false, null);
mouseOut.initCustomEvent('mouseout', false, false, null);
getFunnel().draw(getBasicData(), {
tooltip: {
enabled: true,
},
});
select('#funnel path').node().dispatchEvent(mouseMove);
select('#funnel path').node().dispatchEvent(mouseOut);
assert.equal(null, select('#funnel .d3-funnel-tooltip').node());
});
});
describe('tooltip.format', () => {
it('should render tooltips according to the format provided', () => {
const event = document.createEvent('CustomEvent');
event.initCustomEvent('mousemove', false, false, null);
getFunnel().draw(getBasicData(), {
tooltip: {
enabled: true,
format: '{l} - {v}',
},
});
select('#funnel path').node().dispatchEvent(event);
assert.equal('Node - 1000', select('#funnel .d3-funnel-tooltip').text());
});
});
describe('events.click.block', () => {
it('should invoke the callback function with the correct data', () => {
const event = document.createEvent('CustomEvent');
event.initCustomEvent('click', false, false, null);
const proxy = sinon.fake();
getFunnel().draw(getBasicData(), {
events: {
click: {
block: (e, d) => {
proxy({
index: d.index,
node: d.node,
label: d.label.raw,
value: d.value,
});
},
},
},
});
select('#funnel path').node().dispatchEvent(event);
assert.isTrue(proxy.calledWith({
index: 0,
node: select('#funnel path').node(),
label: 'Node',
value: 1000,
}));
});
it('should not trigger errors when null', () => {
const event = document.createEvent('CustomEvent');
event.initCustomEvent('click', false, false, null);
getFunnel().draw(getBasicData(), {
events: {
click: {
block: null,
},
},
});
select('#funnel path').node().dispatchEvent(event);
});
it('should set the block style to `cursor: pointer` when non-null', () => {
getFunnel().draw(getBasicData(), {
events: {
click: {
block: () => {
},
},
},
});
assert.equal('pointer', select('#funnel path').style('cursor'));
});
});
});
});