App Inventor Chart Components: Project Summary
Last post, I have wrapped up the implemented methods for importing data to the Charts for the App Inventor Charts project that I have been working on. This post will cover one of the last two Chart types which were implemented in this project – The Pie Chart. This post follows up directly from the Chart type selection implementation post.
The Pie Chart, as implemented in this post, looks like this when compared side-by-side in both the Designer and the Android implementations:
With regards to the data of the Chart, it is quite important to note that the Pie Chart is quite a special case compared to other available Charts (e.g. Line, Area, Scatter and Bar). The key difference is that the Pie Chart does not have any axes, meaning that an x value attains a different interpretation.
While a choice could have been made to entirely remove the x value for the Pie Chart, and have the Pie Chart take in a Data component which only has a single value, it was instead decided to interpret the x value as the value’s label, which is then displayed on the Legend.
For example, consider the following Chart:
We have three values on the Pie Chart. The ‘A’ value corresponds to 50, the ‘B’ and ‘C’ values correspond to 25. Here, the ‘A’ value is the x value, and the number 50 is the y value.
In tuple form, the entry is then represented as follows:
Now a question might arise – if the x value now uses a text representation, then should the Chart type also not use a different Chart Data type?
The way this issue was tackled is by having the blocks take input as text rather than numbers. With the introduced tuple concepts, we treat each element in the tuple as a String, each of which is later interpreted by the Chart Data Model itself.
In this case, by allowing text parameters rather than number parameters, we essentially add support for more Chart Data types while also providing some transparency to the user.
Recall how we would import data to a Line Chart Data model via the AddEntry block:
This same block can be used for Pie Chart Data components as well, but would use the x value number as text.
We can specify text input as well to the block, as follows:
With this implementation in mind, we can essentially reuse the same Chart Data component for all 2-dimensional Chart types.
The Pie Chart is also a rather special case due to the special treatment of the legend. Unlike other Chart types, where the legend entries simply correspond to an individual data series, the Pie Chart’s legend typically represents each slice in the Pie Chart. Due to this different representation, manual handling of the Legend is required.
Let us now take a look at the concrete implementation regarding the data of the Pie Chart.
The core method of the Data Series is the getEntryFromTuple method, which is implemented as follows:
In previous cases (Line, Area, Scatter Charts), we have parsed both the x and the y values. However, we do not parse the xValue here to a number, since we outlined the x value to be a text value. Otherwise we parse the y value to a number, since the y value is expected to be a numeric value.
Then, we have the addEntryFromTuple method, which is implemented as follows:
The method, with regards to data adding, is simple – the entry is simply added to the Data Series. However, since we are handling the Legend Entries manually to represent each entry, we have to construct a Legend Entry, and add it to the Legend.
Since we may have multiple Data Series, and they should all share the same Legend, a local reference of the PieChartView which created the Model is kept. In addition, a local List of the Legend Entries is kept to make removal and addition of entries easier.
With regards to the Legend Entry, the following is done to construct it:
With regards to setting the color of the Legend Entry, the principle here is that there might be more data entries than the colors. As such, the colors are alternated between entries in order. To retrieve the color, a modulo operation is then applied using the size of the colors list so that the index is always in range.
Finally, we have the method to remove the entry, which looks as follows:
Here, the removal of the entry itself is simple – the entry with the specified index is simply removed from the entries. With regards to the Legend, the corresponding Legend Entry is removed both from the local List, and from the view. Finally, the Legend Colors have to be updated primarily to fix up the colors, since the alternation of the colors should now be different.
One interesting observation here is that we use the same index to remove the entry, and the Legend Entry. Note how in the addEntryFromTuple method we add an entry and a Legend Entry at the same time. This ensures that they both use the same index in the List, therefore we have ease of access when accessing either the data, or the Legend entry – both correspond to the same index.
The view of the Pie Chart is a very tricky case for the chosen Android library. The ideas that we will use will heavily rely on a post I made earlier that described the implementation of concentric Pie Charts in the MPAndroidChart library. For context, since the MPAndroidChart library does not support Pie Charts with multiple Data Series, we have to come up with manual ways to support this.
The idea that I came up with and implemented with regards to concentric Pie Charts is to make use of multiple Pie Charts, one on top of another, in varying size. This makes it seem as if the Pie Charts are part of one big Pie Chart, where each individual Pie Chart is a ring of the bigger Pie Chart. Alongside this, we also define the root Pie Chart which is the outermost Pie Chart ring, which is also responsible for holding the Legend, since the root Pie Chart is the biggest ring (height and width wise) in the Pie Chart.
In order to realize this idea in practice, we make use of a central Root View (not to be confused with the root Pie Chart) which holds the Pie Charts together. The root view is a RelativeLayout in which the Pie Charts will be centered in and sized in such a way that each smaller Pie Charts appears in the center of the bigger Pie Charts
The code to retrieve the View and construct the Pie Chart View is as follows:
Note the pieCharts List variable, which is meant to store all the Pie Chart views in order. The first view will correspond to the root Chart, the second to the inner Chart closest to the root Chart, the third one being the inner Chart closest to the second Chart, and so on.
A Pie Chart is added to the Pie Charts List whenever a Chart Data Model is created. We treat the createChartDataModel as an event when a new Data Series is added to the View. Since we need a single Pie Chart for every Pie Chart ring, we create a new Pie Chart upon creating a Data Series.
In the snippet, note how the PieChartDataModel has 3 arguments. The actual pieChart is actually passed in to the PieChartDataModel due to it being linked to the Data Series. The PieChartDataModel is constructed as follows:
It is also important to note that not only do we create a new Pie Chart for the Data Series, but we also create a new PieData object to use for the Pie Chart. Hence, we are actually using individual, separated Pie Charts to create a concentric Pie Chart.
Let us take a look at the method to create a Pie Chart ring:
The first step of the method is determining whether the root Pie Chart has been added to the Pie Charts list (meaning that the Chart View has at least 1 Data Series). If this is not the case, a completely new Pie Chart is created with the legend and the description disabled (these properties should only be visible in the root Pie Chart itself due to the way we interpret these Pie Charts)
Next, the properties are set to the Pie Chart to return as the ring. The most important one is the setting of the RelativeLayout parameters, which ensure that the height and width match the root view, and the newly created Pie Chart is centered in the root view.
Finally, the created Pie Chart is added to both the Pie Charts List, and to the root view Layout.
The implementation that we have discussed so far takes care of creating the actual Pie Charts, but does not take care of scaling them accordingly. Pie Chart scaling is quite a heavy operation, and should only be done once. As such, a decision was made to only re-scale the Pie Chart rings upon setting the Pie Chart Radius property, which is expected to be called only after all the components are initialized. The code snippet shows where the method call is placed for resizing the rings:
In the Chart component class, we implement the onInitializeListener interface to listen for events when the application is initialized, and we only set the radius after the initialization is complete to ensure that all Data Series are added to the Pie Chart:
The method to resize the Pie Chart rings is shown below:
The code itself holds the most complex part of the Pie Chart View implementation, since all of the scaling has to be done manually to fit the Pie Chart rings.
In the method, firstly, the reduction factor is calculated in the following way: \(f = (0.75 + h/100)/n\)
Here, h represents the current Pie Hole Radius (meaning the percentage of the Pie Chart that is a hole), and n represents the number of Pie Charts present in the view (or the number of Data Series, in other words).
The reduction factor is essentially the value that will be used to reduce the current radius of the Pie Charts. The reason why a reduction of the radius is needed is because the outer Pie Charts need to have larger holes to fit inner Pie Charts, and as such, the radius must be reduced. Additionally, the Pie Charts itself are scaled down in order to have the more outer Pie Charts bigger than the inner ones.
In essence, the lower the reduction factor, the higher the new hole will be for the outer Pie Charts.
The reason why the formula was chosen based on the following observations:
Next, we calculate the actual radius of the Pie Chart, which is done as follows (r represents the radius, and h represents the hole radius): $$ r = 100 - h $$
The reason why we use the radius for future calculations is because it makes it easier instead of using the hole radius. Since we want to scale the radius itself rather than the hole radius of the Pie Charts, it makes it easier to come up with a reduction factor for the radius, rather than coming up with a scaling factor for the hole radius.
Having the radius calculated, we re-calculate the new hole radius as follows: $$ h = 100 - r * f $$
Having calculated the new hole radius, we iterate through all the Pie Charts, and apply the new Pie Chart radius. We will see later that we apply a completely different scaling to the innermost Pie Chart.
The final step of the loop is scaling the Pie Chart itself, by also using a reducing scaling factor, which is calculated as follows: $$ s = h/100 $$
The reasoning behind this calculation is the fact that the hole radius represents how much space is free in the Pie Chart’s center. For instance, if we have a Pie Chart with a hole radius of 90%, 90% of the Pie Chart is hollow. Since Pie Charts are circular, we can directly apply the scaling to their heights and widths.
One important thing to note is that the last width and height are constantly saved. We essentially keep scaling the Pie Charts down with each coming Chart. We also keep our outermost Pie Chart’s height and width constant as a base case.
Thus, the newly calculated height and width is always a fraction of the last Chart’s height and width. This can be formalized by the following equation (here h is the height, i represents the index of the Pie Chart in the view, and s is the scaling factor): \(h = \begin{cases} h_0, & \text{if } i = 0, \\ s*h_{i-1}, & \text{else}. \end{cases}\)
The way that the Pie Chart radius is changed can be seen in the snippet below:
Firstly, the method checks whether the Pie Chart argument specified is the last Chart in the List (the innermost Chart). If that is not the case, the radius of the hole is simply set to the specified new hole radius.
If the specified Chart is the innermost Chart, however, some logic follows here. Firstly, we have the base case – if the pie Hole Radius is 0, the Pie Chart should be 100% filled, hence we do not draw the hole at all.
In other cases, we do some calculations. Firstly, we calculate the delta difference between the new hole radius, and the current pie hole radius: $$ \delta = h - r $$
Through longer calculations, we find that the delta value is, in fact, always positive in the range \([0, 100]\) (we will not perform these calculations here).
Then, we calculate and set the radius to the inner most Pie Chart. The calculation is as follows (here r is the hole radius to set to the Pie Chart): $$ r = h * (1 + \delta/100) $$
The formula was established while performing observations with various values. It seems that the bigger the difference between the current hole radius, and the new hole radius, the larger the hole radius should be of the innermost Pie Chart. As an example, consider the case where the Pie Chart’s hole only takes up 5% of the Pie Chart. In that case, the innermost Pie Chart’s radius should be very large (95%, to be precise), and thus the delta value should be low (in order to represent the very small hole of the 95% fill Pie Chart).
The reason why we don’t set the radius of the hole to the local pie hole radius property is for the cases where there are quite a few rings (more than 3, for instance). In those scenarios, the innermost Pie Chart simply becomes too large for all the outer rings, to the points where the innermost Pie Chart could be even 6 times larger than any outermost ring. Thus, these calculations are used to somewhat equalize the Pie Chart rings.
While there are, of course, many improvements that could be made here, this works as quite a good approximation of the real innermost Pie Chart ring radius.
Upon adding new data, the Pie Chart View has to be refreshed. Since we treat all the rings as a single Pie Chart, we must also take care of refreshing all the Pie Chart rings accordingly.
The only part that we need to take care about the rings is an extra offset. Since enabling automatic legend offsetting (making the Chart smaller to fit the legend) would displace the outermost Pie Chart to no longer align with the inner Pie Charts, we need to apply the same offset to all the Pie Chart rings whenever the Legend takes up more than 1 line.
As such, the Refresh method is implemented in the following way:
Note how all the Pie Charts are iterated, and the Pie Chart ring offset is updated. The Pie Chart ring offset updating is done in the following way:
Except for the conversion from pixels to dp, the calculation formula for the offset is quite simple (where o is the offset, l is the legend’s needed height in dp): $$ o = min( l/2.5, 25 ) $$
In order for the Pie Charts to not become too small when applying the offset, the offset is capped at a value of 25.
Otherwise, the formula is based on some manual observations. It seems that using the needed height straight away as the offset applies an offset that is far too large. A division of 2 is also quite large in certain cases, creating gaps that are too big. The value of 2.5 was settled for as a constant of applying the offset.
Again, there could be quite a few improvements on this part, but it works relatively well for reasonable amounts of entries in the Pie Chart.
For the curious readers, the pull requests related to the Pie Chart type can be found here:
This has been a post following up on the 5 Chart types which were planned to be implemented in the scope of the 3 months for the project. The post that will follow after this one will be the last post detailing the features, and will cover the last Chart type that we have not covered – the Bar Chart, as well as various styling properties introduced for the Chart components.
Stay tuned!
Overview In the last post, I have thoroughly covered the implemented Pie Chart type for the App Inventor Chart components project that I have been working on...
Overview Last post, I have wrapped up the implemented methods for importing data to the Charts for the App Inventor Charts project that I have been working o...
Overview In the previous post on the App Inventor Chart Components project, I have covered data importing from Files, as well as from the Web. Previously, we...
Overview In the previous post on the App Inventor Chart Components project, we have looked at an extensive description of Chart Data Source concepts and real...
Overview It has been a while since the last post due to a highly busy period! A lot of blog posts were postponed, but now a series of posts will follow in th...
Overview Following up on the App Inventor Chart components project, this blog post will focus on a specific workflow aspect that allows to select the Chart t...
Overview With the workflow for the App Inventor Chart components established, the next step is to define the concepts and format of the Data that can be impo...
Problem While working with Charts in Android using the MPAndroidChart library, one inconsistency that I stumbled upon is the fact that all the Charts support...
Overview In the last post, I have previewed the workflow of the Charts components that I am working on for App Inventor. In this post, I will dive into some ...
Overview Following up on the Chart prototypes, this post will focus on previewing the current progress on the Charts components in App Inventor.
Overview In continuation to the previous previous blog post, this post will focus on a Line Chart Component prototype in App Inventor.
Overview During the initial steps of the project to develop Chart Components for App Inventor, the major focus was on the design of the components such that ...
As the community bonding period is nearing to an end and the coding period is about to begin, I would like give a status update on what happened in the last ...
Introduction I am Evaldas Latoškinas, currently a first year Computer Science & Engineering international student in the Netherlands at TU Delft. Origina...