@@ -202,10 +202,30 @@ class SvgTip { | |||||
} | } | ||||
} | } | ||||
/** | |||||
* Returns the value of a number upto 2 decimal places. | |||||
* @param {Number} d Any number | |||||
*/ | |||||
const VERT_SPACE_OUTSIDE_BASE_CHART = 40; | |||||
const TRANSLATE_Y_BASE_CHART = 20; | |||||
const LEFT_MARGIN_BASE_CHART = 60; | |||||
const RIGHT_MARGIN_BASE_CHART = 40; | |||||
const Y_AXIS_MARGIN = 60; | |||||
const INIT_CHART_UPDATE_TIMEOUT = 700; | |||||
const CHART_POST_ANIMATE_TIMEOUT = 400; | |||||
const DEFAULT_AXIS_CHART_TYPE = 'line'; | |||||
const AXIS_DATASET_CHART_TYPES = ['line', 'bar']; | |||||
const BAR_CHART_SPACE_RATIO = 0.5; | |||||
const MIN_BAR_PERCENT_HEIGHT = 0.01; | |||||
const LINE_CHART_DOT_SIZE = 4; | |||||
const DOT_OVERLAY_SIZE_INCR = 4; | |||||
const DEFAULT_CHAR_WIDTH = 8; | |||||
// Universal constants | |||||
const ANGLE_RATIO = Math.PI / 180; | |||||
const FULL_ANGLE = 360; | |||||
function floatTwo(d) { | function floatTwo(d) { | ||||
return parseFloat(d.toFixed(2)); | return parseFloat(d.toFixed(2)); | ||||
} | } | ||||
@@ -248,6 +268,15 @@ function getStringWidth(string, charWidth) { | |||||
return (string+"").length * charWidth; | return (string+"").length * charWidth; | ||||
} | } | ||||
function getPositionByAngle(angle, radius) { | |||||
return { | |||||
x:Math.sin(angle * ANGLE_RATIO) * radius, | |||||
y:Math.cos(angle * ANGLE_RATIO) * radius, | |||||
}; | |||||
} | |||||
function getBarHeightAndYAttr(yTop, zeroLine) { | function getBarHeightAndYAttr(yTop, zeroLine) { | ||||
let height, y; | let height, y; | ||||
if (yTop <= zeroLine) { | if (yTop <= zeroLine) { | ||||
@@ -372,26 +401,6 @@ function animatePath(paths, newXList, newYList, zeroLine) { | |||||
return pathComponents; | return pathComponents; | ||||
} | } | ||||
const VERT_SPACE_OUTSIDE_BASE_CHART = 40; | |||||
const TRANSLATE_Y_BASE_CHART = 20; | |||||
const LEFT_MARGIN_BASE_CHART = 60; | |||||
const RIGHT_MARGIN_BASE_CHART = 40; | |||||
const Y_AXIS_MARGIN = 60; | |||||
const INIT_CHART_UPDATE_TIMEOUT = 700; | |||||
const CHART_POST_ANIMATE_TIMEOUT = 400; | |||||
const DEFAULT_AXIS_CHART_TYPE = 'line'; | |||||
const AXIS_DATASET_CHART_TYPES = ['line', 'bar']; | |||||
const BAR_CHART_SPACE_RATIO = 0.5; | |||||
const MIN_BAR_PERCENT_HEIGHT = 0.01; | |||||
const LINE_CHART_DOT_SIZE = 4; | |||||
const DOT_OVERLAY_SIZE_INCR = 4; | |||||
const DEFAULT_CHAR_WIDTH = 8; | |||||
const AXIS_TICK_LENGTH = 6; | const AXIS_TICK_LENGTH = 6; | ||||
const LABEL_MARGIN = 4; | const LABEL_MARGIN = 4; | ||||
const FONT_SIZE = 10; | const FONT_SIZE = 10; | ||||
@@ -489,6 +498,16 @@ function makePath(pathStr, className='', stroke='none', fill='none') { | |||||
}); | }); | ||||
} | } | ||||
function makeArcPathStr(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${center.x} ${center.y} | |||||
L${arcStartX} ${arcStartY} | |||||
A ${radius} ${radius} 0 0 ${clockWise ? 1 : 0} | |||||
${arcEndX} ${arcEndY} z`; | |||||
} | |||||
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); | ||||
@@ -917,7 +936,7 @@ const PRESET_COLOR_MAP = { | |||||
}; | }; | ||||
const DEFAULT_COLORS = ['light-blue', 'blue', 'violet', 'red', 'orange', | const DEFAULT_COLORS = ['light-blue', 'blue', 'violet', 'red', 'orange', | ||||
'yellow', 'green', 'light-green', 'purple', 'magenta']; | |||||
'yellow', 'green', 'light-green', 'purple', 'magenta', 'light-grey', 'dark-grey']; | |||||
function limitColor(r){ | function limitColor(r){ | ||||
if (r > 255) return 255; | if (r > 255) return 255; | ||||
@@ -986,13 +1005,11 @@ function getDifferentChart(type, current_type, parent, args) { | |||||
// Okay, this is anticlimactic | // Okay, this is anticlimactic | ||||
// this function will need to actually be 'changeChartType(type)' | // this function will need to actually be 'changeChartType(type)' | ||||
// that will update only the required elements, but for now ... | // that will update only the required elements, but for now ... | ||||
return new Chart(parent, { | |||||
title: args.title, | |||||
data: args.data, | |||||
type: type, | |||||
height: args.height, | |||||
colors: useColor ? args.colors : undefined | |||||
}); | |||||
args.type = type; | |||||
args.colors = useColor ? args.colors : undefined; | |||||
return new Chart(parent, args); | |||||
} | } | ||||
// Leveraging SMIL Animations | // Leveraging SMIL Animations | ||||
@@ -1384,13 +1401,83 @@ class BaseChart { | |||||
} | } | ||||
} | } | ||||
class PercentageChart extends BaseChart { | |||||
class AggregationChart extends BaseChart { | |||||
constructor(parent, args) { | constructor(parent, args) { | ||||
super(parent, args); | super(parent, args); | ||||
this.type = 'percentage'; | |||||
} | |||||
configure(args) { | |||||
super.configure(args); | |||||
this.config.maxSlices = args.maxSlices || 20; | |||||
this.config.maxLegendPoints = args.maxLegendPoints || 20; | |||||
} | |||||
calc() { | |||||
let s = this.state; | |||||
let maxSlices = this.config.maxSlices; | |||||
s.sliceTotals = []; | |||||
let allTotals = this.data.labels.map((label, i) => { | |||||
let total = 0; | |||||
this.data.datasets.map(e => { | |||||
total += e.values[i]; | |||||
}); | |||||
return [total, label]; | |||||
}).filter(d => { return d[0] > 0; }); // keep only positive results | |||||
this.max_slices = 10; | |||||
this.max_legend_points = 6; | |||||
let totals = allTotals; | |||||
if(allTotals.length > maxSlices) { | |||||
// Prune and keep a grey area for rest as per maxSlices | |||||
allTotals.sort((a, b) => { return b[0] - a[0]; }); | |||||
totals = allTotals.slice(0, maxSlices-1); | |||||
let remaining = allTotals.slice(maxSlices-1); | |||||
let sumOfRemaining = 0; | |||||
remaining.map(d => {sumOfRemaining += d[0];}); | |||||
totals.push([sumOfRemaining, 'Rest']); | |||||
this.colors[maxSlices-1] = 'grey'; | |||||
} | |||||
this.labels = []; | |||||
totals.map(d => { | |||||
s.sliceTotals.push(d[0]); | |||||
this.labels.push(d[1]); | |||||
}); | |||||
} | |||||
render() { } | |||||
bindTooltip() { } | |||||
renderLegend() { | |||||
let s = this.state; | |||||
this.legendTotals = s.sliceTotals.slice(0, this.config.maxLegendPoints); | |||||
let x_values = this.formatted_labels && this.formatted_labels.length > 0 | |||||
? this.formatted_labels : this.labels; | |||||
this.legendTotals.map((d, i) => { | |||||
if(d) { | |||||
let stats = $.create('div', { | |||||
className: 'stats', | |||||
inside: this.statsWrapper | |||||
}); | |||||
stats.innerHTML = `<span class="indicator"> | |||||
<i style="background: ${this.colors[i]}"></i> | |||||
<span class="text-muted">${x_values[i]}:</span> | |||||
${d} | |||||
</span>`; | |||||
} | |||||
}); | |||||
} | |||||
} | |||||
class PercentageChart extends AggregationChart { | |||||
constructor(parent, args) { | |||||
super(parent, args); | |||||
this.type = 'percentage'; | |||||
this.setup(); | this.setup(); | ||||
} | } | ||||
@@ -1420,9 +1507,10 @@ class PercentageChart extends BaseChart { | |||||
} | } | ||||
render() { | render() { | ||||
this.grand_total = this.sliceTotals.reduce((a, b) => a + b, 0); | |||||
let s = this.state; | |||||
this.grand_total = s.sliceTotals.reduce((a, b) => a + b, 0); | |||||
this.slices = []; | this.slices = []; | ||||
this.sliceTotals.map((total, i) => { | |||||
s.sliceTotals.map((total, i) => { | |||||
let slice = $.create('div', { | let slice = $.create('div', { | ||||
className: `progress-bar`, | className: `progress-bar`, | ||||
inside: this.percentageBar, | inside: this.percentageBar, | ||||
@@ -1435,42 +1523,8 @@ class PercentageChart extends BaseChart { | |||||
}); | }); | ||||
} | } | ||||
calc() { | |||||
this.sliceTotals = []; | |||||
let all_totals = this.data.labels.map((d, i) => { | |||||
let total = 0; | |||||
this.data.datasets.map(e => { | |||||
total += e.values[i]; | |||||
}); | |||||
return [total, d]; | |||||
}).filter(d => { return d[0] > 0; }); // keep only positive results | |||||
let totals = all_totals; | |||||
if(all_totals.length > this.max_slices) { | |||||
all_totals.sort((a, b) => { return b[0] - a[0]; }); | |||||
totals = all_totals.slice(0, this.max_slices-1); | |||||
let others = all_totals.slice(this.max_slices-1); | |||||
let sum_of_others = 0; | |||||
others.map(d => {sum_of_others += d[0];}); | |||||
totals.push([sum_of_others, 'Rest']); | |||||
this.colors[this.max_slices-1] = 'grey'; | |||||
} | |||||
this.labels = []; | |||||
totals.map(d => { | |||||
this.sliceTotals.push(d[0]); | |||||
this.labels.push(d[1]); | |||||
}); | |||||
this.legend_totals = this.sliceTotals.slice(0, this.max_legend_points); | |||||
} | |||||
bindTooltip() { | bindTooltip() { | ||||
// this.slices.map((slice, i) => { | // this.slices.map((slice, i) => { | ||||
// slice.addEventListener('mouseenter', () => { | // slice.addEventListener('mouseenter', () => { | ||||
// let g_off = getOffset(this.chartWrapper), p_off = getOffset(slice); | // let g_off = getOffset(this.chartWrapper), p_off = getOffset(slice); | ||||
@@ -1479,114 +1533,57 @@ class PercentageChart extends BaseChart { | |||||
// let y = p_off.top - g_off.top - 6; | // let y = p_off.top - g_off.top - 6; | ||||
// let title = (this.formatted_labels && this.formatted_labels.length>0 | // let title = (this.formatted_labels && this.formatted_labels.length>0 | ||||
// ? this.formatted_labels[i] : this.labels[i]) + ': '; | // ? this.formatted_labels[i] : this.labels[i]) + ': '; | ||||
// let percent = (this.sliceTotals[i]*100/this.grand_total).toFixed(1); | |||||
// let percent = (s.sliceTotals[i]*100/this.grand_total).toFixed(1); | |||||
// this.tip.set_values(x, y, title, percent + "%"); | // this.tip.set_values(x, y, title, percent + "%"); | ||||
// this.tip.show_tip(); | // this.tip.show_tip(); | ||||
// }); | // }); | ||||
// }); | // }); | ||||
} | } | ||||
renderLegend() { | |||||
// let x_values = this.formatted_labels && this.formatted_labels.length > 0 | |||||
// ? this.formatted_labels : this.labels; | |||||
// this.legend_totals.map((d, i) => { | |||||
// if(d) { | |||||
// let stats = $.create('div', { | |||||
// className: 'stats', | |||||
// inside: this.statsWrapper | |||||
// }); | |||||
// stats.innerHTML = `<span class="indicator"> | |||||
// <i style="background: ${this.colors[i]}"></i> | |||||
// <span class="text-muted">${x_values[i]}:</span> | |||||
// ${d} | |||||
// </span>`; | |||||
// } | |||||
// }); | |||||
} | |||||
} | } | ||||
const ANGLE_RATIO = Math.PI / 180; | |||||
const FULL_ANGLE = 360; | |||||
class PieChart extends BaseChart { | |||||
class PieChart extends AggregationChart { | |||||
constructor(parent, args) { | constructor(parent, args) { | ||||
super(parent, args); | super(parent, args); | ||||
this.type = 'pie'; | this.type = 'pie'; | ||||
this.elements_to_animate = null; | |||||
this.hoverRadio = args.hoverRadio || 0.1; | this.hoverRadio = args.hoverRadio || 0.1; | ||||
this.max_slices = 10; | |||||
this.max_legend_points = 6; | |||||
this.isAnimate = false; | |||||
this.startAngle = args.startAngle || 0; | this.startAngle = args.startAngle || 0; | ||||
this.clockWise = args.clockWise || false; | this.clockWise = args.clockWise || false; | ||||
this.mouseMove = this.mouseMove.bind(this); | |||||
this.mouseLeave = this.mouseLeave.bind(this); | |||||
this.setup(); | this.setup(); | ||||
} | } | ||||
calc() { | |||||
this.centerX = this.width / 2; | |||||
this.centerY = this.height / 2; | |||||
this.radius = (this.height > this.width ? this.centerX : this.centerY); | |||||
this.slice_totals = []; | |||||
let all_totals = this.data.labels.map((d, i) => { | |||||
let total = 0; | |||||
this.data.datasets.map(e => { | |||||
total += e.values[i]; | |||||
}); | |||||
return [total, d]; | |||||
}).filter(d => { return d[0] > 0; }); // keep only positive results | |||||
let totals = all_totals; | |||||
if(all_totals.length > this.max_slices) { | |||||
all_totals.sort((a, b) => { return b[0] - a[0]; }); | |||||
totals = all_totals.slice(0, this.max_slices-1); | |||||
let others = all_totals.slice(this.max_slices-1); | |||||
let sum_of_others = 0; | |||||
others.map(d => {sum_of_others += d[0];}); | |||||
totals.push([sum_of_others, 'Rest']); | |||||
this.colors[this.max_slices-1] = 'grey'; | |||||
} | |||||
this.labels = []; | |||||
totals.map(d => { | |||||
this.slice_totals.push(d[0]); | |||||
this.labels.push(d[1]); | |||||
}); | |||||
this.legend_totals = this.slice_totals.slice(0, this.max_legend_points); | |||||
configure(args) { | |||||
super.configure(args); | |||||
this.mouseMove = this.mouseMove.bind(this); | |||||
this.mouseLeave = this.mouseLeave.bind(this); | |||||
} | } | ||||
static getPositionByAngle(angle,radius) { | |||||
return { | |||||
x:Math.sin(angle * ANGLE_RATIO) * radius, | |||||
y:Math.cos(angle * ANGLE_RATIO) * radius, | |||||
calc() { | |||||
super.calc(); | |||||
this.center = { | |||||
x: this.width / 2, | |||||
y: this.height / 2 | |||||
}; | }; | ||||
this.radius = (this.height > this.width ? this.center.x : this.center.y); | |||||
} | } | ||||
makeArcPath(startPosition,endPosition){ | |||||
const{centerX,centerY,radius,clockWise} = this; | |||||
return `M${centerX} ${centerY} L${centerX+startPosition.x} ${centerY+startPosition.y} A ${radius} ${radius} 0 0 ${clockWise ? 1 : 0} ${centerX+endPosition.x} ${centerY+endPosition.y} z`; | |||||
} | |||||
render(init) { | render(init) { | ||||
const{radius,clockWise} = this; | const{radius,clockWise} = this; | ||||
this.grand_total = this.slice_totals.reduce((a, b) => a + b, 0); | |||||
this.grand_total = this.state.sliceTotals.reduce((a, b) => a + b, 0); | |||||
const prevSlicesProperties = this.slicesProperties || []; | const prevSlicesProperties = this.slicesProperties || []; | ||||
this.slices = []; | this.slices = []; | ||||
this.elements_to_animate = []; | this.elements_to_animate = []; | ||||
this.slicesProperties = []; | this.slicesProperties = []; | ||||
let curAngle = 180 - this.startAngle; | let curAngle = 180 - this.startAngle; | ||||
this.slice_totals.map((total, i) => { | |||||
this.state.sliceTotals.map((total, i) => { | |||||
const startAngle = curAngle; | const startAngle = curAngle; | ||||
const originDiffAngle = (total / this.grand_total) * FULL_ANGLE; | const originDiffAngle = (total / this.grand_total) * FULL_ANGLE; | ||||
const diffAngle = clockWise ? -originDiffAngle : originDiffAngle; | const diffAngle = clockWise ? -originDiffAngle : originDiffAngle; | ||||
const endAngle = curAngle = curAngle + diffAngle; | const endAngle = curAngle = curAngle + diffAngle; | ||||
const startPosition = PieChart.getPositionByAngle(startAngle,radius); | |||||
const endPosition = PieChart.getPositionByAngle(endAngle,radius); | |||||
const startPosition = getPositionByAngle(startAngle, radius); | |||||
const endPosition = getPositionByAngle(endAngle, radius); | |||||
const prevProperty = init && prevSlicesProperties[i]; | const prevProperty = init && prevSlicesProperties[i]; | ||||
let curStart,curEnd; | let curStart,curEnd; | ||||
if(init){ | if(init){ | ||||
@@ -1596,7 +1593,7 @@ class PieChart extends BaseChart { | |||||
curStart = startPosition; | curStart = startPosition; | ||||
curEnd = endPosition; | curEnd = endPosition; | ||||
} | } | ||||
const curPath = this.makeArcPath(curStart,curEnd); | |||||
const curPath = makeArcPathStr(curStart, curEnd, this.center, this.radius, this.clockWise); | |||||
let slice = makePath(curPath, 'pie-path', 'none', this.colors[i]); | let slice = makePath(curPath, 'pie-path', 'none', this.colors[i]); | ||||
slice.style.transition = 'transform .3s;'; | slice.style.transition = 'transform .3s;'; | ||||
this.drawArea.appendChild(slice); | this.drawArea.appendChild(slice); | ||||
@@ -1609,42 +1606,44 @@ class PieChart extends BaseChart { | |||||
total: this.grand_total, | total: this.grand_total, | ||||
startAngle, | startAngle, | ||||
endAngle, | endAngle, | ||||
angle:diffAngle | |||||
angle: diffAngle | |||||
}); | }); | ||||
if(init){ | if(init){ | ||||
this.elements_to_animate.push([{unit: slice, array: this.slices, index: this.slices.length - 1}, | |||||
{d:this.makeArcPath(startPosition,endPosition)}, | |||||
this.elements_to_animate.push([slice, | |||||
{d: makeArcPathStr(startPosition, endPosition, this.center, this.radius, this.clockWise)}, | |||||
650, "easein",null,{ | 650, "easein",null,{ | ||||
d:curPath | d:curPath | ||||
}]); | }]); | ||||
} | } | ||||
}); | }); | ||||
if(init){ | |||||
runSMILAnimation(this.chartWrapper, this.svg, this.elements_to_animate); | |||||
} | |||||
// if(init){ | |||||
// runSMILAnimation(this.chartWrapper, this.svg, this.elements_to_animate); | |||||
// } | |||||
} | } | ||||
calTranslateByAngle(property){ | calTranslateByAngle(property){ | ||||
const{radius,hoverRadio} = this; | const{radius,hoverRadio} = this; | ||||
const position = PieChart.getPositionByAngle(property.startAngle+(property.angle / 2),radius); | |||||
const position = getPositionByAngle(property.startAngle+(property.angle / 2),radius); | |||||
return `translate3d(${(position.x) * hoverRadio}px,${(position.y) * hoverRadio}px,0)`; | return `translate3d(${(position.x) * hoverRadio}px,${(position.y) * hoverRadio}px,0)`; | ||||
} | } | ||||
hoverSlice(path,i,flag,e){ | hoverSlice(path,i,flag,e){ | ||||
if(!path) return; | if(!path) return; | ||||
const color = this.colors[i]; | const color = this.colors[i]; | ||||
if(flag){ | |||||
transform(path,this.calTranslateByAngle(this.slicesProperties[i])); | |||||
path.style.fill = lightenDarkenColor(color,50); | |||||
if(flag) { | |||||
transform(path, this.calTranslateByAngle(this.slicesProperties[i])); | |||||
path.style.fill = lightenDarkenColor(color, 50); | |||||
let g_off = getOffset(this.svg); | let g_off = getOffset(this.svg); | ||||
let x = e.pageX - g_off.left + 10; | let x = e.pageX - g_off.left + 10; | ||||
let y = e.pageY - g_off.top - 10; | let y = e.pageY - g_off.top - 10; | ||||
let title = (this.formatted_labels && this.formatted_labels.length>0 | |||||
let title = (this.formatted_labels && this.formatted_labels.length > 0 | |||||
? this.formatted_labels[i] : this.labels[i]) + ': '; | ? this.formatted_labels[i] : this.labels[i]) + ': '; | ||||
let percent = (this.slice_totals[i]*100/this.grand_total).toFixed(1); | |||||
let percent = (this.state.sliceTotals[i]*100/this.grand_total).toFixed(1); | |||||
this.tip.set_values(x, y, title, percent + "%"); | this.tip.set_values(x, y, title, percent + "%"); | ||||
this.tip.show_tip(); | this.tip.show_tip(); | ||||
}else{ | |||||
} else { | |||||
transform(path,'translate3d(0,0,0)'); | transform(path,'translate3d(0,0,0)'); | ||||
this.tip.hide_tip(); | this.tip.hide_tip(); | ||||
path.style.fill = color; | path.style.fill = color; | ||||
@@ -1672,26 +1671,6 @@ class PieChart extends BaseChart { | |||||
// this.drawArea.addEventListener('mousemove',this.mouseMove); | // this.drawArea.addEventListener('mousemove',this.mouseMove); | ||||
// this.drawArea.addEventListener('mouseleave',this.mouseLeave); | // this.drawArea.addEventListener('mouseleave',this.mouseLeave); | ||||
} | } | ||||
renderLegend() { | |||||
let x_values = this.formatted_labels && this.formatted_labels.length > 0 | |||||
? this.formatted_labels : this.labels; | |||||
this.legend_totals.map((d, i) => { | |||||
const color = this.colors[i]; | |||||
if(d) { | |||||
let stats = $.create('div', { | |||||
className: 'stats', | |||||
inside: this.statsWrapper | |||||
}); | |||||
stats.innerHTML = `<span class="indicator"> | |||||
<i style="background-color:${color};"></i> | |||||
<span class="text-muted">${x_values[i]}:</span> | |||||
${d} | |||||
</span>`; | |||||
} | |||||
}); | |||||
} | |||||
} | } | ||||
// Playing around with dates | // Playing around with dates | ||||
@@ -2711,7 +2690,8 @@ class AxisChart extends BaseChart { | |||||
this.calcYAxisParameters(this.getAllYValues(), this.type === 'line'); | this.calcYAxisParameters(this.getAllYValues(), this.type === 'line'); | ||||
} | } | ||||
calcXPositions(s=this.state) { | |||||
calcXPositions() { | |||||
let s = this.state; | |||||
let labels = this.data.labels; | let labels = this.data.labels; | ||||
s.datasetLength = labels.length; | s.datasetLength = labels.length; | ||||
@@ -176,6 +176,8 @@ let type_chart = new Chart("#chart-types", { | |||||
xAxisMode: 'tick', | xAxisMode: 'tick', | ||||
yAxisMode: 'span', | yAxisMode: 'span', | ||||
valuesOverPoints: 1, | valuesOverPoints: 1, | ||||
// maxLegendPoints: 6, | |||||
// maxSlices: 3, | |||||
isNavigable: 1, | isNavigable: 1, | ||||
barOptions: { | barOptions: { | ||||
stacked: 1 | stacked: 1 | ||||
@@ -0,0 +1,75 @@ | |||||
import BaseChart from './BaseChart'; | |||||
import { $, getOffset } from '../utils/dom'; | |||||
export default class AggregationChart extends BaseChart { | |||||
constructor(parent, args) { | |||||
super(parent, args); | |||||
} | |||||
configure(args) { | |||||
super.configure(args); | |||||
this.config.maxSlices = args.maxSlices || 20; | |||||
this.config.maxLegendPoints = args.maxLegendPoints || 20; | |||||
} | |||||
calc() { | |||||
let s = this.state; | |||||
let maxSlices = this.config.maxSlices; | |||||
s.sliceTotals = []; | |||||
let allTotals = this.data.labels.map((label, i) => { | |||||
let total = 0; | |||||
this.data.datasets.map(e => { | |||||
total += e.values[i]; | |||||
}); | |||||
return [total, label]; | |||||
}).filter(d => { return d[0] > 0; }); // keep only positive results | |||||
let totals = allTotals; | |||||
if(allTotals.length > maxSlices) { | |||||
// Prune and keep a grey area for rest as per maxSlices | |||||
allTotals.sort((a, b) => { return b[0] - a[0]; }); | |||||
totals = allTotals.slice(0, maxSlices-1); | |||||
let remaining = allTotals.slice(maxSlices-1); | |||||
let sumOfRemaining = 0; | |||||
remaining.map(d => {sumOfRemaining += d[0];}); | |||||
totals.push([sumOfRemaining, 'Rest']); | |||||
this.colors[maxSlices-1] = 'grey'; | |||||
} | |||||
this.labels = []; | |||||
totals.map(d => { | |||||
s.sliceTotals.push(d[0]); | |||||
this.labels.push(d[1]); | |||||
}); | |||||
} | |||||
render() { } | |||||
bindTooltip() { } | |||||
renderLegend() { | |||||
let s = this.state; | |||||
this.legendTotals = s.sliceTotals.slice(0, this.config.maxLegendPoints); | |||||
let x_values = this.formatted_labels && this.formatted_labels.length > 0 | |||||
? this.formatted_labels : this.labels; | |||||
this.legendTotals.map((d, i) => { | |||||
if(d) { | |||||
let stats = $.create('div', { | |||||
className: 'stats', | |||||
inside: this.statsWrapper | |||||
}); | |||||
stats.innerHTML = `<span class="indicator"> | |||||
<i style="background: ${this.colors[i]}"></i> | |||||
<span class="text-muted">${x_values[i]}:</span> | |||||
${d} | |||||
</span>`; | |||||
} | |||||
}); | |||||
} | |||||
} |
@@ -56,7 +56,8 @@ export default class AxisChart extends BaseChart { | |||||
this.calcYAxisParameters(this.getAllYValues(), this.type === 'line'); | this.calcYAxisParameters(this.getAllYValues(), this.type === 'line'); | ||||
} | } | ||||
calcXPositions(s=this.state) { | |||||
calcXPositions() { | |||||
let s = this.state; | |||||
let labels = this.data.labels; | let labels = this.data.labels; | ||||
s.datasetLength = labels.length; | s.datasetLength = labels.length; | ||||
@@ -1,14 +1,11 @@ | |||||
import BaseChart from './BaseChart'; | |||||
import AggregationChart from './AggregationChart'; | |||||
import { $, getOffset } from '../utils/dom'; | import { $, getOffset } from '../utils/dom'; | ||||
export default class PercentageChart extends BaseChart { | |||||
export default class PercentageChart extends AggregationChart { | |||||
constructor(parent, args) { | constructor(parent, args) { | ||||
super(parent, args); | super(parent, args); | ||||
this.type = 'percentage'; | this.type = 'percentage'; | ||||
this.max_slices = 10; | |||||
this.max_legend_points = 6; | |||||
this.setup(); | this.setup(); | ||||
} | } | ||||
@@ -37,9 +34,10 @@ export default class PercentageChart extends BaseChart { | |||||
} | } | ||||
render() { | render() { | ||||
this.grand_total = this.sliceTotals.reduce((a, b) => a + b, 0); | |||||
let s = this.state; | |||||
this.grand_total = s.sliceTotals.reduce((a, b) => a + b, 0); | |||||
this.slices = []; | this.slices = []; | ||||
this.sliceTotals.map((total, i) => { | |||||
s.sliceTotals.map((total, i) => { | |||||
let slice = $.create('div', { | let slice = $.create('div', { | ||||
className: `progress-bar`, | className: `progress-bar`, | ||||
inside: this.percentageBar, | inside: this.percentageBar, | ||||
@@ -52,42 +50,8 @@ export default class PercentageChart extends BaseChart { | |||||
}); | }); | ||||
} | } | ||||
calc() { | |||||
this.sliceTotals = []; | |||||
let all_totals = this.data.labels.map((d, i) => { | |||||
let total = 0; | |||||
this.data.datasets.map(e => { | |||||
total += e.values[i]; | |||||
}); | |||||
return [total, d]; | |||||
}).filter(d => { return d[0] > 0; }); // keep only positive results | |||||
let totals = all_totals; | |||||
if(all_totals.length > this.max_slices) { | |||||
all_totals.sort((a, b) => { return b[0] - a[0]; }); | |||||
totals = all_totals.slice(0, this.max_slices-1); | |||||
let others = all_totals.slice(this.max_slices-1); | |||||
let sum_of_others = 0; | |||||
others.map(d => {sum_of_others += d[0];}); | |||||
totals.push([sum_of_others, 'Rest']); | |||||
this.colors[this.max_slices-1] = 'grey'; | |||||
} | |||||
this.labels = []; | |||||
totals.map(d => { | |||||
this.sliceTotals.push(d[0]); | |||||
this.labels.push(d[1]); | |||||
}); | |||||
this.legend_totals = this.sliceTotals.slice(0, this.max_legend_points); | |||||
} | |||||
bindTooltip() { | bindTooltip() { | ||||
let s = this.state; | |||||
// this.slices.map((slice, i) => { | // this.slices.map((slice, i) => { | ||||
// slice.addEventListener('mouseenter', () => { | // slice.addEventListener('mouseenter', () => { | ||||
// let g_off = getOffset(this.chartWrapper), p_off = getOffset(slice); | // let g_off = getOffset(this.chartWrapper), p_off = getOffset(slice); | ||||
@@ -96,29 +60,11 @@ export default class PercentageChart extends BaseChart { | |||||
// let y = p_off.top - g_off.top - 6; | // let y = p_off.top - g_off.top - 6; | ||||
// let title = (this.formatted_labels && this.formatted_labels.length>0 | // let title = (this.formatted_labels && this.formatted_labels.length>0 | ||||
// ? this.formatted_labels[i] : this.labels[i]) + ': '; | // ? this.formatted_labels[i] : this.labels[i]) + ': '; | ||||
// let percent = (this.sliceTotals[i]*100/this.grand_total).toFixed(1); | |||||
// let percent = (s.sliceTotals[i]*100/this.grand_total).toFixed(1); | |||||
// this.tip.set_values(x, y, title, percent + "%"); | // this.tip.set_values(x, y, title, percent + "%"); | ||||
// this.tip.show_tip(); | // this.tip.show_tip(); | ||||
// }); | // }); | ||||
// }); | // }); | ||||
} | } | ||||
renderLegend() { | |||||
// let x_values = this.formatted_labels && this.formatted_labels.length > 0 | |||||
// ? this.formatted_labels : this.labels; | |||||
// this.legend_totals.map((d, i) => { | |||||
// if(d) { | |||||
// let stats = $.create('div', { | |||||
// className: 'stats', | |||||
// inside: this.statsWrapper | |||||
// }); | |||||
// stats.innerHTML = `<span class="indicator"> | |||||
// <i style="background: ${this.colors[i]}"></i> | |||||
// <span class="text-muted">${x_values[i]}:</span> | |||||
// ${d} | |||||
// </span>`; | |||||
// } | |||||
// }); | |||||
} | |||||
} | } |
@@ -1,89 +1,53 @@ | |||||
import BaseChart from './BaseChart'; | |||||
import AggregationChart from './AggregationChart'; | |||||
import { $, getOffset } from '../utils/dom'; | import { $, getOffset } from '../utils/dom'; | ||||
import { makePath } from '../utils/draw'; | |||||
import { getPositionByAngle } from '../utils/helpers'; | |||||
import { makePath, makeArcPathStr } from '../utils/draw'; | |||||
import { lightenDarkenColor } from '../utils/colors'; | import { lightenDarkenColor } from '../utils/colors'; | ||||
import { runSMILAnimation, transform } from '../utils/animation'; | |||||
const ANGLE_RATIO = Math.PI / 180; | |||||
const FULL_ANGLE = 360; | |||||
import { transform } from '../utils/animation'; | |||||
import { FULL_ANGLE } from '../utils/constants'; | |||||
export default class PieChart extends BaseChart { | |||||
export default class PieChart extends AggregationChart { | |||||
constructor(parent, args) { | constructor(parent, args) { | ||||
super(parent, args); | super(parent, args); | ||||
this.type = 'pie'; | this.type = 'pie'; | ||||
this.elements_to_animate = null; | |||||
this.hoverRadio = args.hoverRadio || 0.1; | this.hoverRadio = args.hoverRadio || 0.1; | ||||
this.max_slices = 10; | |||||
this.max_legend_points = 6; | |||||
this.isAnimate = false; | |||||
this.startAngle = args.startAngle || 0; | this.startAngle = args.startAngle || 0; | ||||
this.clockWise = args.clockWise || false; | this.clockWise = args.clockWise || false; | ||||
this.setup(); | |||||
} | |||||
configure(args) { | |||||
super.configure(args); | |||||
this.mouseMove = this.mouseMove.bind(this); | this.mouseMove = this.mouseMove.bind(this); | ||||
this.mouseLeave = this.mouseLeave.bind(this); | this.mouseLeave = this.mouseLeave.bind(this); | ||||
this.setup(); | |||||
} | } | ||||
calc() { | calc() { | ||||
this.centerX = this.width / 2; | |||||
this.centerY = this.height / 2; | |||||
this.radius = (this.height > this.width ? this.centerX : this.centerY); | |||||
this.slice_totals = []; | |||||
let all_totals = this.data.labels.map((d, i) => { | |||||
let total = 0; | |||||
this.data.datasets.map(e => { | |||||
total += e.values[i]; | |||||
}); | |||||
return [total, d]; | |||||
}).filter(d => { return d[0] > 0; }); // keep only positive results | |||||
let totals = all_totals; | |||||
if(all_totals.length > this.max_slices) { | |||||
all_totals.sort((a, b) => { return b[0] - a[0]; }); | |||||
totals = all_totals.slice(0, this.max_slices-1); | |||||
let others = all_totals.slice(this.max_slices-1); | |||||
let sum_of_others = 0; | |||||
others.map(d => {sum_of_others += d[0];}); | |||||
totals.push([sum_of_others, 'Rest']); | |||||
this.colors[this.max_slices-1] = 'grey'; | |||||
super.calc(); | |||||
this.center = { | |||||
x: this.width / 2, | |||||
y: this.height / 2 | |||||
} | } | ||||
this.labels = []; | |||||
totals.map(d => { | |||||
this.slice_totals.push(d[0]); | |||||
this.labels.push(d[1]); | |||||
}); | |||||
this.legend_totals = this.slice_totals.slice(0, this.max_legend_points); | |||||
this.radius = (this.height > this.width ? this.center.x : this.center.y); | |||||
} | } | ||||
static getPositionByAngle(angle,radius) { | |||||
return { | |||||
x:Math.sin(angle * ANGLE_RATIO) * radius, | |||||
y:Math.cos(angle * ANGLE_RATIO) * radius, | |||||
}; | |||||
} | |||||
makeArcPath(startPosition,endPosition){ | |||||
const{centerX,centerY,radius,clockWise} = this; | |||||
return `M${centerX} ${centerY} L${centerX+startPosition.x} ${centerY+startPosition.y} A ${radius} ${radius} 0 0 ${clockWise ? 1 : 0} ${centerX+endPosition.x} ${centerY+endPosition.y} z`; | |||||
} | |||||
render(init) { | render(init) { | ||||
const{radius,clockWise} = this; | const{radius,clockWise} = this; | ||||
this.grand_total = this.slice_totals.reduce((a, b) => a + b, 0); | |||||
this.grand_total = this.state.sliceTotals.reduce((a, b) => a + b, 0); | |||||
const prevSlicesProperties = this.slicesProperties || []; | const prevSlicesProperties = this.slicesProperties || []; | ||||
this.slices = []; | this.slices = []; | ||||
this.elements_to_animate = []; | this.elements_to_animate = []; | ||||
this.slicesProperties = []; | this.slicesProperties = []; | ||||
let curAngle = 180 - this.startAngle; | let curAngle = 180 - this.startAngle; | ||||
this.slice_totals.map((total, i) => { | |||||
this.state.sliceTotals.map((total, i) => { | |||||
const startAngle = curAngle; | const startAngle = curAngle; | ||||
const originDiffAngle = (total / this.grand_total) * FULL_ANGLE; | const originDiffAngle = (total / this.grand_total) * FULL_ANGLE; | ||||
const diffAngle = clockWise ? -originDiffAngle : originDiffAngle; | const diffAngle = clockWise ? -originDiffAngle : originDiffAngle; | ||||
const endAngle = curAngle = curAngle + diffAngle; | const endAngle = curAngle = curAngle + diffAngle; | ||||
const startPosition = PieChart.getPositionByAngle(startAngle,radius); | |||||
const endPosition = PieChart.getPositionByAngle(endAngle,radius); | |||||
const startPosition = getPositionByAngle(startAngle, radius); | |||||
const endPosition = getPositionByAngle(endAngle, radius); | |||||
const prevProperty = init && prevSlicesProperties[i]; | const prevProperty = init && prevSlicesProperties[i]; | ||||
let curStart,curEnd; | let curStart,curEnd; | ||||
if(init){ | if(init){ | ||||
@@ -93,7 +57,7 @@ export default class PieChart extends BaseChart { | |||||
curStart = startPosition; | curStart = startPosition; | ||||
curEnd = endPosition; | curEnd = endPosition; | ||||
} | } | ||||
const curPath = this.makeArcPath(curStart,curEnd); | |||||
const curPath = makeArcPathStr(curStart, curEnd, this.center, this.radius, this.clockWise); | |||||
let slice = makePath(curPath, 'pie-path', 'none', this.colors[i]); | let slice = makePath(curPath, 'pie-path', 'none', this.colors[i]); | ||||
slice.style.transition = 'transform .3s;'; | slice.style.transition = 'transform .3s;'; | ||||
this.drawArea.appendChild(slice); | this.drawArea.appendChild(slice); | ||||
@@ -106,42 +70,44 @@ export default class PieChart extends BaseChart { | |||||
total: this.grand_total, | total: this.grand_total, | ||||
startAngle, | startAngle, | ||||
endAngle, | endAngle, | ||||
angle:diffAngle | |||||
angle: diffAngle | |||||
}); | }); | ||||
if(init){ | if(init){ | ||||
this.elements_to_animate.push([{unit: slice, array: this.slices, index: this.slices.length - 1}, | |||||
{d:this.makeArcPath(startPosition,endPosition)}, | |||||
this.elements_to_animate.push([slice, | |||||
{d: makeArcPathStr(startPosition, endPosition, this.center, this.radius, this.clockWise)}, | |||||
650, "easein",null,{ | 650, "easein",null,{ | ||||
d:curPath | d:curPath | ||||
}]); | }]); | ||||
} | } | ||||
}); | }); | ||||
if(init){ | |||||
runSMILAnimation(this.chartWrapper, this.svg, this.elements_to_animate); | |||||
} | |||||
// if(init){ | |||||
// runSMILAnimation(this.chartWrapper, this.svg, this.elements_to_animate); | |||||
// } | |||||
} | } | ||||
calTranslateByAngle(property){ | calTranslateByAngle(property){ | ||||
const{radius,hoverRadio} = this; | const{radius,hoverRadio} = this; | ||||
const position = PieChart.getPositionByAngle(property.startAngle+(property.angle / 2),radius); | |||||
const position = getPositionByAngle(property.startAngle+(property.angle / 2),radius); | |||||
return `translate3d(${(position.x) * hoverRadio}px,${(position.y) * hoverRadio}px,0)`; | return `translate3d(${(position.x) * hoverRadio}px,${(position.y) * hoverRadio}px,0)`; | ||||
} | } | ||||
hoverSlice(path,i,flag,e){ | hoverSlice(path,i,flag,e){ | ||||
if(!path) return; | if(!path) return; | ||||
const color = this.colors[i]; | const color = this.colors[i]; | ||||
if(flag){ | |||||
transform(path,this.calTranslateByAngle(this.slicesProperties[i])); | |||||
path.style.fill = lightenDarkenColor(color,50); | |||||
if(flag) { | |||||
transform(path, this.calTranslateByAngle(this.slicesProperties[i])); | |||||
path.style.fill = lightenDarkenColor(color, 50); | |||||
let g_off = getOffset(this.svg); | let g_off = getOffset(this.svg); | ||||
let x = e.pageX - g_off.left + 10; | let x = e.pageX - g_off.left + 10; | ||||
let y = e.pageY - g_off.top - 10; | let y = e.pageY - g_off.top - 10; | ||||
let title = (this.formatted_labels && this.formatted_labels.length>0 | |||||
let title = (this.formatted_labels && this.formatted_labels.length > 0 | |||||
? this.formatted_labels[i] : this.labels[i]) + ': '; | ? this.formatted_labels[i] : this.labels[i]) + ': '; | ||||
let percent = (this.slice_totals[i]*100/this.grand_total).toFixed(1); | |||||
let percent = (this.state.sliceTotals[i]*100/this.grand_total).toFixed(1); | |||||
this.tip.set_values(x, y, title, percent + "%"); | this.tip.set_values(x, y, title, percent + "%"); | ||||
this.tip.show_tip(); | this.tip.show_tip(); | ||||
}else{ | |||||
} else { | |||||
transform(path,'translate3d(0,0,0)'); | transform(path,'translate3d(0,0,0)'); | ||||
this.tip.hide_tip(); | this.tip.hide_tip(); | ||||
path.style.fill = color; | path.style.fill = color; | ||||
@@ -169,24 +135,4 @@ export default class PieChart extends BaseChart { | |||||
// this.drawArea.addEventListener('mousemove',this.mouseMove); | // this.drawArea.addEventListener('mousemove',this.mouseMove); | ||||
// this.drawArea.addEventListener('mouseleave',this.mouseLeave); | // this.drawArea.addEventListener('mouseleave',this.mouseLeave); | ||||
} | } | ||||
renderLegend() { | |||||
let x_values = this.formatted_labels && this.formatted_labels.length > 0 | |||||
? this.formatted_labels : this.labels; | |||||
this.legend_totals.map((d, i) => { | |||||
const color = this.colors[i]; | |||||
if(d) { | |||||
let stats = $.create('div', { | |||||
className: 'stats', | |||||
inside: this.statsWrapper | |||||
}); | |||||
stats.innerHTML = `<span class="indicator"> | |||||
<i style="background-color:${color};"></i> | |||||
<span class="text-muted">${x_values[i]}:</span> | |||||
${d} | |||||
</span>`; | |||||
} | |||||
}); | |||||
} | |||||
} | } |
@@ -38,11 +38,9 @@ export function getDifferentChart(type, current_type, parent, args) { | |||||
// Okay, this is anticlimactic | // Okay, this is anticlimactic | ||||
// this function will need to actually be 'changeChartType(type)' | // this function will need to actually be 'changeChartType(type)' | ||||
// that will update only the required elements, but for now ... | // that will update only the required elements, but for now ... | ||||
return new Chart(parent, { | |||||
title: args.title, | |||||
data: args.data, | |||||
type: type, | |||||
height: args.height, | |||||
colors: useColor ? args.colors : undefined | |||||
}); | |||||
args.type = type; | |||||
args.colors = useColor ? args.colors : undefined; | |||||
return new Chart(parent, args); | |||||
} | } |
@@ -16,7 +16,7 @@ const PRESET_COLOR_MAP = { | |||||
}; | }; | ||||
export const DEFAULT_COLORS = ['light-blue', 'blue', 'violet', 'red', 'orange', | export const DEFAULT_COLORS = ['light-blue', 'blue', 'violet', 'red', 'orange', | ||||
'yellow', 'green', 'light-green', 'purple', 'magenta']; | |||||
'yellow', 'green', 'light-green', 'purple', 'magenta', 'light-grey', 'dark-grey']; | |||||
function limitColor(r){ | function limitColor(r){ | ||||
if (r > 255) return 255; | if (r > 255) return 255; | ||||
@@ -16,4 +16,8 @@ export const MIN_BAR_PERCENT_HEIGHT = 0.01; | |||||
export const LINE_CHART_DOT_SIZE = 4; | export const LINE_CHART_DOT_SIZE = 4; | ||||
export const DOT_OVERLAY_SIZE_INCR = 4; | export const DOT_OVERLAY_SIZE_INCR = 4; | ||||
export const DEFAULT_CHAR_WIDTH = 8; | |||||
export const DEFAULT_CHAR_WIDTH = 8; | |||||
// Universal constants | |||||
export const ANGLE_RATIO = Math.PI / 180; | |||||
export const FULL_ANGLE = 360; |
@@ -108,6 +108,16 @@ export function makePath(pathStr, className='', stroke='none', fill='none') { | |||||
}); | }); | ||||
} | } | ||||
export function makeArcPathStr(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${center.x} ${center.y} | |||||
L${arcStartX} ${arcStartY} | |||||
A ${radius} ${radius} 0 0 ${clockWise ? 1 : 0} | |||||
${arcEndX} ${arcEndY} z`; | |||||
} | |||||
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); | ||||
@@ -1,3 +1,5 @@ | |||||
import { ANGLE_RATIO } from './constants'; | |||||
/** | /** | ||||
* Returns the value of a number upto 2 decimal places. | * Returns the value of a number upto 2 decimal places. | ||||
* @param {Number} d Any number | * @param {Number} d Any number | ||||
@@ -74,3 +76,10 @@ export function bindChange(obj, getFn, setFn) { | |||||
} | } | ||||
}); | }); | ||||
} | } | ||||
export function getPositionByAngle(angle, radius) { | |||||
return { | |||||
x:Math.sin(angle * ANGLE_RATIO) * radius, | |||||
y:Math.cos(angle * ANGLE_RATIO) * radius, | |||||
}; | |||||
} |