skip to Main Content

I’ve created an interactive pie chart using James Alvarez’s Draggable Pie Chart plugin I found on Github. I decided to use an image as the label for each pie piece instead of text. I have the pie chart displaying and functioning as intended, but I’m having issues with how the label images are displaying.

The issue is that the label images have to be rotate/translated along with the pie pieces so they have the proper positions, which results in the logos displaying upside down.

What I would like is for everything to stay as-is, but for the logos to always be right-side-up. Is this possible in the canvas element?

Here’s my code: https://jsfiddle.net/uwx3vv7c/

HTML:

<div id="piechart-controls">
    <canvas id="piechart" width="400" height="400">Your browser is too old!</canvas>
    <div>
        <div class="percentWrapper" style="display: inline-block;">
            <img src="https://cdn4.iconfinder.com/data/icons/seo-web-15/465/web-user-interface_49-128.png">
            <span class="mixPercentage">33%</span>
        </div>
        <div class="percentWrapper" style="display: inline-block;">
            <img src="https://cdn3.iconfinder.com/data/icons/picons-social/57/46-facebook-128.png">
            <span class="mixPercentage">33%</span>
        </div>
        <div class="percentWrapper" style="display: inline-block;">
            <img src="https://cdn3.iconfinder.com/data/icons/picons-social/57/43-twitter-128.png">
            <span class="mixPercentage">34%</span>
        </div>
    </div>
</div>

JS:

(function($){


    $(window).ready(setupPieChart);


    function setupPieChart() {


        let proportions = [
            { proportion: 45, format: { image: "https://cdn4.iconfinder.com/data/icons/seo-web-15/465/web-user-interface_49-128.png" }},
            { proportion: 30, format: { image: "https://cdn3.iconfinder.com/data/icons/picons-social/57/46-facebook-128.png" }},
            { proportion: 25, format: { image: "https://cdn3.iconfinder.com/data/icons/picons-social/57/43-twitter-128.png" }} 
        ];

        let setup = {
            canvas: document.getElementById('piechart'),
            radius: 0.9,
            collapsing: false,
            proportions: proportions,
            drawSegment: drawSegmentOutlineOnly,
            onchange: onPieChartChange,
            minAngle: 1.575
        };

        let newPie = new DraggablePiechart(setup);

        // initial drawing function for pie chart
        function drawSegmentOutlineOnly(context, piechart, centerX, centerY, radius, startingAngle, arcSize, format, collapsed) {

            if (collapsed) { return; }

            // Draw segment
            context.save();
            let endingAngle = startingAngle + arcSize;
            context.beginPath();
            context.moveTo(centerX, centerY);
            context.arc(centerX, centerY, radius, startingAngle, endingAngle, false);
            context.closePath();

            context.fillStyle = '#666';
            context.fill();
            context.stroke();
            context.restore();

            // Draw image
            context.save();
            context.translate(centerX, centerY);
            context.rotate(startingAngle);

            let iconHeight = Math.floor(context.canvas.height / 5);
            let iconWidth = Math.floor(context.canvas.width / 5);
            let dx = (radius / 2) - (iconWidth/2);
            let dy = (radius / 2) - (iconHeight/2);

            let flavorImage = new Image();
            flavorImage.src = format.image;
            context.drawImage(flavorImage, dx, dy, iconWidth, iconHeight);
            context.restore();
        }

        // update the percentages when the pieces are adjusted
        function onPieChartChange(piechart) {

            let percentages = piechart.getAllSliceSizePercentages();
            let percentLabels = $(".mixPercentage");

            for (let i = 0; i < percentages.length; i++) {
                percentLabels.eq(i).html(percentages[i].toFixed(0) + "%");
            }

        }

    }

})(jQuery);

I’ve tried several things, including:

  • not rotating and translating the images, but that causes them to not display properly

  • using the images as background patterns instead of actually drawing them as part of the canvas context, but it either tiles the image or stretches it to the size of the full pie chart.

Bonus question – any way to center them vertically and horizontally in the pie pieces?

2

Answers


  1. Draw scale, rotated image via its center.

    To draw an image rotated and scaled use the following function

    // ctx is the 2D context
    // image is the image to draw
    // x,y where on the canvas the image center will be
    // the scale 1 is no change, < 1 is smaller,  > 1 is larger    
    // angle in radians to rotate the image
    function drawImage(ctx, image, x, y, scale, angle){
        ctx.setTransform(scale, 0, 0, scale, x, y);
        ctx.rotate(angle);
        ctx.drawImage(image, -image.width / 2, -image.height / 2);
        ctx.setTransform(1, 0, 0, 1, 0, 0);  // restore default transform
                                        // not really needed if you call this
                                        // function many times in a row.
                                        // You can restore the default afterwards
    }
    

    If you need to mirror the image, the following function will help, just set the scale for the axis of the image to flip to negative eg drawImage(ctx,image,100,100,1,-1,0); draws image upside down.

    // ctx is the 2D context
    // image is the image to draw
    // x,y where on the canvas the image center will be
    // the scaleX, scaleY 1 is no change, < 1 is smaller,  > 1 is larger    
    // angle in radians to rotate the image
    function drawImageMirror(ctx, image, x, y, scaleX, scaleY, angle){
        ctx.setTransform(scaleX, 0, 0, scaleY, x, y);
        ctx.rotate(angle);
        ctx.drawImage(image, -image.width / 2, -image.height / 2);
        ctx.setTransform(1, 0, 0, 1, 0, 0);  // restore default transform
                                        // not really needed if you call this
                                        // function many times in a row.
                                        // You can restore the default afterwards
    }
    

    To draw at a center of a pie seg that you draw the outside curve with

           context.arc(centerX, centerY, radius, startingAngle, endingAngle, false);
    

    Use the first function

    const scale = Math.min(canvas.width / 5, canvas.height / 5) / flavorImage.width;
    const imgSize = flavorImage.width * scale;
    const centerAngle = (startingAngle + endingAngle) / 2;
    const angle = 0;  // for image directions see comment below
    drawImage(
        context,
        flavorImage,
        Math.cos(centerAngle) * (radius - imgSize) + centerX, 
        Math.sin(centerAngle) * (radius - imgSize) + centerY, 
        scale,
        angle,
     );
    
    // or if you want the top of image to face center use next line
    // const angle = centerAngle - Math.PI / 2;
    // or if you want the bottom of image to face center use next line
    // const angle = centerAngle + Math.PI / 2;
    
    Login or Signup to reply.
  2. Your library gives you the starting angle that you will have to use with the arc() method. It also gives you the arcLength and the center coordinates.

    You already seem to be able to draw the arc segments, but to draw your images, you will need to do some tweaks.

    First, you will move your context’s matrix to the center of the pie chart,
    then you will find the rotation angle so that you face the middle of the current segment, then move on the Y axis to be in the center position of your image, finally, you will rotate by the inverse of the segment’s rotation to make your images face normally.

    // you've got starting angle for arc() method,
    // which itself starts at 3'oclock, while the matrix is noon based
    // so we subtract half PI from the starting angle and add half arcSize to get to the middle
    let rotationAngle = -Math.PI / 2 + (startingAngle + (arcSize / 2));
    // distance from center
    let distance = (radius / 2);
    // position our pointer to the center of the canvas
    context.translate(centerX, centerY);
    // rotate it so we face the middle of the arc
    context.rotate(rotationAngle);
    // move on the Y axis
    context.translate(0, distance);
    
    // now we are at the center X&Y of our arc segment
    // inverse current rotation
    context.rotate(-rotationAngle);
    // draw the image
    context.drawImage(format.image, -iconWidth / 2, -iconHeight / 2, iconWidth, iconHeight);
    
    (function($) {
      $(window).ready(setupPieChart);
    
      function setupPieChart() {
    
        // Instead of storing only the images' src, store directly the HTMLImg elements.
        // Will avoid huge amount of useless loadings
        let proportions = [{
            proportion: 45,
            format: {
              image: Object.assign(new Image, {
                src: "https://cdn4.iconfinder.com/data/icons/seo-web-15/465/web-user-interface_49-128.png"
              })
            }
          },
          {
            proportion: 30,
            format: {
              image: Object.assign(new Image, {
                src: "https://cdn3.iconfinder.com/data/icons/picons-social/57/46-facebook-128.png"
              })
            }
          },
          {
            proportion: 25,
            format: {
              image: Object.assign(new Image, {
                src: "https://cdn3.iconfinder.com/data/icons/picons-social/57/43-twitter-128.png"
              })
            }
          }
        ];
    
        let setup = {
          canvas: document.getElementById('piechart'),
          radius: 0.9,
          collapsing: false,
          proportions: proportions,
          drawSegment: drawSegmentOutlineOnly,
          onchange: onPieChartChange,
          minAngle: 1.575
        };
    
        let newPie = new DraggablePiechart(setup);
    
        // initial drawing function for pie chart
        function drawSegmentOutlineOnly(context, piechart, centerX, centerY, radius, startingAngle, arcSize, format, collapsed) {
          if (collapsed) {
            return;
          }
    
          // because the library needs it for anchors...
          context.save();
    
          // reset context's matrix
          context.setTransform(1, 0, 0, 1, 0, 0);
          // Draw segment
          let endingAngle = startingAngle + arcSize;
          context.beginPath();
          context.moveTo(centerX, centerY);
          context.arc(centerX, centerY, radius, startingAngle, endingAngle, false);
          context.closePath();
    
          context.fillStyle = '#666';
          context.fill();
          context.stroke();
    
          // Draw image
          // you've got starting angle for arc() method, which itself starts at 3'oclock, while the matrix is noon based
          // so we subtract half PI from the starting angle and add half arcSize to get to the middle
          let rotationAngle = -Math.PI / 2 + (startingAngle + (arcSize / 2));
          let iconHeight = Math.floor(context.canvas.height / 5);
          let iconWidth = Math.floor(context.canvas.width / 5);
          // distance from center
          let distance = (radius / 2);
          // position our pointer to the center of the canvas
          context.translate(centerX, centerY);
          // rotate it so we face the middle of the arc
          context.rotate(rotationAngle);
          // move on the Y axis
          context.translate(0, distance);
    
          // now we are at the center X&Y of our arc segment
          // inverse current rotation
          context.rotate(-rotationAngle);
          // draw the image
          context.drawImage(format.image, -iconWidth / 2, -iconHeight / 2, iconWidth, iconHeight);
    
          // because the library needs it for anchors...
          context.restore();
        }
    
        // update the percentages when the pieces are adjusted
        function onPieChartChange(piechart) {
    
          let percentages = piechart.getAllSliceSizePercentages();
          let percentLabels = $(".mixPercentage");
    
          for (let i = 0; i < percentages.length; i++) {
            percentLabels.eq(i).html(percentages[i].toFixed(0) + "%");
          }
    
        }
    
      }
    })(jQuery);
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
    <script src="https://rawgit.com/jamesalvarez/draggable-piechart/master/draggable-piechart.js?ver=4.9"></script>
    <div id="piechart-controls">
      <canvas id="piechart" width="400" height="400">Your browser is too old!</canvas>
      <div>
        <div class="percentWrapper" style="display: inline-block;">
          <img src="https://cdn4.iconfinder.com/data/icons/seo-web-15/465/web-user-interface_49-128.png">
          <span class="mixPercentage">33%</span>
        </div>
        <div class="percentWrapper" style="display: inline-block;">
          <img src="https://cdn3.iconfinder.com/data/icons/picons-social/57/46-facebook-128.png">
          <span class="mixPercentage">33%</span>
        </div>
        <div class="percentWrapper" style="display: inline-block;">
          <img src="https://cdn3.iconfinder.com/data/icons/picons-social/57/43-twitter-128.png">
          <span class="mixPercentage">34%</span>
        </div>
      </div>
    </div>
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search