add pie chart

This commit is contained in:
洛丹 2017-11-03 19:41:45 +08:00 committed by pratu16x7
parent 0bc667e7d1
commit 8712e99951
12 changed files with 294 additions and 20 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

@ -88,7 +88,7 @@ let type_chart = new Chart({
parent: "#chart-types",
title: "My Awesome Chart",
data: type_data,
type: 'bar',
type: 'pie',
height: 250,
// is_series: 1
});
@ -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

@ -85,8 +85,9 @@
});</code></pre>
<div id="chart-types" class="border"></div>
<div class="btn-group chart-type-buttons margin-vertical-px mx-auto" role="group">
<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='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 active" data-type='pie'>Pie 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='percentage'>Percentage Chart</button>
</div>

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,219 @@
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 || true;
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 diffAngle = (total / this.grand_total) * FULL_ANGLE;
const endAngle = curAngle = clockWise ? curAngle - diffAngle : 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);
@ -107,7 +116,13 @@ $.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",
@ -120,7 +135,6 @@ $.animateSVG = (element, props, dur, easing_type="linear", type=undefined, old_v
let anim_element = element.cloneNode(true);
let new_element = element.cloneNode(true);
for(var attributeName in props) {
let animate_element;
if(attributeName === 'transform') {
@ -130,7 +144,6 @@ $.animateSVG = (element, props, dur, easing_type="linear", type=undefined, old_v
}
let current_value = old_values[attributeName] || element.getAttribute(attributeName);
let value = props[attributeName];
let anim_attr = {
attributeName: attributeName,
from: current_value,
@ -138,7 +151,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;
}