angle-uparrow-clockwisearrow-counterclockwisearrow-down-uparrow-leftatcalendarcard-listchatcheckenvelopefolderhouseinfo-circlepencilpeoplepersonperson-fillperson-plusphoneplusquestion-circlesearchtagtrashx

Time series chart with Flask, Bootstrap and Chart.js

Create a time series chart test page with Flask, Bootstrap and Chart.js

16 December 2024 Updated 16 December 2024
post main image

We already have an application build with Flask and Bootstrap and want to add some charts. In this short post we create a single web page with a time series chart using Flask, Bootstrap and Chart.js. This page a test page that we can paste and customize for our application.

As always I am doing this on Ubuntu 22.04 Desktop.

Overview

The test page contains a chart showing time series demo data:

  • The time series data is one sine per hour, one data point per minute, repeated 24 hours.
  • The time series data may have some missing data points.
  • It must be possible to zoom in and out.
  • The chart should be responsive, x-axis only.
  • Use milliseconds or ISO8601.
  • It should be multilingual.

Almost everything is very standard. For the type of x-axis, we can choose between:

  • 'time': the x-axis is consistent with time, data points that are missing are clearly visible.
  • 'timeseries': the x-axis is compressed where data points are missing.

Here we use the 'time' type.

We generate sine data, one sine per hour, with values between -10 and +10, and intentionally add missing data points. Then we set 'spanGaps' to a value, 2 minutes, to ensure that the data points are not connected to each other.

spanGaps: 2 * 60 * 1000

The chart is responsive, but only in the x direction. The height of the graph is fixed and we create a linear y-axis with a minimum value (-10) and a maximum value (10).

To zoom in, we use the mouse wheel (scroll wheel) or by selecting an area with the mouse. We add a button to reset the zoom and return to the original chart.

To support different languages, we load the appropriate module of moment.js, for example, for German:

https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.30.1/locale/de.min.js

The code

Create a virtual environment and install Flask. Then create a project directory with the following structure:

├── app
│   ├── factory.py
│   └── templates
│       ├── base.html
│       └── home.html
└── run.py

As always we start with 'run.py', note that we are using port 5050 here:

# run.py
from app.factory import create_app

app = create_app()

if __name__ == '__main__':
    app.run(
        host='127.0.0.1',
        port=5050,
        use_debugger=True,
        use_reloader=True,
    )

In 'factory.py' we create the data points and specify the language:

# app/factory.py
import datetime
import logging
import math

from flask import current_app, Flask, render_template

def create_app():
    app = Flask(__name__)

    app.jinja_env.auto_reload = True
    app.config['TEMPLATES_AUTO_RELOAD'] = True
    app.logger.setLevel(logging.DEBUG)

    @app.route('/', methods=['GET', 'POST'])
    def chart():
        current_app.logger.debug('()')

        # select locale
        locale = 'de-DE'
        #locale = 'en-US'
        #locale = 'nl-NL'

        chart_locales = {
            'de-DE': {'html': 'de', 'chartjs': 'de-DE', 'momentjs': 'de'},
            'en-US': {'html': 'en', 'chartjs': 'en-US', 'momentjs': None},
            'nl-NL': {'html': 'nl', 'chartjs': 'nl-NL', 'momentjs': 'nl'},
        }

        # 1 sine per hour, for 24 hours, starting now
        dt = datetime.datetime.utcnow()
        ts = int(dt.timestamp())
        chart_data = []
        dt_format_milliseconds = False
        for h in range(24):
            for minute in range(60):
                # x: in milliseconds or iso8601
                dt += datetime.timedelta(minutes=1)
                if dt_format_milliseconds:
                    x = int(dt.timestamp() * 1000)
                else:
                    x = dt.isoformat()
                # y: -10 to 10    
                rad = 6 * minute
                y = 10 * (math.sin(math.radians(rad)))
                # insert missing data points
                if rad > 120 and rad < 160:
                    continue
                chart_data.append({'x': x, 'y': y})

        return render_template(
            '/chart.html',
            page_title='Chart',
            page_contains_charts=True,
            chart_locale=chart_locales[locale],
            chart_data=chart_data,
        )

    return app

We have two template files, the base page template and the chart page template. The base page template can be used by all pages, the chart page template contains the page with our chart.

{# app/templates/base.html #}
<!DOCTYPE html>
<html lang="{{ chart_locale['html'] }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<title>{{ page_title }}</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
</head>
<body>

<main id="main" class="container -fluid">
    <div class="row">
        <div class="col">
        {% block main -%}{% endblock -%}
        </div>
    </div>
</main>

<!-- bootstrap + jquery -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz" crossorigin="anonymous"></script>
<script src="https://code.jquery.com/jquery-3.7.1.min.js" integrity="sha256-/JqT3SQfawRcv/BIHPThkBvs0OEvtFFmqPF/lYI/Cxo=" crossorigin="anonymous"></script>

{%- if page_contains_charts -%}
<!-- chartjs -->
<script src=" https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js "></script>
<!-- moment -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.30.1/moment.min.js"></script>
<!-- moment with locale -->
{%- if chart_locale['momentjs'] is not none -%}
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.30.1/locale/{{ chart_locale['momentjs'] }}.min.js"></script>
{%- endif -%}
<!-- chartjs moment adapter -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/chartjs-adapter-moment/1.0.1/chartjs-adapter-moment.min.js" integrity="sha512-hVy4KxCKgnXi2ok7rlnlPma4JHXI1VPQeempoaclV1GwRHrDeaiuS1pI6DVldaj5oh6Opy2XJ2CTljQLPkaMrQ==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<!-- chartjs zoom -->
<script src="https://cdn.jsdelivr.net/npm/hammerjs@2.0.8"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/chartjs-plugin-zoom/2.2.0/chartjs-plugin-zoom.min.js" integrity="sha512-FRGbE3pigbYamZnw4+uT4t63+QJOfg4MXSgzPn2t8AWg9ofmFvZ/0Z37ZpCawjfXLBSVX2p2CncsmUH2hzsgJg==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>

<script>
$(document).ready(function() {
    moment.locale(chartLocale);
    const ctx = document.getElementById('chart-canvas');
    const chart = new Chart(ctx, {
        type: chartType,
        data: chartData,
        options: chartOptions,
    });
    window.chart = chart;
    function reset_zoom() {
        window.chart.resetZoom();
    }
    $('#reset-zoom-button').click(function(){
        reset_zoom();
    }); 
});
</script>
{%- endif -%}
</body>
</html>
{# app/templates/chart.html #}
{% extends "base.html" %}
{% block main %}
    <h2 class="mb-3">
        {{ page_title }}
    </h2>
    <div class="row border-top border-bottom border-2 border-tertiary">
        <div class="col my-4" style="height: 300px; ">
            <canvas id="chart-canvas"></canvas>
        </div>
    </div>
    <div class="row">
        <div class="col">
            <button id="reset-zoom-button" type="button" class="btn btn-outline-secondary btn-sm my-2 px-1 py-0">
                Reset Zoom
            </button>
        </div>
    </div>

<script>
const chartLocale = "{{ chart_locale['chartjs'] }}";

// colors from:
// https://github.com/chartjs/Chart.js/blob/master/docs/scripts/utils.js
const CHART_COLORS = {
    red: 'rgb(255, 99, 132)',
    orange: 'rgb(255, 159, 64)',
    yellow: 'rgb(255, 205, 86)',
    green: 'rgb(75, 192, 192)',
    blue: 'rgb(54, 162, 235)',
    purple: 'rgb(153, 102, 255)',
    grey: 'rgb(201, 203, 207)'
};

const chartType = 'line';

const dataSet1 = {
    label: 'dataSet1',
    data: {{ chart_data|safe }},
    spanGaps: 2 * 60 * 1000,
    // line & point color & width
    borderColor: CHART_COLORS.blue,
    borderWidth: 1,
    pointBackgroundColor: CHART_COLORS.red,
    pointBorderColor: CHART_COLORS.red,
    pointStyle: 'circle',
    pointRadius: 1,
    pointHoverRadius: 10
};

const chartData = {
    datasets: [
        dataSet1
    ]
};

const xScaleDisplayFormat = 'MMM DD HH:mm:ss';

const chartScales = {
    x: {
        type: 'time',
        ticks: {
            autoSkip: true,
            autoSkipPadding: 10,
        },
        time: {
            tooltipFormat: xScaleDisplayFormat,
            displayFormats: {
                millisecond: xScaleDisplayFormat,
                second: xScaleDisplayFormat,
                minute: xScaleDisplayFormat,
                hour: xScaleDisplayFormat,
            }
        },
    },
    y: {
        type: 'linear',
        min: -10,
        max: 10,
    }
};

const chartZoom = {
    pan: {
        enabled: true,
        mode: 'x',
        modifierKey: 'ctrl',
    },
    zoom: {
        wheel: {
            enabled: true,
        },
        drag: {
            enabled: true
        },
        mode: 'x',
    }
};

const chartOptions = {
    locale: chartLocale,
    responsive: true,
    maintainAspectRatio: false,
    scales: chartScales,
    plugins: {
        zoom: chartZoom,
        legend: {
            display: false
        }
    }
};
</script>
{%- endblock -%}

To run, go to the project project directory and type:

python run.py

Then point your browser at:

http://127.0.0.1:5050

Summary

We created a single web page using Flask, Bootstrap and Chart.js. The chart shows a repeated sine wave. Although not very difficult, it takes time to go through all the configuration options available. We can now customize our chart test page and add it to our application.

Links / credits

Bootstrap
https://getbootstrap.com

Chart.js
https://www.chartjs.org/docs/latest

Flask
https://flask.palletsprojects.com/en/stable

Leave a comment

Comment anonymously or log in to comment.

Comments

Leave a reply

Reply anonymously or log in to reply.