feat: generated funnel chart

This commit is contained in:
Shivam Mishra 2019-09-27 19:42:36 +05:30
parent 9053b01462
commit fff25ecf4f
5 changed files with 74 additions and 65 deletions

View File

@ -1,25 +1,40 @@
import AggregationChart from './AggregationChart'; import AggregationChart from './AggregationChart';
import { getOffset } from '../utils/dom'; import { getOffset } from '../utils/dom';
import { getComponent } from '../objects/ChartComponents'; import { getComponent } from '../objects/ChartComponents';
import { PERCENTAGE_BAR_DEFAULT_HEIGHT, PERCENTAGE_BAR_DEFAULT_DEPTH } from '../utils/constants'; import { getEndpointsForTrapezoid } from '../utils/draw-utils'
import { FUNNEL_CHART_BASE_WIDTH } from '../utils/constants';
export default class FunnelChart extends AggregationChart { export default class FunnelChart extends AggregationChart {
constructor(parent, args) { constructor(parent, args) {
super(parent, args); super(parent, args);
this.type = 'funnel'; this.type = 'funnel';
window.funnel = this;
this.setup(); this.setup();
} }
setMeasures(options) { calc() {
let m = this.measures; super.calc();
this.funnelOptions = options.funnelOptions || {}; let s = this.state;
// calculate width and height options
const baseWidth = FUNNEL_CHART_BASE_WIDTH;
const totalheight = (Math.sqrt(3) * baseWidth) / 2.0;
let opts = this.funnelOptions; // calculate total weightage
opts.height = opts.height || PERCENTAGE_BAR_DEFAULT_HEIGHT; // as height decreases, area decreases by the square of the reduction
// hence, compensating by squaring the index value
m.paddings.right = 30; const reducer = (accumulator, currentValue, index) => accumulator + currentValue * (Math.pow(index+1, 2));
m.legendHeight = 60; const weightage = s.sliceTotals.reduce(reducer, 0.0);
m.baseHeight = (opts.height + opts.depth * 0.5) * 8;
let slicePoints = [];
let startPoint = [[0, 0], [FUNNEL_CHART_BASE_WIDTH, 0]]
s.sliceTotals.forEach((d, i) => {
let height = totalheight * d * Math.pow(i+1, 2) / weightage;
let endPoint = getEndpointsForTrapezoid(startPoint, height);
slicePoints.push([startPoint, endPoint]);
startPoint = endPoint;
})
s.slicePoints = slicePoints;
} }
setupComponents() { setupComponents() {
@ -27,15 +42,11 @@ export default class FunnelChart extends AggregationChart {
let componentConfigs = [ let componentConfigs = [
[ [
'percentageBars', 'funnelSlices',
{ { },
barHeight: this.funnelOptions.height,
barDepth: this.funnelOptions.depth,
},
function() { function() {
return { return {
xPositions: s.xPositions, slicePoints: s.slicePoints,
widths: s.widths,
colors: this.colors colors: this.colors
}; };
}.bind(this) }.bind(this)
@ -49,43 +60,27 @@ export default class FunnelChart extends AggregationChart {
})); }));
} }
calc() {
super.calc();
let s = this.state;
s.xPositions = [];
s.widths = [];
let xPos = 0;
s.sliceTotals.map((value) => {
let width = this.width * value / s.grandTotal;
s.widths.push(width);
s.xPositions.push(xPos);
xPos += width;
});
}
makeDataByIndex() { } makeDataByIndex() { }
bindTooltip() { bindTooltip() {
let s = this.state; // let s = this.state;
this.container.addEventListener('mousemove', (e) => { // this.container.addEventListener('mousemove', (e) => {
let bars = this.components.get('percentageBars').store; // let bars = this.components.get('percentageBars').store;
let bar = e.target; // let bar = e.target;
if(bars.includes(bar)) { // if(bars.includes(bar)) {
let i = bars.indexOf(bar); // let i = bars.indexOf(bar);
let gOff = getOffset(this.container), pOff = getOffset(bar); // let gOff = getOffset(this.container), pOff = getOffset(bar);
let x = pOff.left - gOff.left + parseInt(bar.getAttribute('width'))/2; // let x = pOff.left - gOff.left + parseInt(bar.getAttribute('width'))/2;
let y = pOff.top - gOff.top; // let y = pOff.top - gOff.top;
let title = (this.formattedLabels && this.formattedLabels.length>0 // let title = (this.formattedLabels && this.formattedLabels.length>0
? this.formattedLabels[i] : this.state.labels[i]) + ': '; // ? this.formattedLabels[i] : this.state.labels[i]) + ': ';
let fraction = s.sliceTotals[i]/s.grandTotal; // let fraction = s.sliceTotals[i]/s.grandTotal;
this.tip.setValues(x, y, {name: title, value: (fraction*100).toFixed(1) + "%"}); // this.tip.setValues(x, y, {name: title, value: (fraction*100).toFixed(1) + "%"});
this.tip.showTip(); // this.tip.showTip();
} // }
}); // });
} }
} }

View File

@ -1,5 +1,5 @@
import { makeSVGGroup } from '../utils/draw'; import { makeSVGGroup } from '../utils/draw';
import { makeText, makePath, xLine, yLine, yMarker, yRegion, datasetBar, datasetDot, percentageBar, getPaths, heatSquare } from '../utils/draw'; import { makeText, makePath, xLine, yLine, yMarker, yRegion, datasetBar, datasetDot, percentageBar, getPaths, heatSquare, funnelSlice } from '../utils/draw';
import { equilizeNoOfElements } from '../utils/draw-utils'; import { equilizeNoOfElements } from '../utils/draw-utils';
import { translateHoriLine, translateVertLine, animateRegion, animateBar, import { translateHoriLine, translateVertLine, animateRegion, animateBar,
animateDot, animatePath, animatePathStr } from '../utils/animate'; animateDot, animatePath, animatePathStr } from '../utils/animate';
@ -114,14 +114,18 @@ let componentConfigs = {
if(newData) return []; if(newData) return [];
} }
}, },
funnelSlice: { funnelSlices: {
layerClass: 'funnel-slice', layerClass: 'funnel-slices',
makeElements(data) { makeElements(data) {
return data return data.slicePoints.map((p, i) => {
return funnelSlice('funnel-slice', p[0], p[1], data.colors[i]);
});
}, },
animateElements: {} animateElements(newData) {
} if(newData) return [];
}
},
yAxis: { yAxis: {
layerClass: 'y axis', layerClass: 'y axis',
makeElements(data) { makeElements(data) {

View File

@ -76,6 +76,8 @@ export const DOT_OVERLAY_SIZE_INCR = 4;
export const PERCENTAGE_BAR_DEFAULT_HEIGHT = 20; export const PERCENTAGE_BAR_DEFAULT_HEIGHT = 20;
export const PERCENTAGE_BAR_DEFAULT_DEPTH = 2; export const PERCENTAGE_BAR_DEFAULT_DEPTH = 2;
export const FUNNEL_CHART_BASE_WIDTH = 200
// Fixed 5-color theme, // Fixed 5-color theme,
// More colors are difficult to parse visually // More colors are difficult to parse visually
export const HEATMAP_DISTRIBUTION_SIZE = 5; export const HEATMAP_DISTRIBUTION_SIZE = 5;
@ -99,7 +101,8 @@ export const DEFAULT_COLORS = {
pie: DEFAULT_CHART_COLORS, pie: DEFAULT_CHART_COLORS,
percentage: DEFAULT_CHART_COLORS, percentage: DEFAULT_CHART_COLORS,
heatmap: HEATMAP_COLORS_GREEN, heatmap: HEATMAP_COLORS_GREEN,
donut: DEFAULT_CHART_COLORS donut: DEFAULT_CHART_COLORS,
funnel: DEFAULT_CHART_COLORS,
}; };
// Universal constants // Universal constants

View File

@ -25,14 +25,14 @@ export function equilizeNoOfElements(array1, array2,
return [array1, array2]; return [array1, array2];
} }
export function getEndpointsForTrapezoid(startPositions) { export function getEndpointsForTrapezoid(startPositions, height) {
const endPosition = [] const endPosition = [];
let [point_a, point_b] = startPositions let [point_a, point_b] = startPositions;
// For an equilateral triangle, the angles are always 60 deg. // For an equilateral triangle, the angles are always 60 deg.
// The end points on the polygons can be created using the following formula // The end points on the polygons can be created using the following formula
// //
// end_point_x = start_x +/- height * 1/2 // end_point_x = start_x +/- height * 1/√3
// end_point_y = start_y + height // end_point_y = start_y + height
// //
// b // b
@ -43,13 +43,14 @@ export function getEndpointsForTrapezoid(startPositions) {
// \ | / // \ | /
// \|____________________/ // \|____________________/
// //
// b = h * cos(60 deg) // b = h * tan(30 deg)
// //
endPosition[0] = [point_a[0] + height * 0.5, point_a[1] + height] let multiplicationFactor = 1.0/Math.sqrt(3);
endPosition[1] = [point_b[0] - height * 0.5, point_b[1] + height] endPosition[0] = [point_a[0] + height * multiplicationFactor, point_a[1] + height];
endPosition[1] = [point_b[0] - height * multiplicationFactor, point_b[1] + height];
return endPosition return endPosition;
} }
export function truncateString(txt, len) { export function truncateString(txt, len) {

View File

@ -190,8 +190,14 @@ export function percentageBar(x, y, width, height,
return createSVG("rect", args); return createSVG("rect", args);
} }
export function funnelSlice(className, startPositions, endPosition, height, fill='none') { export function funnelSlice(className, start, end, fill='none') {
return createSVG("polygon") const points = `${start[0].join()} ${start[1].join()} ${end[1].join()} ${end[0].join()}`
let args = {
className: 'funnel-slice',
points: points,
fill: fill
}
return createSVG("polygon", args)
} }
export function heatSquare(className, x, y, size, fill='none', data={}) { export function heatSquare(className, x, y, size, fill='none', data={}) {