Search code examples
chartsqmlslicepie-chart

Arranged lables for QML pie slices


When some values are small in QML pie chart, slice labels are messed up:

enter image description here

How can I arrange slice labels like this?

enter image description here

Note that this is available in telerik and /or dev components for c#.


Solution

  • I used of @luffy 's comment and with some modification, I reached to following code and result:

    import QtQuick 2.4
    
    Rectangle {
        id: root
    
        // public
        property string fontFamily: "sans-serif"
        property int fontPointSize: 9
        property double donutHoleSize: 0.4  //0~1
        property string title: 'title'
        property variant points: []//[['Zero', 60, 'red'], ['One', 40, 'blue']] // y values don't need to add to 100
        width: 500
        height: 700
    
        // private
        onPointsChanged: myCanvas.requestPaint()
    
        Canvas {
            id: myCanvas
            anchors.fill: parent
    
            property double factor: Math.min(width, height)
    
            Text { // title
                text: title
                anchors.horizontalCenter: parent.horizontalCenter
                font.pixelSize: 0.03 * myCanvas.factor
            }
    
            onPaint: {
                var context    = getContext("2d")
                var total      = 0 // automatically calculated from points.y
                var start      = -Math.PI / 2 // Start from vertical. 0 is 3 o'clock and positive is clockwise
                var radius     = 0.25  * myCanvas.factor
                var pixelSize  = 0.03 * myCanvas.factor // text
                context.font   = root.fontPointSize + 'pt ' + root.fontFamily
    
                var i = 0;
                for(i = 0; i < points.length; i++)  total += points[i][1] // total
    
                context.clearRect(0, 0, width, height) // new points data
    
                //--------------------------------------------------------
                var end = 0;
                var center = 0;
                var angle = 0;
                var midSlice = 0;
                var point = 0;
    
                var topRightCnt = 0
                var bottomRightCnt = 0
                var topLeftCnt = 0
                var bottomLeftCnt = 0
                var itemsPos = []
                center = Qt.vector2d(width / 2, height / 2) // center
                for(i = 0; i < points.length; i++) {
                    end    = start + 2 * Math.PI * points[i][1] / total // radians
                    angle = (start + end) / 2 // of line
                    midSlice = Qt.vector2d(Math.cos((end + start) / 2), Math.sin((end + start) / 2)).times(radius) // point on edge/middle of slice
                    point = midSlice.times(1 + 1.4 * (1 - Math.abs(Math.cos(angle)))).plus(center) // elbow of line
                    if(point.x<center.x && point.y<=center.y) {
                        topLeftCnt++;
                        itemsPos[i] = "tl"
                    }
                    else if(point.x<center.x && point.y>center.y) {
                        bottomLeftCnt++;
                        itemsPos[i] = "bl"
                    }
                    else if(point.x>=center.x && point.y<=center.y) {
                        topRightCnt++;
                        itemsPos[i] = "tr"
                    }
                    else {
                        bottomRightCnt++;
                        itemsPos[i] = "br"
                    }
                    start = end // radians
                }
    
                //--------------------------------------------------------
                end = 0;
                angle = 0;
                midSlice = 0;
                point = 0;
                var itemPosCounterTR = 0;
                var itemPosCounterTL = 0;
                var itemPosCounterBR = 0;
                var itemPosCounterBL = 0;
                for(i = 0; i < points.length; i++) {
                    end    = start + 2 * Math.PI * points[i][1] / total // radians
    
                    // pie
                    context.fillStyle = points[i][2]
                    context.beginPath()
                    midSlice = Qt.vector2d(Math.cos((end + start) / 2), Math.sin((end + start) / 2)).times(radius) // point on edge/middle of slice
                    context.arc(center.x, center.y, radius, start, end) // x, y, radius, startingAngle (radians), endingAngle (radians)
                    context.lineTo(center.x, center.y) // center
                    context.fill()
    
                    // text
                    context.fillStyle = points[i][2]
                    angle = (start + end) / 2 // of line
                    point = midSlice.times(1 + 1.4 * (1 - Math.abs(Math.cos(angle)))).plus(center) // elbow of line
    
    
                    //---------------------------------------------
                    var textX = 0;
                    var textY = 0;
                    var dis = 0;
                    var percent   = points[i][1] / total * 100
                    var text      = points[i][0] + ': ' + (percent < 1? '< 1': Math.round(percent)) + '%' // display '< 1%' if < 1
                    var textWidth = context.measureText(text).width
                    var textHeight = 15
                    var diameter = radius * 2
                    var topCircle = center.y - radius
                    var leftCircle = center.x - radius
                    if(itemsPos[i] === "tr") {
                        textX = leftCircle + 1.15 * diameter
                        dis = Math.floor((1.15*radius) / topRightCnt) //Math.floor((height/2) / topRightCnt)
                        dis = (dis < 20 ? 20 : dis)
                        textY = topCircle -(0.15*diameter) + (itemPosCounterTR*dis) + (textHeight/2)
                        itemPosCounterTR++
                    }
                    else if(itemsPos[i] === "br") {
                        textX = leftCircle + 1.15 * diameter
                        dis = Math.floor((1.15*radius) / bottomRightCnt)
                        dis = (dis < 20 ? 20 : dis)
                        textY = topCircle+(1.15*diameter) - ((bottomRightCnt-itemPosCounterBR-1)*dis) - (textHeight/2)
                        itemPosCounterBR++
                    }
                    else if(itemsPos[i] === "tl") {
                        textX = leftCircle - (0.15 * diameter) - textWidth
                        dis = Math.floor((1.15*radius) / topLeftCnt)
                        dis = (dis < 20 ? 20 : dis)
                        textY = topCircle-(0.15*diameter) + ((topLeftCnt-itemPosCounterTL-1)*dis) + (textHeight/2)
                        itemPosCounterTL++;
                    }
                    else {
                        textX = leftCircle - (0.15 * diameter) - textWidth //-0.2 * width - textWidth
                        dis = Math.floor((1.15*radius) / bottomLeftCnt)
                        dis = (dis < 20 ? 20 : dis)
                        textY = topCircle+(1.15*diameter) - (itemPosCounterBL*dis) - (textHeight/2)
                        itemPosCounterBL++
                    }
                    //---------------------------------------------
    
    
                    context.fillText(text, textX, textY)
    
                    // line
                    context.lineWidth   = 1
                    context.strokeStyle = points[i][2]
                    context.beginPath()
                    context.moveTo(center.x + midSlice.x, center.y + midSlice.y) // center
    
                    var endLineX = (point.x < center.x ? (textWidth + 0.5 * pixelSize) : (-0.5 * pixelSize)) + textX;
                    context.lineTo(endLineX, textY+3)
                    context.lineTo(endLineX + (point.x < center.x? -1: 1) * ((0.5 * pixelSize)+textWidth), textY+3) // horizontal
                    context.stroke()
    
                    start = end // radians
                }
    
                if(root.donutHoleSize > 0) {
                    root.donutHoleSize = Math.min(0.99, root.donutHoleSize);
                    var holeRadius = root.donutHoleSize * radius;
                    context.fillStyle = root.color
                    context.beginPath()
                    context.arc(center.x, center.y, holeRadius, 0, 2*Math.PI) // x, y, radius, startingAngle (radians), endingAngle (radians)
                    //context.lineTo(center.x, center.y) // center
                    context.fill()
                }
            }
        }
    
    }
    

    And it's result:

    enter image description here

    Thanks @luffy.