ability to create donut charttags/1.2.0
@@ -166,7 +166,8 @@ const DEFAULT_COLORS = { | |||
line: DEFAULT_CHART_COLORS, | |||
pie: DEFAULT_CHART_COLORS, | |||
percentage: DEFAULT_CHART_COLORS, | |||
heatmap: HEATMAP_COLORS_GREEN | |||
heatmap: HEATMAP_COLORS_GREEN, | |||
donut: DEFAULT_CHART_COLORS | |||
}; | |||
// 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', { | |||
className: className, | |||
d: pathStr, | |||
styles: { | |||
stroke: stroke, | |||
fill: fill | |||
fill: fill, | |||
'stroke-width': strokeWidth | |||
} | |||
}); | |||
} | |||
@@ -537,6 +539,15 @@ function makeArcPathStr(startPosition, endPosition, center, radius, clockWise=1) | |||
${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) { | |||
let gradientId ='path-fill-gradient' + '-' + color + '-' +(lighter ? 'lighter' : 'default'); | |||
let gradientDef = renderVerticalGradient(svgDefElem, gradientId); | |||
@@ -1878,6 +1889,20 @@ class ChartComponent { | |||
} | |||
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: { | |||
layerClass: 'pie-slices', | |||
makeElements(data) { | |||
@@ -3677,6 +3702,152 @@ class AxisChart extends BaseChart { | |||
// 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'; | |||
const chartTypes = { | |||
bar: AxisChart, | |||
@@ -3684,7 +3855,8 @@ const chartTypes = { | |||
// multiaxis: MultiAxisChart, | |||
percentage: PercentageChart, | |||
heatmap: Heatmap, | |||
pie: PieChart | |||
pie: PieChart, | |||
donut: DonutChart, | |||
}; | |||
function getChartByType(chartType = 'line', parent, options) { | |||
@@ -46,6 +46,12 @@ var HEATMAP_COLORS_YELLOW = ['#ebedf0', '#fdf436', '#ffc700', '#ff9100', '#06001 | |||
// 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. | |||
* @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) { | |||
@@ -155,6 +160,8 @@ function addDays(date, numberOfDays) { | |||
date.setDate(date.getDate() + numberOfDays); | |||
} | |||
// Composite Chart | |||
// ================================================================================ | |||
var reportCountList = [152, 222, 199, 287, 534, 709, 1179, 1256, 1632, 1856, 1850]; | |||
var lineCompositeData = { | |||
@@ -324,6 +331,9 @@ var demoConfig = { | |||
} | |||
}; | |||
// import { lineComposite, barComposite } from './demoConfig'; | |||
// ================================================================================ | |||
var Chart = frappe.Chart; // eslint-disable-line no-undef | |||
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", | |||
type: 'axis-mixed', // or 'bar', 'line', 'pie', 'percentage' | |||
type: 'axis-mixed', // or 'bar', 'line', 'pie', 'percentage', 'donut' | |||
height: 300, | |||
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"> | |||
<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='donut'>Donut Chart</button> | |||
<button type="button" class="btn btn-sm btn-secondary" data-type='percentage'>Percentage Chart</button> | |||
</div> | |||
<div class="btn-group export-buttons margin-top mx-auto" role="group"> | |||
@@ -233,7 +234,7 @@ redirect_to: "https://frappe.io/charts" | |||
// default: 0 | |||
}, | |||
// Pie/Percentage charts | |||
// Pie/Percentage/Donut charts | |||
maxLegendPoints: 6, // default: 20 | |||
maxSlices: 10, // default: 20 | |||
@@ -5,6 +5,7 @@ import PercentageChart from './charts/PercentageChart'; | |||
import PieChart from './charts/PieChart'; | |||
import Heatmap from './charts/Heatmap'; | |||
import AxisChart from './charts/AxisChart'; | |||
import DonutChart from './charts/DonutChart'; | |||
const chartTypes = { | |||
bar: AxisChart, | |||
@@ -12,7 +13,8 @@ const chartTypes = { | |||
// multiaxis: MultiAxisChart, | |||
percentage: PercentageChart, | |||
heatmap: Heatmap, | |||
pie: PieChart | |||
pie: PieChart, | |||
donut: DonutChart, | |||
}; | |||
function getChartByType(chartType = 'line', parent, options) { | |||
@@ -0,0 +1,157 @@ | |||
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 = { | |||
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: { | |||
layerClass: 'pie-slices', | |||
makeElements(data) { | |||
@@ -98,7 +98,8 @@ export const DEFAULT_COLORS = { | |||
line: DEFAULT_CHART_COLORS, | |||
pie: DEFAULT_CHART_COLORS, | |||
percentage: DEFAULT_CHART_COLORS, | |||
heatmap: HEATMAP_COLORS_GREEN | |||
heatmap: HEATMAP_COLORS_GREEN, | |||
donut: DEFAULT_CHART_COLORS | |||
}; | |||
// Universal constants | |||
@@ -98,13 +98,14 @@ export function wrapInSVGGroup(elements, className='') { | |||
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', { | |||
className: className, | |||
d: pathStr, | |||
styles: { | |||
stroke: stroke, | |||
fill: fill | |||
fill: fill, | |||
'stroke-width': strokeWidth | |||
} | |||
}); | |||
} | |||
@@ -119,6 +120,15 @@ export function makeArcPathStr(startPosition, endPosition, center, radius, clock | |||
${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) { | |||
let gradientId ='path-fill-gradient' + '-' + color + '-' +(lighter ? 'lighter' : 'default'); | |||
let gradientDef = renderVerticalGradient(svgDefElem, gradientId); | |||