feat: generated funnel chart
This commit is contained in:
parent
9053b01462
commit
fff25ecf4f
@ -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();
|
||||||
}
|
// }
|
||||||
});
|
// });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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) {
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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) {
|
||||||
|
|||||||
@ -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={}) {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user