
Sailfish OS Development: Displaying Charts Using D3.js and QML Canvas
- Tutorial
Hello! This article is a continuation of a series of articles on application development for the Sailfish OS mobile platform. This time we will focus on working with charts in a Sailfish application. We will talk about finding and connecting a library and how we display graphs of mathematical functions. Note that the proposed solution is not limited to the Saiflsh OS platform and is generally suitable for any QtQuick application.
We decided to create a calculator application that would satisfy the needs of engineers, students and schoolchildren working with devices running Sailfish OS. Our application should have contained the following components:
To the block for solving equations, we decided to add the functionality of displaying graphs. To solve this problem within the framework of a QML application, the following approaches can be applied:
The QuickQanava library works with Qt 5.8, which is not yet available on the Sailfish OS platform. The QML Canvas object allows you to use the high-level JavaScript language and also provides an API compatible with the W3C standard, which opens up the possibility of using third-party libraries.
Due to the fact that the graph display does not require serious calculations and it is not necessary to redraw the scene often, we decided to use QML Canvas in the project with the use of an external JavaScript library.
The QML Canvas element allows you to draw straight and curved lines, simple and complex shapes, graphics, and links to graphic images. He can also draw text, colors, shadows, gradients and patterns, as well as manipulate the image at the pixel level. The Canvas output, besides being displayed on the screen, can be saved as an image file or serialized in a URL.
Canvas rendering is performed using the Context2D object , usually as a result of the processing of the paint signal . The object itself implements the HTML Canvas 2D Context specification , which is also implemented in the HTML Canvas object., which allows you to use JavaScript libraries designed for use in web browsers for QML applications. Currently, three-dimensional context is not supported by the Context2D object .
Consider the simplest example of connecting a QML Canvas to your application:
The first line in the example, we include QtQuick 2.0 , then define the Canvas element and set the id , width and height parameters . Due to the fact that the element itself does not have elements and can occupy any space, it needs to specify dimensions either by explicitly setting the width and height, or by connecting the edges of the element with other elements on the page. If you do not specify a size, the item will not be visible. In the example, we use the first approach.
The paint signal is triggered when the Canvas element is activated . Its processing occurs in the onPaint method . In it, we get the context to display and store it in a variablectx . A full description of the parameters for getContext can be, for example, here . Be careful, Qt only provides access to the two-dimensional display context.
Next, we use the context to display the rectangle. ctx.fillStyle sets the fill color of the rectangle. The first three parameters determine the color according to the components of red, green and blue, and the fourth component determines the transparency. ctx.fillRect (x, y, w, h) draws it using x and y as the coordinates of the beginning, and w and h as the width and height.
The entire list of context methods that can be used for drawing can be viewedin the official documentation . We will not consider all the methods in this article, we only note that the coordinates of the image begin in the upper left corner. The OX axis grows to the right, and the OY axis goes up and down.
Of course, the task set before us, we could solve directly using the Context2D API , but we decided to consider the possibility of using external libraries. Due to the fact that this API is available in all major browsers, developers for Sailfish OS can use a large number of existing libraries that facilitate the implementation of target functions. In our application, we decided to use the D3.js library .
D3.js is a JavaScript library for processing and visualizing data. Currently, D3.js is one of the most popular frameworks used for graphical data processing and creating all kinds of charts and graphs.
D3.js itself is a large project that allows you to solve many problems, so there is no single way to integrate this library into HTML applications. We used a fairly simple integration approach, but others should also work successfully in your applications.
First, the library must be downloaded and made available on the target device. We remind you that QML components on Sailfish OS are not compiled into resources, but are delivered as separate files. As a result, all JavaScript dependencies are also desirable as separate files.
D3.js comes in a separate file called d3.js, as well as a minified version, which is located in d3.min.js. During development, we found out that the minified version is not loaded correctly by the QML engine, so it is recommended to use the full version - it works without any complaints.
For our application, we placed the d3.js file in the qml / pages directory of our project. The entire contents of this directory is copied to the target device, so the file is also copied along with the project. The file was also included in the DISTFILES list in the QML project so that QtCreator would show it in the list of other files.
As part of the application, we need to display graphs of three functions on a two-dimensional plane. All functions considered depend on the value of the abscissa. To display them qualitatively on a segment, we decided to calculate intermediate values on the segment currently displayed.
We put the general logic for the construction into a separate Plot component . It provides the following functionality:
In specific places of use, it remains for us to determine only 1 function that will calculate the values of the graph.
Consider the structure of the base component.
First, we connect the libraries we need: a set of QtQuick components, as well as the D3.js library itself. Connecting JavaScript files is similar to connecting other QML files. To solve this problem, the import keyword is also used .
Full information on connecting JavaScript files can be found in the official documentation . The main aspect when importing is specifying a name through which all functions defined in this document will be available. In our code, we gave the name of this object to D3 .
The root element of Plot is Canvason which we display information. To perform calculations and gesture processing in this element, we defined a set of properties and functions. The key one is onPaint - the handler for the image rendering event.
The child element with respect to Canvas is Item , which is just a container for PinchArea and MouseArea objects . These objects were added to handle the pinch gesture, to control the level of approximation, and drag, to control the position of the coordinate axes. Gesture handlers update the coordinates that are used to draw the graph.
We will not consider the rendering process step by step, since it is not of great interest on the one hand, and on the other hand, you can look at the source code of the application and figure out the details yourself. At the same time, we will look at important points that can cause difficulties.
To display the key elements: coordinate grid and function graph, the d3.line function is used . This function allows you to display arbitrary polyline and straight lines. The input to the function is an array of data. In order to use it, you must configure the following parameters:
Consider an example of forming an image of a graph line.
First, we set up scales, d3.scaleLinear , which simplify our work with scaling the graph. It is enough to indicate the physical boundaries of the image in the call to the range () method and the boundaries of the graph in the call to the domain () method . Scales for abscissas, ordinates are formed and written into the xScale and yScale variables , respectively.
Then we describe a line that will take an array of graph values as parameters. In the x () method call, we pass a function that extracts the first element of the array and converts it using the xScale scale . A similar function is passed as an argument to the y () method call, only accesses to the second element of the array. Then we set up the method of communication between the elements, in our case it is d3.curveNatural . D3.js supports a huge number of options for constructing curves, you can read about them in the official documentation . At the end of the construction of the line, we associate it with the graphic context of our image.
To draw a line, just call the created line and pass it an array of the necessary coordinates:
In a similar way, lines are drawn to display the axes.
It is worth noting that at the beginning of each drawing the canvas is completely cleared. This is necessary so that the previous image does not interfere with the display of the current state. And new images appear when the user zooms in on the chart or the borders of the display of elements change.
Using this component, we implemented the display of three graphs for each of the functions. For each chart, we created a page. On these pages, values are calculated to display the graph.
The general structure of each chart is shown below.
The root element is the page that is pushed onto the stack to display the graph. The parameters are the coefficients of the equation, the initial boundaries for displaying the graph, as well as a line showing the location of the roots of the function.
Next, we turn off the navigation back if the user interacts with the chart. This prevents gestures from accidentally returning from the page.
The only element on the page is the Plot element . We explicitly indicate that it takes up all available space, it will be used to display the graph. We also define the drawPlot method . This method will be called every time when you need to re-display the function.
As an argument, it is passed the line that was configured, as shown above, in the Plot element . We call it and pass it the result of the getPoints () method . The last method forms a set of points that will be specific to each individual chart.
We hope that using this information you can easily implement similar functionality in your application. You can also familiarize yourself with the implementation of functions in the Matrix Calculator application in more detail by installing it from the OpenRepos.net repository , or look at working with the library in the source code, which is available in the BitBucket repository .
Screenshots of the application are given below:
UPD : Added screenshots of the application.
Task description
We decided to create a calculator application that would satisfy the needs of engineers, students and schoolchildren working with devices running Sailfish OS. Our application should have contained the following components:
- Calculator with two modes of operation: simple and advanced.
- A subsystem of computations over matrices that supports addition, multiplication of matrices, calculation of their rank and determinant, as well as transposition.
- Block solving the following equations: power equations up to 4 degrees, exponential and trigonometric equations.
To the block for solving equations, we decided to add the functionality of displaying graphs. To solve this problem within the framework of a QML application, the following approaches can be applied:
- Connect an external library like QuickQanava .
- Use a QML Canvas Object .
- Implement your own component in C ++ and connect it to the application.
The QuickQanava library works with Qt 5.8, which is not yet available on the Sailfish OS platform. The QML Canvas object allows you to use the high-level JavaScript language and also provides an API compatible with the W3C standard, which opens up the possibility of using third-party libraries.
Due to the fact that the graph display does not require serious calculations and it is not necessary to redraw the scene often, we decided to use QML Canvas in the project with the use of an external JavaScript library.
QML Canvas and Context2D
The QML Canvas element allows you to draw straight and curved lines, simple and complex shapes, graphics, and links to graphic images. He can also draw text, colors, shadows, gradients and patterns, as well as manipulate the image at the pixel level. The Canvas output, besides being displayed on the screen, can be saved as an image file or serialized in a URL.
Canvas rendering is performed using the Context2D object , usually as a result of the processing of the paint signal . The object itself implements the HTML Canvas 2D Context specification , which is also implemented in the HTML Canvas object., which allows you to use JavaScript libraries designed for use in web browsers for QML applications. Currently, three-dimensional context is not supported by the Context2D object .
Consider the simplest example of connecting a QML Canvas to your application:
import QtQuick 2.0
Canvas {
id: mycanvas
width: 100
height: 200
onPaint: {
var ctx = getContext("2d");
ctx.fillStyle = Qt.rgba(1, 0, 0, 1);
ctx.fillRect(0, 0, width, height);
}
}
The first line in the example, we include QtQuick 2.0 , then define the Canvas element and set the id , width and height parameters . Due to the fact that the element itself does not have elements and can occupy any space, it needs to specify dimensions either by explicitly setting the width and height, or by connecting the edges of the element with other elements on the page. If you do not specify a size, the item will not be visible. In the example, we use the first approach.
The paint signal is triggered when the Canvas element is activated . Its processing occurs in the onPaint method . In it, we get the context to display and store it in a variablectx . A full description of the parameters for getContext can be, for example, here . Be careful, Qt only provides access to the two-dimensional display context.
Next, we use the context to display the rectangle. ctx.fillStyle sets the fill color of the rectangle. The first three parameters determine the color according to the components of red, green and blue, and the fourth component determines the transparency. ctx.fillRect (x, y, w, h) draws it using x and y as the coordinates of the beginning, and w and h as the width and height.
The entire list of context methods that can be used for drawing can be viewedin the official documentation . We will not consider all the methods in this article, we only note that the coordinates of the image begin in the upper left corner. The OX axis grows to the right, and the OY axis goes up and down.
Using external libraries
Of course, the task set before us, we could solve directly using the Context2D API , but we decided to consider the possibility of using external libraries. Due to the fact that this API is available in all major browsers, developers for Sailfish OS can use a large number of existing libraries that facilitate the implementation of target functions. In our application, we decided to use the D3.js library .
Short Review D3.js
D3.js is a JavaScript library for processing and visualizing data. Currently, D3.js is one of the most popular frameworks used for graphical data processing and creating all kinds of charts and graphs.
D3.js itself is a large project that allows you to solve many problems, so there is no single way to integrate this library into HTML applications. We used a fairly simple integration approach, but others should also work successfully in your applications.
Integration of D3.js in a QML application
First, the library must be downloaded and made available on the target device. We remind you that QML components on Sailfish OS are not compiled into resources, but are delivered as separate files. As a result, all JavaScript dependencies are also desirable as separate files.
D3.js comes in a separate file called d3.js, as well as a minified version, which is located in d3.min.js. During development, we found out that the minified version is not loaded correctly by the QML engine, so it is recommended to use the full version - it works without any complaints.
For our application, we placed the d3.js file in the qml / pages directory of our project. The entire contents of this directory is copied to the target device, so the file is also copied along with the project. The file was also included in the DISTFILES list in the QML project so that QtCreator would show it in the list of other files.
Creating a component to display a graph
As part of the application, we need to display graphs of three functions on a two-dimensional plane. All functions considered depend on the value of the abscissa. To display them qualitatively on a segment, we decided to calculate intermediate values on the segment currently displayed.
We put the general logic for the construction into a separate Plot component . It provides the following functionality:
- Display grid with captions.
- Change displayed coordinates using gestures.
- Graph display by calculated values. A specific function must be implemented in the places of use, the base type does not provide this function.
In specific places of use, it remains for us to determine only 1 function that will calculate the values of the graph.
Consider the structure of the base component.
import QtQuick 2.0
import "d3.js" as D3
Canvas {
// Определение различных свойств
onPaint { } // Отображение графика
// Набор утилитарных функций
Item {
PinchArea {} // Обработка жеста щипка
MouseArea {} // Обработка перетаскивания
}
}
First, we connect the libraries we need: a set of QtQuick components, as well as the D3.js library itself. Connecting JavaScript files is similar to connecting other QML files. To solve this problem, the import keyword is also used .
Full information on connecting JavaScript files can be found in the official documentation . The main aspect when importing is specifying a name through which all functions defined in this document will be available. In our code, we gave the name of this object to D3 .
The root element of Plot is Canvason which we display information. To perform calculations and gesture processing in this element, we defined a set of properties and functions. The key one is onPaint - the handler for the image rendering event.
The child element with respect to Canvas is Item , which is just a container for PinchArea and MouseArea objects . These objects were added to handle the pinch gesture, to control the level of approximation, and drag, to control the position of the coordinate axes. Gesture handlers update the coordinates that are used to draw the graph.
Overview of the display process
We will not consider the rendering process step by step, since it is not of great interest on the one hand, and on the other hand, you can look at the source code of the application and figure out the details yourself. At the same time, we will look at important points that can cause difficulties.
To display the key elements: coordinate grid and function graph, the d3.line function is used . This function allows you to display arbitrary polyline and straight lines. The input to the function is an array of data. In order to use it, you must configure the following parameters:
- Configure generators to get ordinates and abscissas from an array element.
- Indicate the method of connecting the lines to each other.
- Indicate the graphical context on which to display the information.
Consider an example of forming an image of a graph line.
var context = plot.getContext('2d');
var xScale = d3.scaleLinear()
.range([leftMargin, width])
.domain([minX, maxX]);
var yScale = d3.scaleLinear()
.range([height - bottomMargin, 0])
.domain([minY, maxY]);
var line = d3.line().x(function (d) {
return xScale(d[0]);
}).y(function (d) {
return yScale(d[1]);
}).curve(d3.curveNatural).context(context);
First, we set up scales, d3.scaleLinear , which simplify our work with scaling the graph. It is enough to indicate the physical boundaries of the image in the call to the range () method and the boundaries of the graph in the call to the domain () method . Scales for abscissas, ordinates are formed and written into the xScale and yScale variables , respectively.
Then we describe a line that will take an array of graph values as parameters. In the x () method call, we pass a function that extracts the first element of the array and converts it using the xScale scale . A similar function is passed as an argument to the y () method call, only accesses to the second element of the array. Then we set up the method of communication between the elements, in our case it is d3.curveNatural . D3.js supports a huge number of options for constructing curves, you can read about them in the official documentation . At the end of the construction of the line, we associate it with the graphic context of our image.
To draw a line, just call the created line and pass it an array of the necessary coordinates:
line([[1, 2], [2, 15], [3, 8], [4, 6]])
In a similar way, lines are drawn to display the axes.
It is worth noting that at the beginning of each drawing the canvas is completely cleared. This is necessary so that the previous image does not interfere with the display of the current state. And new images appear when the user zooms in on the chart or the borders of the display of elements change.
Using the Plot Component
Using this component, we implemented the display of three graphs for each of the functions. For each chart, we created a page. On these pages, values are calculated to display the graph.
The general structure of each chart is shown below.
Page {
property var elem
property var border
property var rootLine
id: page
backNavigation: plot.controlNavigation()
Plot {
id: plot
anchors.margins: Theme.horizontalPageMargin
width: parent.width
height: parent.height
function drawPlot(line) {
line(getPoints());
}
function getPoints() {
// Вычисление значений для отображений.
}
}
}
The root element is the page that is pushed onto the stack to display the graph. The parameters are the coefficients of the equation, the initial boundaries for displaying the graph, as well as a line showing the location of the roots of the function.
Next, we turn off the navigation back if the user interacts with the chart. This prevents gestures from accidentally returning from the page.
The only element on the page is the Plot element . We explicitly indicate that it takes up all available space, it will be used to display the graph. We also define the drawPlot method . This method will be called every time when you need to re-display the function.
As an argument, it is passed the line that was configured, as shown above, in the Plot element . We call it and pass it the result of the getPoints () method . The last method forms a set of points that will be specific to each individual chart.
Matrix Calculator App
We hope that using this information you can easily implement similar functionality in your application. You can also familiarize yourself with the implementation of functions in the Matrix Calculator application in more detail by installing it from the OpenRepos.net repository , or look at working with the library in the source code, which is available in the BitBucket repository .
Screenshots of the application are given below:
![]() | ![]() |
UPD : Added screenshots of the application.