Mueez Khan
Mueez Khan
Mueez Khan

"The dashboard was created to support water management decisions by individuals, communities, and utilities to promote better stewardship of water resources and greater resilience during droughts or water supply challenges." — Boerne UtilitiesIt wasn't only the web app that was new, rather I also recalculated the data necessary to render each of the , , and on the web app.
If you're on a mobile device then for the rest of this post (or at least for the tables and chart) you may want to rotate your phone to landscape mode .
Much of the rest of this post is based on a Jupyter Notebook and assumes you're familiar with local Python development/analysis.Daily total water demand chart on the Boerne Water Data Dashboard.
We'll download the latest water utility data from boernetx.thewaterdata.com/data/utility-data.csv.
After downloading the data, we'll install polars, plotly, and numpy by running the following:
pip install polars plotly numpyimport polars as pl
df = pl.read_csv("utility-data.csv")
df.head()| Date | City Wells | Boerne City Lake | GBRA Total | GBRA Del. To KW | Canyon Lake (GBRA) | Reclaimed Water (WWTRC) | Total Water Demand | Water Treatment Plant Rainfall | City of Boerne Population | City Water Utility Service Population | Reclaimed Water Service Poulation |
| 5/19/2000 | 373 | 382 | 755 | ||||||||
| 5/20/2000 | 326 | 527 | 853 | ||||||||
| 5/21/2000 | 347 | 554 | 901 | ||||||||
| 5/22/2000 | 473 | 398 | 871 | ||||||||
| 5/23/2000 | 564 | 553 | 1,117 |
Date column from str to dateDate column's datatype from str to date so we can perform operations on the data.
df = df.with_columns(
pl.col("Date").str.to_date("%m/%d/%Y")
)
df.head()| Date | City Wells | Boerne City Lake | GBRA Total | GBRA Del. To KW | Canyon Lake (GBRA) | Reclaimed Water (WWTRC) | Total Water Demand | Water Treatment Plant Rainfall | City of Boerne Population | City Water Utility Service Population | Reclaimed Water Service Poulation |
| 2000-05-19 | 373 | 382 | 755 | ||||||||
| 2000-05-20 | 326 | 527 | 853 | ||||||||
| 2000-05-21 | 347 | 554 | 901 | ||||||||
| 2000-05-22 | 473 | 398 | 871 | ||||||||
| 2000-05-23 | 564 | 553 | 1,117 |
Date column to have a data type of date, so we can perform certain date-related operations on the dataframe using Polars.
For the actual implementation used for the Boerne Water Data Dashboard, we also run a filter for values from the most recent year(s) since it's not necessary to recalculate demand data for more historic records on each run.
Total Water Demand column in Millions of Gallons per Day (MGD)Total Water Demand column we'll want to perform the following:
str to f64 (e.g. decimal numbers)10006 digitsdf = df.with_columns(
(pl.col("Total Water Demand").str.replace_all(",", "", literal=True).cast(pl.Float64) / 1000).round(6)
)
df.head()| Date | City Wells | Boerne City Lake | GBRA Total | GBRA Del. To KW | Canyon Lake (GBRA) | Reclaimed Water (WWTRC) | Total Water Demand | Water Treatment Plant Rainfall | City of Boerne Population | City Water Utility Service Population | Reclaimed Water Service Poulation |
| 2000-05-19 | 373 | 382 | 0.755 | ||||||||
| 2000-05-20 | 326 | 527 | 0.853 | ||||||||
| 2000-05-21 | 347 | 554 | 0.901 | ||||||||
| 2000-05-22 | 473 | 398 | 0.871 | ||||||||
| 2000-05-23 | 564 | 553 | 1.117 |
Date and Total Water Demand columns.
df = df.select(
[
pl.col("Date"),
pl.col("Total Water Demand")
]
)
df.head()| Date | Total Water Demand |
| 2000-05-19 | 0.755 |
| 2000-05-20 | 0.853 |
| 2000-05-21 | 0.901 |
| 2000-05-22 | 0.871 |
| 2000-05-23 | 1.117 |
Year (e.g. 2025) - for partitioning the dataframe by yearMonth and day (e.g. Jan-01) - for the x-axis in each visualizationdf = df.with_columns(
pl.col("Date").dt.year().alias("Year")
).with_columns(
pl.col("Date").dt.strftime("%b-%d").alias("Month and day")
)
df.head()| Date | Total Water Demand | Year | Month and day |
| 2000-05-19 | 0.755 | 2000 | May-19 |
| 2000-05-20 | 0.853 | 2000 | May-20 |
| 2000-05-21 | 0.901 | 2000 | May-21 |
| 2000-05-22 | 0.871 | 2000 | May-22 |
| 2000-05-23 | 1.117 | 2000 | May-23 |
Note that in the actual implementation we add another step to add a row of null data with the date February 29 on non-leap years for the plot to render properly. If the year has data past Feb 28 and there is no row for Feb 29 (non-leap years) then we add a Feb 29 entry with a null Total Water Demand value (allows for proper Plotly chart render when overlaying years).
Year column to get several dataframes, one for each year.
dfs = df.partition_by("Year")dfs and view them as an interactive visualization using the Plotly express library.
import plotly.express as px
for curr_df in dfs:
year = curr_df.item(0, "Year")
px.scatter(
curr_df,
x="Month and day",
y="Total Water Demand",
title=f"Daily Total Water Demand (MGD) in {year}",
).show()
print("")
There are many other changes and steps involved to get to the visualization displayed on the Boerne Water Data Dashboard. However we have fulfilled the purpose of demonstrating how to get from the water utility data to a rudimentary interactive visualization.
In the actual implementation I used the Rust Polars library to extract, transform, and load the data while using the react-plotly.js library with Next.js to organize interactive charts on the water data dashboard.
Daily total water demand chart embedded below.
Updates made to this blog post over time.
Submit a comment about this post that may be shared.
Receive updates during my career.
Technical writing on work experiences, projects, and tools you may find useful.