Complex Plotting

Learn how to create more complex plots

If you've chosen to have full-customization on the dashboard you're going to build, you can use our charming_fork_zephyr library and follow Apache Echarts' specification.

Below is an example of how it can be implemented for the Creating the Dashboard tutorial:


Again as before, we can have all the logic in a single function build_dashboard, which takes as argument the aggregated_data Map we just built. Let's break down an example:

use zephyr_sdk::{
    charting::{Dashboard, DashboardEntry, Table}
use charming_fork_zephyr::{component::{Axis, Grid, Legend, Title}, element::{AreaStyle, AxisType, Color, ColorStop, Tooltip, Trigger}, series::{Bar, Line}, Chart};


pub fn build_dashboard<'a>(env: &EnvClient, aggregated_data: HashMap<&'a str, HashMap<&'a str, AggregatedData>>, collaterals: &Vec<Collateral>, borroweds: &Vec<Borrowed>) -> Dashboard {
    let mut dashboard = Dashboard::new().title(&"Blend Porotocol Dashboard").description(&"Explore the Blend protocol's mainnet activity.");
    let categories: Vec<String> = vec!["Supply".into(), "Collateral".into(), "Borrowed".into()];

    for (pool, assets) in aggregated_data {
        let auctions_table = {
            let positions_count = get_from_ledger(env, &pool);
            let table = Table::new().columns(vec!["count".into()]).row(vec![positions_count.to_string()]);
            DashboardEntry::new().title("Current Unique Users With Positions").table(table)
        };

        dashboard = dashboard.entry(auctions_table);

        let val = get_from_instance(env, pool, "Name");
        let ScVal::String(string) = val else {panic!()};
        let pool = string.to_utf8_string().unwrap();
        
        env.log().debug("Iterating over data", None);

        for (asset, data) in assets {
            let meta: StellarAssetContractMetadata = env.from_scval(&get_from_instance(env, asset, "METADATA"));
//            let asset = soroban_string_to_string(env, meta.name);
            let denom = soroban_string_to_string(env, meta.symbol);
            let asset = denom.clone();


            let bar = {
                let chart = Chart::new().legend(Legend::new().show(true).left("150px").top("3%")).tooltip(Tooltip::new().trigger(Trigger::Axis))
                .x_axis(Axis::new().type_(AxisType::Category).data(categories.clone()))
                .y_axis(Axis::new().type_(AxisType::Value)).series(Bar::new()
                .name(format!("Pool: {}, Asset {}", pool, asset))
                .data(vec![data.total_supply as i64 / STROOP as i64, data.total_collateral as i64 / STROOP as i64, data.total_borrowed as i64 / STROOP as i64]));

                DashboardEntry::new().title("Distribution all time").chart(chart)
            };
            
            dashboard = dashboard.entry(bar);
      }
      
      dashboard
}

Be aware that to build the charts, we need to import some types from the Zephyr SDK and Charmig. Then we create the dashboard calling Dashboard::new(). As you can see we can set a title and a description for the dashboard.

Now we start iterating through our map for each pool and asset:

Building a Table

The first element of our dashboard is going to be a table stating for each liquidity pool the current unique users with a position in it.

for (pool, assets) in aggregated_data {
        let auctions_table = {
            let positions_count = get_from_ledger(env, &pool);
            let table = Table::new().columns(vec!["count".into()]).row(vec![positions_count.to_string()]);
            DashboardEntry::new().title("Current Unique Users With Positions").table(table)
        };

        dashboard = dashboard.entry(auctions_table);
}

For each pool in our aggregated_data Map, we count how many unique users’ positions there are with the get_from_ledger(env, &pool) method, we create a table with Table::new() and define the column count, while the row will be the count. To add the table to the dashboard, we create a new Dashboard Entry with DashboardEntry::new(), set its title, and insert our table. Finally, we can add our entry (the table) to the previously created dashboard.

Building a Chart

Let's look at an example of how to plot a chart. Specifically, we will create multiple bar charts showing the distribution of supply, collateral, and borrowed amounts over time for each pool and asset. The chart can be defined as follows:

for (asset, data) in assets {
            let meta: StellarAssetContractMetadata = env.from_scval(&get_from_instance(env, asset, "METADATA"));
            let denom = soroban_string_to_string(env, meta.symbol);
            let asset = denom.clone();


            let bar = {
                let chart = Chart::new().legend(Legend::new().show(true).left("150px").top("3%")).tooltip(Tooltip::new().trigger(Trigger::Axis))
                .x_axis(Axis::new().type_(AxisType::Category).data(categories.clone()))
                .y_axis(Axis::new().type_(AxisType::Value)).series(Bar::new()
                .name(format!("Pool: {}, Asset {}", pool, asset))
                .data(vec![data.total_supply as i64 / STROOP as i64, data.total_collateral as i64 / STROOP as i64, data.total_borrowed as i64 / STROOP as i64]));

                DashboardEntry::new().title("Distribution all time").chart(chart)
            };

For each asset and each pool, we obtain the asset name as a string (from a Soroban String—more details in Working with Data). We then create the chart using Chart::new(), define its dimensions and basic parameters, and set up the axes with .x_axis(Axis::new()) and .y_axis(Axis::new()). On the x-axis, we want to display the categories: supply, collateral, and borrowed amounts. We specify that the axis type will be 'Category' using .type_(AxisType::Category), and set our categories with .data(categories.clone()), defined as follows:

let categories: Vec<String> = vec!["Supply".into(), "Collateral".into(), "Borrowed".into()];

On y instead, we will have the values of the respective categories .type_(AxisType::Value)) expressed in a bar chart: .series(Bar::new()).

We then use .name() to specify the chart's name, including the specific pool and asset being plotted. Finally, we provide the data to be plotted using .data(), accessing the total_supply, total_collateral, and total_borrowed fields of the AggregatedData instances. Note that previously, we only aggregated data for total_supply (refer to the complete example for details on other aggregations).

As before, we create a new entry with DashboardEntry::new(), set its title, and add the entry to the dashboard element. Now we are all set and set dashboard as the return of our function.


To continue the tutorial, go back to Creating the Dashboard.

Last updated