You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

221 regels
6.8 KiB

  1. import BaseChart from './BaseChart';
  2. import $ from '../utils/dom';
  3. import { lighten_darken_color } from '../utils/colors';
  4. import { runSVGAnimation, transform } from '../utils/animate';
  5. const ANGLE_RATIO = Math.PI / 180;
  6. const FULL_ANGLE = 360;
  7. export default class PieChart extends BaseChart {
  8. constructor(args) {
  9. super(args);
  10. this.type = 'pie';
  11. this.get_y_label = this.format_lambdas.y_label;
  12. this.get_x_tooltip = this.format_lambdas.x_tooltip;
  13. this.get_y_tooltip = this.format_lambdas.y_tooltip;
  14. this.elements_to_animate = null;
  15. this.hoverRadio = args.hoverRadio || 0.1;
  16. this.max_slices = 10;
  17. this.max_legend_points = 6;
  18. this.isAnimate = false;
  19. this.colors = args.colors;
  20. this.startAngle = args.startAngle || 0;
  21. this.clockWise = args.clockWise || false;
  22. if(!this.colors || this.colors.length < this.data.labels.length) {
  23. this.colors = ['#7cd6fd', '#5e64ff', '#743ee2', '#ff5858', '#ffa00a',
  24. '#FEEF72', '#28a745', '#98d85b', '#b554ff', '#ffa3ef'];
  25. }
  26. this.mouseMove = this.mouseMove.bind(this);
  27. this.mouseLeave = this.mouseLeave.bind(this);
  28. this.setup();
  29. }
  30. setup_values() {
  31. this.centerX = this.width / 2;
  32. this.centerY = this.height / 2;
  33. this.radius = (this.height > this.width ? this.centerX : this.centerY);
  34. this.slice_totals = [];
  35. let all_totals = this.data.labels.map((d, i) => {
  36. let total = 0;
  37. this.data.datasets.map(e => {
  38. total += e.values[i];
  39. });
  40. return [total, d];
  41. }).filter(d => { return d[0] > 0; }); // keep only positive results
  42. let totals = all_totals;
  43. if(all_totals.length > this.max_slices) {
  44. all_totals.sort((a, b) => { return b[0] - a[0]; });
  45. totals = all_totals.slice(0, this.max_slices-1);
  46. let others = all_totals.slice(this.max_slices-1);
  47. let sum_of_others = 0;
  48. others.map(d => {sum_of_others += d[0];});
  49. totals.push([sum_of_others, 'Rest']);
  50. this.colors[this.max_slices-1] = 'grey';
  51. }
  52. this.labels = [];
  53. totals.map(d => {
  54. this.slice_totals.push(d[0]);
  55. this.labels.push(d[1]);
  56. });
  57. this.legend_totals = this.slice_totals.slice(0, this.max_legend_points);
  58. }
  59. static getPositionByAngle(angle,radius){
  60. return {
  61. x:Math.sin(angle * ANGLE_RATIO) * radius,
  62. y:Math.cos(angle * ANGLE_RATIO) * radius,
  63. };
  64. }
  65. makeArcPath(startPosition,endPosition){
  66. const{centerX,centerY,radius,clockWise} = this;
  67. 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`;
  68. }
  69. make_graph_components(init){
  70. const{radius,clockWise} = this;
  71. this.grand_total = this.slice_totals.reduce((a, b) => a + b, 0);
  72. const prevSlicesProperties = this.slicesProperties || [];
  73. this.slices = [];
  74. this.elements_to_animate = [];
  75. this.slicesProperties = [];
  76. let curAngle = 180 - this.startAngle;
  77. this.slice_totals.map((total, i) => {
  78. const startAngle = curAngle;
  79. const originDiffAngle = (total / this.grand_total) * FULL_ANGLE;
  80. const diffAngle = clockWise ? -originDiffAngle : originDiffAngle;
  81. const endAngle = curAngle = curAngle + diffAngle;
  82. const startPosition = PieChart.getPositionByAngle(startAngle,radius);
  83. const endPosition = PieChart.getPositionByAngle(endAngle,radius);
  84. const prevProperty = init && prevSlicesProperties[i];
  85. let curStart,curEnd;
  86. if(init){
  87. curStart = prevProperty?prevProperty.startPosition : startPosition;
  88. curEnd = prevProperty? prevProperty.endPosition : startPosition;
  89. }else{
  90. curStart = startPosition;
  91. curEnd = endPosition;
  92. }
  93. const curPath = this.makeArcPath(curStart,curEnd);
  94. let slice = $.createSVG('path',{
  95. inside:this.draw_area,
  96. className:'pie-path',
  97. style:'transition:transform .3s;',
  98. d:curPath,
  99. fill:this.colors[i]
  100. });
  101. this.slices.push(slice);
  102. this.slicesProperties.push({
  103. startPosition,
  104. endPosition,
  105. value:total,
  106. total:this.grand_total,
  107. startAngle,
  108. endAngle,
  109. angle:diffAngle
  110. });
  111. if(init){
  112. this.elements_to_animate.push([{unit: slice, array: this.slices, index: this.slices.length - 1},
  113. {d:this.makeArcPath(startPosition,endPosition)},
  114. 650, "easein",null,{
  115. d:curPath
  116. }]);
  117. }
  118. });
  119. if(init){
  120. this.run_animation();
  121. }
  122. }
  123. run_animation() {
  124. // if(this.isAnimate) return ;
  125. // this.isAnimate = true;
  126. if(!this.elements_to_animate || this.elements_to_animate.length === 0) return;
  127. let anim_svg = runSVGAnimation(this.svg, this.elements_to_animate);
  128. if(this.svg.parentNode == this.chart_wrapper) {
  129. this.chart_wrapper.removeChild(this.svg);
  130. this.chart_wrapper.appendChild(anim_svg);
  131. }
  132. // Replace the new svg (data has long been replaced)
  133. setTimeout(() => {
  134. // this.isAnimate = false;
  135. if(anim_svg.parentNode == this.chart_wrapper) {
  136. this.chart_wrapper.removeChild(anim_svg);
  137. this.chart_wrapper.appendChild(this.svg);
  138. }
  139. }, 650);
  140. }
  141. calTranslateByAngle(property){
  142. const{radius,hoverRadio} = this;
  143. const position = PieChart.getPositionByAngle(property.startAngle+(property.angle / 2),radius);
  144. return `translate3d(${(position.x) * hoverRadio}px,${(position.y) * hoverRadio}px,0)`;
  145. }
  146. hoverSlice(path,i,flag,e){
  147. if(!path) return;
  148. if(flag){
  149. transform(path,this.calTranslateByAngle(this.slicesProperties[i]));
  150. path.setAttribute('fill',lighten_darken_color(this.colors[i],50));
  151. let g_off = $.offset(this.svg);
  152. let x = e.pageX - g_off.left + 10;
  153. let y = e.pageY - g_off.top - 10;
  154. let title = (this.formatted_labels && this.formatted_labels.length>0
  155. ? this.formatted_labels[i] : this.labels[i]) + ': ';
  156. let percent = (this.slice_totals[i]*100/this.grand_total).toFixed(1);
  157. this.tip.set_values(x, y, title, percent + "%");
  158. this.tip.show_tip();
  159. }else{
  160. transform(path,'translate3d(0,0,0)');
  161. this.tip.hide_tip();
  162. path.setAttribute('fill',this.colors[i]);
  163. }
  164. }
  165. mouseMove(e){
  166. const target = e.target;
  167. let prevIndex = this.curActiveSliceIndex;
  168. let prevAcitve = this.curActiveSlice;
  169. for(let i = 0; i < this.slices.length; i++){
  170. if(target === this.slices[i]){
  171. this.hoverSlice(prevAcitve,prevIndex,false);
  172. this.curActiveSlice = target;
  173. this.curActiveSliceIndex = i;
  174. this.hoverSlice(target,i,true,e);
  175. break;
  176. }
  177. }
  178. }
  179. mouseLeave(){
  180. this.hoverSlice(this.curActiveSlice,this.curActiveSliceIndex,false);
  181. }
  182. bind_tooltip() {
  183. this.draw_area.addEventListener('mousemove',this.mouseMove);
  184. this.draw_area.addEventListener('mouseleave',this.mouseLeave);
  185. }
  186. show_summary() {
  187. let x_values = this.formatted_labels && this.formatted_labels.length > 0
  188. ? this.formatted_labels : this.labels;
  189. this.legend_totals.map((d, i) => {
  190. if(d) {
  191. let stats = $.create('div', {
  192. className: 'stats',
  193. inside: this.stats_wrapper
  194. });
  195. stats.innerHTML = `<span class="indicator">
  196. <i style="background-color:${this.colors[i]};"></i>
  197. <span class="text-muted">${x_values[i]}:</span>
  198. ${d}
  199. </span>`;
  200. }
  201. });
  202. }
  203. }