diff --git a/src/js/charts/FunnelChart.js b/src/js/charts/FunnelChart.js index 509b258..feb37b6 100644 --- a/src/js/charts/FunnelChart.js +++ b/src/js/charts/FunnelChart.js @@ -1,25 +1,40 @@ import AggregationChart from './AggregationChart'; import { getOffset } from '../utils/dom'; 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 { constructor(parent, args) { super(parent, args); this.type = 'funnel'; + window.funnel = this; this.setup(); } - setMeasures(options) { - let m = this.measures; - this.funnelOptions = options.funnelOptions || {}; + calc() { + super.calc(); + 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; - opts.height = opts.height || PERCENTAGE_BAR_DEFAULT_HEIGHT; + // calculate total weightage + // as height decreases, area decreases by the square of the reduction + // hence, compensating by squaring the index value - m.paddings.right = 30; - m.legendHeight = 60; - m.baseHeight = (opts.height + opts.depth * 0.5) * 8; + const reducer = (accumulator, currentValue, index) => accumulator + currentValue * (Math.pow(index+1, 2)); + const weightage = s.sliceTotals.reduce(reducer, 0.0); + + 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() { @@ -27,15 +42,11 @@ export default class FunnelChart extends AggregationChart { let componentConfigs = [ [ - 'percentageBars', - { - barHeight: this.funnelOptions.height, - barDepth: this.funnelOptions.depth, - }, + 'funnelSlices', + { }, function() { return { - xPositions: s.xPositions, - widths: s.widths, + slicePoints: s.slicePoints, colors: this.colors }; }.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() { } bindTooltip() { - let s = this.state; - this.container.addEventListener('mousemove', (e) => { - let bars = this.components.get('percentageBars').store; - let bar = e.target; - if(bars.includes(bar)) { + // let s = this.state; + // this.container.addEventListener('mousemove', (e) => { + // let bars = this.components.get('percentageBars').store; + // let bar = e.target; + // if(bars.includes(bar)) { - let i = bars.indexOf(bar); - let gOff = getOffset(this.container), pOff = getOffset(bar); + // let i = bars.indexOf(bar); + // let gOff = getOffset(this.container), pOff = getOffset(bar); - let x = pOff.left - gOff.left + parseInt(bar.getAttribute('width'))/2; - let y = pOff.top - gOff.top; - let title = (this.formattedLabels && this.formattedLabels.length>0 - ? this.formattedLabels[i] : this.state.labels[i]) + ': '; - let fraction = s.sliceTotals[i]/s.grandTotal; + // let x = pOff.left - gOff.left + parseInt(bar.getAttribute('width'))/2; + // let y = pOff.top - gOff.top; + // let title = (this.formattedLabels && this.formattedLabels.length>0 + // ? this.formattedLabels[i] : this.state.labels[i]) + ': '; + // let fraction = s.sliceTotals[i]/s.grandTotal; - this.tip.setValues(x, y, {name: title, value: (fraction*100).toFixed(1) + "%"}); - this.tip.showTip(); - } - }); + // this.tip.setValues(x, y, {name: title, value: (fraction*100).toFixed(1) + "%"}); + // this.tip.showTip(); + // } + // }); } } diff --git a/src/js/objects/ChartComponents.js b/src/js/objects/ChartComponents.js index cdb662f..8d2edb4 100644 --- a/src/js/objects/ChartComponents.js +++ b/src/js/objects/ChartComponents.js @@ -1,5 +1,5 @@ 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 { translateHoriLine, translateVertLine, animateRegion, animateBar, animateDot, animatePath, animatePathStr } from '../utils/animate'; @@ -114,14 +114,18 @@ let componentConfigs = { if(newData) return []; } }, - funnelSlice: { - layerClass: 'funnel-slice', + funnelSlices: { + layerClass: 'funnel-slices', 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: { layerClass: 'y axis', makeElements(data) { diff --git a/src/js/utils/constants.js b/src/js/utils/constants.js index 041cfaa..903194a 100644 --- a/src/js/utils/constants.js +++ b/src/js/utils/constants.js @@ -76,6 +76,8 @@ export const DOT_OVERLAY_SIZE_INCR = 4; export const PERCENTAGE_BAR_DEFAULT_HEIGHT = 20; export const PERCENTAGE_BAR_DEFAULT_DEPTH = 2; +export const FUNNEL_CHART_BASE_WIDTH = 200 + // Fixed 5-color theme, // More colors are difficult to parse visually export const HEATMAP_DISTRIBUTION_SIZE = 5; @@ -99,7 +101,8 @@ export const DEFAULT_COLORS = { pie: DEFAULT_CHART_COLORS, percentage: DEFAULT_CHART_COLORS, heatmap: HEATMAP_COLORS_GREEN, - donut: DEFAULT_CHART_COLORS + donut: DEFAULT_CHART_COLORS, + funnel: DEFAULT_CHART_COLORS, }; // Universal constants diff --git a/src/js/utils/draw-utils.js b/src/js/utils/draw-utils.js index 23577c0..70ec65e 100644 --- a/src/js/utils/draw-utils.js +++ b/src/js/utils/draw-utils.js @@ -25,14 +25,14 @@ export function equilizeNoOfElements(array1, array2, return [array1, array2]; } -export function getEndpointsForTrapezoid(startPositions) { - const endPosition = [] - let [point_a, point_b] = startPositions +export function getEndpointsForTrapezoid(startPositions, height) { + const endPosition = []; + let [point_a, point_b] = startPositions; // For an equilateral triangle, the angles are always 60 deg. // 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 // // 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] - endPosition[1] = [point_b[0] - height * 0.5, point_b[1] + height] + let multiplicationFactor = 1.0/Math.sqrt(3); + 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) { diff --git a/src/js/utils/draw.js b/src/js/utils/draw.js index 32cfeda..488f416 100644 --- a/src/js/utils/draw.js +++ b/src/js/utils/draw.js @@ -190,8 +190,14 @@ export function percentageBar(x, y, width, height, return createSVG("rect", args); } -export function funnelSlice(className, startPositions, endPosition, height, fill='none') { - return createSVG("polygon") +export function funnelSlice(className, start, end, fill='none') { + 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={}) {