Skip to content

Commit

Permalink
Implemented animation of depth dependent timeseries
Browse files Browse the repository at this point in the history
* animate_depth_timeseries creates aplotly animation of a timeseries
* doc/animate has an example
* test-data/animate/animate_depth_timeseries_expected contains expected frames
* Added dependency on plotly and dash
* Added test dependency on kaleido for testing plotly animation
  • Loading branch information
orting committed Jan 16, 2024
1 parent 7fa7e61 commit a8f5325
Show file tree
Hide file tree
Showing 38 changed files with 4,091 additions and 0 deletions.
2 changes: 2 additions & 0 deletions daisy_vis/animate/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
'''Functions for animating dlf data'''
from .animate_depth_timeseries import *
60 changes: 60 additions & 0 deletions daisy_vis/animate/animate_depth_timeseries.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# pylint: disable=missing-module-docstring
import plotly.express as px
from daisy_vis.transform import daisy_time_to_timestamp, depth_wide_to_long

__all__ = [
'animate_depth_timeseries'
]

def animate_depth_timeseries(var_name, dlf, *,
figsize=(1000,1000),
title=None,
var_lim=None,
depth_lim=None
):
# pylint: disable=too-many-arguments
'''
Parameters
----------
var_name : str
dlf : daisy_vis.io.dlf.Dlf
figsize : tuple of int, optional
(width, height) of figure in pixels
title : str, optional
Title of figure
var_lim : (float, float), optional
Limit of `var_name` values that are plotted. If None it is set to the data limit with a 10%
margin.
depth_lim : (float,float), optional
Limit of depth. If None it is set to the (lowest depth - 10, 10)
Returns
-------
plotly.graph_objs.Figure
'''
dlf = daisy_time_to_timestamp(dlf, 'time')
dlf = depth_wide_to_long(dlf, var_name, 'time', 'z')
if figsize is None:
width, height = None, None
else:
width, height = figsize
if var_lim is None:
var_min = dlf.body[var_name].min()
var_max = dlf.body[var_name].max()
var_lim = var_min - abs(var_min)*0.1, var_max + abs(var_max)*0.1
if depth_lim is None:
depth_lim = dlf.body['z'].min() - 10, 10
return px.scatter(dlf.body,
x=var_name,
y='z',
animation_frame='time',
title=title,
width=width,
height=height,
range_x=var_lim,
range_y=depth_lim,)
18 changes: 18 additions & 0 deletions daisy_vis/animate/test/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
'''Test data for tests in the amimate module'''
import pytest
from daisy_vis.io.dlf import read_dlf

@pytest.fixture
def depth_timeseries():
'''A depth time series'''
return read_dlf('test-data/daily/DailyP/DailyP-Daily-WaterFlux.dlf')

@pytest.fixture
def depth_timeseries_outdir():
'''Outdir for generated files'''
return 'test-data/animate/animate_depth_timeseries_actual'

@pytest.fixture
def depth_timeseries_expected_dir():
'''Outdir for reference files'''
return 'test-data/animate/animate_depth_timeseries_expected'
36 changes: 36 additions & 0 deletions daisy_vis/animate/test/test_animate_depth_timeseries.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
'''Test animate_depth_timeseries'''
import os
import filecmp
import pytest
import plotly
from daisy_vis.animate import animate_depth_timeseries

@pytest.mark.filterwarnings(r'ignore:setDaemon\(\) is deprecated, set the daemon attribute instead')
def test_png_rendering_is_the_same(depth_timeseries,
depth_timeseries_outdir,
depth_timeseries_expected_dir):
'''Save animated timeseries as a series of png files and compare with an existing reference'''
os.makedirs(depth_timeseries_outdir, exist_ok=True)
fig = animate_depth_timeseries('q', depth_timeseries)
actual_files = set()
for frame in range(len(fig.frames)):
fig.layout['sliders'][0]['active'] = frame
fig = plotly.graph_objects.Figure(data=fig['frames'][frame]['data'],
frames=fig['frames'],
layout=fig.layout)
fname = f'frame-{frame:02d}.png'
fig.write_image(os.path.join(depth_timeseries_outdir, fname))
actual_files.add(fname)

expected_files = {
entry.name for entry in os.scandir(depth_timeseries_expected_dir) if entry.is_file()
}
assert actual_files <= expected_files <= actual_files

match, mismatch, errors = filecmp.cmpfiles(depth_timeseries_outdir,
depth_timeseries_expected_dir,
expected_files,
shallow=False)
assert len(match) == len(expected_files)
assert len(mismatch) == 0
assert len(errors) == 0
5 changes: 5 additions & 0 deletions doc/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,8 @@

* [Plot annual values](plot/plot_annual_values.py). Also available as a [jupyter notebook](plot/plot_annual_values.ipynb) [![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/daisy-model/daisy-vis/main?labpath=doc%2Fplot%2Fplot_annual_values.ipynb)
* [Plot daily values](plot/plot_daily_values.py). Also available as a [jupyter notebook](plot/plot_daily_values.ipynb) [![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/daisy-model/daisy-vis/main?labpath=doc%2Fplot%2Fplot_daily_values.ipynb)

## Animation
Requires plotly and dash (for running outside a notebook)

* [Annimate depth dependent time series](animate/animate_depth_dependent.py). Also available as a [jupyter notebook](animate/animate_depth_dependent.py)
3,934 changes: 3,934 additions & 0 deletions doc/animate/animate_depth_dependent.ipynb

Large diffs are not rendered by default.

30 changes: 30 additions & 0 deletions doc/animate/animate_depth_dependent.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
'''Animate daily waterflux'''
import os
import sys
from dash import Dash, dcc, html
from daisy_vis.io.dlf import read_dlf
from daisy_vis.animate import animate_depth_timeseries

def main():
'''Run as `python <path/to/animate_depth_dependent.py>`'''
dirname = os.path.dirname(os.path.realpath(sys.argv[0]))
path = os.path.join(
dirname, '..', '..', 'test-data', 'daily', 'DailyP', 'DailyP-Daily-WaterFlux.dlf'
)
dlf = read_dlf(path)
var_name = 'q'
fig = animate_depth_timeseries(var_name, dlf)

app = Dash(__name__)
app.layout = html.Div(children=[
html.H1(children='DailyP'),
html.Div(children='Daily logged waterflux'),
dcc.Graph(
id='daily-waterflux',
figure=fig
)
])
app.run(debug=True)

if __name__ == '__main__':
main()
2 changes: 2 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
pandas
matplotlib
plotly
dash
1 change: 1 addition & 0 deletions requirements_test.txt
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
pytest
pytest-mpl
kaleido
2 changes: 2 additions & 0 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ platforms = any
install_requires =
pandas
matplotlib
plotly
dash
packages = find_namespace:
package_dir =
=.
Expand Down
1 change: 1 addition & 0 deletions test-data/animate/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
animate_depth_timeseries_actual
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit a8f5325

Please sign in to comment.