ability to create donut chart

This commit is contained in:
ratnajeetshyamkunwar 2019-04-09 21:05:48 +05:30
parent 81cddd45a3
commit 8d00770207
17 changed files with 385 additions and 20 deletions

View File

@ -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) {

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -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

File diff suppressed because one or more lines are too long

View File

@ -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

View File

@ -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) {

154
src/js/charts/DonutChart.js Normal file
View File

@ -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);
}
}

View File

@ -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) {

View File

@ -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

View File

@ -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);