作者 | SHA1 | 备注 | 提交日期 |
---|---|---|---|
|
999d5acc74 | style: linting fixes | 5 年前 |
|
ce67836445 | chore: revert frappe-charts.esm.js | 5 年前 |
|
2c35a2f35b
|
refactor: remove window assignment | 5 年前 |
|
f32cf4bde7 | feat: centered chart | 5 年前 |
|
b099ffe1c9 | feat: fixed funnel chart tooltip | 5 年前 |
|
fff25ecf4f | feat: generated funnel chart | 5 年前 |
|
9053b01462 | refactor: move endPoint creation to draw-utils | 5 年前 |
|
2451e58df9 | chore: enabled funnel option | 5 年前 |
|
edf6077eb4 | feat: kick start funnel Chart | 5 年前 |
@@ -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) { | ||||
@@ -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; | ||||
@@ -71,7 +99,7 @@ export function getSplineCurvePointsStr(xList, yList) { | |||||
angle: Math.atan2(lengthY, lengthX) | angle: Math.atan2(lengthY, lengthX) | ||||
}; | }; | ||||
}; | }; | ||||
let controlPoint = (current, previous, next, reverse) => { | let controlPoint = (current, previous, next, reverse) => { | ||||
let p = previous || current; | let p = previous || current; | ||||
let n = next || current; | let n = next || current; | ||||
@@ -82,18 +110,18 @@ export function getSplineCurvePointsStr(xList, yList) { | |||||
let y = current[1] + Math.sin(angle) * length; | let y = current[1] + Math.sin(angle) * length; | ||||
return [x, y]; | return [x, y]; | ||||
}; | }; | ||||
let bezierCommand = (point, i, a) => { | let bezierCommand = (point, i, a) => { | ||||
let cps = controlPoint(a[i - 1], a[i - 2], point); | let cps = controlPoint(a[i - 1], a[i - 2], point); | ||||
let cpe = controlPoint(point, a[i - 1], a[i + 1], true); | 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]}`; | return `C ${cps[0]},${cps[1]} ${cpe[0]},${cpe[1]} ${point[0]},${point[1]}`; | ||||
}; | }; | ||||
let pointStr = (points, command) => { | let pointStr = (points, command) => { | ||||
return points.reduce((acc, point, i, a) => i === 0 | return points.reduce((acc, point, i, a) => i === 0 | ||||
? `${point[0]},${point[1]}` | ? `${point[0]},${point[1]}` | ||||
: `${acc} ${command(point, i, a)}`, ''); | : `${acc} ${command(point, i, a)}`, ''); | ||||
}; | }; | ||||
return pointStr(points, bezierCommand); | return pointStr(points, bezierCommand); | ||||
} | } |
@@ -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, | ||||
@@ -322,7 +332,7 @@ function makeHoriLine(y, label, x1, x2, options={}) { | |||||
if(!options.stroke) options.stroke = BASE_LINE_COLOR; | if(!options.stroke) options.stroke = BASE_LINE_COLOR; | ||||
if(!options.lineType) options.lineType = ''; | if(!options.lineType) options.lineType = ''; | ||||
if (options.shortenNumbers) label = shortenLargeNumber(label); | if (options.shortenNumbers) label = shortenLargeNumber(label); | ||||
let className = 'line-horizontal ' + options.className + | let className = 'line-horizontal ' + options.className + | ||||
(options.lineType === "dashed" ? "dashed": ""); | (options.lineType === "dashed" ? "dashed": ""); | ||||
@@ -583,7 +593,7 @@ export function getPaths(xList, yList, color, options={}, meta={}) { | |||||
// Spline | // Spline | ||||
if (options.spline) | if (options.spline) | ||||
pointsStr = getSplineCurvePointsStr(xList, yList); | pointsStr = getSplineCurvePointsStr(xList, yList); | ||||
let path = makePath("M"+pointsStr, 'line-graph-path', color); | let path = makePath("M"+pointsStr, 'line-graph-path', color); | ||||
// HeatLine | // HeatLine | ||||