QML 饼图切片的排列标签

Arranged lables for QML pie slices

当QML饼图中的某些值较小时,扇区标签乱七八糟:

如何排列这样的切片标签?

请注意,这在 C# 的 telerik 和/或开发组件中可用。

我参考了@luffy的评论,做了一些修改,得到了以下代码和结果:

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()
            }
        }
    }

}

结果是:

谢谢@路飞