Streamlit-echarts - early build

Hello all !

EDIT : source repo https://github.com/andfanilo/streamlit-echarts

Are there people familiar with echarts here ? I’m not very familiar with echarts myself and documentation can be daunting sometimes…so I’d like you to test the streamlit-echarts package.

# you need to be in an environment with Streamlit custom components
pip install streamlit-echarts
streamlit run app.py

with some sample files in the examples folder of the project https://github.com/andfanilo/streamlit-echarts/tree/master/examples. Following is a sample :

app.py
import pandas as pd
from random import randint
import streamlit as st

from streamlit_echarts import JsCode
from streamlit_echarts import st_echarts


def main():
    PAGES = {
        "Basic line chart": render_basic_line,
        "Basic area chart": render_basic_area,
        "Stacked area chart": render_stacked_area,
        "Mixed line and bar": render_mixed_line_bar,
        "Custom pie chart": render_custom_pie,
        "Effect scatter chart": render_effect_scatter,
        "Calendar heatmap": render_calendar_heatmap,
        "Basic treemap": render_treemap,
        "Datazoom": render_datazoom,
        "Dataset": render_dataset,
        "Map": render_map,
        "Click event": render_event,
    }

    st.title("Hello ECharts !")
    st.sidebar.header("Configuration")
    page = st.sidebar.selectbox("Choose an example", options=list(PAGES.keys()))
    PAGES[page]()


def render_basic_line():
    with st.echo("below"):
        options = {
            "xAxis": {
                "type": "category",
                "data": ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"],
            },
            "yAxis": {"type": "value"},
            "series": [
                {"data": [820, 932, 901, 934, 1290, 1330, 1320], "type": "line"}
            ],
        }
        st_echarts(
            options=options, height="400px",
        )
        st_echarts(
            options=options, height="400px", theme="dark",
        )


def render_basic_area():
    with st.echo("below"):
        options = {
            "xAxis": {
                "type": "category",
                "boundaryGap": False,
                "data": ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"],
            },
            "yAxis": {"type": "value"},
            "series": [
                {
                    "data": [820, 932, 901, 934, 1290, 1330, 1320],
                    "type": "line",
                    "areaStyle": {},
                }
            ],
        }
        st_echarts(options=options)


def render_stacked_area():
    with st.echo("below"):
        options = {
            "title": {"text": "堆叠区域图"},
            "tooltip": {
                "trigger": "axis",
                "axisPointer": {
                    "type": "cross",
                    "label": {"backgroundColor": "#6a7985"},
                },
            },
            "legend": {"data": ["邮件营销", "联盟广告", "视频广告", "直接访问", "搜索引擎"]},
            "toolbox": {"feature": {"saveAsImage": {}}},
            "grid": {"left": "3%", "right": "4%", "bottom": "3%", "containLabel": True},
            "xAxis": [
                {
                    "type": "category",
                    "boundaryGap": False,
                    "data": ["周一", "周二", "周三", "周四", "周五", "周六", "周日"],
                }
            ],
            "yAxis": [{"type": "value"}],
            "series": [
                {
                    "name": "邮件营销",
                    "type": "line",
                    "stack": "总量",
                    "areaStyle": {},
                    "data": [120, 132, 101, 134, 90, 230, 210],
                },
                {
                    "name": "联盟广告",
                    "type": "line",
                    "stack": "总量",
                    "areaStyle": {},
                    "data": [220, 182, 191, 234, 290, 330, 310],
                },
                {
                    "name": "视频广告",
                    "type": "line",
                    "stack": "总量",
                    "areaStyle": {},
                    "data": [150, 232, 201, 154, 190, 330, 410],
                },
                {
                    "name": "直接访问",
                    "type": "line",
                    "stack": "总量",
                    "areaStyle": {},
                    "data": [320, 332, 301, 334, 390, 330, 320],
                },
                {
                    "name": "搜索引擎",
                    "type": "line",
                    "stack": "总量",
                    "label": {"normal": {"show": True, "position": "top"}},
                    "areaStyle": {},
                    "data": [820, 932, 901, 934, 1290, 1330, 1320],
                },
            ],
        }
        st_echarts(options)


def render_mixed_line_bar():
    with st.echo("below"):
        options = {
            "tooltip": {
                "trigger": "axis",
                "axisPointer": {"type": "cross", "crossStyle": {"color": "#999"}},
            },
            "toolbox": {
                "feature": {
                    "dataView": {"show": True, "readOnly": False},
                    "magicType": {"show": True, "type": ["line", "bar"]},
                    "restore": {"show": True},
                    "saveAsImage": {"show": True},
                }
            },
            "legend": {"data": ["蒸发量", "降水量", "平均温度"]},
            "xAxis": [
                {
                    "type": "category",
                    "data": [
                        "1月",
                        "2月",
                        "3月",
                        "4月",
                        "5月",
                        "6月",
                        "7月",
                        "8月",
                        "9月",
                        "10月",
                        "11月",
                        "12月",
                    ],
                    "axisPointer": {"type": "shadow"},
                }
            ],
            "yAxis": [
                {
                    "type": "value",
                    "name": "水量",
                    "min": 0,
                    "max": 250,
                    "interval": 50,
                    "axisLabel": {"formatter": "{value} ml"},
                },
                {
                    "type": "value",
                    "name": "温度",
                    "min": 0,
                    "max": 25,
                    "interval": 5,
                    "axisLabel": {"formatter": "{value} °C"},
                },
            ],
            "series": [
                {
                    "name": "蒸发量",
                    "type": "bar",
                    "data": [
                        2.0,
                        4.9,
                        7.0,
                        23.2,
                        25.6,
                        76.7,
                        135.6,
                        162.2,
                        32.6,
                        20.0,
                        6.4,
                        3.3,
                    ],
                },
                {
                    "name": "降水量",
                    "type": "bar",
                    "data": [
                        2.6,
                        5.9,
                        9.0,
                        26.4,
                        28.7,
                        70.7,
                        175.6,
                        182.2,
                        48.7,
                        18.8,
                        6.0,
                        2.3,
                    ],
                },
                {
                    "name": "平均温度",
                    "type": "line",
                    "yAxisIndex": 1,
                    "data": [
                        2.0,
                        2.2,
                        3.3,
                        4.5,
                        6.3,
                        10.2,
                        20.3,
                        23.4,
                        23.0,
                        16.5,
                        12.0,
                        6.2,
                    ],
                },
            ],
        }
        st_echarts(options)


def render_custom_pie():
    with st.echo("below"):
        pie_options = {
            "backgroundColor": "#2c343c",
            "title": {
                "text": "Customized Pie",
                "left": "center",
                "top": 20,
                "textStyle": {"color": "#ccc"},
            },
            "tooltip": {"trigger": "item", "formatter": "{a} <br/>{b} : {c} ({d}%)"},
            "visualMap": {
                "show": False,
                "min": 80,
                "max": 600,
                "inRange": {"colorLightness": [0, 1]},
            },
            "series": [
                {
                    "name": "Source of interview",
                    "type": "pie",
                    "radius": "55%",
                    "center": ["50%", "50%"],
                    "data": [
                        {"value": 235, "name": "Video Ad"},
                        {"value": 274, "name": "Affiliate Ad"},
                        {"value": 310, "name": "Email marketing"},
                        {"value": 335, "name": "Direct access"},
                        {"value": 400, "name": "Search engine"},
                    ],
                    "roseType": "radius",
                    "label": {"color": "rgba(255, 255, 255, 0.3)"},
                    "labelLine": {
                        "lineStyle": {"color": "rgba(255, 255, 255, 0.3)"},
                        "smooth": 0.2,
                        "length": 10,
                        "length2": 20,
                    },
                    "itemStyle": {
                        "color": "#c23531",
                        "shadowBlur": 200,
                        "shadowColor": "rgba(0, 0, 0, 0.5)",
                    },
                    "animationType": "scale",
                    "animationEasing": "elasticOut",
                }
            ],
        }
        st_echarts(options=pie_options)


def render_effect_scatter():
    with st.echo("below"):
        options = {
            "xAxis": {"scale": True},
            "yAxis": {"scale": True},
            "series": [
                {
                    "type": "effectScatter",
                    "symbolSize": 20,
                    "data": [[161.2, 51.6], [167.5, 59]],
                },
                {
                    "type": "scatter",
                    "data": [
                        [161.2, 51.6],
                        [167.5, 59.0],
                        [159.5, 49.2],
                        [157.0, 63.0],
                        [155.8, 53.6],
                        [170.0, 59.0],
                        [159.1, 47.6],
                        [166.0, 69.8],
                        [176.2, 66.8],
                        [160.2, 75.2],
                        [172.5, 55.2],
                        [170.9, 54.2],
                        [172.9, 62.5],
                        [153.4, 42.0],
                        [160.0, 50.0],
                        [176.5, 71.8],
                        [164.4, 55.5],
                        [160.7, 48.6],
                        [174.0, 66.4],
                        [163.8, 67.3],
                    ],
                },
            ],
        }
        st_echarts(options)


def render_calendar_heatmap():
    with st.echo("below"):

        def get_virtual_data(year):
            date_list = pd.date_range(
                start=f"{year}-01-01", end=f"{year + 1}-01-01", freq="D"
            )
            return [[d.strftime("%Y-%m-%d"), randint(1, 10000)] for d in date_list]

        options = {
            "title": {"top": 30, "left": "center", "text": "2016年某人每天的步数"},
            "tooltip": {},
            "visualMap": {
                "min": 0,
                "max": 10000,
                "type": "piecewise",
                "orient": "horizontal",
                "left": "center",
                "top": 65,
                "textStyle": {"color": "#000"},
            },
            "calendar": {
                "top": 120,
                "left": 30,
                "right": 30,
                "cellSize": ["auto", 13],
                "range": "2016",
                "itemStyle": {"borderWidth": 0.5},
                "yearLabel": {"show": False},
            },
            "series": {
                "type": "heatmap",
                "coordinateSystem": "calendar",
                "data": get_virtual_data(2016),
            },
        }
        st_echarts(options)


def render_treemap():
    with st.echo("below"):
        options = {
            "series": [
                {
                    "type": "treemap",
                    "data": [
                        {
                            "name": "nodeA",
                            "value": 10,
                            "children": [
                                {"name": "nodeAa", "value": 4},
                                {"name": "nodeAb", "value": 6},
                            ],
                        },
                        {
                            "name": "nodeB",
                            "value": 20,
                            "children": [
                                {
                                    "name": "nodeBa",
                                    "value": 20,
                                    "children": [{"name": "nodeBa1", "value": 20}],
                                }
                            ],
                        },
                    ],
                }
            ]
        }
        st_echarts(options)


def render_datazoom():
    with st.echo("below"):
        data = [
            ["14.616", "7.241", "0.896"],
            ["3.958", "5.701", "0.955"],
            ["2.768", "8.971", "0.669"],
            ["9.051", "9.710", "0.171"],
            ["14.046", "4.182", "0.536"],
            ["12.295", "1.429", "0.962"],
            ["4.417", "8.167", "0.113"],
            ["0.492", "4.771", "0.785"],
            ["7.632", "2.605", "0.645"],
            ["14.242", "5.042", "0.368"],
        ]
        option_js = {
            "xAxis": {"type": "value"},
            "yAxis": {"type": "value"},
            "dataZoom": [{"type": "slider", "start": 10, "end": 60}],
            "series": [
                {
                    "type": "scatter",
                    "itemStyle": {"opacity": 0.8},
                    "symbolSize": JsCode(
                        """function (val) {  return val[2] * 40; }"""
                    ).js_code,
                    "data": data,
                }
            ],
        }
        st_echarts(options=option_js)


def render_dataset():
    with st.echo("below"):
        options = {
            "legend": {},
            "tooltip": {},
            "dataset": {
                "source": [
                    ["product", "2015", "2016", "2017"],
                    ["Matcha Latte", 43.3, 85.8, 93.7],
                    ["Milk Tea", 83.1, 73.4, 55.1],
                    ["Cheese Cocoa", 86.4, 65.2, 82.5],
                    ["Walnut Brownie", 72.4, 53.9, 39.1],
                ]
            },
            "xAxis": {"type": "category"},
            "yAxis": {},
            "series": [{"type": "bar"}, {"type": "bar"}, {"type": "bar"}],
        }
        st_echarts(options, renderer="svg")


def render_map():
    with st.echo("below"):
        options = {
            "backgroundColor": "#404a59",
            "title": {
                "text": "全国主要城市空气质量",
                "subtext": "data from PM25.in",
                "sublink": "http://www.pm25.in",
                "left": "center",
                "textStyle": {"color": "#fff"},
            },
            "tooltip": {"trigger": "item"},
            "legend": {
                "orient": "vertical",
                "top": "bottom",
                "left": "right",
                "data": ["pm2.5"],
                "textStyle": {"color": "#fff"},
            },
            "visualMap": {
                "min": 0,
                "max": 300,
                "splitNumber": 5,
                "color": ["#d94e5d", "#eac736", "#50a3ba"],
                "textStyle": {"color": "#fff"},
            },
            "geo": {
                "map": "china",
                "label": {"emphasis": {"show": False}},
                "itemStyle": {
                    "normal": {"areaColor": "#323c48", "borderColor": "#111"},
                    "emphasis": {"areaColor": "#2a333d"},
                },
            },
        }
        st_echarts(options)


def render_event():
    with st.echo("below"):
        st.markdown("Click on chart elements")
        options = {
            "xAxis": {
                "data": ["shirt", "cardign", "chiffon shirt", "pants", "heels", "socks"]
            },
            "yAxis": {},
            "series": [
                {"name": "sales", "type": "bar", "data": [5, 20, 36, 10, 10, 20]}
            ],
        }
        events = {"click": "function(params, echarts) {alert('click detection');}"}
        st_echarts(options, events=events)


if __name__ == "__main__":
    main()
app_pyecharts.py
import random

import streamlit as st
from pyecharts import options as opts
from pyecharts.charts import Bar
from pyecharts.charts import Geo
from pyecharts.charts import Timeline
from pyecharts.commons.utils import JsCode
from pyecharts.faker import Faker

from streamlit_echarts import st_pyecharts


def main():
    PAGES = {
        "Basic rendering": render_basic,
        "Custom themes": render_custom,
        "Filter with legend": render_filter_legend,
        "Vertical datazoom": render_vertical_datazoom,
        "Timeline": render_timeline,
        "Chart with randomization": render_randomize,
        "JsCode coloring": render_js,
        "Map": render_map,
    }

    st.header("Hello Pyecharts !")
    st.sidebar.header("Configuration")
    page = st.sidebar.selectbox("Choose an example", options=list(PAGES.keys()))
    PAGES[page]()


def render_basic():
    with st.echo("below"):
        b = (
            Bar()
            .add_xaxis(["Microsoft", "Amazon", "IBM", "Oracle", "Google", "Alibaba"])
            .add_yaxis(
                "2017-2018 Revenue in (billion $)", [21.2, 20.4, 10.3, 6.08, 4, 2.2]
            )
            .set_global_opts(
                title_opts=opts.TitleOpts(
                    title="Top cloud providers 2018", subtitle="2017-2018 Revenue"
                ),
                toolbox_opts=opts.ToolboxOpts(),
            )
        )
        st_pyecharts(b)


def render_custom():
    with st.echo("below"):
        b = (
            Bar()
            .add_xaxis(["Microsoft", "Amazon", "IBM", "Oracle", "Google", "Alibaba"])
            .add_yaxis(
                "2017-2018 Revenue in (billion $)", [21.2, 20.4, 10.3, 6.08, 4, 2.2]
            )
            .set_global_opts(
                title_opts=opts.TitleOpts(
                    title="Top cloud providers 2018", subtitle="2017-2018 Revenue"
                )
            )
        )
        st_pyecharts(b, theme="dark")

        st_pyecharts(
            b,
            theme={
                "backgroundColor": "#f4cccc",
                "textStyle": {"color": "rgba(255, 0, 0, 0.8)"},
            },
        )


def render_filter_legend():
    with st.echo("below"):
        c = (
            Bar(
                init_opts=opts.InitOpts(
                    animation_opts=opts.AnimationOpts(
                        animation_delay=1000, animation_easing="elasticOut"
                    )
                )
            )
            .add_xaxis(Faker.choose())
            .add_yaxis("商家A", Faker.values())
            .add_yaxis("商家B", Faker.values())
            .set_global_opts(
                title_opts=opts.TitleOpts(title="Bar-动画配置基本示例", subtitle="我是副标题")
            )
        )
        st_pyecharts(c)


def render_vertical_datazoom():
    with st.echo("below"):
        c = (
            Bar()
            .add_xaxis(Faker.days_attrs)
            .add_yaxis("商家A", Faker.days_values, color=Faker.rand_color())
            .set_global_opts(
                title_opts=opts.TitleOpts(title="Bar-DataZoom(slider-垂直)"),
                datazoom_opts=opts.DataZoomOpts(orient="vertical"),
            )
        )
        st_pyecharts(c, height="400px")


def render_timeline():
    with st.echo("below"):
        x = Faker.choose()
        tl = Timeline()
        for i in range(2015, 2020):
            bar = (
                Bar()
                .add_xaxis(x)
                .add_yaxis("商家A", Faker.values())
                .add_yaxis("商家B", Faker.values())
                .set_global_opts(title_opts=opts.TitleOpts("某商店{}年营业额".format(i)))
            )
            tl.add(bar, "{}年".format(i))
        st_pyecharts(tl)


def render_randomize():
    with st.echo("below"):
        b = (
            Bar()
            .add_xaxis(["Microsoft", "Amazon", "IBM", "Oracle", "Google", "Alibaba"])
            .add_yaxis(
                "2017-2018 Revenue in (billion $)", random.sample(range(100), 10)
            )
            .set_global_opts(
                title_opts=opts.TitleOpts(
                    title="Top cloud providers 2018", subtitle="2017-2018 Revenue"
                ),
                toolbox_opts=opts.ToolboxOpts(),
            )
        )
        st_pyecharts(
            b, key="echarts"
        )  # Add key argument to not remount component at every Streamlit run
        st.button("Randomize data")


def render_js():
    with st.echo("below"):
        st.markdown(
            """Overwrite chart colors with JS. 
        Under 50 : red. Between 50 - 100 : blue. Over 100 : green"""
        )
        color_function = """
                function (params) {
                    if (params.value > 0 && params.value < 50) {
                        return 'red';
                    } else if (params.value > 50 && params.value < 100) {
                        return 'blue';
                    }
                    return 'green';
                }
                """
        c = (
            Bar()
            .add_xaxis(Faker.choose())
            .add_yaxis(
                "商家A",
                Faker.values(),
                itemstyle_opts=opts.ItemStyleOpts(color=JsCode(color_function)),
            )
            .add_yaxis(
                "商家B",
                Faker.values(),
                itemstyle_opts=opts.ItemStyleOpts(color=JsCode(color_function)),
            )
            .add_yaxis(
                "商家C",
                Faker.values(),
                itemstyle_opts=opts.ItemStyleOpts(color=JsCode(color_function)),
            )
            .set_global_opts(title_opts=opts.TitleOpts(title="Bar-自定义柱状颜色"))
        )
        st_pyecharts(c)


def render_map():
    with st.echo("below"):
        g = (
            Geo()
            .add_schema(maptype="china")
            .add("geo", [list(z) for z in zip(Faker.provinces, Faker.values())])
            .set_series_opts(label_opts=opts.LabelOpts(is_show=False))
            .set_global_opts(
                visualmap_opts=opts.VisualMapOpts(),
                title_opts=opts.TitleOpts(title="Geo-基本示例"),
            )
        )
        st_pyecharts(g)


if __name__ == "__main__":
    main()

You should be able to at least copy examples from echarts examples and add quotes everywhere to make it a Python dict before passing to st_echarts, or Pyecharts examples by removing .render() part before sending to Streamlit component st_pyecharts. I’ll be focusing more on echarts examples than pyecharts for now.

Still working on documenting everything, so most of the documentation is the provided 2 sample files :laughing:

Other caveats to be aware for this build :

  • only China maps work
  • only global mouse events work
  • Defining the theme in Pyecharts when instantiating chart like Bar(init_opts=opts.InitOpts(theme=ThemeType.LIGHT))
    does not work, you need to call theme in st_pyecharts(c, theme=ThemeType.LIGHT).

st_pyecharts vs st.html

While this package provides a st_pyecharts method, if you’re using pyecharts you can directly embed your pyecharts visualization inside st.html by passing the output of the chart’s .render_embed().

from pyecharts.charts import Bar
from pyecharts import options as opts
import streamlit as st

c = (Bar()
    .add_xaxis(["Microsoft", "Amazon", "IBM", "Oracle", "Google", "Alibaba"])
    .add_yaxis('2017-2018 Revenue in (billion $)', [21.2, 20.4, 10.3, 6.08, 4, 2.2])
    .set_global_opts(title_opts=opts.TitleOpts(title="Top cloud providers 2018", subtitle="2017-2018 Revenue"),
                     toolbox_opts=opts.ToolboxOpts())
    .render_embed() # generate a local HTML file
)
st.html(c, width=1000, height=1000)

Using st_pyecharts is still something you would want if you need to change data regularly
without remounting the component, check for examples app_pyecharts.py for Chart with randomization example.

5 Likes

Woohoo! Thanks for putting this together, and writing some a thorough post about it, @andfanilo!

(And also, for going the extra mile and publishing the component as a wheel!)

This is awesome.

5 Likes

@andfanilo Haven’t heard of this library. What is the advantage of this over Vega Lite which I personally love but also has a streamlit implemenation

Hey @func4plus1,

I’m also a regular user of Altair/Vega-lite for Data science/EDA. For echarts I do find the level of interactivity and native transitions between states in the chart super nice to look at with not too much code :slight_smile: plus they support tons of chart types. For slick demo data apps to show results to customers I may go for echarts. Check out their examples page.

In the hand if you’re a Vega-lite user, juggling between it and echarts shouldn’t be too hard.

2 Likes