App Inventor Chart Components: Project Summary
In the previous post on the App Inventor Chart Components project, we have looked at an extensive description of Chart Data Source concepts and realizations of Data Source importing via TinyDB and CloudDB components. This blog post will follow up on the post by covering CSV, JSON and Online data importing implementations to the Chart Data component.
We will first start by taking a look at importing from files. More specifically, CSV and JSON formatted files. Before we start going into the actual details of how the importing itself will function, let’s first take a look at the CSV and JSON formats themselves.
The CSV format looks as follows:
A CSV file consists of multiple rows, where each row’s element is separated by a comma.
When importing Data from a CSV formatted file, we interpret the very first row as the “header” of the file. This is a row which basically contains the names of the columns themselves.
It is also important to note that in our case, we are more interested in the columns than the rows. We may consider each column as a single array, so in the case of the example, the X column would be the following:
The JSON (JavaScript Object Notation) equivalent of the CSV example is the following:
In the JSON format, each value is essentially a key-value pair. We will interpret the key as the name of the element (much like the column name in the CSV file), and the value as the element (or elements, if the element is an array).
More information on the JSON format can be found here.
With the formats of the supported Files established, let’s take a look at the actual process of the importing.
One important characteristic to note of CSV Files is that, in fact, the data is interpreted on a row-by-row or a column-by-column basis. What this means for us is that we essentially have to operate on data on a per-column or per-row basis, while our data requirements are actually more than a single column of data, since we make use of tuples which hold the x and the y value (for the 2-dimensional Data scenario)
However, this is not quite the case for JSON formatted files, which allow nested Lists. However, due to the nature of data (mostly data will most likely come in an array rather than a nested List containing both x and y values), we will apply the same ideas to JSON data parsing as we do to CSV data parsing.
The conclusion is then as follows – since we are operating on individual columns, we will need to allow selection of columns for each dimension of the Data. If the data consists of tuples containing x and y entries, we should allow picking a column for both x and y, and then combine the two columns to create the entries. If the data consists of 3 dimensions (x, y and z entries), then we should allow to pick a column for both the x, the y and the z dimension.
The following concept representation of a CSVFile specialized component (which is now adapted to support JSON as well) illustrates the process with an example:
Let us now look at each piece of the process in depth.
The DataFile component (then CSVFile, as in the example) is a non-visible component which is able to read in files, parse their contents from CSV/JSON to generate rows, columns and column names, which are then stored internally. These properties can then be fetched and made use of by the Chart Data components.
In App Inventor, the properties and the blocks of the component look as follows:
The DataFile importing process in blocks is quite similar to the shown CSVFile implementation:
Alternatively, instead of the first block which changes the Source, the file can be set via Designer properties.
Most of the heavy lifting is, in fact, done by the DataFile. The reading in the code is done as follows:
Firstly, the Source File is specified, and then the method to read from the File is invoked:
Afterwards, the File itself is opened and then the AsyncRead command is issued, as follows:
The details of the InputStream creation from the File logic is not as important here. The same implementation is used as in the previously existing File component.
Finally, the File itself is read asynchronously, as follows:
The following happens here:
After this process is over, the data is ready to be imported to the Chart.
An important part of CSV/JSON parsing is the algorithm which retrieves the transpose of the rows/columns. The code, in it’s entirety, looks as follows:
When calling the transpose method, the expected input is a matrix (or, in other words, an n x m table, where n is the height, and m is the width), meaning that the argument should be a List of Lists (which is indeed the case for our parsed rows/columns). In the method itself, each List is treated as if it were the same size, even if that is not the case. To compensate for the missing entries of smaller Lists, blank entries are added to specify that the value does not exist in that position.
The List size that is applied in generating the transpose is the maximum List size of the nested Lists in the matrix.
The first important fact to note is that when getting the transpose, the width and height interchange. For example, if the matrix consisted of 3 columns, each having 5 rows, the transpose will consist of 5 columns, each having 3 rows.
This is what happens in the getTranspose method – first, the maximum entry count within the matrix’s nested Lists is calculated. If the matrix is a List of columns, this calculates the general row size to apply for the matrix.
The second important fact is that the transpose interchanges a row with a column. This is what is done in the getTranspose method’s loop. Consider the example of a List of columns; If there are 5 columns with a row size of 3, in the end the transpose will have 3 columns with a row size of 5 each. In this case, the method takes care of generating the 3 columns, one-by-one, using the getTransposeEntry method.
The getTransposeEntry essentially shifts all the elements from each List of the matrix to a new List. All of the matrix’s inner Lists are iterated, and then the index-th value is added, if it exists.
The following example illustrates this concept further:
Note how the 5x3 matrix becomes a 3x5 matrix. The most important operation is the shifting of data itself; Note how all the elements from the first column in the original matrix are then put into the first row of the transpose matrix. This is exactly what this method handles, and allows us to create columns from rows.
While the CSV parsing is relatively straight forward (the rows are parsed in order, and the transpose is taken to retrieve the columns), JSON parsing becomes a bit more interesting.
The following method handles parsing a JSON formatted String to return a YailList of columns:
The first important thing to note are the List checks. In App Inventor’s JSONUtils, a JSON object which contains both the Key and the Value is a List. Therefore, we are expecting formats as follows:
Note that the outermost curly brackets represent an object – they hold all the necessary key-value pairs inside (hence the List check, since the outermost object is a List of Objects).
The inner objects consist of both a key and a value pair ((object1, value1) for instance), hence the List check on each entry of the outermost object.
Next, it is important to note that each key-value pair is interpreted as a column, where the key is the column name. In this case, “object1” would the column name, and the values of value1 would be the values.
The algorithm for constructing a single column is as follows
With this established interpretation of JSON files in mind, we are then able to establish consistency between JSON and CSV parsed files. In the future, it might also be a good idea to also support some more natural JSON representations of the data, such as the following:
However, this would require some sort of deviation from the DataFile functionality on its own, and would require far different handling for JSON and CSV respectively.
In a previous post, I have defined Chart Data Source concepts to ease implementing new Chart Data Sources and create consistency between Chart Data importing from components.
Since the DataFile is also a possible Chart Data Source, we will define it as such:
This time, the key value of the ChartDataSource is a YailList. Here, the key denotes the columns to retrieve from the Data Source itself (the elements of the YailList are expected to be String). The returned value is also a YailList (each List entry representing a column), but wrapped in a Future object due to data retrieval being processed asynchronously.
The implemented method to retrieve a Data Value with the key then look as follows:
A runnable is submitted to the single-threaded runner to run the data retrieval in order. Since data parsing is also submitted to this single-threaded runner, we can ensure that the correct data will be present in the DataFile.
Each column is then retrieved one by one, and added to a resulting List. If a column is not found, it is substituted by an empty List and left up for interpretation to the Chart Data components.
After the DataFile does the parsing, the next step is importing the Data itself. The importing is invoked as follows:
Note how the Future object is retrieved outside the asynchronously executed Runnable block (to preserve ordering of value retrieval).
After the value itself is retrieved in the asynchronous Runnable, the Chart Data Model takes care of importing the retrieved columns.
The importFromColumns method itself only calls the method that constructs tuples from the columns, and then imports them via the importFromList method.
The getTuplesFromColumns method does the following:
A rather important observation is that if a column is empty, all of its values will be substituted by default values. Empty columns are essentially interpreted as the default option to fill in extra values, such as to allow importing, for example, a single column into the Data Series.
To provide a relatively simple and quick to use interface in the Designer itself, the DataFile has been adapted to also parse a limited subset of data (10 entries, plus the column names). With this implementation in order, property setters were made specifically for the DataFile. Whenever a DataFile Source is attached, the columns can be chosen via dropdown menus, and the data of the Data Series is updated to represent the chosen columns.
The following animation demonstrates this feature:
In addition, a feature was implemented which allows to drag & drop a DataFile component (with a valid file selected) onto a Chart directly, which automatically creates Data components with the corresponding properties (Source attached and columns set):
Another component that is a viable Chart Data Source is the pre-existing Web component. In essence, the Web component could be made to behave similarly to the DataFile component. Since content of various types is received over web requests, we can apply a very similar handling to the Web component as we do to the DataFile component.
The core part of the Web component data parsing is that the type of the file is known. Therefore, based on the MIME type alone, the type of the Data can be determined. For instance, if the response type is the text/csv MIME type, we can apply CSV parsing. If we have the application/json type, we can apply JSON parsing. After determining the type, the parsing process is roughly the same as the DataFile parsing process (apart from reading the file, since we get the raw response content directly)
The data importing process from the Web component is as follows:
The first step of the Data importing process starts from actually sending the GET request with the Web component. The idea behind the importing of the Web component is that the response content is saved locally in the Web component, which can then be retrieved by Chart Data components. Doing so ensures that a request can be sent once, and then the latest data can be reused as many times as needed by Data observers from the Web component. Not only does this increase performance (at the cost of memory usage), but this also limits the number of requests required to send (which is quite an important factor, especially for APIs which allow limited requests).
The local data updating starts from a pre-existing method which performs the requests itself:
The additions as part of the Web data importing change is in the else part of the if-else block. As soon as the content of a response is retrieved, the local data columns are updated, and all the Data Observers are notified. Note the null variable convention in use here – the null key indicates that all the observers should be notified, and the null value is used because the columns are already stored locally, therefore no value has to be passed in. We will see later on why this choice was made, but the main reason for this is to avoid interpreting the data on the Web component itself, and leave the task up for the Data components.
The updateColumns method looks as follows:
Note how one of the arguments is the type of the retrieved response. If the responseType String contains the word JSON, the data is assumed to be JSON formatted. Otherwise, if the responseType contains the term “csv” or the type starts with the prefix “text/”, CSV parsing is attempted. The reason for this is because types like “text/plain” could still be CSV formatted.
Another point to note is that in the case of CSV, the columns are parsed, and then the transpose is taken. The reason for this is because CSV parsing returns the rows, but only the columns are stored locally.
After the Data has been successfully retrieved from the response, the next step is to actually make the data retrievable by other components.
Recall that the type of the returned Data Value is a Future object wrapping a YailList object. The getDataValue method (which is the method from the ChartDataSource interface).
Before we get into the details, one important thing to note is that in the Web component, all request sending operations are ran asynchronously, but not in order (so Runnables are simply executed as new Threads). However, this might pose a problem when fetching the Data itself, since we actually need the requests to be finished before we retrieve the Data.
To implement this, a decision was made to keep track of the last running Runnable as a FutureTask object. Instead of executing Threads without tracking anything, FutureTasks are executed asynchronously, and stored locally. When Data is retrieved, the idea is to then wait for the last known task to end before returning the data.
The code for this then looks as follows:
In the getDataValueTask, first the get method is called on the last known task to wait for the task to end, and then the getColumns method is invoked with the key value (which represents the columns to retrieve) to return the result. Since the FutureTask uses a Callable, the corresponding columns can later be retrieved from the FutureTask.
With regards to the getColumns method, the method functions very similarly to the DataFile getDataValue method.
As we have defined the methods and concepts in retrieving the Data, the last step is to retrieve and import it to the Chart Data components.
The method which imports Data from the Web component looks as follows:
In essence, the method does exactly the same – the Future object holding the columns is retrieved, and then the Future object’s Data value is retrieved asynchronously, and then the columns are imported.
In the future, this might be generalized to both the DataFile and Web component, so that two highly similar methods would not be used.
With this method being called as the last step, the data is finally imported into the Data Series.
In addition to static Data retrieval, the Web component can very much act like an observable Data Source. After responses are retrieved, the Data Observers can then be notified of the changes (we have seen the notifyDataObservers method call in the processRequest method)
The first step to add Observable Chart Data Source support is adding an if-else statement for the Web component (much like it was done for the TinyDB and CloudDB components). The same is also done for the DataFile component to add support for an attachable DataFile source (although a decision was made to not make the DataFile component and observable source).
The final resulting if-else blocks in the Source property look as follows:
While this is not ideal, and changes can be made to reduce some redundancy, it is still quite clear that some form of case-by-case handling is necessary, since Web and DataFile components, for instance, take a List of columns as the key input, while the TinyDB and CloudDB components take a single Tag as the key value.
Finally, in the method which handles the event to listen for Data Source value changes, the following if block is added for the Web component:
We have previously seen the getTuplesFromColumns method when importing Data from columns. Since the Web component holds all the columns, a decision was made to instead handle the data selection in the Chart Data components itself, to leave the Web component’s data more neutral, and up for interpretation for other sources.
The reason why we need to get the specific columns is to be able to import the Data selectively, as well as remove the older data on value changes.
Another point to recall is that the Web component invoked the notifyDataObservers method with a null key and value; The reason why this is done should now be clear – the Chart Data components interpret the data themselves by fetching the required columns, therefore neither the key, nor the value are important when receiving this event.
With these implementations, the Web component was converted to an Observable Chart Data Source.
With regards to importing itself, as with all the other Data Source components, it can be done in 3 ways – via changing the Source, via changing the Designer properties and via blocks.
Via Designer properties, the setup would look as follows:
The equivalent importing in blocks would look as follows:
For the curious readers, the main pull requests related the CSV, JSON and Web import features can be found here:
This has been quite another lengthy post describing more Data Source importing options and concepts. A final post on Data Source importing will revolve around Real Time Data Sources, and this post concludes the static & observable non-real time Chart Data Source importing options.
As the project is nearing to an end, the next post will follow very soon.
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...