Skip to content

✨ add fill_value option to gap handlers #218

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
May 24, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,9 @@ Additionally, this notebook also shows some more advanced functionalities, such
* How to add (shaded) confidence bounds to your time series
* The flexibility of configuring different aggregation-algorithms and number of shown samples per trace
* How plotly-resampler can be used for logarithmic x-axes and an implementation of a logarithmic aggregation algorithm, i.e., [LogLTTB](example_utils/loglttb.py)
* Using different `fill_value` for gap handling of filled area plots.

**Note**: the basic example notebook requires `plotly-resampler>=0.9.0rc3`.

### 1.2 Figurewidget example

Expand Down
99 changes: 92 additions & 7 deletions examples/basic_example.ipynb

Large diffs are not rendered by default.

23 changes: 19 additions & 4 deletions plotly_resampler/aggregation/gap_handler_interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,19 @@


class AbstractGapHandler(ABC):
def __init__(self, fill_value: Optional[float] = None):
"""Constructor of AbstractGapHandler.

Parameters
----------
fill_value: float, optional
The value to fill the gaps with, by default None.
Note that setting this value to 0 for filled area plots is particularly
useful.

"""
self.fill_value = fill_value

@abstractmethod
def _get_gap_mask(self, x_agg: np.ndarray) -> Optional[np.ndarray]:
"""Get a boolean mask indicating the indices where there are gaps.
Expand All @@ -32,13 +45,13 @@ def _get_gap_mask(self, x_agg: np.ndarray) -> Optional[np.ndarray]:
"""
pass

def insert_none_between_gaps(
def insert_fill_value_between_gaps(
self,
x_agg: np.ndarray,
y_agg: np.ndarray,
idxs: np.ndarray,
) -> Tuple[np.ndarray, np.ndarray]:
"""Insert None values in the y_agg array when there are gaps.
"""Insert the fill_value in the y_agg array where there are gaps.

Gaps are determined by the x_agg array. The `_get_gap_mask` method is used to
determine a boolean mask indicating the indices where there are gaps.
Expand All @@ -48,7 +61,7 @@ def insert_none_between_gaps(
x_agg: np.ndarray
The x array. This is used to determine the gaps.
y_agg: np.ndarray
The y array. A copy of this array will be expanded with None values where
The y array. A copy of this array will be expanded with fill_values where
there are gaps.
idxs: np.ndarray
The index array. This is relevant aggregators that perform data point
Expand Down Expand Up @@ -83,6 +96,8 @@ def insert_none_between_gaps(
# Set the NaN values
# We add the gap index offset (via the np.arange) to the indices to account for
# the repeats (i.e., expanded y_agg array).
y_agg_exp_nan[np.where(gap_mask)[0] + np.arange(gap_mask.sum())] = None
y_agg_exp_nan[
np.where(gap_mask)[0] + np.arange(gap_mask.sum())
] = self.fill_value

return y_agg_exp_nan, idx_exp_nan
2 changes: 1 addition & 1 deletion plotly_resampler/aggregation/plotly_aggregator_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,7 @@ def aggregate(
if np.issubdtype(xdt, np.timedelta64) or np.issubdtype(xdt, np.datetime64):
agg_x_parsed = agg_x_parsed.view("int64")

agg_y, indices = gap_handler.insert_none_between_gaps(
agg_y, indices = gap_handler.insert_fill_value_between_gaps(
agg_x_parsed, agg_y, indices
)
if isinstance(downsampler, DataPointSelector):
Expand Down
35 changes: 35 additions & 0 deletions tests/test_aggregators.py
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,41 @@ def test_wrap_aggregate_x_gaps(downsampler, series):
assert pd.Series(y_agg).isna().sum() == 3


@pytest.mark.parametrize(
"downsampler",
[EveryNthPoint, LTTB, MinMaxAggregator, MinMaxLTTB, MinMaxOverlapAggregator],
)
@pytest.mark.parametrize("series", [lf("float_series")])
def test_wrap_aggregate_x_gaps_float_fill_value(downsampler, series):
idx = np.arange(len(series))
idx[1000:] += 1000
idx[2000:] += 1500
idx[8000:] += 2500
series.index = idx
# 1. test with the default fill value (i.e., None)
x_agg, y_agg, indices = wrap_aggregate(
hf_x=series.index,
# add a constant to the series to ensure that the fill value is not used
hf_y=series.values + 1000,
downsampler=downsampler(),
gap_handler=MedDiffGapHandler(),
n_out=100,
)
assert len(x_agg) == len(y_agg) == len(indices)
assert pd.Series(y_agg).isnull().sum() == 3
# 2. test with a custom default fill value (i.e., 0)
x_agg, y_agg, indices = wrap_aggregate(
hf_x=series.index,
# add a constant to the series to ensure that the fill value is not used
hf_y=series.values + 1000,
downsampler=downsampler(),
gap_handler=MedDiffGapHandler(fill_value=0),
n_out=100,
)
assert len(x_agg) == len(y_agg) == len(indices)
assert pd.Series(y_agg == 0).sum() == 3


@pytest.mark.parametrize(
"downsampler", [EveryNthPoint, LTTB, MinMaxLTTB, MinMaxOverlapAggregator]
)
Expand Down