Streamlit-echarts

Thanks Fanilo. I think it could be related to the issue you linked to above, not sure though. It often happens on the first load of the page and then starts working after rerunning the script once or twice.

1 Like

Thanks Fanilo for this wonderful component. Do you have any plan to expand this component for dynamic plot in echarts (Examples - Apache ECharts) and Radar Charts (Examples - Apache ECharts).

Thanks,
Krishna

1 Like

Hello @Krishna_Kant, thanks for using my component, really appreciate :slight_smile:

I have actually looked a bit into the animation/dynamic stuff months ago, and did not understand why my component did not render them correctly. Thanks for reminding me.

Are you able to put a JSON options of a dynamic plot in an issue in Github here so I remember to have a look at it somewhere this month?

Thanks a lot :slight_smile:
Fanilo

Thanks. I have create an issue on github.
Looking forward to have dynamic plot with echarts in streamlit.
Thanks once again for streamlit-echarts components.

1 Like

Hi @andfanilo ,

I am trying to use st_echarts component with placeholder. The idea is to animate existing echart plot.

placeholder.write(st_echarts(option, height=“500px”))

In most cases i am getting DuplicateWidgetID error (due to not having unique key). When i add a key argument, instead of replacing old canvas, it creates additional figures.

st_echarts(option, height=“500px”, key=str(i))

Here is a snippet, Let me know if there is any workaround.

for i in range(10):

placeholder.empty()

placeholder.write(st_echarts(option, height="500px"))

time.sleep(5)

Once again Thank you for echarts component.

Best,
Krishna

Hello @Krishna_Kant,


2 small fixes to your app before diving into the real problem:

  • The key for your chart should stay the same for it to stay and be updated in the app. For example:
data = [random.randint(0, 100) for _ in range(7)]
option = {
    "xAxis": {
        "type": "category",
        "data": ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"],
    },
    "yAxis": {"type": "value"},
    "series": [
        {
            "data": data,
            "type": "bar",
            "showBackground": True,
            "backgroundStyle": {"color": "rgba(180, 180, 180, 0.2)"},
        }
    ],
}
st_echarts(option, height="500px", key="chart") # <-- preserve me with updated options on next Streamlit run
st.button("Regenerate data")
  • For the placeholder to work correctly with st_echarts, you need to use the context manager version instead of st.write(st_echarts):
st.title("Hello world!")
placeholder = st.empty()
st.button("Regenerate data")
st.caption("By Fanilo")

data = [random.randint(0, 100) for _ in range(7)]
option = {
    "xAxis": {
        "type": "category",
        "data": ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"],
    },
    "yAxis": {"type": "value"},
    "series": [
        {
            "data": data,
            "type": "bar",
            "showBackground": True,
            "backgroundStyle": {"color": "rgba(180, 180, 180, 0.2)"},
        }
    ],
}

with placeholder: # <-- holds the st_echarts
    st_echarts(option, height="500px", key="chart")

Now after those little fixes…my logic was,

for i in range(10):
    st_echarts(..., key="chart")

won’t work because you are creating 10 echarts with the same key instead of reusing the same echart instance. So you need to find a way to loop your Streamlit app while keeping the echarts creation as a one time event in your script, and keeping your iteration number in some state variable.

Using a combination of session_state and experimental_rerun:

if "iteration" not in st.session_state:
    st.session_state["iteration"] = 0

data = [random.randint(0, 100) for _ in range(7)]
option = {
    "xAxis": {
        "type": "category",
        "data": ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"],
    },
    "yAxis": {"type": "value"},
    "series": [
        {
            "data": data,
            "type": "bar",
            "showBackground": True,
            "backgroundStyle": {"color": "rgba(180, 180, 180, 0.2)"},
        }
    ],
}

while st.session_state["iteration"] < 10:
    st.markdown(f"Iteration number {st.session_state['iteration']}")

    st_echarts(option, height="500px", key="chart")  # <-- the same echarts, but with new data, is reused between reruns
    time.sleep(1)
    st.session_state["iteration"] += 1

    if st.session_state["iteration"] < 10:
        st.experimental_rerun()

looks like it works, let’s add the placeholder:

st.title("Hello world!")
placeholder = st.empty()
st.caption("By Fanilo")

if "iteration" not in st.session_state:
    st.session_state["iteration"] = 0

data = [random.randint(0, 100) for _ in range(7)]
option = {
    "xAxis": {
        "type": "category",
        "data": ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"],
    },
    "yAxis": {"type": "value"},
    "series": [
        {
            "data": data,
            "type": "bar",
            "showBackground": True,
            "backgroundStyle": {"color": "rgba(180, 180, 180, 0.2)"},
        }
    ],
}

while st.session_state["iteration"] < 10:
    st.markdown(f"Iteration number {st.session_state['iteration']}")

    with placeholder:
        st_echarts(option, height="500px", key="chart")
    time.sleep(1)
    st.session_state["iteration"] += 1

    if st.session_state["iteration"] < 10:
        st.experimental_rerun()

test

Hope this helps you get started on those more advanced Streamlit concepts!
Fanilo

Merci!

I will try this workaround. I will also have a look into advance Streamlit concepts.
Now, echarts can be used to update real-time plots :smile: This is cool.

Dynamic plot still remain an issue (sorry to remind you again).

Merci beaucoup,
K.

Actually now I’m not sure what you mean about dynamic plots :o I was thinking about something unrelated

My technique when converting examples from the ECharts page to Python dict is to add console.log(option) at the end of the echarts example, open the web console in your browser devtools and then copy/analyze the option there


If you understand my sample app in my previous answer, with some work you can replicate the line dynamic plot:

import datetime
import random
import time
import streamlit as st
from streamlit_echarts import JsCode
from streamlit_echarts import st_echarts

st.set_page_config(layout="wide")
st.title("Animated ECharts - Dynamic Data + Time Axis")

origin_day = datetime.date(1997, 9, 3)


def _generate_datum(origin_day, day):
    date = origin_day + datetime.timedelta(days=day)
    return {
        "name": date.strftime("%Y/%m/%d"),
        "value": [date.strftime("%Y/%m/%d"), random.randint(1800, 2000)],
    }


if "data" not in st.session_state:
    st.session_state["data"] = [
        _generate_datum(origin_day, day) for day in range(1, 365)
    ]

option = {
    "title": {"text": "Something"},
    "tooltip": {
        "trigger": "axis",
        "formatter": JsCode(
            "function(params){params=params[0];var date=new Date(params.name);return date.getDate()+'/'+(date.getMonth()+1)+'/'+date.getFullYear()+' : '+params.value[1]}"
        ).js_code,
        "axisPointer": {"animation": False},
    },
    "xAxis": {"type": "time", "splitLine": {"show": False}},
    "yAxis": {
        "type": "value",
        "boundaryGap": [0, "100%"],
        "splitLine": {"show": False},
    },
    "series": [
        {
            "name": "模拟数据",
            "type": "line",
            "showSymbol": False,
            "hoverAnimation": False,
            "data": st.session_state["data"],
        }
    ],
}

st_echarts(option, height="600px", key="chart")

latest_day = datetime.datetime.strptime(
    st.session_state["data"][-1]["name"], "%Y/%m/%d"
)
new_data = st.session_state["data"][5:] + [
    _generate_datum(latest_day, day) for day in range(1, 6)
]

time.sleep(1)
st.session_state["data"] = new_data
st.experimental_rerun()

test


Your second example is a radar plot, most of the JSON you can translate into Python code:

import streamlit as st
from streamlit_echarts import st_echarts

st.set_page_config(layout="wide")
st.title("Radar plot")

START_YEAR = 2001
END_YEAR = 2029


def _generate_datum(year):
    i = year - 2000
    return {
        "name": "浏览器(数据纯属虚构) ",
        "type": "radar",
        "symbol": "none",
        "lineStyle": {"width": 1},
        "emphasis": {"areaStyle": {"color": "rgba(0,250,0,0.3)"}},
        "data": [
            {
                "value": [
                    (40 - i) * 10,
                    (38 - i) * 4 + 60,
                    i * 5 + 10,
                    i * 9,
                    i * i / 2,
                ],
                "name": str(year),
            }
        ],
    }


option = {
    "title": {"text": "浏览器占比变化", "subtext": "纯属虚构", "top": 10, "left": 10},
    "tooltip": {"trigger": "item"},
    "legend": {
        "type": "scroll",
        "bottom": 10,
        "data": [str(y) for y in range(START_YEAR, END_YEAR)],
    },
    "visualMap": {
        "top": "middle",
        "right": 10,
        "color": ["red", "yellow"],
        "calculable": True,
    },
    "radar": {
        "indicator": [
            {"text": "IE8-", "max": 400},
            {"text": "IE9+", "max": 400},
            {"text": "Safari", "max": 400},
            {"text": "Firefox", "max": 400},
            {"text": "Chrome", "max": 400},
        ]
    },
    "series": [_generate_datum(year) for year in range(START_YEAR, END_YEAR)],
}

st_echarts(option, height="800px")

test2


With those under the belt, you can now replicate about 85-90% of the echarts gallery I’d say :slight_smile: !

Thanks once again. I agree, it is working nicely. I was not exploring st.session_state previously.

@andfanilo - FYI, I’ve finally got a project underway to start using streamlit-echarts! :crossed_fingers:t4:

2 Likes

You’ll love it @asehmi! :raised_hands:

2 Likes

Really, really nice work Fanilo! :star_struck:

Hi!
Thanks for this amazing component! It’s by far my favorite!!
I’m still discovering what could be done with it, and I’ve seen that we can add interactivity with events.
Unfortunately, I was not able to do it myself.
Do you have an example of how to use events?
For example, on a bar chart, when clicking on a bar, a specific action is triggered with the data related to this bar?

Hi @Pierre

There’s a bit in the events section of the README that you can map back to the ECharts events docs.

Basically in the following:

from streamlit_echarts import st_echarts

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": "bar"}],
}

st_echarts(
    options=options,
    events={
        "click": "function(params) { alert(params.name + '  ' + params.seriesName) }"
    },
    height="700px",
)

the events={"click": "function(params) { alert(params.name + ' ' + params.seriesName) }"} is equivalent to the following JS:

myChart.on('click', function (params) {
   alert(params.name + ' ' + params.seriesName);
});

Everytime you click a bar you will see display an alert with the name of the graph and the series name from the JSON options. In the echarts docs you will find all the params values accessible, so you can do much more like digging through the params argument to see what part of the graph was clicked.

All possible params values
{
    // component name of clicked component
    // e.g., 'series', 'markLine', 'markPoint', 'timeLine'
    componentType: string,
    // series type (useful when componentType is 'series')
    // e.g., 'line', 'bar', 'pie'
    seriesType: string,
    // series index in option.series (useful when componentType is 'series')
    seriesIndex: number,
    // series name (useful when componentType is 'series')
    seriesName: string,
    // data name, or category name
    name: string,
    // data index in input data array
    dataIndex: number,
    // raw input data item
    data: Object,
    // Some series, such as sankey or graph, maintains both nodeData and edgeData,
    // in which case, dataType is set to be 'node' or 'edge' to identify.
    // On the other hand, most other series have only one type of data,
    // where dataType is not needed.
    dataType: string,
    // input data value
    value: number|Array
    // color of component (useful when componentType is 'series')
    color: string
}

For example this will only work on a bar click, not a line click, to display the name of the series and its data (should have given a name to the series instead of the default one, anyway):

from streamlit_echarts import st_echarts

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": "bar"},
        {"data": [82, 93, 91, 34, 190, 330, 120], "type": "line"},
    ],
}

st_echarts(
    options=options,
    events={
        "click": "function(params) {if(params.seriesType=='bar'){alert( params.seriesName + ' ' + params.data)}}"
    },
    height="700px",
)

test


Apparently you can do chart.on(eventName, query, handler); which looks super cool and is not implemented yet on my side, let me put that into my backlog :slight_smile: .


Also

myChart.on('legendselectchanged', function (params) {
    // obtain selecting status of clicked legend
    var isSelected = params.selected[params.name];
    // print in console
    console.log((isSelected ? 'select' : 'unselect') + 'legend' + params.name);
    // print all legend status
    console.log(params.selected);
});

looks like it’s working:

from streamlit_echarts import st_echarts

# https://echarts.apache.org/examples/en/editor.html?c=line-stack
options = {
    "title": {"text": "折线图堆叠"},
    "tooltip": {"trigger": "axis"},
    "legend": {"data": ["邮件营销", "联盟广告", "视频广告", "直接访问", "搜索引擎"]},
    "grid": {"left": "3%", "right": "4%", "bottom": "3%", "containLabel": True},
    "toolbox": {"feature": {"saveAsImage": {}}},
    "xAxis": {
        "type": "category",
        "boundaryGap": False,
        "data": ["周一", "周二", "周三", "周四", "周五", "周六", "周日"],
    },
    "yAxis": {"type": "value"},
    "series": [
        {
            "name": "邮件营销",
            "type": "line",
            "stack": "总量",
            "data": [120, 132, 101, 134, 90, 230, 210],
        },
        {
            "name": "联盟广告",
            "type": "line",
            "stack": "总量",
            "data": [220, 182, 191, 234, 290, 330, 310],
        },
        {
            "name": "视频广告",
            "type": "line",
            "stack": "总量",
            "data": [150, 232, 201, 154, 190, 330, 410],
        },
        {
            "name": "直接访问",
            "type": "line",
            "stack": "总量",
            "data": [320, 332, 301, 334, 390, 330, 320],
        },
        {
            "name": "搜索引擎",
            "type": "line",
            "stack": "总量",
            "data": [820, 932, 901, 934, 1290, 1330, 1320],
        },
    ],
}

st_echarts(
    options=options,
    events={
        "legendselectchanged": "function(params){var isSelected=params.selected[params.name];console.log((isSelected ? 'select' : 'unselect') + 'legend' + params.name); console.log(params.selected);}"
    },
    height="700px",
)

so you can interact on parts of the graph as long as you know their name in the option.


Now bear in mind the JS Code that will be run in events will be limited in scope. You won’t have access to the echarts instance to update your graph dynamically with setOption upon events like

myChart.on('click', function (parmas) {
    $.get('detail?q=' + params.name, function (detail) {
        myChart.setOption({
            series: [{
                name: 'pie',
                // present data distribution  of a single bar through pie chart
                data: [detail.data]
            }]
        });
    });
});

nor will you have access to the Streamlit JS module to send back data into Streamlit upon clicking on an object, like :

events={"click": "function(params) {Streamlit.setComponentValue(1)}"}

let’s just say the Streamlit and echarts instance are renamed and “lost” when I build the streamlit-echarts package. It’s like not having access to the “source code” anymore.

Basically you have all vanilla Javascript features in events, no CDN/nodeJS modules/jQuery etc…

If you need advanced interactivity like clicking on a bar in echarts and get a value back in Streamlit, you’ll need to deploy it yourself. I’ve done a similar exercise in Plotly.js here and Chart.js here to show you examples.


Hope this helps a bit. Happy reading :slight_smile:

Fanilo

2 Likes

@andfanilo - Good morning!

I’ve seen how you’ve used JSCode in the options for tooltip formatters. What’s your advice to send arbitrary JS functions via options?

For example, in this echarts example, I’d like to pass the calculateMA() function. If that’s not supported, then I guess I need to re-implement this function in Python and pass the output data series instead.

Or would you suggest I use pyecharts?

Thanks in advance for your input.
Arvindra

1 Like

Hola!

I usually use JsCode for things that should be computed on the fly by echarts, generally tooltip label that takes hovered data as input to output some HTML.

Everything that can be prepared on the Python side (usually anything that is built into the options) I convert into Python. In the case of calculateMA() I will definitely convert that to Python code and prepare all the data to put into options on the Python side. Anytime the script reruns for new data, the options will be recomputed on the Python side, and echarts on the frontend will transition between the two options.

Nice use case, I did not look at this example :slight_smile:
Fanilo

2 Likes

Much appreciated. I’m trying to build some very complicated technical analysis charts (like the one I showed) and need to share this code with Web/JS developers, so doing too much in Python isn’t going to be a scalable way of working. I’m first going to have a go “injecting” a static HTML Streamlit component script (similar to the one I used in my toggle buttons component) at the end of the ECharts script, or simply load it using components.html. If that doesn’t work I’ll persevere with st_echarts.

I’ll keep you informed.

Thanks,
Arvindra

1 Like

For everybody looking for a simpler way for bi-directional communication from EChart events (JS) back to python.

events = {
   "click": "function(p){console.log(p); delete p.event; window.parent.postMessage({isStreamlitMessage:!0,type:'streamlit:setComponentValue','value':p},'*');}"
} 
r = st_pyecharts(c,  width=9000, height=640, events=events, key='chart')
st.write(json.dumps(r))
  • Don’t forget to assign the “key” parameter, otherwise, st_pyecharts will not return any value.
2 Likes

This is exactly what I came here for. Thank you for posting it. Excited to try it out.

2 Likes

Thank You! This is really useful.

I would really appreciate if Streamlit-echarts will be maintained in future. It is a killer component, and it would be great if streamlit can incorporate echarts like plotly and provide support for it.

1 Like