How To Build Interactive Charts In Python Using Htmx And Echarts

In this post, we will show you how to include javascript libraries like echarts into your htmx workflow using htmx extensions.

Prerequisites

  • Basic knowledge of pyecharts & htmx - If you haven't already, please check out this post about how to set up pyecharts for your Django/Python application. 
  • The code for this tutorial can be found here.

Concepts Covered

We want to make our filters to the chart so we can filter on dates.

  • htmx extensions - we will create an htmx extension to handle parsing data from the backend and updating the chart.
  • triggering client side actions from the backend
  • hyperscript - a small example of how to use the hyperscript library with htmx

Chart View

We will create a view to handle htmx requests. The view will use parameters provided in the request to return data for the correct period and chart type.

def chart_view_hx(request):
  """Returns chart options for echarts"""
  
  period = request.GET.get("period", "week")
  chart_id = request.GET.get("chart_id")
  chart_type = request.GET.get("chart_type")
  
  days_in_period = {
    "week": 7,
    "month": 30,
  }
  filter_by = days_in_period.get(period,"week")

  # simulate fetching this from your database
  chart_title = CHART_TYPES.get(chart_type, "page_views")
  chart_data = fake_chart_data(filter_by, chart_title)

   # render the chart and update options to include id
  chart = line_chart(chart_data)
  chart.options["id"] = chart_id
  chart._prepare_render()
  data = chart.json_contents
  
  response = HttpResponse(content=chart.json_contents)
  
  # optional 
  # if using the django_htmx library you can attach any clientside 
  # events here . For example

  # trigger_client_event(response, 'filterchanged', params={})
  return response

Notes

  • period = request.GET(..) - fetch the period, chart_id, chart_type from the request
  • chart_data = fake_chart_data() - this simulates fetching data from an api or your database.
  • chart = line_chart(chart_data) - call the method that build the chart. This method will return an instance of a Line chart with all the options we need to update the chart
  • chart.options["id"] - here we add the id of the chart we want to update and then call the prepare_render method to ensure that the data is converted to json.
  • trigger_client_event - If you need to piggyback additional events to be triggered on the client-side, you can attach them here.
    • If you are using Django, the django_htmx library provides the trigger_client_event method for this
    • You can also do this for yourself by attaching the HX-Trigger to the response. See the htmx docs for more details on this.

Update the urls

django_project/urls.py

urlpatterns = [
 ...
    path('charts', chart_view_hx, name='charts-hx')
 ] + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)

Notes

  • path('charts', ..) - add the view to urls.py.
  • static(settings.STATIC_URL..) - You will notice we have also added the Django static helper function here so we can staticfiles in debug mode.

Add htmx extension

Htmx provides an extension mechanism for defining and using extensions within htmx-based applications.

static/js/ext/hx-echarts/js

htmx.defineExtension('echarts', {
    transformResponse: function (text, xhr, elt) {
        // parse json data
        var data = JSON.parse(text);

        // fetch echart element
        var option = data;
        var chartContainer = document.getElementById(data.id);
        var chart = echarts.getInstanceByDom(chartContainer);

        // clean up options and update chart
        delete data.id;
        chart.setOption(option);

    }
});

Notes

  • htmx.defineExtension('echarts', {} - define an extension called echarts. This will be referenced on the element by setting the hx-ext attribute i.e.<div hx-ext='echarts'>
  • var data = JSON(parse(text)) - parse the chart data from the backend
  • var chartContainer = document.getElementById(data.id); - fetch the chartContainer element from the DOM using the id provided in our data
  • delete data.id - we clean up our options object to remove the id. We only needed the Id so we could identify the chart to update.
  • chart.setOption(option) - finally, use the setOption()method from the echarts library to update the data on the chart.

Update the templates

Next, we need to update our chart template to include the filter buttons.

We will create a new filter_button component for the filters.

components/filter_button.html

  <button 
    hx-get="{% url 'charts-hx' %}?chart_id={{ c.chart_id }}&chart_type=page_views&period={{period}}"
    hx-swap="none"
    id="id_last_week_filter_btn" 
    class="bg-white rounded shadow p-2 text-sm flex items-center font-semibold mr-2 cursor-pointer
    {% if selected %}text-indigo-600 {% else %}text-gray-600 {% endif %}">{{label}}
  </button>

Notes

  • hx-get="{% url 'charts-hx' %}... - include the url and required parameters
  • hx-swap ='none' - set this to none, the response from the backend will not be swapped out.
  • {{chart_type}},{{chart_id}},{{period}} - the component expects these variables to be available in the context
  • {% if selected .. %} - here we are adding a bit of styling to highlight the selected button.

The main template will remain largely unchanged from the previous tutorial. We have modified it to include the filter_buttons. To keep things simple, we have removed the additional charts.

dashboard.html


<head>
  ...
  <script src="https://unpkg.com/htmx.org@1.7.0"></script>
  <script src="{% static 'js/ext/hx-echarts.js'%}"></script>
  ...
</head>
<div class='flex' hx-ext="echarts">
   {% include 'components/chart_filter.html' with period='week' c=charts.0 label='Last Week' selected=True %}
   {% include 'components/chart_filter.html' with period='month' c=charts.0 label='Last Month' %} 
</div>
   

Notes

  • <head >..</head> - the head section has been modified to include the htmx library, the echarts extension script.
  • {% include 'components/chart_filter.html' - include the components in the main template and define the variables for the chart, label, the period to be used in the filter as well as the

UI Improvements

We can improve it by letting our users know which tab is active. This can be achieved using vanilla javascript or alpinejs. In this example, we will use hyperscript, a companion library for htmx to achieve this.

dashboard.html

<head>
  ...
  <script src="https://unpkg.com/htmx.org@1.7.0"></script>
  <script src="https://unpkg.com/hyperscript.org@0.9.5"></script>
  <script src="{% static 'js/ext/hx-echarts.js'%}"></script>
  ...
</head>
<div class='flex' hx-ext="echarts" 
    _="on htmx:afterOnLoad take .text-indigo-600 for event.target"
 >
   {% include 'components/chart_filter.html' with period='week' c=charts.0 label='Last Week' selected=True %}
   {% include 'components/chart_filter.html' with period='month' c=charts.0 label='Last Month' %} 
</div>

Notes

  • <head></head> - we have updated the head section to include hyperscript.
  • _="on htmx:afterOnLoad take ..." after the response is received, the text-indigo-600 class will be moved to the clicked button.

Conclusion

In this post, we learned how to use htmx extensions with the echarts library. The htmx library provides a convenient mechanism to organize your codebase and integrate other libraries into your htmx workflow.

Further reading

Last Updated 18 Aug 2022