@@ -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) { | |||
return parseFloat(d.toFixed(2)); | |||
} | |||
@@ -248,6 +268,15 @@ function getStringWidth(string, 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) { | |||
let height, y; | |||
if (yTop <= zeroLine) { | |||
@@ -372,26 +401,6 @@ function animatePath(paths, newXList, newYList, zeroLine) { | |||
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 LABEL_MARGIN = 4; | |||
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) { | |||
let gradientId ='path-fill-gradient' + '-' + color + '-' +(lighter ? 'lighter' : 'default'); | |||
let gradientDef = renderVerticalGradient(svgDefElem, gradientId); | |||
@@ -917,7 +936,7 @@ const PRESET_COLOR_MAP = { | |||
}; | |||
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){ | |||
if (r > 255) return 255; | |||
@@ -986,13 +1005,11 @@ function getDifferentChart(type, current_type, parent, args) { | |||
// Okay, this is anticlimactic | |||
// this function will need to actually be 'changeChartType(type)' | |||
// 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 | |||
@@ -1384,13 +1401,83 @@ class BaseChart { | |||
} | |||
} | |||
class PercentageChart extends BaseChart { | |||
class AggregationChart extends BaseChart { | |||
constructor(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(); | |||
} | |||
@@ -1420,9 +1507,10 @@ class PercentageChart extends BaseChart { | |||
} | |||
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.sliceTotals.map((total, i) => { | |||
s.sliceTotals.map((total, i) => { | |||
let slice = $.create('div', { | |||
className: `progress-bar`, | |||
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() { | |||
// this.slices.map((slice, i) => { | |||
// slice.addEventListener('mouseenter', () => { | |||
// 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 title = (this.formatted_labels && this.formatted_labels.length>0 | |||
// ? 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.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) { | |||
super(parent, args); | |||
this.type = 'pie'; | |||
this.elements_to_animate = null; | |||
this.hoverRadio = args.hoverRadio || 0.1; | |||
this.max_slices = 10; | |||
this.max_legend_points = 6; | |||
this.isAnimate = false; | |||
this.startAngle = args.startAngle || 0; | |||
this.clockWise = args.clockWise || false; | |||
this.mouseMove = this.mouseMove.bind(this); | |||
this.mouseLeave = this.mouseLeave.bind(this); | |||
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) { | |||
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 || []; | |||
this.slices = []; | |||
this.elements_to_animate = []; | |||
this.slicesProperties = []; | |||
let curAngle = 180 - this.startAngle; | |||
this.slice_totals.map((total, i) => { | |||
this.state.sliceTotals.map((total, i) => { | |||
const startAngle = curAngle; | |||
const originDiffAngle = (total / this.grand_total) * FULL_ANGLE; | |||
const diffAngle = clockWise ? -originDiffAngle : originDiffAngle; | |||
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]; | |||
let curStart,curEnd; | |||
if(init){ | |||
@@ -1596,7 +1593,7 @@ class PieChart extends BaseChart { | |||
curStart = startPosition; | |||
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]); | |||
slice.style.transition = 'transform .3s;'; | |||
this.drawArea.appendChild(slice); | |||
@@ -1609,42 +1606,44 @@ class PieChart extends BaseChart { | |||
total: this.grand_total, | |||
startAngle, | |||
endAngle, | |||
angle:diffAngle | |||
angle: diffAngle | |||
}); | |||
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,{ | |||
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){ | |||
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)`; | |||
} | |||
hoverSlice(path,i,flag,e){ | |||
if(!path) return; | |||
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 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 | |||
let title = (this.formatted_labels && this.formatted_labels.length > 0 | |||
? 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.show_tip(); | |||
}else{ | |||
} else { | |||
transform(path,'translate3d(0,0,0)'); | |||
this.tip.hide_tip(); | |||
path.style.fill = color; | |||
@@ -1672,26 +1671,6 @@ class PieChart extends BaseChart { | |||
// this.drawArea.addEventListener('mousemove',this.mouseMove); | |||
// 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 | |||
@@ -2711,7 +2690,8 @@ class AxisChart extends BaseChart { | |||
this.calcYAxisParameters(this.getAllYValues(), this.type === 'line'); | |||
} | |||
calcXPositions(s=this.state) { | |||
calcXPositions() { | |||
let s = this.state; | |||
let labels = this.data.labels; | |||
s.datasetLength = labels.length; | |||
@@ -176,6 +176,8 @@ let type_chart = new Chart("#chart-types", { | |||
xAxisMode: 'tick', | |||
yAxisMode: 'span', | |||
valuesOverPoints: 1, | |||
// maxLegendPoints: 6, | |||
// maxSlices: 3, | |||
isNavigable: 1, | |||
barOptions: { | |||
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'); | |||
} | |||
calcXPositions(s=this.state) { | |||
calcXPositions() { | |||
let s = this.state; | |||
let labels = this.data.labels; | |||
s.datasetLength = labels.length; | |||
@@ -1,14 +1,11 @@ | |||
import BaseChart from './BaseChart'; | |||
import AggregationChart from './AggregationChart'; | |||
import { $, getOffset } from '../utils/dom'; | |||
export default class PercentageChart extends BaseChart { | |||
export default class PercentageChart extends AggregationChart { | |||
constructor(parent, args) { | |||
super(parent, args); | |||
this.type = 'percentage'; | |||
this.max_slices = 10; | |||
this.max_legend_points = 6; | |||
this.setup(); | |||
} | |||
@@ -37,9 +34,10 @@ export default class PercentageChart extends BaseChart { | |||
} | |||
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.sliceTotals.map((total, i) => { | |||
s.sliceTotals.map((total, i) => { | |||
let slice = $.create('div', { | |||
className: `progress-bar`, | |||
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() { | |||
let s = this.state; | |||
// this.slices.map((slice, i) => { | |||
// slice.addEventListener('mouseenter', () => { | |||
// 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 title = (this.formatted_labels && this.formatted_labels.length>0 | |||
// ? 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.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 { makePath } from '../utils/draw'; | |||
import { getPositionByAngle } from '../utils/helpers'; | |||
import { makePath, makeArcPathStr } from '../utils/draw'; | |||
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) { | |||
super(parent, args); | |||
this.type = 'pie'; | |||
this.elements_to_animate = null; | |||
this.hoverRadio = args.hoverRadio || 0.1; | |||
this.max_slices = 10; | |||
this.max_legend_points = 6; | |||
this.isAnimate = false; | |||
this.startAngle = args.startAngle || 0; | |||
this.clockWise = args.clockWise || false; | |||
this.setup(); | |||
} | |||
configure(args) { | |||
super.configure(args); | |||
this.mouseMove = this.mouseMove.bind(this); | |||
this.mouseLeave = this.mouseLeave.bind(this); | |||
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'; | |||
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) { | |||
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 || []; | |||
this.slices = []; | |||
this.elements_to_animate = []; | |||
this.slicesProperties = []; | |||
let curAngle = 180 - this.startAngle; | |||
this.slice_totals.map((total, i) => { | |||
this.state.sliceTotals.map((total, i) => { | |||
const startAngle = curAngle; | |||
const originDiffAngle = (total / this.grand_total) * FULL_ANGLE; | |||
const diffAngle = clockWise ? -originDiffAngle : originDiffAngle; | |||
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]; | |||
let curStart,curEnd; | |||
if(init){ | |||
@@ -93,7 +57,7 @@ export default class PieChart extends BaseChart { | |||
curStart = startPosition; | |||
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]); | |||
slice.style.transition = 'transform .3s;'; | |||
this.drawArea.appendChild(slice); | |||
@@ -106,42 +70,44 @@ export default class PieChart extends BaseChart { | |||
total: this.grand_total, | |||
startAngle, | |||
endAngle, | |||
angle:diffAngle | |||
angle: diffAngle | |||
}); | |||
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,{ | |||
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){ | |||
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)`; | |||
} | |||
hoverSlice(path,i,flag,e){ | |||
if(!path) return; | |||
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 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 | |||
let title = (this.formatted_labels && this.formatted_labels.length > 0 | |||
? 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.show_tip(); | |||
}else{ | |||
} else { | |||
transform(path,'translate3d(0,0,0)'); | |||
this.tip.hide_tip(); | |||
path.style.fill = color; | |||
@@ -169,24 +135,4 @@ export default class PieChart extends BaseChart { | |||
// this.drawArea.addEventListener('mousemove',this.mouseMove); | |||
// 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 | |||
// this function will need to actually be 'changeChartType(type)' | |||
// 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', | |||
'yellow', 'green', 'light-green', 'purple', 'magenta']; | |||
'yellow', 'green', 'light-green', 'purple', 'magenta', 'light-grey', 'dark-grey']; | |||
function limitColor(r){ | |||
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 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) { | |||
let gradientId ='path-fill-gradient' + '-' + color + '-' +(lighter ? 'lighter' : 'default'); | |||
let gradientDef = renderVerticalGradient(svgDefElem, gradientId); | |||
@@ -1,3 +1,5 @@ | |||
import { ANGLE_RATIO } from './constants'; | |||
/** | |||
* Returns the value of a number upto 2 decimal places. | |||
* @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, | |||
}; | |||
} |