Skip to content

Commit

Permalink
Merge pull request #19 from jobar8/jobar8/issue7
Browse files Browse the repository at this point in the history
New Export to image file dialogue
  • Loading branch information
jobar8 authored Oct 21, 2024
2 parents 16ab612 + 192a74a commit 57c1ea6
Show file tree
Hide file tree
Showing 4 changed files with 105 additions and 12 deletions.
4 changes: 3 additions & 1 deletion src/attractor_explorer/attractors.py
Original file line number Diff line number Diff line change
Expand Up @@ -244,8 +244,10 @@ def _save(self):
if self.output_examples_filename == self.param.input_examples_filename.default:
msg = 'Cannot override the default attractors file.'
raise FileExistsError(msg)
with (Path(self.data_folder) / self.output_examples_filename).open('w') as f: # type: ignore
output_path = Path(self.data_folder) / self.output_examples_filename # type: ignore
with output_path.open('w') as f:
yaml.dump(list(self.param.example.objects), f)
print(f'Parameters have been saved to {output_path}') # noqa: T201

def __call__(self):
return self.example
Expand Down
84 changes: 75 additions & 9 deletions src/attractor_explorer/attractors_explorer.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,13 @@
"""
# pyright: reportAttributeAccessIssue=false, reportArgumentType=false

from pathlib import Path

import panel as pn
import param

from attractor_explorer import attractors as at
from attractor_explorer.shared import colormaps, render_attractor
from attractor_explorer.shared import colormaps, render_attractor, save_image

try:
from attractor_explorer._version import __version__
Expand All @@ -24,22 +26,35 @@
RESOLUTIONS = {'Low': 200_000, 'Medium': 10_000_000, 'High': 50_000_000, 'Very High': 100_000_000}
PLOT_SIZE = 800
SIDEBAR_WIDTH = 360
CSS = """
GLOBAL_CSS = """
:root {
--background-color: black
--design-background-color: black;
--design-background-text-color: #ff7f00; # orange
--panel-surface-color: black;
}
"""
MODAL_CSS = """
#sidebar {
background-color: black;
# background-color: black;
}
#pn-Modal {
--dialog-width: 33%;
}
"""
# Hack can be removed when https://github.com/holoviz/panel/issues/7360 has been solved
CMAP_CSS_HACK = 'div, div:hover {background: #2b3035; color: white}'

pn.extension('katex', design='material', global_css=[CSS], sizing_mode='stretch_width') # type: ignore
pn.config.throttled = True
pn.extension(
'katex', # type: ignore
design='material',
global_css=[GLOBAL_CSS],
sizing_mode='stretch_width',
notifications=True,
throttled=False,
)

pn.state.notifications.position = 'center-center'


class AttractorsExplorer(pn.viewable.Viewer):
Expand All @@ -61,6 +76,7 @@ class AttractorsExplorer(pn.viewable.Viewer):
bounds=(1, None),
softbounds=(1, RESOLUTIONS['Very High']),
doc='Number of points',
label='Number of points',
precedence=0.8,
)
colormap = pn.widgets.ColorMap(
Expand Down Expand Up @@ -106,7 +122,14 @@ def update_attractor(self):
ats.param_sets.current = lambda: ats.attractor_type


pn.template.FastListTemplate(
def _callback(event): # noqa: ARG001
template.open_modal()


save_button = pn.widgets.Button(name='Export to Image File', margin=(20, 10))
save_button.on_click(_callback)

template = pn.template.FastListTemplate(
title='Attractor Explorer',
sidebar=[
pn.Param(
Expand All @@ -126,6 +149,7 @@ def update_attractor(self):
),
ats.colormap,
ats.interpolation,
save_button,
pn.layout.Card(
pn.Param(
ats.param_sets.param,
Expand All @@ -142,11 +166,11 @@ def update_attractor(self):
},
show_name=False,
),
title='Load and Save',
title='Load and Save Parameters',
collapsed=True,
header_color='white',
header_background='#2c71b4',
margin=(40, 10, 100, 10),
margin=(20, 10, 100, 10),
),
],
main=[
Expand All @@ -161,4 +185,46 @@ def update_attractor(self):
header_background='teal',
theme='dark',
theme_toggle=False,
).servable('Attractor Explorer') # .show(port=5006, open=False)
raw_css=[MODAL_CSS],
)


class ImageSaver(pn.viewable.Viewer):
"""
Dialog for saving attractor to image file.
"""

output_folder = param.Foldername('output', search_paths=[Path(__file__).parent.as_posix()], check_exists=False)
output_filename = param.String('attractor.png')
n_points = param.Integer(500_000_000, label='Number of points')
image_size = param.Integer(1000, label='Image size in pixels')
background_color = param.String(default='black')
save = param.Action(lambda x: x._save(), precedence=0.99)

def _save(self):
"""Create and save attractor image as png file."""
trajectory = ats.attractor_type(n=self.n_points) # type: ignore
img = render_attractor(trajectory, cmap=ats.colormap.value, size=self.image_size, how=ats.interpolation.value)
output_path = Path(self.output_folder) / self.output_filename # type: ignore
try:
save_image(next(img), output_path, color=self.background_color)
except Exception as err:
pn.state.notifications.error(f'Error: {err}', duration=0) # type: ignore
else:
msg = f'The image has been saved to {output_path}'
pn.state.notifications.success(msg, duration=0) # type: ignore
print(msg) # noqa: T201

def __panel__(self):
return pn.Param(
self,
widgets={
'background_color': {'widget_type': pn.widgets.ColorPicker(name='Background Colour', value='#000000')}
},
)


image_saver = ImageSaver(name='Choose settings for the export:')
template.modal.extend(['# Export to image file...', image_saver])

template.servable('Attractor Explorer') # .show(port=5006, open=False)
9 changes: 8 additions & 1 deletion src/attractor_explorer/shared.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""Support functions for dashboards."""

from collections.abc import Generator
from pathlib import Path
from typing import Generator

import datashader as ds
import pandas as pd
Expand Down Expand Up @@ -121,3 +122,9 @@ def render_attractor(
cvs = ds.Canvas(plot_width=size, plot_height=size)
agg = getattr(cvs, plot_type)(trajectory, 'x', 'y', agg=ds.count())
yield ds.tf.shade(agg, cmap=cmap, **kwargs)


def save_image(img: ds.tf.Image, output_path: Path | str, color: str = 'black') -> None:
"""Export image to png file."""
output_image = ds.tf.set_background(img, color=color)
output_image.to_pil().save(output_path, format='png')
20 changes: 19 additions & 1 deletion tests/test_shared.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,31 @@
"""Tests for the shared.py module."""

from unittest.mock import MagicMock, patch

import datashader as ds
import numpy as np
import pandas as pd

from attractor_explorer.shared import render_attractor
from attractor_explorer.shared import render_attractor, save_image


def test_render():
trajectory = pd.DataFrame(np.random.default_rng().random((30, 2)), columns=list('xy'))
image = list(render_attractor(trajectory, plot_type='points', cmap=None, size=400))
assert isinstance(image[0], ds.tf.Image)


@patch('attractor_explorer.shared.ds.tf.set_background')
def test_save_image(mock_set_background):
# Mocking the image object
mock_img = MagicMock()
mock_output_image = MagicMock()
mock_set_background.return_value = mock_output_image
mock_output_image.to_pil.return_value.save = MagicMock()

output_path = 'test_output.png'
color = 'blue'

save_image(mock_img, output_path, color)
mock_set_background.assert_called_once_with(mock_img, color=color)
mock_output_image.to_pil.return_value.save.assert_called_once_with(output_path, format='png')

0 comments on commit 57c1ea6

Please sign in to comment.