diff --git a/dist/frappe-charts.esm.js b/dist/frappe-charts.esm.js index 7947013..929e772 100644 --- a/dist/frappe-charts.esm.js +++ b/dist/frappe-charts.esm.js @@ -167,7 +167,8 @@ 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 @@ -299,10 +300,6 @@ class SvgTip { } } -/** - * Returns the value of a number upto 2 decimal places. - * @param {Number} d Any number - */ function floatTwo(d) { return parseFloat(d.toFixed(2)); } @@ -382,6 +379,34 @@ function equilizeNoOfElements(array1, array2, return [array1, array2]; } +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/√3 + // end_point_y = start_y + height + // + // b + // _______________________________ + // \ |_| / + // \ | / + // \ | h / + // \ | / + // \|____________________/ + // + // b = h * tan(30 deg) + // + + 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; +} + function truncateString(txt, len) { if (!txt) { return; @@ -428,7 +453,7 @@ function getSplineCurvePointsStr(xList, yList) { angle: Math.atan2(lengthY, lengthX) }; }; - + let controlPoint = (current, previous, next, reverse) => { let p = previous || current; let n = next || current; @@ -439,19 +464,19 @@ function getSplineCurvePointsStr(xList, yList) { let y = current[1] + Math.sin(angle) * length; return [x, y]; }; - + let bezierCommand = (point, i, a) => { let cps = controlPoint(a[i - 1], a[i - 2], point); let cpe = controlPoint(point, a[i - 1], a[i + 1], true); return `C ${cps[0]},${cps[1]} ${cpe[0]},${cpe[1]} ${point[0]},${point[1]}`; }; - + let pointStr = (points, command) => { return points.reduce((acc, point, i, a) => i === 0 ? `${point[0]},${point[1]}` : `${acc} ${command(point, i, a)}`, ''); }; - + return pointStr(points, bezierCommand); } @@ -682,6 +707,16 @@ function percentageBar(x, y, width, height, return createSVG("rect", args); } +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) +} + function heatSquare(className, x, y, size, fill='none', data={}) { let args = { className: className, @@ -814,7 +849,7 @@ function makeHoriLine(y, label, x1, x2, options={}) { if(!options.stroke) options.stroke = BASE_LINE_COLOR; if(!options.lineType) options.lineType = ''; if (options.shortenNumbers) label = shortenLargeNumber(label); - + let className = 'line-horizontal ' + options.className + (options.lineType === "dashed" ? "dashed": ""); @@ -1075,7 +1110,7 @@ function getPaths(xList, yList, color, options={}, meta={}) { // Spline if (options.spline) pointsStr = getSplineCurvePointsStr(xList, yList); - + let path = makePath("M"+pointsStr, 'line-graph-path', color); // HeatLine @@ -2043,6 +2078,18 @@ let componentConfigs = { if(newData) return []; } }, + funnelSlices: { + layerClass: 'funnel-slices', + makeElements(data) { + return data.slicePoints.map((p, i) => { + return funnelSlice('funnel-slice', p[0], p[1], data.colors[i]); + }); + }, + + animateElements(newData) { + if(newData) return []; + } + }, yAxis: { layerClass: 'y axis', makeElements(data) { @@ -3972,7 +4019,90 @@ class DonutChart extends AggregationChart { } } -// import MultiAxisChart from './charts/MultiAxisChart'; +class FunnelChart extends AggregationChart { + constructor(parent, args) { + super(parent, args); + this.type = 'funnel'; + window.funnel = this; + this.setup(); + } + + calc() { + super.calc(); + let s = this.state; + + // calculate width and height options + const totalheight = this.height * 0.9; + const baseWidth = (2 * totalheight) / Math.sqrt(3); + + + const reducer = (accumulator, currentValue, index) => accumulator + currentValue; + const weightage = s.sliceTotals.reduce(reducer, 0.0); + + const center_x_offset = this.center.x - baseWidth / 2; + const center_y_offset = this.center.y - totalheight / 2; + + let slicePoints = []; + let startPoint = [[center_x_offset, center_y_offset], [center_x_offset + baseWidth, center_y_offset]]; + s.sliceTotals.forEach((d, i) => { + let height = totalheight * d / weightage; + let endPoint = getEndpointsForTrapezoid(startPoint, height); + slicePoints.push([startPoint, endPoint]); + startPoint = endPoint; + }); + s.slicePoints = slicePoints; + } + + setupComponents() { + let s = this.state; + + let componentConfigs = [ + [ + 'funnelSlices', + { }, + function() { + return { + slicePoints: s.slicePoints, + colors: this.colors + }; + }.bind(this) + ] + ]; + + this.components = new Map(componentConfigs + .map(args => { + let component = getComponent(...args); + return [args[0], component]; + })); + } + + makeDataByIndex() { } + + bindTooltip() { + function getPolygonWidth(slice) { + const points = slice.points; + return points[1].x - points[0].x + } + + this.container.addEventListener('mousemove', (e) => { + let slices = this.components.get('funnelSlices').store; + let slice = e.target; + if(slices.includes(slice)) { + let i = slices.indexOf(slice); + + let gOff = getOffset(this.container), pOff = getOffset(slice); + let x = pOff.left - gOff.left + getPolygonWidth(slice)/2; + let y = pOff.top - gOff.top; + let title = (this.formatted_labels && this.formatted_labels.length > 0 + ? this.formatted_labels[i] : this.state.labels[i]) + ': '; + let percent = (this.state.sliceTotals[i] * 100 / this.state.grandTotal).toFixed(1); + this.tip.setValues(x, y, {name: title, value: percent + "%"}); + this.tip.showTip(); + } + }); + } +} + const chartTypes = { bar: AxisChart, line: AxisChart, @@ -3981,6 +4111,7 @@ const chartTypes = { heatmap: Heatmap, pie: PieChart, donut: DonutChart, + funnel: FunnelChart, }; function getChartByType(chartType = 'line', parent, options) { diff --git a/docs/index.html b/docs/index.html index 1f5ba76..a8f0421 100644 --- a/docs/index.html +++ b/docs/index.html @@ -83,6 +83,7 @@ redirect_to: "https://frappe.io/charts" +