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};pubfnbuild_dashboard<'a>(env:&EnvClient, aggregated_data:HashMap<&'astr, HashMap<&'astr, AggregatedData>>, collaterals:&Vec<Collateral>, borroweds:&Vec<Borrowed>) ->Dashboard {letmut 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");letScVal::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 asi64/ STROOP asi64, data.total_collateral asi64/ STROOP asi64, data.total_borrowed asi64/ STROOP asi64]));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 asi64/ STROOP asi64, data.total_collateral asi64/ STROOP asi64, data.total_borrowed asi64/ STROOP asi64]));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.