Compare commits
9 Commits
master
...
funnel-cha
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
999d5acc74 | ||
|
|
ce67836445 | ||
|
|
2c35a2f35b | ||
|
|
f32cf4bde7 | ||
|
|
b099ffe1c9 | ||
|
|
fff25ecf4f | ||
|
|
9053b01462 | ||
|
|
2451e58df9 | ||
|
|
edf6077eb4 |
@ -83,6 +83,7 @@ redirect_to: "https://frappe.io/charts"
|
|||||||
<button type="button" class="btn btn-sm btn-secondary active" data-type='axis-mixed'>Mixed</button>
|
<button type="button" class="btn btn-sm btn-secondary active" data-type='axis-mixed'>Mixed</button>
|
||||||
<button type="button" class="btn btn-sm btn-secondary" data-type='pie'>Pie Chart</button>
|
<button type="button" class="btn btn-sm btn-secondary" data-type='pie'>Pie Chart</button>
|
||||||
<button type="button" class="btn btn-sm btn-secondary" data-type='donut'>Donut Chart</button>
|
<button type="button" class="btn btn-sm btn-secondary" data-type='donut'>Donut Chart</button>
|
||||||
|
<button type="button" class="btn btn-sm btn-secondary" data-type='funnel'>Funnel Chart</button>
|
||||||
<button type="button" class="btn btn-sm btn-secondary" data-type='percentage'>Percentage Chart</button>
|
<button type="button" class="btn btn-sm btn-secondary" data-type='percentage'>Percentage Chart</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="btn-group export-buttons margin-top mx-auto" role="group">
|
<div class="btn-group export-buttons margin-top mx-auto" role="group">
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import PieChart from './charts/PieChart';
|
|||||||
import Heatmap from './charts/Heatmap';
|
import Heatmap from './charts/Heatmap';
|
||||||
import AxisChart from './charts/AxisChart';
|
import AxisChart from './charts/AxisChart';
|
||||||
import DonutChart from './charts/DonutChart';
|
import DonutChart from './charts/DonutChart';
|
||||||
|
import FunnelChart from './charts/FunnelChart';
|
||||||
|
|
||||||
const chartTypes = {
|
const chartTypes = {
|
||||||
bar: AxisChart,
|
bar: AxisChart,
|
||||||
@ -15,6 +16,7 @@ const chartTypes = {
|
|||||||
heatmap: Heatmap,
|
heatmap: Heatmap,
|
||||||
pie: PieChart,
|
pie: PieChart,
|
||||||
donut: DonutChart,
|
donut: DonutChart,
|
||||||
|
funnel: FunnelChart,
|
||||||
};
|
};
|
||||||
|
|
||||||
function getChartByType(chartType = 'line', parent, options) {
|
function getChartByType(chartType = 'line', parent, options) {
|
||||||
|
|||||||
85
src/js/charts/FunnelChart.js
Normal file
85
src/js/charts/FunnelChart.js
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
import AggregationChart from './AggregationChart';
|
||||||
|
import { getOffset } from '../utils/dom';
|
||||||
|
import { getComponent } from '../objects/ChartComponents';
|
||||||
|
import { getEndpointsForTrapezoid } from '../utils/draw-utils';
|
||||||
|
|
||||||
|
export default class FunnelChart extends AggregationChart {
|
||||||
|
constructor(parent, args) {
|
||||||
|
super(parent, args);
|
||||||
|
this.type = 'funnel';
|
||||||
|
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) => 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 => {
|
||||||
|
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];
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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,6 +114,18 @@ let componentConfigs = {
|
|||||||
if(newData) return [];
|
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: {
|
yAxis: {
|
||||||
layerClass: 'y axis',
|
layerClass: 'y axis',
|
||||||
makeElements(data) {
|
makeElements(data) {
|
||||||
|
|||||||
@ -99,7 +99,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,6 +25,34 @@ export function equilizeNoOfElements(array1, array2,
|
|||||||
return [array1, array2];
|
return [array1, array2];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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/√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;
|
||||||
|
}
|
||||||
|
|
||||||
export function truncateString(txt, len) {
|
export function truncateString(txt, len) {
|
||||||
if (!txt) {
|
if (!txt) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@ -190,6 +190,16 @@ export function percentageBar(x, y, width, height,
|
|||||||
return createSVG("rect", args);
|
return createSVG("rect", args);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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={}) {
|
export function heatSquare(className, x, y, size, fill='none', data={}) {
|
||||||
let args = {
|
let args = {
|
||||||
className: className,
|
className: className,
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user