[Mod] Polygraph (custom charting API) [polygraph]

Post Reply
User avatar
sorcerykid
Member
Posts: 1847
Joined: Fri Aug 26, 2016 15:36
GitHub: sorcerykid
In-game: Nemo
Location: Illinois, USA

[Mod] Polygraph (custom charting API) [polygraph]

by sorcerykid » Post

Polygraph Mod v2.0
polygraph (by sorcerykid)

Polygraph is a formspec-based charting API for Minetest, providing a rich set of output parameters and custom rendering hooks within an object-oriented framework.

Since I've had many requests for source code from the just_test_tribute subgame, I finally decided to release this mod with instructions and code examples for other developers. Polygraph is intended to be compatible with all versions of Minetest 0.4.14+.

Repository:

https://bitbucket.org/sorcerykid/polygraph

Download Archive (.zip)
Download Archive (.tar.gz)

Dependencies:

None.

Source Code License:

The MIT License (MIT)

Installation:
  1. Unzip the archive into the mods directory of your game
  2. Rename the stopwatch-master directory to "polygraph"
  3. Add "polygraph" as a dependency for any mods using the API
Getting Started

Data visualization in Minetest is simple and easy with Polygraph. Thanks to an extensive set of parameters, you can embed dynamic charts within any formspec. Here are screenshots of mods using the Polygraph API:
  • Image

    Image

    Image
To begin, you will need an ordered dataset to generate the graph. Typically, this is accomplished with an array.
  • Code: Select all

    local dataset = { 0, 0.1, 0.2, 0.2, 0.3, 0.4 }
Next, you must create a SimpleChart object, passing this dataset as a parameter to the constructor. The constructor also accepts a definition table, but we'll use the default options for now.
  • Code: Select all

    local graph = SimpleChart( dataset, { } )
The SimpleChart object has only two methods, draw( ) and push( ). We're concerned with the former since that is essential to rendering the chart. Later we'll consider use-cases for the push( ) method.
  • Code: Select all

    local formspec = "size[12,8]"
         .. default.gui_bg_img
         .. graph.draw( GRAPH_TYPEDOT, 0, 0 )
    
    minetest.create_form( nil, player_name, formspec )
For this tutorial, we're focusing on a scatter plot graph, but there are also bar and line graph types available. The last two parameters control the Y and X axis shift (useful for scrolling). We'll leave those at zero for now.

As you can see, this creates a rudimentary chart with labels along the X and Y axes and labels for each value, as well as horizontal rules. These elements can be customized as you'll see below.
  • Image
To customize your chart, you will first need to create a definition table. The following settings are available.
  • Graph Options
    vert_off - vertical offset of x-axis from formspec top (default 6)
    vert_int - vertical spacing between y-axis coordinates (default 1)
    vert_pad - vertical padding of x-axis coordinates (default 0.5)
    horz_off - horizontal offset of y-axis from formspec left (default 1)
    horz_int - horizontal spacing between x-axis coordinates (default 0.5)
    horz_pad - horizontal padding of y-axis coordinates (default 0.6)
    vert_dec - decimal places for y-axis labels (default 1)

    Data Options
    y_range - number of data points along y-axis (default 4)
    y_start - starting index of y-axis (default -1)
    y_scale - scale factor of y-axis (default 0.5)
    x_range - number of data points along the x-axis (default 20)
    x_scale - scale factor of x-axis (default 1, integers only)
    x_start - starting index of x-axis (default 0)

    Rendering Properties
    bar_color - color of the bars, lines, or points
    box_color - color of the background
    ref_color - color of the horizontal rules
    tag_color - color of the value labels
    idx_color - color of the X-axis and Y-axis labels

    Rendering Hooks
    on_plot_x - callback during x-axis and value plot
    on_plot_y - callback during y-axis plot
Be aware that the position and size units are inventory based, as is customary for all Minetest formspecs. By changing just a few of the options above, we can fine-tune the layout of our chart:
  • Code: Select all

    local graph = SimpleChart( dataset, {
            vert_off = 4,
            horz_int = 1,
            y_start = -0.5,
            x_range = 6,
    } )
    Image
Rendering hooks make it possible to tap directly into the plotting functions of Polygraph. This is useful if you want to alter the output dynamically, as shown in the examples below.
  • SimpleChart::on_plot_x( x, x_index, v_min, v_max, v, prop, meta )
    • x - the x-axis coordinate
    • x_index - the absolute x-index (based on the formula x_start + x * x_scale + x_shift)
    • v_min - the minimum value that can be plotted
    • v_max - the maximum value that can be plotted
    • v - the current value being plotted (the value must be returned, even if unchanged, otherwise it will not be plotted)
    • prop - properties derived from the chart definition: idx_label, idx_color, bar_color, tag_label, tag_color
    • meta - a persistent state table, for use during rendering
  • SimpleChart::on_plot_y( y, y_index, v_min, v_max, prop, meta )
    • y - the y-axis coordinate
    • y_index - the absolute y-index (based on the formula y_start + y * y_scale + y_shift)
    • v_min - the minimum value that can be plotted
    • v_max - the maximum value that can be plotted
    • prop - properties derived from the chart definition: idx_label, idx_color, ref_color, ref_width
    • meta - a persistent state table, for use during rendering
With these rendering hooks, we can further refine the appearance of each value and correctly format the x-axis labels as well.
  • Code: Select all

    local graph = SimpleChart( dataset, {
            vert_off = 4,
            horz_int = 1,
            y_start = -0.5,
            x_range = 6,
            on_plot_x = function ( x, x_index, v_min, v_max, v, prop, meta )
                    if v < 0.2 then
                            prop.bar_color = '#00FF00'
                            prop.tag_color = '#00FF00'
                    elseif v < 0.4 then
                            prop.bar_color = '#FFFF00'
                            prop.tag_color = '#FFFF00'
                    else
                            prop.bar_color = '#FF0000'
                            prop.tag_color = '#FF0000'
                    end
                    prop.idx_label = x_index .. " sec"
                    return v
            end
    } )
    Image
Notice how we are able to intercept and override the rendering properties at every step along the X-axis and the Y-axis using just the rendering hooks alone. This affords a great deal of control to programmatically alter the output depending on the dataset values or the X and Y indices themselves.

For example, you could suppress the X-axis labels and/or Y-axis labels by setting prop.idx_label to an empty string in the corresponding rendering hooks. Imagine that the values in our dataset above represents samples taken every 0.5 seconds. Using the following trick, we can avoid clutter by alternating the X-axis labels:
  • Code: Select all

    prop.idx_label = x_index % 2 == 0 and string.format( "%0.1f sec", x_index / 2 ) or ""
    
    Image
You could, of course, apply the same technique to the value labels as well, particularly if the text becomes very long and unwieldy. The possibilities are only limited by your imagination.

No doubt you'll be working with much larger datasets than a few elements, so scrolling is imperative. Fortunately, the draw method already accounts for this by accepting both an x-shift and a y-shift for the draw method. You merely need to pass the appropriate starting index. An optional state-table can also be provided for use by the rendering hooks.
  • SimpleChart::draw( graph_type, x_shift, y_shift, meta )
    • graph_type - the built-in renderer to use: GRAPH_TYPEDOT, GRAPH_TYPE_BAR, or GRAPH_TYPESEG (required)
    • x_shift - scroll position of x-index (required)
    • y_shift - scroll position of y-index (required)
    • meta - a persistent state table, for use during rendering
Let's increase the dataset and implement back and forward buttons for the example above. Here is our fully working prototype.
  • Code: Select all

    function display_chart( player_name )
            local dataset = { 0, 0.1, 0.2, 0.2, 0.3, 0.4, 0.5, 0.3, 0.2, 0.1, 0.1, 0, 0, 0.2, 0.4, 0.4, 0.2, 0.3 }
    
            local graph = SimpleChart( dataset, {
                    vert_off = 4,
                    horz_int = 1,
                    y_start = -0.5,
                    x_range = 6,
                    on_plot_x = function ( x, x_index, v_min, v_max, v, prop, meta )
                            if v < 0.2 then
                                    prop.bar_color = '#00FF00'
                                    prop.tag_color = '#00FF00'
                            elseif v < 0.4 then
                                    prop.bar_color = '#FFFF00'
                                    prop.tag_color = '#FFFF00'
                            else
                                    prop.bar_color = '#FF0000'
                                    prop.tag_color = '#FF0000'
                            end
                            prop.idx_label = x_index % 2 == 0 and string.format( "%0.1f sec", x_index / 2 ) or ""
                            return v
                    end
            } )
    
            local function get_formspec( page )
                    return "size[8,6]"
                            .. default.gui_bg_img
                            .. graph.draw( GRAPH_TYPEDOT, page * 6, 0 )
                            .. "button[3,5.2;1,1;prev;<<]"
                            .. "button[4,5.2;1,1;next;>>]"
            end
    
            minetest.create_form( { page = 0 }, player_name, get_formspec( 0 ), function ( meta, player, fields )
                    if fields.quit then return end
    
                    if fields.prev and meta.page > 0 then
                            meta.page = meta.page - 1
                    elseif fields.next and meta.page + 1 < #dataset / 6 then
                            meta.page = meta.page + 1
                    end
                    minetest.update_form( player_name, get_formspec( meta.page ) )
            end )
    end
    Image
In most situations, of course, the dataset would be derived from an external data source or generated on the fly. It would be very easy to adapt the function above for this purpose. For inspiration, take a look at the included example.lua file, as it shows how to plot the results of the perlin 2-d noise generator.
  • Image
Earlier I'd mentioned that the SimpleChart object includes a push method. This exists for situations where the chart is to be maintained as an opaque datastore. You can also pass an array to the constructor, and then modify the array afterward (since only a reference is maintained by the object). But that may be less desirable, from an object-oriented perspective.

So far we've been charting all-numeric datasets. But what about when the dependent variable needs to be represented in textual or graphical form along the X-axis? That is possible with the CustomChart object. It is similar to the SimpleChart, but with far fewer options:
  • Graph Options
    horz_off - horizontal offset of chart from formspec left (default 1)
    vert_off - vertical offset of chart from formspec top (default 6)
    horz_int - horizontal spacing between x-axis coordinates (default 0.5)

    Data Options
    x_range - number of data points along the x-axis (default 20)
    x_scale - scale factor of x-axis (default 1)
    x_start - starting x-index (default 0)

    Rendering Hook
    on_plot - callback during value plot
Notice how there are no settings for the y-axis and only a single rendering hook. This is because plotting of the dependent variable is fully user-customizable. Polygraph merely provides the horizontal and vertical positions to plot. The return value is the string to insert into the formspec. This will typically consist of a label[] or image[] element.
  • CustomChart::on_plot( x, x_index, v, meta )
    • x - the x-axis coordinate
    • x_index - the absolute x-index (based on the formula x_start + x * x_scale + x_shift)
    • v - the current value being plotted
    • meta - a persistent state table, for use during rendering
Like before, a draw( ) method exists for rendering the formspec, but with two fewer parameters:
  • CustomChart::draw( x_shift, meta )
    • x_shift - scroll position of x-index (required)
    • meta - a persistent state table, for use during rendering
A good use of the CustomChart would be to display a status indicator that changes over the course of time.

unknown
New member
Posts: 8
Joined: Thu Sep 20, 2018 14:59
GitHub: alex-unknown
In-game: unknown

Re: [Mod] Polygraph (custom charting API) [polygraph]

by unknown » Post

I think this mod would be amazing!!!

but it does not work...
- the function minetest.create_form isn't defined. Maybe it does not work in the newest Minetest version.
- furthermore, the funtion graph.draw() returns nil.

I really hope that there is a solution, because I want to use this api for a stock exchange mod!

User avatar
rubenwardy
Moderator
Posts: 6977
Joined: Tue Jun 12, 2012 18:11
GitHub: rubenwardy
IRC: rubenwardy
In-game: rubenwardy
Location: Bristol, United Kingdom
Contact:

Re: [Mod] Polygraph (custom charting API) [polygraph]

by rubenwardy » Post

unknown wrote: - the function minetest.create_form isn't defined. Maybe it does not work in the newest Minetest version.
minetest.create_form is not a Minetest API, it's a function which the formspecs mod by the same author pollutes into the minetest namespace: viewtopic.php?f=9&t=19303
Renewed Tab (my browser add-on) | Donate | Mods | Minetest Modding Book

Hello profile reader

User avatar
sorcerykid
Member
Posts: 1847
Joined: Fri Aug 26, 2016 15:36
GitHub: sorcerykid
In-game: Nemo
Location: Illinois, USA

Re: [Mod] Polygraph (custom charting API) [polygraph]

by sorcerykid » Post

unknown wrote:I really hope that there is a solution, because I want to use this api for a stock exchange mod!
Thanks for the feedback!

The examples above (and in the examples.lua file) make use of the ActiveFormspecs framework. However, it's not a hard dependency, as the Polygraph mod itself is fully compatible with the minetest.show_formspec( ) method, although it won't be nearly as secure. I'll add a note in the original post to clarify this point.

As for the issues with the draw( ) method, if the return value is nil then it's possible that no dataset was provided. What parameters are you passing to the constructor?

Post Reply

Who is online

Users browsing this forum: No registered users and 18 guests