In this chapter, we’ll present a more elaborate example: we’ll create a pie chart item that visualizes the data of a Qt Quick ListModel similar to the Qt Widget example [http://qt-project.org/doc/qt-4.8/itemviews-chart.html]. The ListModel’s values are shown in a ListView next to the chart. If one of its entries is selected, the according sector of the pie chart gets highlighted.
The final application looks like this:
Let’s start with the layout of the application and the context in which the pie chart lives. This means setting up the ListModel, placing the Canvas and creating the ListView.
The data model holds roles for the item’s labels, values and color values. We populate this model with an example dataset:
import QtQuick 2.0
Rectangle {
id: root
width: 640
height: 360
color: "#3C3C3C"
ListModel {
id: model
ListElement { label:"Scientific Research";
value:21; color:"#99e600" }
ListElement { label:"Engineering & Design";
value:18; color:"#99cc00" }
ListElement { label:"Automotive";
value:14; color:"#99b300" }
ListElement { label:"Aerospace";
value:13; color:"#9f991a" }
ListElement { label:"Automation & Machine Tools";
value:13; color:"#a48033" }
ListElement { label:"Medical & Bioinformatics";
value:13; color:"#a9664d" }
ListElement { label:"Imaging & Special Effects";
value:12; color:"#ae4d66" }
ListElement { label:"Defense";
value:11; color:"#b33380" }
ListElement { label:"Test & Measurement Systems";
value:9; color:"#a64086" }
ListElement { label:"Oil & Gas";
value:9; color:"#994d8d" }
ListElement { label:"Entertainment & Broadcasting";
value:7; color:"#8d5a93" }
ListElement { label:"Financial";
value:6; color:"#806699" }
ListElement { label:"Consumer Electronics";
value:4; color:"#8073a6" }
ListElement { label:"Other";
value:38; color:"#8080b3" }
}
...
The canvas that shows the pie chart is placed in the left half of the application. We’ll be implementing the drawing later in the following sections:
...
Canvas {
id: canvas
anchors.top: parent.top
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.right: parent.horizontalCenter
}
...
On the right side, we display the model’s data in a list view, which shows the item’s labels and values in colored rows. It represents the legend of the chart so to say:
...
ListView {
id: view
anchors.top: parent.top
anchors.bottom: parent.bottom
anchors.left: parent.horizontalCenter
anchors.right: parent.right
anchors.margins: 16
clip: true
focus: true
model: model
delegate: Item {
width: view.width
height: 32
Rectangle {
anchors.fill: parent
anchors.margins: 1
radius: 2
color: model.color
border.color: Qt.lighter(root.color)
}
Text {
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left
anchors.margins: 8
text: model.label
color: "#1C1C1C"
}
Text {
anchors.verticalCenter: parent.verticalCenter
anchors.right: parent.right
anchors.margins: 8
text: model.value
color: "#1C1C1C"
}
}
}
}
A pie chart is a circular chart consisting of several sectors - one for each data set. The area of each sector is proportional to the value it represents. So let’s first take a look at how to draw one of the pie chart sectors.
The size of a sector is defined by the length of its arc or the arc’s subtending angle. If you think of the whole pie having an angle of 2 PI, each sector should cover an angle of value * (2 * PI) / SUM_OF_VALUES.
To draw the arc, we use the arc(real x, real y, real radius, real startAngle, real endAngle, bool anticlockwise) function. This function creates a circular path on the circumference of a circle, which is centered around (x, y) and has the given radius. To complete the pie segment, we also need to draw the lines that go from the circle’s center to the arc’s edges. Therefore we move the cursor’s position to the center point, draw the arc, and then draw a line back to the center. The arc() function automatically generates a line from the cursor’s initial position at the center to the starting point of the arc in addition to the arc itself.
/****************************************************************************
**
** Copyright (C) 2012 Digia Plc and/or its subsidiary(-ies).
** Contact: http://www.qt-project.org/legal
**
** $QT_BEGIN_LICENSE:BSD$
** You may use this file under the terms of the BSD license as follows:
**
** "Redistribution and use in source and binary forms, with or without
** modification, are permitted provided that the following conditions are
** met:
** * Redistributions of source code must retain the above copyright
** notice, this list of conditions and the following disclaimer.
** * Redistributions in binary form must reproduce the above copyright
** notice, this list of conditions and the following disclaimer in
** the documentation and/or other materials provided with the
** distribution.
** * Neither the name of Digia Plc and its Subsidiary(-ies) nor the names
** of its contributors may be used to endorse or promote products derived
** from this software without specific prior written permission.
**
**
** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE."
**
** $QT_END_LICENSE$
**
****************************************************************************/
// begin a new path
ctx.beginPath()
// move the cursor to the center
ctx.moveTo(centerX, centerY)
// add the arc including the line to the beginning of the arc
ctx.arc(centerX, centerY, radius, startAngle, endAngle, anticlockwise)
// add the line back to the center
ctx.lineTo(centerX, centerY)
// fill the piece
ctx.fillStyle = fillStyle
ctx.fill()
// stroke the piece
ctx.lineWidth = lineWidth
ctx.strokeStyle = strokeStyle
ctx.stroke()
This is how the output of an application using the code described above looks:
Next, we will draw the complete pie chart. After this step, the application looks like this:
In the onPaint handler, we iterate over the model’s elements and draw one pie sector for each entry (with its specific start and end angle, and filled with the previously assigned color). To make this set of pieces sum up to form a full circle, we also need to know the model’s sum of values. We calculate this value in a JavaScript function.
...
Canvas {
id: canvas
anchors.top: parent.top
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.right: parent.horizontalCenter
// enable anti-aliasing
smooth: true
onPaint: {
var ctx = canvas.getContext('2d')
ctx.clearRect(0, 0, width, height)
// store the circles properties
var centerX = width / 2
var centerY = height / 2
var radius = 0.9 * Math.min(width, height) / 2
var startAngle = 0.0
var endAngle = 0.0
// calculate the factor that scales the angles
// to make the sectors sum up to a full circle
var angleFactor = 2 * Math.PI / modelSum()
ctx.lineWidth = 2
ctx.strokeStyle = Qt.lighter(root.color)
// iterate over the model's elements
for (var index = 0; index < model.count; index++) {
// calculate the start and end angles
startAngle = endAngle
endAngle = startAngle + model.get(index).value * angleFactor
ctx.fillStyle = model.get(index).color
// draw the piece
ctx.beginPath()
ctx.moveTo(centerX, centerY)
ctx.arc(centerX, centerY, radius, startAngle, endAngle, false)
ctx.lineTo(centerX, centerY)
ctx.fill()
ctx.stroke()
}
}
// calculate the model's sum of values
function modelSum() {
var modelSum = 0
for (var index = 0; index < model.count; index++) {
modelSum += model.get(index).value
}
return modelSum
}
}
...
In this chapter we’ll enhance the pie chart’s appearance and also provide interactivity. This is how the application looks after the enhancement:
In order to make the application more alive, we change the chart’s appearance according to the list view’s currently selected item. We add a mouse area to the list view and mark the current item with a highlight. To make the canvas respond to changes in the selection, we request a repaint whenever the current item changes.
...
ListView {
id: view
anchors.top: parent.top
anchors.bottom: parent.bottom
anchors.left: parent.horizontalCenter
anchors.right: parent.right
anchors.margins: 16
clip: true
focus: true
model: model
delegate: Item {
width: view.width
height: 32
Rectangle {
anchors.fill: parent
anchors.margins: 1
radius: 2
color: model.color
border.color: Qt.lighter(root.color)
}
Text {
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left
anchors.margins: 8
text: model.label
color: "#1C1C1C"
}
Text {
anchors.verticalCenter: parent.verticalCenter
anchors.right: parent.right
anchors.margins: 8
text: model.value
color: "#1C1C1C"
}
// handle mouse clicks
MouseArea {
anchors.fill: parent
onClicked: {
view.currentIndex = index
}
}
}
// highlight the currently selected item
highlight: Item {
z: 10
width: view.currentItem.width
height: view.currentItem.height
Rectangle {
anchors.fill: parent
anchors.margins: 1
radius: 2
color: "transparent"
border.width: 3
border.color: Qt.lighter(model.get(view.currentIndex).color)
Behavior on border.color {
PropertyAnimation {}
}
}
}
// request a repaint of the canvas whenever
// the currently selected item changes
onCurrentIndexChanged: {
canvas.requestPaint()
}
}
...
The pie chart sector representing the selected entry is also highlighted by increasing the sector’s radius by 2%. We also want the highlighted sector to be on the right side of the chart, so we rotate the canvas based on the list view’s currentItem property. To smooth this change, we apply a behavior to the rotation using a spring animation. Finally, we overlay a radial gradient from transparent white to a darker gray to further brush up the pie chart’s appearance.
...
Canvas {
id: canvas
anchors.top: parent.top
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.right: parent.horizontalCenter
smooth: true
// animate the rotation
Behavior on rotation {
SpringAnimation { spring: 1; damping: 0.2 }
}
onPaint: {
var ctx = canvas.getContext('2d')
ctx.clearRect(0, 0, width, height)
var centerX = width / 2
var centerY = height / 2
var radius = 0.9 * Math.min(width, height) / 2
var radiusFactor = 1.0
var startAngle = 0.0
var endAngle = 0.0
var angleFactor = 2 * Math.PI / modelSum()
ctx.lineWidth = 2
ctx.strokeStyle = Qt.lighter(root.color)
for (var index = 0; index < model.count; index++) {
startAngle = endAngle
endAngle = startAngle + model.get(index).value * angleFactor
// scale the currently selected piece and
// rotate the canvas element accordingly
if (index == view.currentIndex) {
radiusFactor = 1.02
canvas.rotation = - 180 / Math.PI * (startAngle +
(endAngle - startAngle) / 2)
} else {
radiusFactor = 1.0
}
ctx.fillStyle = model.get(index).color
ctx.beginPath()
ctx.moveTo(centerX, centerY)
ctx.arc(centerX, centerY, radius * radiusFactor,
startAngle, endAngle, false)
ctx.lineTo(centerX, centerY)
ctx.fill()
ctx.stroke()
}
// overlay a radial gradient
var gradient = ctx.createRadialGradient(centerX, centerY,
0, centerX, centerY, radius)
gradient.addColorStop(0.0, Qt.rgba(1.0, 1.0, 1.0, 0.0))
gradient.addColorStop(1.0, Qt.rgba(0.0, 0.0, 0.0, 0.3))
ctx.beginPath()
ctx.moveTo(centerX, centerY)
ctx.arc(centerX, centerY, radius, 0, 2 * Math.PI, false)
ctx.fillStyle = gradient
ctx.fill()
}
function modelSum() {
var modelSum = 0
for (var index = 0; index < model.count; index++) {
modelSum += model.get(index).value
}
return modelSum
}
}
...
Now we’re done! We’ve successfully created a nice looking pie chart.
What’s Next?
The next chapter demonstrates how to port HTML5 Canvas code into the Canvas Item of Qt Quick.