Skip to content

Commit

Permalink
Merge pull request #1660 from astrofrog/slider-format-function
Browse files Browse the repository at this point in the history
Factor out code to find minimal format for set of values
  • Loading branch information
astrofrog authored Apr 14, 2018
2 parents 3e0c798 + 3764166 commit caa8d60
Show file tree
Hide file tree
Showing 5 changed files with 61 additions and 18 deletions.
2 changes: 1 addition & 1 deletion CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ v0.13.0 (unreleased)

* Improve the display of data cube slice labels to include only the
precision required given the separation of world coordinate values.
[#1500]
[#1500, #1660]

* Removed the ability to edit the marker symbol in the style dialog
since this isn't recognized by any viewer anymore. [#1560]
Expand Down
25 changes: 22 additions & 3 deletions glue/utils/array.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
__all__ = ['unique', 'shape_to_string', 'view_shape', 'stack_view',
'coerce_numeric', 'check_sorted', 'broadcast_to', 'unbroadcast',
'iterate_chunks', 'combine_slices', 'nanmean', 'nanmedian', 'nansum',
'nanmin', 'nanmax']
'nanmin', 'nanmax', 'format_minimal']


def unbroadcast(array):
Expand Down Expand Up @@ -369,5 +369,24 @@ def nanmax(array, axis=None):
return bt.nanmax(array, axis=axis)


array = np.random.random((2, 5, 4, 2, 3))
nanmean(array, axis=(1, 2))
def format_minimal(values):
"""
Find the shortest format that can be used to represent all values in an
array such that all the string representations are different.
The current implementation is not incredibly efficient, but it takes only
~30ms for a 1000 element array and 200ms for a 10000 element array. One
could probably make a more efficient implementation but this is good enough
for now for what we use it for.
"""
values = np.asarray(values)
if np.max(np.abs(values)) > 1e5 or np.min(np.diff(values)) < 1e-5:
fmt_type = 'e'
else:
fmt_type = 'f'
for ndec in range(1, 15):
fmt = '{{:.{0}{1}}}'.format(ndec, fmt_type)
strings = [fmt.format(x) for x in values]
if len(strings) == len(set(strings)):
break
return fmt, strings
32 changes: 31 additions & 1 deletion glue/utils/tests/test_array.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@

from ..array import (view_shape, coerce_numeric, stack_view, unique, broadcast_to,
shape_to_string, check_sorted, pretty_number, unbroadcast,
iterate_chunks, combine_slices, nanmean, nanmedian, nansum, nanmin, nanmax)
iterate_chunks, combine_slices, nanmean, nanmedian, nansum,
nanmin, nanmax, format_minimal)


@pytest.mark.parametrize(('before', 'ref_after', 'ref_indices'),
Expand Down Expand Up @@ -229,3 +230,32 @@ def test_combine_slices_hypot(beg1, end1, stp1, beg2, end2, stp2, length):
actual = list(range(*combine_slices(slice1, slice2, length).indices(length)))

assert actual == expected


def test_format_minimal():

# TODO: in future could consider detecting integer cases
fmt, strings = format_minimal([133, 1444, 3300])
assert fmt == "{:.1f}"
assert strings == ['133.0', '1444.0', '3300.0']

# TODO: in future could consider detecting if all intervals are integers
fmt, strings = format_minimal([133., 1444., 3300.])
assert fmt == "{:.1f}"
assert strings == ['133.0', '1444.0', '3300.0']

fmt, strings = format_minimal([3, 4.3, 4.4, 5])
assert fmt == "{:.1f}"
assert strings == ['3.0', '4.3', '4.4', '5.0']

fmt, strings = format_minimal([3, 4.325, 4.326, 5])
assert fmt == "{:.3f}"
assert strings == ['3.000', '4.325', '4.326', '5.000']

fmt, strings = format_minimal([-3, 0., 0.993e-4, 5])
assert fmt == "{:.4f}"
assert strings == ['-3.0000', '0.0000', '0.0001', '5.0000']

fmt, strings = format_minimal([-3, 0., 0.993e-8, 5])
assert fmt == "{:.1e}"
assert strings == ['-3.0e+00', '0.0e+00', '9.9e-09', '5.0e+00']
10 changes: 2 additions & 8 deletions glue/viewers/common/qt/data_slice_widget.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

from qtpy import QtCore, QtWidgets
from glue.utils.qt import load_ui
from glue.utils import nonpartial
from glue.utils import nonpartial, format_minimal
from glue.icons.qt import get_icon
from glue.core.state_objects import State, CallbackProperty
from glue.external.echo.qt import autoconnect_callbacks_to_qt
Expand Down Expand Up @@ -72,13 +72,7 @@ def __init__(self, label='', world=None, lo=0, hi=10,
# a string, every string value is different.

if world is not None and len(world) > 1:
if np.max(np.abs(world)) > 1e5 or np.max(np.abs(world)) < 1e-5:
fmt_type = 'e'
else:
fmt_type = 'f'
relative = np.abs(np.diff(world) / world[:-1])
ndec = max(2, min(int(np.ceil(-np.log10(np.min(relative)))) + 1, 15))
self.label_fmt = "{:." + str(ndec) + fmt_type + "}"
self.label_fmt = format_minimal(world)[0]
else:
self.label_fmt = "{:g}"

Expand Down
10 changes: 5 additions & 5 deletions glue/viewers/common/qt/tests/test_data_slice_widget.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,26 +38,26 @@ def test_slice_world(self):

# Check switching between world and pixel coordinates
s.state.slice_center = 0
assert s.state.slider_label == '1.00'
assert s.state.slider_label == '1.0'
s.state.use_world = False
assert s.state.slider_label == '0'
s.state.slice_center = 3
assert s.state.slider_label == '3'
s.state.use_world = True
assert s.state.slider_label == '5.50'
assert s.state.slider_label == '5.5'

# Round to nearest
s.state.slider_label = '11'
assert s.state.slice_center == 5
assert s.state.slider_label == '12.00'
assert s.state.slider_label == '12.0'

# Make sure out of bound values work
s.state.slider_label = '20'
assert s.state.slice_center == 5
assert s.state.slider_label == '12.00'
assert s.state.slider_label == '12.0'
s.state.slider_label = '-10'
assert s.state.slice_center == 0
assert s.state.slider_label == '1.00'
assert s.state.slider_label == '1.0'

# And disable world and try and set by pixel
s.state.use_world = False
Expand Down

0 comments on commit caa8d60

Please sign in to comment.