交互式甜甜圈网络图表

Interactive Doughnut Web Chart

我有一个图表,我正在努力使它更加用户友好,但我一直 运行 遇到问题试图添加一个东西,其他东西出错了。

我想要完成的是:图表从一个处于 'hovered' 状态的图表开始。当鼠标悬停在上面时,它有一个边框,图表中间的文本也会随着标签和百分比更新。我还希望背景是 SVG 图像(如果不可能则不重要)。

我做了一个 fiddle - 我也想删除工具提示。

如有任何帮助,我们将不胜感激!

我正在努力完成的 IMG:

我的HTML代码:

<div id="doughnutChart" class="chart"></div>

jQuery

jQuery(function(){
  jQuery("#doughnutChart").drawDoughnutChart([
    { title: "Holiday Fund",         value : 5,  color: "#2C3E50" },
    { title: "Emergencies", value:  20,   color: "#FC4349" },
    { title: "Loans",      value:  20,   color: "#6DBCDB" },
    { title: "Widows",        value : 27,   color: "#F7E248" },
    { title: "Medical Support",        value : 28,   color: "#D7DADB" },
  ]);
});
/*!
 * jquery.drawDoughnutChart.js
 * Version: 0.4.1(Beta)
 * Inspired by Chart.js(http://www.chartjs.org/)
 *
 * Copyright 2014 hiro
 * https://github.com/githiro/drawDoughnutChart
 * Released under the MIT license.
 * 
 */
;(function($, undefined) {
  $.fn.drawDoughnutChart = function(data, options) {
    var $this = this,
      W = $this.width(),
      H = $this.height(),
      centerX = W/2,
      centerY = H/2,
      cos = Math.cos,
      sin = Math.sin,
      PI = Math.PI,
      settings = $.extend({
        segmentShowStroke : true,
        segmentStrokeColor : "#0C1013",
        segmentStrokeWidth : 1,
        baseColor: "rgba(0,0,0,0.5)",
        baseOffset: 4,
        edgeOffset : 10,//offset from edge of $this
        percentageInnerCutout : 75,
        animation : true,
        animationSteps : 90,
        animationEasing : "easeInOutExpo",
        animateRotate : true,
        tipOffsetX: -8,
        tipOffsetY: -45,
        tipClass: "doughnutTip",
        summaryClass: "doughnutSummary",
        summaryTitle: "",
        summaryTitleClass: "doughnutSummaryTitle",
        summaryNumberClass: "doughnutSummaryNumber",
        beforeDraw: function() {  },
        afterDrawed : function() {  },
        onPathEnter : function(e,data) {  },
        onPathLeave : function(e,data) {  }
      }, options),
      animationOptions = {
        linear : function (t) {
          return t;
        },
        easeInOutExpo: function (t) {
          var v = t<.5 ? 8*t*t*t*t : 1-8*(--t)*t*t*t;
          return (v>1) ? 1 : v;
        }
      },
      requestAnimFrame = function() {
        return window.requestAnimationFrame ||
          window.webkitRequestAnimationFrame ||
          window.mozRequestAnimationFrame ||
          window.oRequestAnimationFrame ||
          window.msRequestAnimationFrame ||
          function(callback) {
            window.setTimeout(callback, 1000 / 60);
          };
      }();

    settings.beforeDraw.call($this);

    var $svg = $('<svg width="' + W + '" height="' + H + '" viewBox="0 0 ' + W + ' ' + H + '" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"></svg>').appendTo($this),
        $paths = [],
        easingFunction = animationOptions[settings.animationEasing],
        doughnutRadius = Min([H / 2,W / 2]) - settings.edgeOffset,
        cutoutRadius = doughnutRadius * (settings.percentageInnerCutout / 100),
        segmentTotal = 0;

    //Draw base doughnut
    var baseDoughnutRadius = doughnutRadius + settings.baseOffset,
        baseCutoutRadius = cutoutRadius - settings.baseOffset;
    $(document.createElementNS('http://www.w3.org/2000/svg', 'path'))
      .attr({
        "d": getHollowCirclePath(baseDoughnutRadius, baseCutoutRadius),
        "fill": settings.baseColor
      })
      .appendTo($svg);

    //Set up pie segments wrapper
    var $pathGroup = $(document.createElementNS('http://www.w3.org/2000/svg', 'g'));
    $pathGroup.attr({opacity: 0}).appendTo($svg);

    //Set up tooltip
    var $tip = $('<div class="' + settings.tipClass + '" />').appendTo('body').hide(),
        tipW = $tip.width(),
        tipH = $tip.height();


    for (var i = 0, len = data.length; i < len; i++) {
      segmentTotal += data[i].value;
      $paths[i] = $(document.createElementNS('http://www.w3.org/2000/svg', 'path'))
        .attr({
          "stroke-width": settings.segmentStrokeWidth,
          "stroke": settings.segmentStrokeColor,
          "fill": data[i].color,
          "data-order": i,
          "class": 'counter-'+i
        })
        .appendTo($pathGroup)
        .on("mouseenter", pathMouseEnter)
        .on("mouseleave", pathMouseLeave)
        .on("mousemove", pathMouseMove);
    }

     //Set up center text area
    var summarySize = (cutoutRadius - (doughnutRadius - cutoutRadius)) * 2,
        $summary = $('<div class="' + settings.summaryClass + '" />')
                   .appendTo($this)
                   .css({ 
                     width: summarySize + "px",
                     height: summarySize + "px",
                     "margin-left": -(summarySize / 2) + "px",
                     "margin-top": -(summarySize / 2) + "px"
                   });
    var $summaryTitle = $('<p class="' + settings.summaryTitleClass + '">' + data[0].title + "<br />" + data[0].value + '%' + '</p>').appendTo($summary);
    //var $summaryNumber = $('<p class="' + settings.summaryNumberClass + '"></p>').appendTo($summary).css({opacity: 0});


    //Animation start
    animationLoop(drawPieSegments);

    //Functions
    function getHollowCirclePath(doughnutRadius, cutoutRadius) {
        //Calculate values for the path.
        //We needn't calculate startRadius, segmentAngle and endRadius, because base doughnut doesn't animate.
        var startRadius = -1.570,// -Math.PI/2
            segmentAngle = 6.2831,// 1 * ((99.9999/100) * (PI*2)),
            endRadius = 4.7131,// startRadius + segmentAngle
            startX = centerX + cos(startRadius) * doughnutRadius,
            startY = centerY + sin(startRadius) * doughnutRadius,
            endX2 = centerX + cos(startRadius) * cutoutRadius,
            endY2 = centerY + sin(startRadius) * cutoutRadius,
            endX = centerX + cos(endRadius) * doughnutRadius,
            endY = centerY + sin(endRadius) * doughnutRadius,
            startX2 = centerX + cos(endRadius) * cutoutRadius,
            startY2 = centerY + sin(endRadius) * cutoutRadius;
        var cmd = [
          'M', startX, startY,
          'A', doughnutRadius, doughnutRadius, 0, 1, 1, endX, endY,//Draw outer circle
          'Z',//Close path
          'M', startX2, startY2,//Move pointer
          'A', cutoutRadius, cutoutRadius, 0, 1, 0, endX2, endY2,//Draw inner circle
          'Z'
        ];
        cmd = cmd.join(' ');
        return cmd;
    };
    function pathMouseEnter(e) {
      var order = $(this).data().order;
      $tip.text(data[order].title + ": " + data[order].value)
          .fadeIn(200);
      settings.onPathEnter.apply($(this),[e,data]);
      $('.doughnutSummaryTitle').html(data[order].title + "<br />" + data[order].value + '%')
    }
    function pathMouseLeave(e) {
      $tip.hide();
      settings.onPathLeave.apply($(this),[e,data]);
    }
    function pathMouseMove(e) {
      $tip.css({
        top: e.pageY + settings.tipOffsetY,
        left: e.pageX - $tip.width() / 2 + settings.tipOffsetX
      });
    }
    function drawPieSegments (animationDecimal) {
      var startRadius = -PI / 2,//-90 degree
          rotateAnimation = 1;
      if (settings.animation && settings.animateRotate) rotateAnimation = animationDecimal;//count up between0~1

      //drawDoughnutText(animationDecimal, segmentTotal);

      $pathGroup.attr("opacity", animationDecimal);

      //If data have only one value, we draw hollow circle(#1).
      if (data.length === 1 && (4.7122 < (rotateAnimation * ((data[0].value / segmentTotal) * (PI * 2)) + startRadius))) {
        $paths[0].attr("d", getHollowCirclePath(doughnutRadius, cutoutRadius));
        return;
      }
      for (var i = 0, len = data.length; i < len; i++) {
        var segmentAngle = rotateAnimation * ((data[i].value / segmentTotal) * (PI * 2)),
            endRadius = startRadius + segmentAngle,
            largeArc = ((endRadius - startRadius) % (PI * 2)) > PI ? 1 : 0,
            startX = centerX + cos(startRadius) * doughnutRadius,
            startY = centerY + sin(startRadius) * doughnutRadius,
            endX2 = centerX + cos(startRadius) * cutoutRadius,
            endY2 = centerY + sin(startRadius) * cutoutRadius,
            endX = centerX + cos(endRadius) * doughnutRadius,
            endY = centerY + sin(endRadius) * doughnutRadius,
            startX2 = centerX + cos(endRadius) * cutoutRadius,
            startY2 = centerY + sin(endRadius) * cutoutRadius;
        var cmd = [
          'M', startX, startY,//Move pointer
          'A', doughnutRadius, doughnutRadius, 0, largeArc, 1, endX, endY,//Draw outer arc path
          'L', startX2, startY2,//Draw line path(this line connects outer and innner arc paths)
          'A', cutoutRadius, cutoutRadius, 0, largeArc, 0, endX2, endY2,//Draw inner arc path
          'Z'//Cloth path
        ];
        $paths[i].attr("d", cmd.join(' '));
        startRadius += segmentAngle;
      }
    }
    //function drawDoughnutText(animationDecimal, segmentTotal) {
     // $summaryNumber
      //  .css({opacity: animationDecimal})
      //  .text((segmentTotal * animationDecimal).toFixed(1));
    //}
    function animateFrame(cnt, drawData) {
      var easeAdjustedAnimationPercent =(settings.animation)? CapValue(easingFunction(cnt), null, 0) : 1;
      drawData(easeAdjustedAnimationPercent);
    }
    function animationLoop(drawData) {
      var animFrameAmount = (settings.animation)? 1 / CapValue(settings.animationSteps, Number.MAX_VALUE, 1) : 1,
          cnt =(settings.animation)? 0 : 1;
      requestAnimFrame(function() {
          cnt += animFrameAmount;
          animateFrame(cnt, drawData);
          if (cnt <= 1) {
            requestAnimFrame(arguments.callee);
          } else {
            settings.afterDrawed.call($this);
          }
      });
    }
    function Max(arr) {
      return Math.max.apply(null, arr);
    }
    function Min(arr) {
      return Math.min.apply(null, arr);
    }
    function isNumber(n) {
      return !isNaN(parseFloat(n)) && isFinite(n);
    }
    function CapValue(valueToCap, maxValue, minValue) {
      if (isNumber(maxValue) && valueToCap > maxValue) return maxValue;
      if (isNumber(minValue) && valueToCap < minValue) return minValue;
      return valueToCap;
    }
    return $this;
  };
})(jQuery);

CSS

.chart {
  position: absolute;
  width: 450px;
  height: 450px;
  top: 50%;
  left: 50%;
  margin: -225px 0 0 -225px;
}
.doughnutTip {
  position: absolute;
  min-width: 30px;
  max-width: 300px;
  padding: 5px 15px;
  border-radius: 1px;
  background: rgba(0,0,0,.8);
  color: #ddd;
  font-size: 17px;
  text-shadow: 0 1px 0 #000;
  text-transform: uppercase;
  text-align: center;
  line-height: 1.3;
  letter-spacing: .06em;
  box-shadow: 0 1px 3px rgba(0,0,0,0.5);
  pointer-events: none;
  &::after {
      position: absolute;
      left: 50%;
      bottom: -6px;
      content: "";
      height: 0;
      margin: 0 0 0 -6px;
      border-right: 5px solid transparent;
      border-left: 5px solid transparent;
      border-top: 6px solid rgba(0,0,0,.7);
      line-height: 0;
  }
}
.doughnutSummary {
  position: absolute;
  top: 50%;
  left: 50%;
  color: #000;
  text-align: center;
  text-shadow: 0 -1px 0 #111;
  cursor: default;
}
.doughnutSummaryTitle {
  position: absolute;
  top: 50%;
  width: 100%;
  margin-top: -27%;
  font-size: 22px;
  letter-spacing: .06em;
}
.doughnutSummaryNumber {
  position: absolute;
  top: 50%;
  width: 100%;
  margin-top: -15%;
  font-size: 55px;
}
.chart path:hover { opacity: 0.65; } 

JSFIDDLE

无需修改原始插件drawDoughnutChart,您可以覆盖默认设置。

对于外笔画,您可以绘制另一个图表,将其隐藏,然后使用插件公开的 afterDrawedonPathEnteronPathLeave 回调来执行逻辑。

对于每个饼图的背景,您可以使用svg pattern

Javascript:

var firstSelected = 0;

var seed = [
    { title: "Holiday Fund", value : 5,  color: "url(#dots2)" },
    { title: "Emergencies", value:  20, color: "url(#diagonal1)" },
    { title: "Loans", value:  20, color: "url(#dots1)" },
    { title: "Widows", value : 27, color: "url(#diagonal2)" },
    { title: "Medical Support", value : 28, color: "url(#hatch1)" },
  ];

var seed2 = [
    { value : 5,  color: "#9fa1ac" },
    { value:  20, color: "#ef5123" },
    { value:  20, color: "#ef5123" },
    { value : 27, color: "#9fa1ac" },
    { value : 28, color: "#ef5123" },
  ];

var chartOptions = {
    baseOffset: 0,
    segmentShowStroke : false,
    segmentStrokeColor : 'transparent',
    baseColor: 'transparent',
    percentageInnerCutout : 60,        
    onPathEnter: function (e, data) {
        var order = $(this).data().order;
        $('#doughnutChart .doughnutSummaryTitle').html(data[order].title);
        $('#doughnutChart .doughnutSummaryNumber').html(data[order].value + '%');
        $('#doughnutChart .doughnutSummary').show();
        $('#doughnutBg g').find('path').fadeOut(300);
        $('#doughnutBg g').find('path:eq('+(order)+')').fadeIn(500)            
    },
    onPathLeave: function (e, data) {
        $('#doughnutBg g').find('path').fadeOut(300);
        $('#doughnutChart .doughnutSummary').hide();
    },
    afterDrawed : function () {
        $('#doughnutChart .doughnutSummaryTitle').html(seed[firstSelected].title);
        $('#doughnutChart .doughnutSummaryNumber').html(seed[firstSelected].value + '%');            
        $('#doughnutChart .doughnutSummary').css({width: '160px', height: '60px', marginLeft: '-80px', marginTop: '-30px'});
        $('#doughnutChart .doughnutSummary').show();
        $('#doughnutBg g').find('path:eq('+firstSelected+')').fadeIn(500);
    }
} ;

var chartOptions2 = {
    baseOffset: 0,
    segmentStrokeColor : 'transparent',        
    segmentShowStroke : false,
    percentageInnerCutout : 95
} ;

jQuery("#doughnutChart").drawDoughnutChart(seed, chartOptions);
jQuery("#doughnutBg").drawDoughnutChart(seed2, chartOptions2);

CSS:

.chart {
  position: absolute;
  width: 400px;
  height: 400px;
  top: 50%;
  left: 50%;
  margin: -200px 0 0 -200px;
}

.bgchart {
  position: absolute;
  width: 430px;
  height: 430px;
  top: 50%;
  left: 50%;
  margin: -215px 0 0 -215px;
}

.chart .doughnutSummary,
.bgchart .doughnutSummary{
  display: none;
}

.chart path:hover { opacity: 0.65; }
.bgchart path { display: none }

HTML:

<svg width="8cm" height="4cm" viewBox="0 0 800 400" version="1.1"
 xmlns="http://www.w3.org/2000/svg">
  <defs>
    <pattern id="hatch1" patternUnits="userSpaceOnUse" x="0" y="0" width="10" height="10">
      <g style="fill:none; stroke:#ef5123; stroke-width:1">
        <path d="M0,0 l10,10"/><path d="M10,0 l-10,10"/>
      </g>
    </pattern>
    <pattern id="hatch2" patternUnits="userSpaceOnUse" x="0" y="0" width="10" height="10">
      <g style="fill:none; stroke:#9fa1ac; stroke-width:1">
        <path d="M0,0 l10,10"/><path d="M10,0 l-10,10"/>
      </g>
    </pattern>        
    <pattern id="diagonal1" x="0" y="0" width="6" height="6" patternUnits="userSpaceOnUse" patternTransform="rotate(130)">
       <rect x="0" y="0" width="2" height="6" style="stroke:none; fill:#ef5123;" />
    </pattern>
    <pattern id="diagonal2" x="0" y="0" width="6" height="6" patternUnits="userSpaceOnUse" patternTransform="rotate(130)">
       <rect x="0" y="0" width="2" height="6" style="stroke:none; fill:#9fa1ac;" />
    </pattern>      
    <pattern id="dots1" x="0" y="0" width="5" height="5" patternUnits="userSpaceOnUse" patternTransform="rotate(45)">
       <circle cx="2" cy="2" r="1" style="stroke:none; fill:#ef5123;" />
    </pattern>
    <pattern id="dots2" x="0" y="0" width="5" height="5" patternUnits="userSpaceOnUse" patternTransform="rotate(45)">
       <circle cx="2" cy="2" r="1" style="stroke:none; fill:#9fa1ac;" />
    </pattern>         
  </defs>
</svg>
<div class="bgchart" id="doughnutBg"></div>
<div class="chart" id="doughnutChart"></div>

DEMO