Merge pull request #28 from sheweichun/master

Add Pie Charts!
This commit is contained in:
Prateeksha Singh 2017-11-04 08:13:03 +05:30 committed by GitHub
commit 1f50544bcc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 294 additions and 23 deletions

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

@ -100,8 +100,10 @@ Array.prototype.slice.call(
let btn = e.target;
let type = btn.getAttribute('data-type');
type_chart = type_chart.get_different_chart(type);
let newChart = type_chart.get_different_chart(type);
if(newChart){
type_chart = newChart;
}
Array.prototype.slice.call(
btn.parentNode.querySelectorAll('button')).map(el => {
el.classList.remove('active');
@ -418,3 +420,5 @@ function shuffle(array) {
return array;
}

View File

@ -88,6 +88,7 @@
<button type="button" class="btn btn-sm btn-secondary active" data-type='bar'>Bar Chart</button>
<button type="button" class="btn btn-sm btn-secondary" data-type='line'>Line Chart</button>
<button type="button" class="btn btn-sm btn-secondary" data-type='scatter'>Scatter Chart</button>
<button type="button" class="btn btn-sm btn-secondary" data-type='pie'>Pie Chart</button>
<button type="button" class="btn btn-sm btn-secondary" data-type='percentage'>Percentage Chart</button>
</div>
<p class="text-muted">

View File

@ -4,6 +4,7 @@ import BarChart from './charts/BarChart';
import LineChart from './charts/LineChart';
import ScatterChart from './charts/ScatterChart';
import PercentageChart from './charts/PercentageChart';
import PieChart from './charts/PieChart';
import Heatmap from './charts/Heatmap';
// if (ENV !== 'production') {
@ -19,7 +20,8 @@ const chartTypes = {
bar: BarChart,
scatter: ScatterChart,
percentage: PercentageChart,
heatmap: Heatmap
heatmap: Heatmap,
pie: PieChart
};
function getChartByType(chartType = 'line', options) {

View File

@ -38,7 +38,7 @@ export default class BaseChart {
}
this.has_legend = has_legend;
this.chart_types = ['line', 'scatter', 'bar', 'percentage', 'heatmap'];
this.chart_types = ['line', 'scatter', 'bar', 'percentage', 'heatmap', 'pie'];
this.set_margins(height);
}
@ -51,10 +51,11 @@ export default class BaseChart {
// Only across compatible types
let compatible_types = {
bar: ['line', 'scatter', 'percentage'],
line: ['scatter', 'bar', 'percentage'],
scatter: ['line', 'bar', 'percentage'],
percentage: ['bar', 'line', 'scatter'],
bar: ['line', 'scatter', 'percentage', 'pie'],
line: ['scatter', 'bar', 'percentage', 'pie'],
pie: ['line', 'scatter', 'percentage', 'bar'],
scatter: ['line', 'bar', 'percentage', 'pie'],
percentage: ['bar', 'line', 'scatter', 'pie'],
heatmap: []
};

View File

@ -0,0 +1,220 @@
import BaseChart from './BaseChart';
import $ from '../helpers/dom';
import { lightenDarkenColor } from '../helpers/utils';
const ANGLE_RATIO = Math.PI / 180;
const FULL_ANGLE = 360;
export default class PieChart extends BaseChart {
constructor(args) {
super(args);
this.type = 'pie';
this.get_y_label = this.format_lambdas.y_label;
this.get_x_tooltip = this.format_lambdas.x_tooltip;
this.get_y_tooltip = this.format_lambdas.y_tooltip;
this.elements_to_animate = null;
this.hoverRadio = args.hoverRadio || 0.1;
this.max_slices = 10;
this.max_legend_points = 6;
this.isAnimate = false;
this.colors = args.colors;
this.startAngle = args.startAngle || 0;
this.clockWise = args.clockWise || false;
if(!this.colors || this.colors.length < this.data.labels.length) {
this.colors = ['#7cd6fd', '#5e64ff', '#743ee2', '#ff5858', '#ffa00a',
'#FEEF72', '#28a745', '#98d85b', '#b554ff', '#ffa3ef'];
}
this.mouseMove = this.mouseMove.bind(this);
this.mouseLeave = this.mouseLeave.bind(this);
this.setup();
}
setup_values() {
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);
}
setup_utils() { }
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`;
}
make_graph_components(init){
const{radius,clockWise} = this;
this.grand_total = this.slice_totals.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) => {
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 prevProperty = init && prevSlicesProperties[i];
let curStart,curEnd;
if(init){
curStart = prevProperty?prevProperty.startPosition : startPosition;
curEnd = prevProperty? prevProperty.endPosition : startPosition;
}else{
curStart = startPosition;
curEnd = endPosition;
}
const curPath = this.makeArcPath(curStart,curEnd);
let slice = $.createSVG('path',{
inside:this.draw_area,
className:'pie-path',
style:'transition:transform .3s;',
d:curPath,
fill:this.colors[i]
});
this.slices.push(slice);
this.slicesProperties.push({
startPosition,
endPosition,
value:total,
total:this.grand_total,
startAngle,
endAngle,
angle:diffAngle
});
if(init){
this.elements_to_animate.push([{unit: slice, array: this.slices, index: this.slices.length - 1},
{d:this.makeArcPath(startPosition,endPosition)},
650, "easein",null,{
d:curPath
}]);
}
});
if(init){
this.run_animation();
}
}
run_animation() {
// if(this.isAnimate) return ;
// this.isAnimate = true;
if(!this.elements_to_animate || this.elements_to_animate.length === 0) return;
let anim_svg = $.runSVGAnimation(this.svg, this.elements_to_animate);
if(this.svg.parentNode == this.chart_wrapper) {
this.chart_wrapper.removeChild(this.svg);
this.chart_wrapper.appendChild(anim_svg);
}
// Replace the new svg (data has long been replaced)
setTimeout(() => {
// this.isAnimate = false;
if(anim_svg.parentNode == this.chart_wrapper) {
this.chart_wrapper.removeChild(anim_svg);
this.chart_wrapper.appendChild(this.svg);
}
}, 650);
}
calTranslateByAngle(property){
const{radius,hoverRadio} = this;
const position = PieChart.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;
if(flag){
$.transform(path,this.calTranslateByAngle(this.slicesProperties[i]));
path.setAttribute('fill',lightenDarkenColor(this.colors[i],50));
let g_off = $.offset(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.labels[i]) + ': ';
let percent = (this.slice_totals[i]*100/this.grand_total).toFixed(1);
this.tip.set_values(x, y, title, percent + "%");
this.tip.show_tip();
}else{
$.transform(path,'translate3d(0,0,0)');
this.tip.hide_tip();
path.setAttribute('fill',this.colors[i]);
}
}
mouseMove(e){
const target = e.target;
let prevIndex = this.curActiveSliceIndex;
let prevAcitve = this.curActiveSlice;
for(let i = 0; i < this.slices.length; i++){
if(target === this.slices[i]){
this.hoverSlice(prevAcitve,prevIndex,false);
this.curActiveSlice = target;
this.curActiveSliceIndex = i;
this.hoverSlice(target,i,true,e);
break;
}
}
}
mouseLeave(){
this.hoverSlice(this.curActiveSlice,this.curActiveSliceIndex,false);
}
bind_tooltip() {
this.draw_area.addEventListener('mousemove',this.mouseMove);
this.draw_area.addEventListener('mouseleave',this.mouseLeave);
}
show_summary() {
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.stats_wrapper
});
stats.innerHTML = `<span class="indicator">
<i style="background-color:${this.colors[i]};"></i>
<span class="text-muted">${x_values[i]}:</span>
${d}
</span>`;
}
});
}
}

View File

@ -2,6 +2,16 @@ export default function $(expr, con) {
return typeof expr === "string"? (con || document).querySelector(expr) : expr || null;
}
const EASING = {
ease: "0.25 0.1 0.25 1",
linear: "0 0 1 1",
// easein: "0.42 0 1 1",
easein: "0.1 0.8 0.2 1",
easeout: "0 0 0.58 1",
easeinout: "0.42 0 0.58 1"
};
$.findNodeIndex = (node) =>
{
var i = 0;
@ -83,7 +93,6 @@ $.runSVGAnimation = (svg_container, elements) => {
let anim_element, new_element;
element[0] = obj.unit;
[anim_element, new_element] = $.animateSVG(...element);
new_elements.push(new_element);
@ -108,15 +117,15 @@ $.runSVGAnimation = (svg_container, elements) => {
return anim_svg;
};
$.transform = (element, style)=>{
element.style.transform = style;
element.style.webkitTransform = style;
element.style.msTransform = style;
element.style.mozTransform = style;
element.style.oTransform = style;
};
$.animateSVG = (element, props, dur, easing_type="linear", type=undefined, old_values={}) => {
let easing = {
ease: "0.25 0.1 0.25 1",
linear: "0 0 1 1",
// easein: "0.42 0 1 1",
easein: "0.1 0.8 0.2 1",
easeout: "0 0 0.58 1",
easeinout: "0.42 0 0.58 1"
};
let anim_element = element.cloneNode(true);
let new_element = element.cloneNode(true);
@ -138,7 +147,7 @@ $.animateSVG = (element, props, dur, easing_type="linear", type=undefined, old_v
begin: "0s",
dur: dur/1000 + "s",
values: current_value + ";" + value,
keySplines: easing[easing_type],
keySplines: EASING[easing_type],
keyTimes: "0;1",
calcMode: "spline",
fill: 'freeze'

View File

@ -11,6 +11,25 @@ export function arrays_equal(arr1, arr2) {
return are_equal;
}
function limitColor(r){
if (r > 255) return 255;
else if (r < 0) return 0;
return r;
}
export function lightenDarkenColor(col,amt) {
let usePound = false;
if (col[0] == "#") {
col = col.slice(1);
usePound = true;
}
let num = parseInt(col,16);
let r = limitColor((num >> 16) + amt);
let b = limitColor(((num >> 8) & 0x00FF) + amt);
let g = limitColor((num & 0x0000FF) + amt);
return (usePound?"#":"") + (g | (b << 8) | (r << 16)).toString(16);
}
export function shuffle(array) {
// https://stackoverflow.com/a/2450976/6495043
// Awesomeness: https://bost.ocks.org/mike/shuffle/

View File

@ -187,6 +187,7 @@
color: #6c7680;
}
.indicator::before,
.indicator i ,
.indicator-right::after {
content: '';
display: inline-block;
@ -194,7 +195,7 @@
width: 8px;
border-radius: 8px;
}
.indicator::before {
.indicator::before,.indicator i {
margin: 0 4px 0 0px;
}
.indicator-right::after {
@ -203,71 +204,85 @@
.background.grey,
.indicator.grey::before,
.indicator.grey i,
.indicator-right.grey::after {
background: #bdd3e6;
}
.background.light-grey,
.indicator.light-grey::before,
.indicator.light-grey i,
.indicator-right.light-grey::after {
background: #F0F4F7;
}
.background.blue,
.indicator.blue::before,
.indicator.blue i,
.indicator-right.blue::after {
background: #5e64ff;
}
.background.red,
.indicator.red::before,
.indicator.red i,
.indicator-right.red::after {
background: #ff5858;
}
.background.green,
.indicator.green::before,
.indicator.green i,
.indicator-right.green::after {
background: #28a745;
}
.background.light-green,
.indicator.light-green::before,
.indicator.light-green i,
.indicator-right.light-green::after {
background: #98d85b;
}
.background.orange,
.indicator.orange::before,
.indicator.orange i,
.indicator-right.orange::after {
background: #ffa00a;
}
.background.violet,
.indicator.violet::before,
.indicator.violet i,
.indicator-right.violet::after {
background: #743ee2;
}
.background.dark-grey,
.indicator.dark-grey::before,
.indicator.dark-grey i,
.indicator-right.dark-grey::after {
background: #b8c2cc;
}
.background.black,
.indicator.black::before,
.indicator.black i,
.indicator-right.black::after {
background: #36414C;
}
.background.yellow,
.indicator.yellow::before,
.indicator.yellow i,
.indicator-right.yellow::after {
background: #FEEF72;
}
.background.light-blue,
.indicator.light-blue::before,
.indicator.light-blue i,
.indicator-right.light-blue::after {
background: #7CD6FD;
}
.background.purple,
.indicator.purple::before,
.indicator.purple i,
.indicator-right.purple::after {
background: #b554ff;
}
.background.magenta,
.indicator.magenta::before,
.indicator.magenta i,
.indicator-right.magenta::after {
background: #ffa3ef;
}