@@ -166,7 +166,8 @@ const DEFAULT_COLORS = { | |||||
line: DEFAULT_CHART_COLORS, | line: DEFAULT_CHART_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 | |||||
}; | }; | ||||
// Universal constants | // Universal constants | ||||
@@ -516,13 +517,14 @@ function makeSVGGroup(className, transform='', parent=undefined) { | |||||
function makePath(pathStr, className='', stroke='none', fill='none') { | |||||
function makePath(pathStr, className='', stroke='none', fill='none', strokeWidth=0) { | |||||
return createSVG('path', { | return createSVG('path', { | ||||
className: className, | className: className, | ||||
d: pathStr, | d: pathStr, | ||||
styles: { | styles: { | ||||
stroke: stroke, | stroke: stroke, | ||||
fill: fill | |||||
fill: fill, | |||||
'stroke-width': strokeWidth | |||||
} | } | ||||
}); | }); | ||||
} | } | ||||
@@ -537,6 +539,15 @@ function makeArcPathStr(startPosition, endPosition, center, radius, clockWise=1) | |||||
${arcEndX} ${arcEndY} z`; | ${arcEndX} ${arcEndY} z`; | ||||
} | } | ||||
function makeArcStrokePathStr(startPosition, endPosition, center, radius, clockWise=1){ | |||||
let [arcStartX, arcStartY] = [center.x + startPosition.x, center.y + startPosition.y]; | |||||
let [arcEndX, arcEndY] = [center.x + endPosition.x, center.y + endPosition.y]; | |||||
return `M${arcStartX} ${arcStartY} | |||||
A ${radius} ${radius} 0 0 ${clockWise ? 1 : 0} | |||||
${arcEndX} ${arcEndY}`; | |||||
} | |||||
function makeGradient(svgDefElem, color, lighter = false) { | function makeGradient(svgDefElem, color, lighter = false) { | ||||
let gradientId ='path-fill-gradient' + '-' + color + '-' +(lighter ? 'lighter' : 'default'); | let gradientId ='path-fill-gradient' + '-' + color + '-' +(lighter ? 'lighter' : 'default'); | ||||
let gradientDef = renderVerticalGradient(svgDefElem, gradientId); | let gradientDef = renderVerticalGradient(svgDefElem, gradientId); | ||||
@@ -1878,6 +1889,20 @@ class ChartComponent { | |||||
} | } | ||||
let componentConfigs = { | let componentConfigs = { | ||||
donutSlices: { | |||||
layerClass: 'donut-slices', | |||||
makeElements(data) { | |||||
return data.sliceStrings.map((s, i) => { | |||||
let slice = makePath(s, 'donut-path', data.colors[i], 'none', data.strokeWidth); | |||||
slice.style.transition = 'transform .3s;'; | |||||
return slice; | |||||
}); | |||||
}, | |||||
animateElements(newData) { | |||||
return this.store.map((slice, i) => animatePathStr(slice, newData.sliceStrings[i])); | |||||
}, | |||||
}, | |||||
pieSlices: { | pieSlices: { | ||||
layerClass: 'pie-slices', | layerClass: 'pie-slices', | ||||
makeElements(data) { | makeElements(data) { | ||||
@@ -3677,6 +3702,152 @@ class AxisChart extends BaseChart { | |||||
// removeDataPoint(index = 0) {} | // removeDataPoint(index = 0) {} | ||||
} | } | ||||
class DonutChart extends AggregationChart { | |||||
constructor(parent, args) { | |||||
super(parent, args); | |||||
this.type = 'donut'; | |||||
this.initTimeout = 0; | |||||
this.init = 1; | |||||
this.setup(); | |||||
} | |||||
configure(args) { | |||||
super.configure(args); | |||||
this.mouseMove = this.mouseMove.bind(this); | |||||
this.mouseLeave = this.mouseLeave.bind(this); | |||||
this.hoverRadio = args.hoverRadio || 0.1; | |||||
this.config.startAngle = args.startAngle || 0; | |||||
this.clockWise = args.clockWise || false; | |||||
this.strokeWidth = args.strokeWidth || 30; | |||||
} | |||||
calc() { | |||||
super.calc(); | |||||
let s = this.state; | |||||
this.radius = (this.height > this.width ? this.center.x - (this.strokeWidth / 2) : this.center.y - (this.strokeWidth / 2)); | |||||
const { radius, clockWise } = this; | |||||
const prevSlicesProperties = s.slicesProperties || []; | |||||
s.sliceStrings = []; | |||||
s.slicesProperties = []; | |||||
let curAngle = 180 - this.config.startAngle; | |||||
s.sliceTotals.map((total, i) => { | |||||
const startAngle = curAngle; | |||||
const originDiffAngle = (total / s.grandTotal) * FULL_ANGLE; | |||||
const diffAngle = clockWise ? -originDiffAngle : originDiffAngle; | |||||
const endAngle = curAngle = curAngle + diffAngle; | |||||
const startPosition = getPositionByAngle(startAngle, radius); | |||||
const endPosition = getPositionByAngle(endAngle, radius); | |||||
const prevProperty = this.init && prevSlicesProperties[i]; | |||||
let curStart,curEnd; | |||||
if(this.init) { | |||||
curStart = prevProperty ? prevProperty.startPosition : startPosition; | |||||
curEnd = prevProperty ? prevProperty.endPosition : startPosition; | |||||
} else { | |||||
curStart = startPosition; | |||||
curEnd = endPosition; | |||||
} | |||||
const curPath = makeArcStrokePathStr(curStart, curEnd, this.center, this.radius, this.clockWise); | |||||
s.sliceStrings.push(curPath); | |||||
s.slicesProperties.push({ | |||||
startPosition, | |||||
endPosition, | |||||
value: total, | |||||
total: s.grandTotal, | |||||
startAngle, | |||||
endAngle, | |||||
angle: diffAngle | |||||
}); | |||||
}); | |||||
this.init = 0; | |||||
} | |||||
setupComponents() { | |||||
let s = this.state; | |||||
let componentConfigs = [ | |||||
[ | |||||
'donutSlices', | |||||
{ }, | |||||
function() { | |||||
return { | |||||
sliceStrings: s.sliceStrings, | |||||
colors: this.colors, | |||||
strokeWidth: this.strokeWidth, | |||||
}; | |||||
}.bind(this) | |||||
] | |||||
]; | |||||
this.components = new Map(componentConfigs | |||||
.map(args => { | |||||
let component = getComponent(...args); | |||||
return [args[0], component]; | |||||
})); | |||||
} | |||||
calTranslateByAngle(property){ | |||||
const{radius,hoverRadio} = this; | |||||
const position = getPositionByAngle(property.startAngle+(property.angle / 2),radius); | |||||
return `translate3d(${(position.x) * hoverRadio}px,${(position.y) * hoverRadio}px,0)`; | |||||
} | |||||
hoverSlice(path,i,flag,e){ | |||||
if(!path) return; | |||||
const color = this.colors[i]; | |||||
if(flag) { | |||||
transform(path, this.calTranslateByAngle(this.state.slicesProperties[i])); | |||||
path.style.stroke = lightenDarkenColor(color, 50); | |||||
let g_off = getOffset(this.svg); | |||||
let x = e.pageX - g_off.left + 10; | |||||
let y = e.pageY - g_off.top - 10; | |||||
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(); | |||||
} else { | |||||
transform(path,'translate3d(0,0,0)'); | |||||
this.tip.hideTip(); | |||||
path.style.stroke = color; | |||||
} | |||||
} | |||||
bindTooltip() { | |||||
this.container.addEventListener('mousemove', this.mouseMove); | |||||
this.container.addEventListener('mouseleave', this.mouseLeave); | |||||
} | |||||
mouseMove(e){ | |||||
const target = e.target; | |||||
let slices = this.components.get('donutSlices').store; | |||||
let prevIndex = this.curActiveSliceIndex; | |||||
let prevAcitve = this.curActiveSlice; | |||||
if(slices.includes(target)) { | |||||
let i = slices.indexOf(target); | |||||
this.hoverSlice(prevAcitve, prevIndex,false); | |||||
this.curActiveSlice = target; | |||||
this.curActiveSliceIndex = i; | |||||
this.hoverSlice(target, i, true, e); | |||||
} else { | |||||
this.mouseLeave(); | |||||
} | |||||
} | |||||
mouseLeave(){ | |||||
this.hoverSlice(this.curActiveSlice,this.curActiveSliceIndex,false); | |||||
} | |||||
} | |||||
// import MultiAxisChart from './charts/MultiAxisChart'; | // import MultiAxisChart from './charts/MultiAxisChart'; | ||||
const chartTypes = { | const chartTypes = { | ||||
bar: AxisChart, | bar: AxisChart, | ||||
@@ -3684,7 +3855,8 @@ const chartTypes = { | |||||
// multiaxis: MultiAxisChart, | // multiaxis: MultiAxisChart, | ||||
percentage: PercentageChart, | percentage: PercentageChart, | ||||
heatmap: Heatmap, | heatmap: Heatmap, | ||||
pie: PieChart | |||||
pie: PieChart, | |||||
donut: DonutChart, | |||||
}; | }; | ||||
function getChartByType(chartType = 'line', parent, options) { | function getChartByType(chartType = 'line', parent, options) { | ||||
@@ -46,6 +46,12 @@ var HEATMAP_COLORS_YELLOW = ['#ebedf0', '#fdf436', '#ffc700', '#ff9100', '#06001 | |||||
// Universal constants | // Universal constants | ||||
/** | |||||
* Returns the value of a number upto 2 decimal places. | |||||
* @param {Number} d Any number | |||||
*/ | |||||
/** | /** | ||||
* Returns whether or not two given arrays are equal. | * Returns whether or not two given arrays are equal. | ||||
* @param {Array} arr1 First array | * @param {Array} arr1 First array | ||||
@@ -114,7 +120,6 @@ var MONTH_NAMES_SHORT = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", | |||||
// https://stackoverflow.com/a/11252167/6495043 | |||||
function clone(date) { | function clone(date) { | ||||
@@ -155,6 +160,8 @@ function addDays(date, numberOfDays) { | |||||
date.setDate(date.getDate() + numberOfDays); | date.setDate(date.getDate() + numberOfDays); | ||||
} | } | ||||
// Composite Chart | |||||
// ================================================================================ | |||||
var reportCountList = [152, 222, 199, 287, 534, 709, 1179, 1256, 1632, 1856, 1850]; | var reportCountList = [152, 222, 199, 287, 534, 709, 1179, 1256, 1632, 1856, 1850]; | ||||
var lineCompositeData = { | var lineCompositeData = { | ||||
@@ -324,6 +331,9 @@ var demoConfig = { | |||||
} | } | ||||
}; | }; | ||||
// import { lineComposite, barComposite } from './demoConfig'; | |||||
// ================================================================================ | |||||
var Chart = frappe.Chart; // eslint-disable-line no-undef | var Chart = frappe.Chart; // eslint-disable-line no-undef | ||||
var lc = demoConfig.lineComposite; | var lc = demoConfig.lineComposite; | ||||
@@ -670,3 +680,4 @@ document.querySelector('.export-heatmap').addEventListener('click', function () | |||||
}); | }); | ||||
}()); | }()); | ||||
//# sourceMappingURL=index.min.js.map |
@@ -65,7 +65,7 @@ redirect_to: "https://frappe.io/charts" | |||||
}, | }, | ||||
title: "My Awesome Chart", | title: "My Awesome Chart", | ||||
type: 'axis-mixed', // or 'bar', 'line', 'pie', 'percentage' | |||||
type: 'axis-mixed', // or 'bar', 'line', 'pie', 'percentage', 'donut' | |||||
height: 300, | height: 300, | ||||
colors: ['purple', '#ffa3ef', 'light-blue'], | colors: ['purple', '#ffa3ef', 'light-blue'], | ||||
@@ -82,6 +82,7 @@ redirect_to: "https://frappe.io/charts" | |||||
<div class="btn-group aggr-type-buttons margin-top mx-auto" role="group"> | <div class="btn-group aggr-type-buttons margin-top mx-auto" role="group"> | ||||
<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='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"> | ||||
@@ -233,7 +234,7 @@ redirect_to: "https://frappe.io/charts" | |||||
// default: 0 | // default: 0 | ||||
}, | }, | ||||
// Pie/Percentage charts | |||||
// Pie/Percentage/Donut charts | |||||
maxLegendPoints: 6, // default: 20 | maxLegendPoints: 6, // default: 20 | ||||
maxSlices: 10, // default: 20 | maxSlices: 10, // default: 20 | ||||
@@ -5,6 +5,7 @@ import PercentageChart from './charts/PercentageChart'; | |||||
import PieChart from './charts/PieChart'; | 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'; | |||||
const chartTypes = { | const chartTypes = { | ||||
bar: AxisChart, | bar: AxisChart, | ||||
@@ -12,7 +13,8 @@ const chartTypes = { | |||||
// multiaxis: MultiAxisChart, | // multiaxis: MultiAxisChart, | ||||
percentage: PercentageChart, | percentage: PercentageChart, | ||||
heatmap: Heatmap, | heatmap: Heatmap, | ||||
pie: PieChart | |||||
pie: PieChart, | |||||
donut: DonutChart, | |||||
}; | }; | ||||
function getChartByType(chartType = 'line', parent, options) { | function getChartByType(chartType = 'line', parent, options) { | ||||
@@ -0,0 +1,154 @@ | |||||
import AggregationChart from './AggregationChart'; | |||||
import { getComponent } from '../objects/ChartComponents'; | |||||
import { getOffset } from '../utils/dom'; | |||||
import { getPositionByAngle } from '../utils/helpers'; | |||||
import { makeArcStrokePathStr } from '../utils/draw'; | |||||
import { lightenDarkenColor } from '../utils/colors'; | |||||
import { transform } from '../utils/animation'; | |||||
import { FULL_ANGLE } from '../utils/constants'; | |||||
export default class DonutChart extends AggregationChart { | |||||
constructor(parent, args) { | |||||
super(parent, args); | |||||
this.type = 'donut'; | |||||
this.initTimeout = 0; | |||||
this.init = 1; | |||||
this.setup(); | |||||
} | |||||
configure(args) { | |||||
super.configure(args); | |||||
this.mouseMove = this.mouseMove.bind(this); | |||||
this.mouseLeave = this.mouseLeave.bind(this); | |||||
this.hoverRadio = args.hoverRadio || 0.1; | |||||
this.config.startAngle = args.startAngle || 0; | |||||
this.clockWise = args.clockWise || false; | |||||
this.strokeWidth = args.strokeWidth || 30; | |||||
} | |||||
calc() { | |||||
super.calc(); | |||||
let s = this.state; | |||||
this.radius = (this.height > this.width ? this.center.x - (this.strokeWidth / 2) : this.center.y - (this.strokeWidth / 2)); | |||||
const { radius, clockWise } = this; | |||||
const prevSlicesProperties = s.slicesProperties || []; | |||||
s.sliceStrings = []; | |||||
s.slicesProperties = []; | |||||
let curAngle = 180 - this.config.startAngle; | |||||
s.sliceTotals.map((total, i) => { | |||||
const startAngle = curAngle; | |||||
const originDiffAngle = (total / s.grandTotal) * FULL_ANGLE; | |||||
const diffAngle = clockWise ? -originDiffAngle : originDiffAngle; | |||||
const endAngle = curAngle = curAngle + diffAngle; | |||||
const startPosition = getPositionByAngle(startAngle, radius); | |||||
const endPosition = getPositionByAngle(endAngle, radius); | |||||
const prevProperty = this.init && prevSlicesProperties[i]; | |||||
let curStart,curEnd; | |||||
if(this.init) { | |||||
curStart = prevProperty ? prevProperty.startPosition : startPosition; | |||||
curEnd = prevProperty ? prevProperty.endPosition : startPosition; | |||||
} else { | |||||
curStart = startPosition; | |||||
curEnd = endPosition; | |||||
} | |||||
const curPath = makeArcStrokePathStr(curStart, curEnd, this.center, this.radius, this.clockWise); | |||||
s.sliceStrings.push(curPath); | |||||
s.slicesProperties.push({ | |||||
startPosition, | |||||
endPosition, | |||||
value: total, | |||||
total: s.grandTotal, | |||||
startAngle, | |||||
endAngle, | |||||
angle: diffAngle | |||||
}); | |||||
}); | |||||
this.init = 0; | |||||
} | |||||
setupComponents() { | |||||
let s = this.state; | |||||
let componentConfigs = [ | |||||
[ | |||||
'donutSlices', | |||||
{ }, | |||||
function() { | |||||
return { | |||||
sliceStrings: s.sliceStrings, | |||||
colors: this.colors, | |||||
strokeWidth: this.strokeWidth, | |||||
}; | |||||
}.bind(this) | |||||
] | |||||
]; | |||||
this.components = new Map(componentConfigs | |||||
.map(args => { | |||||
let component = getComponent(...args); | |||||
return [args[0], component]; | |||||
})); | |||||
} | |||||
calTranslateByAngle(property){ | |||||
const{radius,hoverRadio} = this; | |||||
const position = getPositionByAngle(property.startAngle+(property.angle / 2),radius); | |||||
return `translate3d(${(position.x) * hoverRadio}px,${(position.y) * hoverRadio}px,0)`; | |||||
} | |||||
hoverSlice(path,i,flag,e){ | |||||
if(!path) return; | |||||
const color = this.colors[i]; | |||||
if(flag) { | |||||
transform(path, this.calTranslateByAngle(this.state.slicesProperties[i])); | |||||
path.style.stroke = lightenDarkenColor(color, 50); | |||||
let g_off = getOffset(this.svg); | |||||
let x = e.pageX - g_off.left + 10; | |||||
let y = e.pageY - g_off.top - 10; | |||||
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(); | |||||
} else { | |||||
transform(path,'translate3d(0,0,0)'); | |||||
this.tip.hideTip(); | |||||
path.style.stroke = color; | |||||
} | |||||
} | |||||
bindTooltip() { | |||||
this.container.addEventListener('mousemove', this.mouseMove); | |||||
this.container.addEventListener('mouseleave', this.mouseLeave); | |||||
} | |||||
mouseMove(e){ | |||||
const target = e.target; | |||||
let slices = this.components.get('donutSlices').store; | |||||
let prevIndex = this.curActiveSliceIndex; | |||||
let prevAcitve = this.curActiveSlice; | |||||
if(slices.includes(target)) { | |||||
let i = slices.indexOf(target); | |||||
this.hoverSlice(prevAcitve, prevIndex,false); | |||||
this.curActiveSlice = target; | |||||
this.curActiveSliceIndex = i; | |||||
this.hoverSlice(target, i, true, e); | |||||
} else { | |||||
this.mouseLeave(); | |||||
} | |||||
} | |||||
mouseLeave(){ | |||||
this.hoverSlice(this.curActiveSlice,this.curActiveSliceIndex,false); | |||||
} | |||||
} |
@@ -69,6 +69,20 @@ class ChartComponent { | |||||
} | } | ||||
let componentConfigs = { | let componentConfigs = { | ||||
donutSlices: { | |||||
layerClass: 'donut-slices', | |||||
makeElements(data) { | |||||
return data.sliceStrings.map((s, i) => { | |||||
let slice = makePath(s, 'donut-path', data.colors[i], 'none', data.strokeWidth); | |||||
slice.style.transition = 'transform .3s;'; | |||||
return slice; | |||||
}); | |||||
}, | |||||
animateElements(newData) { | |||||
return this.store.map((slice, i) => animatePathStr(slice, newData.sliceStrings[i])); | |||||
}, | |||||
}, | |||||
pieSlices: { | pieSlices: { | ||||
layerClass: 'pie-slices', | layerClass: 'pie-slices', | ||||
makeElements(data) { | makeElements(data) { | ||||
@@ -98,7 +98,8 @@ export const DEFAULT_COLORS = { | |||||
line: DEFAULT_CHART_COLORS, | line: DEFAULT_CHART_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 | |||||
}; | }; | ||||
// Universal constants | // Universal constants | ||||
@@ -98,13 +98,14 @@ export function wrapInSVGGroup(elements, className='') { | |||||
return g; | return g; | ||||
} | } | ||||
export function makePath(pathStr, className='', stroke='none', fill='none') { | |||||
export function makePath(pathStr, className='', stroke='none', fill='none', strokeWidth=0) { | |||||
return createSVG('path', { | return createSVG('path', { | ||||
className: className, | className: className, | ||||
d: pathStr, | d: pathStr, | ||||
styles: { | styles: { | ||||
stroke: stroke, | stroke: stroke, | ||||
fill: fill | |||||
fill: fill, | |||||
'stroke-width': strokeWidth | |||||
} | } | ||||
}); | }); | ||||
} | } | ||||
@@ -119,6 +120,15 @@ export function makeArcPathStr(startPosition, endPosition, center, radius, clock | |||||
${arcEndX} ${arcEndY} z`; | ${arcEndX} ${arcEndY} z`; | ||||
} | } | ||||
export function makeArcStrokePathStr(startPosition, endPosition, center, radius, clockWise=1){ | |||||
let [arcStartX, arcStartY] = [center.x + startPosition.x, center.y + startPosition.y]; | |||||
let [arcEndX, arcEndY] = [center.x + endPosition.x, center.y + endPosition.y]; | |||||
return `M${arcStartX} ${arcStartY} | |||||
A ${radius} ${radius} 0 0 ${clockWise ? 1 : 0} | |||||
${arcEndX} ${arcEndY}`; | |||||
} | |||||
export function makeGradient(svgDefElem, color, lighter = false) { | export function makeGradient(svgDefElem, color, lighter = false) { | ||||
let gradientId ='path-fill-gradient' + '-' + color + '-' +(lighter ? 'lighter' : 'default'); | let gradientId ='path-fill-gradient' + '-' + color + '-' +(lighter ? 'lighter' : 'default'); | ||||
let gradientDef = renderVerticalGradient(svgDefElem, gradientId); | let gradientDef = renderVerticalGradient(svgDefElem, gradientId); | ||||