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()
}
}
}
}
结果是:
谢谢@路飞
当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()
}
}
}
}
结果是:
谢谢@路飞