From 223ef01ad7f57bcd2f0d56c8c7bf8ec5f067a127 Mon Sep 17 00:00:00 2001 From: jvdd Date: Sun, 17 Jul 2022 10:25:09 +0200 Subject: [PATCH 01/49] :pray: enable numerically unstable test --- tests/test_figurewidget_resampler.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_figurewidget_resampler.py b/tests/test_figurewidget_resampler.py index 139d9e17..52301783 100644 --- a/tests/test_figurewidget_resampler.py +++ b/tests/test_figurewidget_resampler.py @@ -1495,7 +1495,6 @@ def test_fwr_time_based_data_ms(): def test_fwr_time_based_data_s(): - return # TODO: enable this test once #84 is merged # See: https://github.com/predict-idlab/plotly-resampler/issues/93 n = 100_000 fig = FigureWidgetResampler( From 39c871cc27a7b93d2103a47868e485a4dffcee8f Mon Sep 17 00:00:00 2001 From: jonas Date: Sun, 17 Jul 2022 13:39:14 +0200 Subject: [PATCH 02/49] :pineapple: improving examples --- examples/README.md | 10 +- .../{dash_app.py => dash_app_folder.py} | 92 +++++++++---------- examples/dash_apps/dash_app_minimal_cache.py | 88 ++++++++++++++++++ examples/dash_apps/dash_app_minimal_global.py | 81 ++++++++++++++++ 4 files changed, 221 insertions(+), 50 deletions(-) rename examples/dash_apps/{dash_app.py => dash_app_folder.py} (63%) create mode 100644 examples/dash_apps/dash_app_minimal_cache.py create mode 100644 examples/dash_apps/dash_app_minimal_global.py diff --git a/examples/README.md b/examples/README.md index 108e7382..33cf6cb7 100644 --- a/examples/README.md +++ b/examples/README.md @@ -20,11 +20,13 @@ Additionally, this notebook highlights how to use the `FigureWidget` its on-clic The [dash_apps](dash_apps/) folder contains example dash apps in which `plotly-resampler` is integrated -| app-name | description | -| --- | --- | -| [file visualization](dash_apps/dash_app.py) | load and visualize multiple `.parquet` files with plotly-resampler | +| app-name | description | +|------------------------------------------------------------------| --- | +| [minimal example (*global variable*)](dash_apps/dash_app_minimal_global.py) | *bad practice*: minimal example in which a global `FigureResampler` variable is used | +| [minimal example (*server side caching*)](dash_apps/dash_app_minimal_cache.py) | *good practice*: minimal example in which we perform server side caching of the `FigureResampler` variable | +| [file visualization](dash_apps/dash_app.py) | load and visualize multiple `.parquet` files with plotly-resampler | | [dynamic sine generator](dash_apps/construct_dynamic_figures.py) | expeonential sine generator which uses [pattern matching callbacks](https://dash.plotly.com/pattern-matching-callbacks) to remove and construct plotly-resampler graphs dynamically | -| [dynamic static graph](dash_apps/dash_app_coarse_fine.py) | Visualization dashboard in which a dynamic (i.e., plotly-resampler graph) and static graph (i.e., go.Figure) are shown (made for [this issue](https://github.com/predict-idlab/plotly-resampler/issues/56)). Relayout events on the coarse graph update the dynamic graph. +| [dynamic static graph](dash_apps/dash_app_coarse_fine.py) | Visualization dashboard in which a dynamic (i.e., plotly-resampler graph) and static graph (i.e., go.Figure) are shown (made for [this issue](https://github.com/predict-idlab/plotly-resampler/issues/56)). Relayout events on the coarse graph update the dynamic graph. ## 2. Other apps diff --git a/examples/dash_apps/dash_app.py b/examples/dash_apps/dash_app_folder.py similarity index 63% rename from examples/dash_apps/dash_app.py rename to examples/dash_apps/dash_app_folder.py index b245a67f..28fefacd 100644 --- a/examples/dash_apps/dash_app.py +++ b/examples/dash_apps/dash_app_folder.py @@ -1,7 +1,7 @@ -"""Minimal dash app example. +"""Dash file parquet visualization app example. -In this usecase, we have dropdowns which allows the end-user to select files, which are -visualized using FigureResampler after clicking on a button. +In this use case, we have dropdowns which allows the end-user to select multiple +parquet files, which are visualized using FigureResampler after clicking on a button. """ @@ -14,31 +14,34 @@ import dash_bootstrap_components as dbc import pandas as pd import plotly.graph_objects as go -import trace_updater from dash import Input, Output, State, dcc, html +from dash_extensions.enrich import ( + DashProxy, + ServersideOutput, + ServersideOutputTransform, +) from plotly.subplots import make_subplots from plotly_resampler import FigureResampler +from trace_updater import TraceUpdater from callback_helpers import multiple_folder_file_selector +# --------------------------------------Globals --------------------------------------- +app = DashProxy( + __name__, + external_stylesheets=[dbc.themes.LUX], + transforms=[ServersideOutputTransform()], +) -# Globals -app = dash.Dash(__name__, external_stylesheets=[dbc.themes.LUX]) -fig: FigureResampler = FigureResampler() -# NOTE, this reference to a FigureResampler is essential to preserve throughout the -# whole dash app! If your dash apps want to create a new go.Figure(), you should not -# construct a new FigureResampler object, but replace the figure of this FigureResampler -# object by using the FigureResampler.replace() method. -# Example: see the plot_multiple_files function in this file. - - -# ------------------------------- File selection logic ------------------------------- +# --------- File selection configurations --------- name_folder_list = [ { # the key-string below is the title which will be shown in the dash app - "dash example data": {"folder": Path("../data")}, - "other name same folder": {"folder": Path("../data")}, + "example data": {"folder": Path(__file__).parent.parent.joinpath("data")}, + "other folder": { + "folder": Path(__file__).parent.parent.joinpath("data") + }, }, # NOTE: A new item om this level creates a new file-selector card. # { "PC data": { "folder": Path("/home/jonas/data/wesad/empatica/") } } @@ -47,8 +50,7 @@ ] -# ------------------------------------ DASH logic ------------------------------------- -# First we construct the app layout +# --------- DASH layout logic --------- def serve_layout() -> dbc.Container: """Constructs the app's layout. @@ -69,15 +71,13 @@ def serve_layout() -> dbc.Container: [ # Add file selection layout (+ assign callbacks) dbc.Col(multiple_folder_file_selector(app, name_folder_list), md=2), - # Add the graph and the trace updater component + # Add the graph, the dcc.Store (for serialization) and the + # TraceUpdater (for efficient data updating) components dbc.Col( [ dcc.Graph(id="graph-id", figure=go.Figure()), - trace_updater.TraceUpdater( - id="trace-updater", - gdID="graph-id", - sequentialUpdate=False, - ), + dcc.Loading(dcc.Store(id="store")), + TraceUpdater(id="trace-updater", gdID="graph-id"), ], md=10, ), @@ -92,13 +92,8 @@ def serve_layout() -> dbc.Container: app.layout = serve_layout() -# Register the graph update callbacks to the layout -fig.register_update_graph_callback( - app=app, graph_id="graph-id", trace_updater_id="trace-updater" -) - - -# ------------------------------ Visualization logic --------------------------------- +# ------------------------------------ DASH logic ------------------------------------- +# --------- graph construction logic + callback --------- def plot_multiple_files(file_list: List[Union[str, Path]]) -> FigureResampler: """Code to create the visualizations. @@ -112,13 +107,7 @@ def plot_multiple_files(file_list: List[Union[str, Path]]) -> FigureResampler: Returns a view of the existing, global FigureResampler object. """ - global fig - - # NOTE, we do not construct a new FigureResampler object, but replace the figure of - # the figureResampler object. Otherwise the coupled callbacks would be lost and it - # is not (straightforward) to construct dynamic callbacks in dash. - - fig.replace(make_subplots(rows=len(file_list), shared_xaxes=False)) + fig = FigureResampler(make_subplots(rows=len(file_list), shared_xaxes=False)) fig.update_layout(height=min(900, 350 * len(file_list))) for i, f in enumerate(file_list, 1): @@ -126,7 +115,8 @@ def plot_multiple_files(file_list: List[Union[str, Path]]) -> FigureResampler: if "timestamp" in df.columns: df = df.set_index("timestamp") - for c in df.columns: + for c in df.columns[::-1]: + print(df[c].dtype) fig.add_trace(go.Scattergl(name=c), hf_x=df.index, hf_y=df[c], row=i, col=1) return fig @@ -147,14 +137,11 @@ def plot_multiple_files(file_list: List[Union[str, Path]]) -> FigureResampler: @app.callback( - Output("graph-id", "figure"), + [Output("graph-id", "figure"), ServersideOutput("store", "data")], [Input("plot-button", "n_clicks"), *selector_states], prevent_initial_call=True, ) -def plot_graph( - n_clicks, - *folder_list, -): +def plot_graph(n_clicks, *folder_list): it = iter(folder_list) file_list: List[Path] = [] for folder, files in zip(it, it): @@ -167,11 +154,24 @@ def plot_graph( ctx = dash.callback_context if len(ctx.triggered) and "plot-button" in ctx.triggered[0]["prop_id"]: if len(file_list): - return plot_multiple_files(file_list) + fig: FigureResampler = plot_multiple_files(file_list) + return fig, fig else: raise dash.exceptions.PreventUpdate() +# --------- Figure update callback --------- +@app.callback( + Output("trace-updater", "updateData"), + Input("graph-id", "relayoutData"), + State("store", "data"), # The server side cached FigureResampler per session + prevent_initial_call=True, +) +def update_fig(relayoutdata, fig): + if fig is None: + raise dash.exceptions.PreventUpdate() + return fig.construct_update_data(relayoutdata) + # --------------------------------- Running the app --------------------------------- if __name__ == "__main__": diff --git a/examples/dash_apps/dash_app_minimal_cache.py b/examples/dash_apps/dash_app_minimal_cache.py new file mode 100644 index 00000000..84128049 --- /dev/null +++ b/examples/dash_apps/dash_app_minimal_cache.py @@ -0,0 +1,88 @@ +"""Minimal dash app example. + +Click on a button, and see a plotly-resampler graph of a noisy sinusoid. +No dynamic graph construction / pattern matching callbacks are needed. + +This example uses the dash-extensions its ServersideOutput functionality to cache +the FigureResampler per user/session on the server side. This way, no global figure +variable is used and shows the best practice of using plotly-resampler Figures within +dash-apps. + +""" + +import dash +import dash_bootstrap_components as dbc +import numpy as np +import plotly.graph_objects as go +from dash import Input, Output, State, dcc, html +from dash_extensions.enrich import ( + DashProxy, + ServersideOutput, + ServersideOutputTransform, +) +from plotly_resampler import FigureResampler +from trace_updater import TraceUpdater + +# Data that will be used for the plotly-resampler figures +_n = 1_000_000 +x = np.arange(_n) +noisy_sin = (3 + np.sin(x / 200) + np.random.randn(len(x)) / 10) * x / 1_000 + +# --------------------------------------Globals --------------------------------------- +app = DashProxy( + __name__, + external_stylesheets=[dbc.themes.LUX], + transforms=[ServersideOutputTransform()], +) + +app.layout = html.Div( + [ + dbc.Container(html.H1("plotly-resamper + dash-extensions"), + style={"textAlign": "center"}), + html.Button("plot chart", id="plot-button", n_clicks=0), + html.Hr(), + + # The graph and it's needed components to serialize and update efficiently + # Note: we also add a dcc.Store component, which will be used to link the + # server side cached FigureResampler object + dcc.Graph(id="graph-id"), + dcc.Loading(dcc.Store(id="store")), + TraceUpdater(id="trace-updater", gdID="graph-id"), + ] +) + + +# ------------------------------------ DASH logic ------------------------------------- +# The callback used to construct and store the FigureResampler on the serverside +@app.callback( + [Output("graph-id", "figure"), ServersideOutput("store", "data")], + Input("plot-button", "n_clicks"), + prevent_initial_call=True, + memoize=True, +) +def plot_graph(n_clicks): + ctx = dash.callback_context + if len(ctx.triggered) and "plot-button" in ctx.triggered[0]["prop_id"]: + fig: FigureResampler = FigureResampler(go.Figure()) + fig.add_trace(go.Scattergl(name="log"), hf_x=x, hf_y=noisy_sin * .9999995 ** x) + fig.add_trace(go.Scattergl(name="exp"), hf_x=x, hf_y=noisy_sin * 1.000002 ** x) + return fig, fig + else: + raise dash.exceptions.PreventUpdate() + + +@app.callback( + Output("trace-updater", "updateData"), + Input("graph-id", "relayoutData"), + State("store", "data"), # The server side cached FigureResampler per session + prevent_initial_call=True, +) +def update_fig(relayoutdata, fig): + if fig is None: + raise dash.exceptions.PreventUpdate() + return fig.construct_update_data(relayoutdata) + + +# --------------------------------- Running the app --------------------------------- +if __name__ == "__main__": + app.run_server(debug=True, port=9023) diff --git a/examples/dash_apps/dash_app_minimal_global.py b/examples/dash_apps/dash_app_minimal_global.py new file mode 100644 index 00000000..33c21bc7 --- /dev/null +++ b/examples/dash_apps/dash_app_minimal_global.py @@ -0,0 +1,81 @@ +"""Minimal dash app example. + +Click on a button, and see a plotly-resampler graph of a noisy sinusoid. +No dynamic graph construction / pattern matching callbacks are needed. + +This example uses a global FigureResampler object, which is considered a bad practice. +source: https://dash.plotly.com/sharing-data-between-callbacks: + + Dash is designed to work in multi-user environments where multiple people view the + application at the same time and have independent sessions. + If your app uses and modifies a global variable, then one user's session could set + the variable to some value which would affect the next user's session. + +""" + +import dash +import dash_bootstrap_components as dbc +import numpy as np +import plotly.graph_objects as go +from dash import Input, Output, dcc, html + +from plotly_resampler import FigureResampler +from trace_updater import TraceUpdater + +# Data that will be used for the plotly-resampler figures +_n = 1_000_000 +x = np.arange(_n) +noisy_sin = (3 + np.sin(x / 200) + np.random.randn(len(x)) / 10) * x / 1_000 + + +# --------------------------------------Globals --------------------------------------- +app = dash.Dash(__name__, external_stylesheets=[dbc.themes.LUX]) +fig: FigureResampler = FigureResampler() +# NOTE: in this example, this reference to a FigureResampler is essential to preserve +# throughout the whole dash app! If your dash apps want to create a new go.Figure(), +# you should not construct a new FigureResampler object, but replace the figure of this +# FigureResampler object by using the FigureResampler.replace() method. + +app.layout = html.Div( + [ + dbc.Container( + html.H1("plotly-resampler global variable"), style={"textAlign": "center"} + ), + html.Button("plot chart", id="plot-button", n_clicks=0), + html.Hr(), + + # The graph and it's needed components to update efficiently + dcc.Graph(id="graph-id"), + TraceUpdater(id="trace-updater", gdID="graph-id"), + ] +) + + +# ------------------------------------ DASH logic ------------------------------------- +# The callback used to construct and store the graph's data on the serverside +@app.callback( + Output("graph-id", "figure"), + Input("plot-button", "n_clicks"), + prevent_initial_call=True, +) +def plot_graph(n_clicks): + ctx = dash.callback_context + if len(ctx.triggered) and "plot-button" in ctx.triggered[0]["prop_id"]: + # Note how the replace method is used here on the global figure object + global fig + fig.replace(go.Figure()) + fig.add_trace(go.Scattergl(name="log"), hf_x=x, hf_y=noisy_sin * .9999995 ** x) + fig.add_trace(go.Scattergl(name="exp"), hf_x=x, hf_y=noisy_sin * 1.000002 ** x) + return fig + else: + raise dash.exceptions.PreventUpdate() + + +# Register the graph update callbacks to the layout +fig.register_update_graph_callback( + app=app, graph_id="graph-id", trace_updater_id="trace-updater" +) + +# --------------------------------- Running the app --------------------------------- +if __name__ == "__main__": + app.run_server(debug=True, port=9023) From 107d7da19eed5b781905663e2fe37aad6c801dcf Mon Sep 17 00:00:00 2001 From: jonas Date: Sun, 17 Jul 2022 18:01:58 +0200 Subject: [PATCH 03/49] :dash: improving examples & updating :lock: --- examples/README.md | 11 +- examples/dash_apps/dash_app_coarse_fine.py | 179 +++++-------- examples/dash_apps/dash_app_folder.py | 58 +---- .../dash_apps/{ => utils}/callback_helpers.py | 33 ++- .../dash_apps/utils/graph_construction.py | 36 +++ examples/requirements.txt | 3 + poetry.lock | 244 ++++++------------ pyproject.toml | 28 +- 8 files changed, 242 insertions(+), 350 deletions(-) rename examples/dash_apps/{ => utils}/callback_helpers.py (77%) create mode 100644 examples/dash_apps/utils/graph_construction.py create mode 100644 examples/requirements.txt diff --git a/examples/README.md b/examples/README.md index 33cf6cb7..d87a563f 100644 --- a/examples/README.md +++ b/examples/README.md @@ -1,7 +1,12 @@ # plotly-resampler examples -This directory withholds several examples, indicating the applicability of -plotly-resampler in various use cases. +This directory withholds several examples, highlighting the applicability of +plotly-resampler for various use cases. + +To succesfully run these examples, make sure that you've installed all the requirements by running: +```bash +pip install -r requirements.txt +``` ## 0. basic example @@ -24,7 +29,7 @@ which `plotly-resampler` is integrated |------------------------------------------------------------------| --- | | [minimal example (*global variable*)](dash_apps/dash_app_minimal_global.py) | *bad practice*: minimal example in which a global `FigureResampler` variable is used | | [minimal example (*server side caching*)](dash_apps/dash_app_minimal_cache.py) | *good practice*: minimal example in which we perform server side caching of the `FigureResampler` variable | -| [file visualization](dash_apps/dash_app.py) | load and visualize multiple `.parquet` files with plotly-resampler | +| [file visualization](dash_apps/dash_app_folder.py) | load and visualize multiple `.parquet` files with plotly-resampler | | [dynamic sine generator](dash_apps/construct_dynamic_figures.py) | expeonential sine generator which uses [pattern matching callbacks](https://dash.plotly.com/pattern-matching-callbacks) to remove and construct plotly-resampler graphs dynamically | | [dynamic static graph](dash_apps/dash_app_coarse_fine.py) | Visualization dashboard in which a dynamic (i.e., plotly-resampler graph) and static graph (i.e., go.Figure) are shown (made for [this issue](https://github.com/predict-idlab/plotly-resampler/issues/56)). Relayout events on the coarse graph update the dynamic graph. diff --git a/examples/dash_apps/dash_app_coarse_fine.py b/examples/dash_apps/dash_app_coarse_fine.py index 905a1ef2..453b4b86 100644 --- a/examples/dash_apps/dash_app_coarse_fine.py +++ b/examples/dash_apps/dash_app_coarse_fine.py @@ -1,11 +1,11 @@ -"""Minimal dash app example. +"""Dash file parquet visualization app example with a coarse and fine-grained view. -In this usecase, we have dropdowns which allows the end-user to select files, which are -visualized using FigureResampler after clicking on a button. +In this use case, we have dropdowns which allows end-users to select multiple +parquet files, which are visualized using FigureResampler after clicking on a button. -There a two graphs displayed a coarse and a dynamic graph. -Interactions with the coarse will affect the dynamic graph range. -Note that the autosize of the coarse graph is not linked. +There a two graphs displayed; a coarse and a dynamic graph. Interactions with the +coarse graph will affect the dynamic graph it's shown range. Note that the autosize +of the coarse graph is not linked. TODO: add an rectangle on the coarse graph @@ -18,32 +18,35 @@ import dash_bootstrap_components as dbc import pandas as pd import plotly.graph_objects as go -import trace_updater from pathlib import Path from typing import List, Union from dash import Input, Output, State, dcc, html -from plotly_resampler import FigureResampler - -from callback_helpers import multiple_folder_file_selector +from dash_extensions.enrich import ( + DashProxy, + ServersideOutput, + ServersideOutputTransform, +) +from plotly_resampler import FigureResampler +from trace_updater import TraceUpdater -# Globals -app = dash.Dash(__name__, external_stylesheets=[dbc.themes.LUX]) -fr_fig: FigureResampler = FigureResampler() -# NOTE, this reference to a FigureResampler is essential to preserve throughout the -# whole dash app! If your dash apps want to create a new go.Figure(), you should not -# construct a new FigureResampler object, but replace the figure of this FigureResampler -# object by using the FigureResampler.replace() method. -# Example: see the plot_multiple_files function in this file. +from utils.callback_helpers import multiple_folder_file_selector, get_selector_states +from utils.graph_construction import visualize_multiple_files +# --------------------------------------Globals --------------------------------------- +app = DashProxy( + __name__, + external_stylesheets=[dbc.themes.LUX], + transforms=[ServersideOutputTransform()], +) -# ------------------------------- File selection logic ------------------------------- +# --------- File selection configurations --------- name_folder_list = [ { # the key-string below is the title which will be shown in the dash app - "dash example data": {"folder": Path("../data")}, - "other name same folder": {"folder": Path("../data")}, + "example data": {"folder": Path(__file__).parent.parent.joinpath("data")}, + "other folder": {"folder": Path(__file__).parent.parent.joinpath("data")}, }, # NOTE: A new item om this level creates a new file-selector card. # { "PC data": { "folder": Path("/home/jonas/data/wesad/empatica/") } } @@ -52,8 +55,7 @@ ] -# ------------------------------------ DASH logic ------------------------------------- -# First we construct the app layout +# --------- DASH layout logic --------- def serve_layout() -> dbc.Container: """Constructs the app's layout. @@ -79,19 +81,20 @@ def serve_layout() -> dbc.Container: ), md=2, ), - # Add the graphs and the trace updater component + # Add the graphs, the dcc.Store (for serialization) and the + # TraceUpdater (for efficient data updating) components dbc.Col( [ # The coarse graph whose updates will fetch data for the - # broad graph dcc.Graph( id="coarse-graph", figure=go.Figure(), config={"modeBarButtonsToAdd": ["drawrect"]}, ), + html.Br(), dcc.Graph(id="plotly-resampler-graph", figure=go.Figure()), - # The broad graph - trace_updater.TraceUpdater( + dcc.Loading(dcc.Store(id="store")), + TraceUpdater( id="trace-updater", gdID="plotly-resampler-graph" ), ], @@ -108,91 +111,18 @@ def serve_layout() -> dbc.Container: app.layout = serve_layout() -# Register the graph update callbacks to the layout -@app.callback( - Output("trace-updater", "updateData"), - Input("coarse-graph", "relayoutData"), - Input("plotly-resampler-graph", "relayoutData"), - prevent_initial_call=True, -) -def update_dynamic_fig(coarse_grained_relayout, fine_grained_relayout): - global fr_fig - - ctx = dash.callback_context - trigger_id = ctx.triggered[0].get("prop_id", "").split(".")[0] - - if trigger_id == "plotly-resampler-graph": - return fr_fig.construct_update_data(fine_grained_relayout) - elif trigger_id == "coarse-graph": - if "shapes" in coarse_grained_relayout: - print(coarse_grained_relayout) - cl_k = coarse_grained_relayout.keys() - # We do not resample when and autorange / autosize event takes place - matches = fr_fig._re_matches(re.compile(r"xaxis\d*.range\[0]"), cl_k) - if len(matches): - return fr_fig.construct_update_data(coarse_grained_relayout) - - return dash.no_update - - -# ------------------------------ Visualization logic --------------------------------- -def plot_multiple_files(file_list: List[Union[str, Path]]) -> FigureResampler: - """Code to create the visualizations. - - Parameters - ---------- - file_list: List[Union[str, Path]] - - Returns - ------- - FigureResampler - Returns a view of the existing, global FigureResampler object. - - """ - global fr_fig - - # NOTE, we do not construct a new FigureResampler object, but replace the figure of - # the figureResampler object. Otherwise the coupled callbacks would be lost and it - # is not (straightforward) to construct dynamic callbacks in dash. - fr_fig._global_n_shown_samples = 3000 - fr_fig.replace(go.Figure()) - fr_fig.update_layout(height=min(900, 350 * len(file_list))) - - for f in file_list: - df = pd.read_parquet(f) # should be replaced by more generic data loading code - if "timestamp" in df.columns: - df = df.set_index("timestamp") - - for c in df.columns: - fr_fig.add_trace(go.Scatter(name=c), hf_x=df.index, hf_y=df[c]) - return fr_fig - - -# Note: the list sum-operations flattens the list -selector_states = list( - sum( - [ - ( - State(f"folder-selector{i}", "value"), - State(f"file-selector{i}", "value"), - ) - for i in range(1, len(name_folder_list) + 1) - ], - (), - ) -) - - +# ------------------------------------ DASH logic ------------------------------------- +# --------- graph construction logic + callback --------- @app.callback( - Output("coarse-graph", "figure"), - Output("plotly-resampler-graph", "figure"), - [Input("plot-button", "n_clicks"), *selector_states], + [ + Output("coarse-graph", "figure"), + Output("plotly-resampler-graph", "figure"), + ServersideOutput("store", "data"), + ], + [Input("plot-button", "n_clicks"), *get_selector_states(len(name_folder_list))], prevent_initial_call=True, ) -def plot_graph( - n_clicks, - *folder_list, -): +def construct_plot_graph(n_clicks, *folder_list): it = iter(folder_list) file_list: List[Path] = [] for folder, files in zip(it, it): @@ -206,8 +136,12 @@ def plot_graph( ctx = dash.callback_context if len(ctx.triggered) and "plot-button" in ctx.triggered[0]["prop_id"]: if len(file_list): - dynamic_fig = plot_multiple_files(file_list) - coarse_fig = go.Figure(dynamic_fig) + # Create two graphs, a dynamic plotly-resampler graph and a coarse graph + dynamic_fig: FigureResampler = visualize_multiple_files(file_list) + coarse_fig: go.Figure = go.Figure( + FigureResampler(dynamic_fig, default_n_shown_samples=3_000) + ) + coarse_fig.update_layout(title="coarse view", height=250) coarse_fig.update_layout(margin=dict(l=0, r=0, b=0, t=40, pad=10)) coarse_fig.update_layout(showlegend=False) @@ -224,12 +158,31 @@ def plot_graph( ) ) - # coarse_fig['layout'].update(dict(title='coarse view', title_x=0.5, height=250)) - return coarse_fig, dynamic_fig + return coarse_fig, dynamic_fig, dynamic_fig else: raise dash.exceptions.PreventUpdate() +# Register the graph update callbacks to the layout +@app.callback( + Output("trace-updater", "updateData"), + Input("coarse-graph", "relayoutData"), + Input("plotly-resampler-graph", "relayoutData"), + State("store", "data"), + prevent_initial_call=True, +) +def update_dynamic_fig(coarse_grained_relayout, fine_grained_relayout, fr_fig): + ctx = dash.callback_context + trigger_id = ctx.triggered[0].get("prop_id", "").split(".")[0] + + if trigger_id == "plotly-resampler-graph": + return fr_fig.construct_update_data(fine_grained_relayout) + elif trigger_id == "coarse-graph": + return fr_fig.construct_update_data(coarse_grained_relayout) + + return dash.no_update + + # --------------------------------- Running the app --------------------------------- if __name__ == "__main__": app.run_server(debug=True, port=9023) diff --git a/examples/dash_apps/dash_app_folder.py b/examples/dash_apps/dash_app_folder.py index 28fefacd..28017a64 100644 --- a/examples/dash_apps/dash_app_folder.py +++ b/examples/dash_apps/dash_app_folder.py @@ -8,11 +8,10 @@ __author__ = "Jonas Van Der Donckt" from pathlib import Path -from typing import List, Union +from typing import List import dash import dash_bootstrap_components as dbc -import pandas as pd import plotly.graph_objects as go from dash import Input, Output, State, dcc, html @@ -21,11 +20,11 @@ ServersideOutput, ServersideOutputTransform, ) -from plotly.subplots import make_subplots from plotly_resampler import FigureResampler from trace_updater import TraceUpdater -from callback_helpers import multiple_folder_file_selector +from utils.callback_helpers import multiple_folder_file_selector, get_selector_states +from utils.graph_construction import visualize_multiple_files # --------------------------------------Globals --------------------------------------- app = DashProxy( @@ -39,9 +38,7 @@ { # the key-string below is the title which will be shown in the dash app "example data": {"folder": Path(__file__).parent.parent.joinpath("data")}, - "other folder": { - "folder": Path(__file__).parent.parent.joinpath("data") - }, + "other folder": {"folder": Path(__file__).parent.parent.joinpath("data")}, }, # NOTE: A new item om this level creates a new file-selector card. # { "PC data": { "folder": Path("/home/jonas/data/wesad/empatica/") } } @@ -93,52 +90,9 @@ def serve_layout() -> dbc.Container: # ------------------------------------ DASH logic ------------------------------------- -# --------- graph construction logic + callback --------- -def plot_multiple_files(file_list: List[Union[str, Path]]) -> FigureResampler: - """Code to create the visualizations. - - Parameters - ---------- - file_list: List[Union[str, Path]] - - Returns - ------- - FigureResampler - Returns a view of the existing, global FigureResampler object. - - """ - fig = FigureResampler(make_subplots(rows=len(file_list), shared_xaxes=False)) - fig.update_layout(height=min(900, 350 * len(file_list))) - - for i, f in enumerate(file_list, 1): - df = pd.read_parquet(f) # should be replaced by more generic data loading code - if "timestamp" in df.columns: - df = df.set_index("timestamp") - - for c in df.columns[::-1]: - print(df[c].dtype) - fig.add_trace(go.Scattergl(name=c), hf_x=df.index, hf_y=df[c], row=i, col=1) - return fig - - -# Note: the list sum-operations flattens the list -selector_states = list( - sum( - [ - ( - State(f"folder-selector{i}", "value"), - State(f"file-selector{i}", "value"), - ) - for i in range(1, len(name_folder_list) + 1) - ], - (), - ) -) - - @app.callback( [Output("graph-id", "figure"), ServersideOutput("store", "data")], - [Input("plot-button", "n_clicks"), *selector_states], + [Input("plot-button", "n_clicks"), *get_selector_states(len(name_folder_list))], prevent_initial_call=True, ) def plot_graph(n_clicks, *folder_list): @@ -154,7 +108,7 @@ def plot_graph(n_clicks, *folder_list): ctx = dash.callback_context if len(ctx.triggered) and "plot-button" in ctx.triggered[0]["prop_id"]: if len(file_list): - fig: FigureResampler = plot_multiple_files(file_list) + fig: FigureResampler = visualize_multiple_files(file_list) return fig, fig else: raise dash.exceptions.PreventUpdate() diff --git a/examples/dash_apps/callback_helpers.py b/examples/dash_apps/utils/callback_helpers.py similarity index 77% rename from examples/dash_apps/callback_helpers.py rename to examples/dash_apps/utils/callback_helpers.py index 77650af3..b2985a66 100644 --- a/examples/dash_apps/callback_helpers.py +++ b/examples/dash_apps/utils/callback_helpers.py @@ -7,7 +7,7 @@ from typing import Dict, List import dash_bootstrap_components as dbc -from dash import Input, Output, dcc, html +from dash import Input, Output, State, dcc, html from functional import seq @@ -20,8 +20,8 @@ def _update_file_widget(folder): set( list( seq(Path(folder).iterdir()) - .filter(lambda x: x.is_file() and x.name.endswith("parquet")) - .map(lambda x: x.name) + .filter(lambda x: x.is_file() and x.name.endswith("parquet")) + .map(lambda x: x.name) ) ) ) @@ -33,7 +33,6 @@ def _register_selection_callbacks(app, ids=None): ids = [""] for id in ids: - app.callback( Output(f"file-selector{id}", "options"), [Input(f"folder-selector{id}", "value")], @@ -41,7 +40,7 @@ def _register_selection_callbacks(app, ids=None): def multiple_folder_file_selector( - app, name_folders_list: List[Dict[str, dict]], multi=True + app, name_folders_list: List[Dict[str, dict]], multi=True ) -> dbc.Card: """Constructs a folder user date selector @@ -107,3 +106,27 @@ def multiple_folder_file_selector( _register_selection_callbacks(app=app, ids=range(1, len(name_folders_list) + 1)) return selector + + +def get_selector_states(n: int) -> List[State]: + """Return a list of all the folder-file selector fields, which are used as State + + Parameters + ---------- + n: int + The number of folder selectors + + """ + # Note: the list sum-operations flattens the list + return list( + sum( + [ + ( + State(f"folder-selector{i}", "value"), + State(f"file-selector{i}", "value"), + ) + for i in range(1, n + 1) + ], + (), + ) + ) diff --git a/examples/dash_apps/utils/graph_construction.py b/examples/dash_apps/utils/graph_construction.py new file mode 100644 index 00000000..08e637b7 --- /dev/null +++ b/examples/dash_apps/utils/graph_construction.py @@ -0,0 +1,36 @@ +from pathlib import Path +from typing import List, Union + +import pandas as pd +import plotly.graph_objects as go +from plotly.subplots import make_subplots + +from plotly_resampler import FigureResampler + + +# --------- graph construction logic + callback --------- +def visualize_multiple_files(file_list: List[Union[str, Path]]) -> FigureResampler: + """Create FigureResampler where each subplot row represents all signals from a file. + + Parameters + ---------- + file_list: List[Union[str, Path]] + + Returns + ------- + FigureResampler + Returns a view of the existing, global FigureResampler object. + + """ + fig = FigureResampler(make_subplots(rows=len(file_list), shared_xaxes=False)) + fig.update_layout(height=min(900, 350 * len(file_list))) + + for i, f in enumerate(file_list, 1): + df = pd.read_parquet(f) # should be replaced by more generic data loading code + if "timestamp" in df.columns: + df = df.set_index("timestamp") + + for c in df.columns[::-1]: + print(df[c].dtype) + fig.add_trace(go.Scattergl(name=c), hf_x=df.index, hf_y=df[c], row=i, col=1) + return fig diff --git a/examples/requirements.txt b/examples/requirements.txt new file mode 100644 index 00000000..ba0b2e64 --- /dev/null +++ b/examples/requirements.txt @@ -0,0 +1,3 @@ +pyfunctional>=1.4.3 +dash-bootstrap-components>=1.2.0 +dash-extensions>=0.1.4 \ No newline at end of file diff --git a/poetry.lock b/poetry.lock index 8c74824e..5f833399 100644 --- a/poetry.lock +++ b/poetry.lock @@ -250,7 +250,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" [[package]] name = "coverage" -version = "6.4.1" +version = "6.4.2" description = "Code coverage measurement for Python" category = "dev" optional = false @@ -304,20 +304,6 @@ dev = ["coloredlogs (>=15.0.1)", "fire (>=0.4.0)", "PyYAML (>=5.4.1)"] diskcache = ["diskcache (>=5.2.1)", "multiprocess (>=0.70.12)", "psutil (>=5.8.0)"] testing = ["beautifulsoup4 (>=4.8.2)", "lxml (>=4.6.2)", "percy (>=2.0.2)", "pytest (>=6.0.2)", "requests[security] (>=2.21.0)", "selenium (>=3.141.0)", "waitress (>=1.4.4)", "cryptography (<3.4)"] -[[package]] -name = "dash-bootstrap-components" -version = "1.2.0" -description = "Bootstrap themed components for use in Plotly Dash" -category = "dev" -optional = false -python-versions = ">=3.6, <4" - -[package.dependencies] -dash = ">=2.0.0" - -[package.extras] -pandas = ["numpy", "pandas"] - [[package]] name = "dash-core-components" version = "2.0.0" @@ -366,17 +352,6 @@ category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" -[[package]] -name = "dill" -version = "0.3.5.1" -description = "serialize all of python" -category = "dev" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*, !=3.6.*" - -[package.extras] -graph = ["objgraph (>=1.7.2)"] - [[package]] name = "docutils" version = "0.17.1" @@ -1059,7 +1034,7 @@ name = "orjson" version = "3.7.7" description = "Fast, correct Python JSON library supporting dataclasses, datetimes, and numpy" category = "main" -optional = false +optional = true python-versions = ">=3.7" [[package]] @@ -1241,11 +1216,11 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" [[package]] name = "pyarrow" -version = "6.0.1" +version = "8.0.0" description = "Python library for Apache Arrow" category = "dev" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" [package.dependencies] numpy = ">=1.16.6" @@ -1310,21 +1285,6 @@ python-versions = "*" docs = ["sphinx (>=1.4.8)"] test = ["mock (>=1.0.1)", "hypothesis (>=3.5.3)", "pytest (>=3.0.3)", "pytest-cov (>=2.2.1)", "pytest-timeout (>=1.0.0,<2)", "pytest-faulthandler (>=1.3.0,<2)", "codecov (>=2.0.5)", "wheel (>=0.29)"] -[[package]] -name = "pyfunctional" -version = "1.4.3" -description = "Package for creating data pipelines with chain functional programming" -category = "dev" -optional = false -python-versions = ">=3.6.1,<4.0" - -[package.dependencies] -dill = ">=0.2.5" -tabulate = "<=1.0.0" - -[package.extras] -all = ["pandas (>=1.0.3,<2.0.0)"] - [[package]] name = "pygments" version = "2.12.0" @@ -1789,17 +1749,6 @@ python-versions = ">=3.5" lint = ["flake8", "mypy", "docutils-stubs"] test = ["pytest"] -[[package]] -name = "tabulate" -version = "0.8.10" -description = "Pretty-print tabular data" -category = "dev" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" - -[package.extras] -widechars = ["wcwidth"] - [[package]] name = "tenacity" version = "6.3.1" @@ -2058,7 +2007,7 @@ cffi = ["cffi (>=1.11)"] [metadata] lock-version = "1.1" python-versions = "^3.7.1,<3.11" -content-hash = "801a6c76200c5f8f3fba1f3b2e84ac837c049429bdb57bfdc5d1640d131c220a" +content-hash = "ba58b87cd6434d5973701a3dc0886e2d1400a9c2fce92a3c48e87915480ad14d" [metadata.files] alabaster = [ @@ -2171,9 +2120,6 @@ brotli = [ {file = "Brotli-1.0.9-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ee83d3e3a024a9618e5be64648d6d11c37047ac48adff25f12fa4226cf23d1c"}, {file = "Brotli-1.0.9-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:19598ecddd8a212aedb1ffa15763dd52a388518c4550e615aed88dc3753c0f0c"}, {file = "Brotli-1.0.9-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:44bb8ff420c1d19d91d79d8c3574b8954288bdff0273bf788954064d260d7ab0"}, - {file = "Brotli-1.0.9-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e23281b9a08ec338469268f98f194658abfb13658ee98e2b7f85ee9dd06caa91"}, - {file = "Brotli-1.0.9-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:3496fc835370da351d37cada4cf744039616a6db7d13c430035e901443a34daa"}, - {file = "Brotli-1.0.9-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b83bb06a0192cccf1eb8d0a28672a1b79c74c3a8a5f2619625aeb6f28b3a82bb"}, {file = "Brotli-1.0.9-cp310-cp310-win32.whl", hash = "sha256:26d168aac4aaec9a4394221240e8a5436b5634adc3cd1cdf637f6645cecbf181"}, {file = "Brotli-1.0.9-cp310-cp310-win_amd64.whl", hash = "sha256:622a231b08899c864eb87e85f81c75e7b9ce05b001e59bbfbf43d4a71f5f32b2"}, {file = "Brotli-1.0.9-cp35-cp35m-macosx_10_6_intel.whl", hash = "sha256:c83aa123d56f2e060644427a882a36b3c12db93727ad7a7b9efd7d7f3e9cc2c4"}, @@ -2185,18 +2131,12 @@ brotli = [ {file = "Brotli-1.0.9-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:40d15c79f42e0a2c72892bf407979febd9cf91f36f495ffb333d1d04cebb34e4"}, {file = "Brotli-1.0.9-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:93130612b837103e15ac3f9cbacb4613f9e348b58b3aad53721d92e57f96d46a"}, {file = "Brotli-1.0.9-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:87fdccbb6bb589095f413b1e05734ba492c962b4a45a13ff3408fa44ffe6479b"}, - {file = "Brotli-1.0.9-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:6d847b14f7ea89f6ad3c9e3901d1bc4835f6b390a9c71df999b0162d9bb1e20f"}, - {file = "Brotli-1.0.9-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:495ba7e49c2db22b046a53b469bbecea802efce200dffb69b93dd47397edc9b6"}, - {file = "Brotli-1.0.9-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:4688c1e42968ba52e57d8670ad2306fe92e0169c6f3af0089be75bbac0c64a3b"}, {file = "Brotli-1.0.9-cp36-cp36m-win32.whl", hash = "sha256:61a7ee1f13ab913897dac7da44a73c6d44d48a4adff42a5701e3239791c96e14"}, {file = "Brotli-1.0.9-cp36-cp36m-win_amd64.whl", hash = "sha256:1c48472a6ba3b113452355b9af0a60da5c2ae60477f8feda8346f8fd48e3e87c"}, {file = "Brotli-1.0.9-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:3b78a24b5fd13c03ee2b7b86290ed20efdc95da75a3557cc06811764d5ad1126"}, {file = "Brotli-1.0.9-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:9d12cf2851759b8de8ca5fde36a59c08210a97ffca0eb94c532ce7b17c6a3d1d"}, {file = "Brotli-1.0.9-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:6c772d6c0a79ac0f414a9f8947cc407e119b8598de7621f39cacadae3cf57d12"}, {file = "Brotli-1.0.9-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29d1d350178e5225397e28ea1b7aca3648fcbab546d20e7475805437bfb0a130"}, - {file = "Brotli-1.0.9-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:7bbff90b63328013e1e8cb50650ae0b9bac54ffb4be6104378490193cd60f85a"}, - {file = "Brotli-1.0.9-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:ec1947eabbaf8e0531e8e899fc1d9876c179fc518989461f5d24e2223395a9e3"}, - {file = "Brotli-1.0.9-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:12effe280b8ebfd389022aa65114e30407540ccb89b177d3fbc9a4f177c4bd5d"}, {file = "Brotli-1.0.9-cp37-cp37m-win32.whl", hash = "sha256:f909bbbc433048b499cb9db9e713b5d8d949e8c109a2a548502fb9aa8630f0b1"}, {file = "Brotli-1.0.9-cp37-cp37m-win_amd64.whl", hash = "sha256:97f715cf371b16ac88b8c19da00029804e20e25f30d80203417255d239f228b5"}, {file = "Brotli-1.0.9-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:e16eb9541f3dd1a3e92b89005e37b1257b157b7256df0e36bd7b33b50be73bcb"}, @@ -2204,9 +2144,6 @@ brotli = [ {file = "Brotli-1.0.9-cp38-cp38-manylinux1_i686.whl", hash = "sha256:b663f1e02de5d0573610756398e44c130add0eb9a3fc912a09665332942a2efb"}, {file = "Brotli-1.0.9-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:5b6ef7d9f9c38292df3690fe3e302b5b530999fa90014853dcd0d6902fb59f26"}, {file = "Brotli-1.0.9-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8a674ac10e0a87b683f4fa2b6fa41090edfd686a6524bd8dedbd6138b309175c"}, - {file = "Brotli-1.0.9-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:e2d9e1cbc1b25e22000328702b014227737756f4b5bf5c485ac1d8091ada078b"}, - {file = "Brotli-1.0.9-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:b336c5e9cf03c7be40c47b5fd694c43c9f1358a80ba384a21969e0b4e66a9b17"}, - {file = "Brotli-1.0.9-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:85f7912459c67eaab2fb854ed2bc1cc25772b300545fe7ed2dc03954da638649"}, {file = "Brotli-1.0.9-cp38-cp38-win32.whl", hash = "sha256:35a3edbe18e876e596553c4007a087f8bcfd538f19bc116917b3c7522fca0429"}, {file = "Brotli-1.0.9-cp38-cp38-win_amd64.whl", hash = "sha256:269a5743a393c65db46a7bb982644c67ecba4b8d91b392403ad8a861ba6f495f"}, {file = "Brotli-1.0.9-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:2aad0e0baa04517741c9bb5b07586c642302e5fb3e75319cb62087bd0995ab19"}, @@ -2214,9 +2151,6 @@ brotli = [ {file = "Brotli-1.0.9-cp39-cp39-manylinux1_i686.whl", hash = "sha256:16d528a45c2e1909c2798f27f7bf0a3feec1dc9e50948e738b961618e38b6a7b"}, {file = "Brotli-1.0.9-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:56d027eace784738457437df7331965473f2c0da2c70e1a1f6fdbae5402e0389"}, {file = "Brotli-1.0.9-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9bf919756d25e4114ace16a8ce91eb340eb57a08e2c6950c3cebcbe3dff2a5e7"}, - {file = "Brotli-1.0.9-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:e4c4e92c14a57c9bd4cb4be678c25369bf7a092d55fd0866f759e425b9660806"}, - {file = "Brotli-1.0.9-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:e48f4234f2469ed012a98f4b7874e7f7e173c167bed4934912a29e03167cf6b1"}, - {file = "Brotli-1.0.9-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:9ed4c92a0665002ff8ea852353aeb60d9141eb04109e88928026d3c8a9e5433c"}, {file = "Brotli-1.0.9-cp39-cp39-win32.whl", hash = "sha256:cfc391f4429ee0a9370aa93d812a52e1fee0f37a81861f4fdd1f4fb28e8547c3"}, {file = "Brotli-1.0.9-cp39-cp39-win_amd64.whl", hash = "sha256:854c33dad5ba0fbd6ab69185fec8dab89e13cda6b7d191ba111987df74f38761"}, {file = "Brotli-1.0.9-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:9749a124280a0ada4187a6cfd1ffd35c350fb3af79c706589d98e088c5044267"}, @@ -2306,47 +2240,47 @@ colorama = [ {file = "colorama-0.4.5.tar.gz", hash = "sha256:e6c6b4334fc50988a639d9b98aa429a0b57da6e17b9a44f0451f930b6967b7a4"}, ] coverage = [ - {file = "coverage-6.4.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f1d5aa2703e1dab4ae6cf416eb0095304f49d004c39e9db1d86f57924f43006b"}, - {file = "coverage-6.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4ce1b258493cbf8aec43e9b50d89982346b98e9ffdfaae8ae5793bc112fb0068"}, - {file = "coverage-6.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:83c4e737f60c6936460c5be330d296dd5b48b3963f48634c53b3f7deb0f34ec4"}, - {file = "coverage-6.4.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:84e65ef149028516c6d64461b95a8dbcfce95cfd5b9eb634320596173332ea84"}, - {file = "coverage-6.4.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f69718750eaae75efe506406c490d6fc5a6161d047206cc63ce25527e8a3adad"}, - {file = "coverage-6.4.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e57816f8ffe46b1df8f12e1b348f06d164fd5219beba7d9433ba79608ef011cc"}, - {file = "coverage-6.4.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:01c5615d13f3dd3aa8543afc069e5319cfa0c7d712f6e04b920431e5c564a749"}, - {file = "coverage-6.4.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:75ab269400706fab15981fd4bd5080c56bd5cc07c3bccb86aab5e1d5a88dc8f4"}, - {file = "coverage-6.4.1-cp310-cp310-win32.whl", hash = "sha256:a7f3049243783df2e6cc6deafc49ea123522b59f464831476d3d1448e30d72df"}, - {file = "coverage-6.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:ee2ddcac99b2d2aec413e36d7a429ae9ebcadf912946b13ffa88e7d4c9b712d6"}, - {file = "coverage-6.4.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:fb73e0011b8793c053bfa85e53129ba5f0250fdc0392c1591fd35d915ec75c46"}, - {file = "coverage-6.4.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:106c16dfe494de3193ec55cac9640dd039b66e196e4641fa8ac396181578b982"}, - {file = "coverage-6.4.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:87f4f3df85aa39da00fd3ec4b5abeb7407e82b68c7c5ad181308b0e2526da5d4"}, - {file = "coverage-6.4.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:961e2fb0680b4f5ad63234e0bf55dfb90d302740ae9c7ed0120677a94a1590cb"}, - {file = "coverage-6.4.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:cec3a0f75c8f1031825e19cd86ee787e87cf03e4fd2865c79c057092e69e3a3b"}, - {file = "coverage-6.4.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:129cd05ba6f0d08a766d942a9ed4b29283aff7b2cccf5b7ce279d50796860bb3"}, - {file = "coverage-6.4.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:bf5601c33213d3cb19d17a796f8a14a9eaa5e87629a53979a5981e3e3ae166f6"}, - {file = "coverage-6.4.1-cp37-cp37m-win32.whl", hash = "sha256:269eaa2c20a13a5bf17558d4dc91a8d078c4fa1872f25303dddcbba3a813085e"}, - {file = "coverage-6.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:f02cbbf8119db68455b9d763f2f8737bb7db7e43720afa07d8eb1604e5c5ae28"}, - {file = "coverage-6.4.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ffa9297c3a453fba4717d06df579af42ab9a28022444cae7fa605af4df612d54"}, - {file = "coverage-6.4.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:145f296d00441ca703a659e8f3eb48ae39fb083baba2d7ce4482fb2723e050d9"}, - {file = "coverage-6.4.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d67d44996140af8b84284e5e7d398e589574b376fb4de8ccd28d82ad8e3bea13"}, - {file = "coverage-6.4.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2bd9a6fc18aab8d2e18f89b7ff91c0f34ff4d5e0ba0b33e989b3cd4194c81fd9"}, - {file = "coverage-6.4.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3384f2a3652cef289e38100f2d037956194a837221edd520a7ee5b42d00cc605"}, - {file = "coverage-6.4.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:9b3e07152b4563722be523e8cd0b209e0d1a373022cfbde395ebb6575bf6790d"}, - {file = "coverage-6.4.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:1480ff858b4113db2718848d7b2d1b75bc79895a9c22e76a221b9d8d62496428"}, - {file = "coverage-6.4.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:865d69ae811a392f4d06bde506d531f6a28a00af36f5c8649684a9e5e4a85c83"}, - {file = "coverage-6.4.1-cp38-cp38-win32.whl", hash = "sha256:664a47ce62fe4bef9e2d2c430306e1428ecea207ffd68649e3b942fa8ea83b0b"}, - {file = "coverage-6.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:26dff09fb0d82693ba9e6231248641d60ba606150d02ed45110f9ec26404ed1c"}, - {file = "coverage-6.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d9c80df769f5ec05ad21ea34be7458d1dc51ff1fb4b2219e77fe24edf462d6df"}, - {file = "coverage-6.4.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:39ee53946bf009788108b4dd2894bf1349b4e0ca18c2016ffa7d26ce46b8f10d"}, - {file = "coverage-6.4.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f5b66caa62922531059bc5ac04f836860412f7f88d38a476eda0a6f11d4724f4"}, - {file = "coverage-6.4.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fd180ed867e289964404051a958f7cccabdeed423f91a899829264bb7974d3d3"}, - {file = "coverage-6.4.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:84631e81dd053e8a0d4967cedab6db94345f1c36107c71698f746cb2636c63e3"}, - {file = "coverage-6.4.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:8c08da0bd238f2970230c2a0d28ff0e99961598cb2e810245d7fc5afcf1254e8"}, - {file = "coverage-6.4.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:d42c549a8f41dc103a8004b9f0c433e2086add8a719da00e246e17cbe4056f72"}, - {file = "coverage-6.4.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:309ce4a522ed5fca432af4ebe0f32b21d6d7ccbb0f5fcc99290e71feba67c264"}, - {file = "coverage-6.4.1-cp39-cp39-win32.whl", hash = "sha256:fdb6f7bd51c2d1714cea40718f6149ad9be6a2ee7d93b19e9f00934c0f2a74d9"}, - {file = "coverage-6.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:342d4aefd1c3e7f620a13f4fe563154d808b69cccef415415aece4c786665397"}, - {file = "coverage-6.4.1-pp36.pp37.pp38-none-any.whl", hash = "sha256:4803e7ccf93230accb928f3a68f00ffa80a88213af98ed338a57ad021ef06815"}, - {file = "coverage-6.4.1.tar.gz", hash = "sha256:4321f075095a096e70aff1d002030ee612b65a205a0a0f5b815280d5dc58100c"}, + {file = "coverage-6.4.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a9032f9b7d38bdf882ac9f66ebde3afb8145f0d4c24b2e600bc4c6304aafb87e"}, + {file = "coverage-6.4.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e0524adb49c716ca763dbc1d27bedce36b14f33e6b8af6dba56886476b42957c"}, + {file = "coverage-6.4.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d4548be38a1c810d79e097a38107b6bf2ff42151900e47d49635be69943763d8"}, + {file = "coverage-6.4.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f23876b018dfa5d3e98e96f5644b109090f16a4acb22064e0f06933663005d39"}, + {file = "coverage-6.4.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6fe75dcfcb889b6800f072f2af5a331342d63d0c1b3d2bf0f7b4f6c353e8c9c0"}, + {file = "coverage-6.4.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:2f8553878a24b00d5ab04b7a92a2af50409247ca5c4b7a2bf4eabe94ed20d3ee"}, + {file = "coverage-6.4.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:d774d9e97007b018a651eadc1b3970ed20237395527e22cbeb743d8e73e0563d"}, + {file = "coverage-6.4.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d56f105592188ce7a797b2bd94b4a8cb2e36d5d9b0d8a1d2060ff2a71e6b9bbc"}, + {file = "coverage-6.4.2-cp310-cp310-win32.whl", hash = "sha256:d230d333b0be8042ac34808ad722eabba30036232e7a6fb3e317c49f61c93386"}, + {file = "coverage-6.4.2-cp310-cp310-win_amd64.whl", hash = "sha256:5ef42e1db047ca42827a85e34abe973971c635f83aed49611b7f3ab49d0130f0"}, + {file = "coverage-6.4.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:25b7ec944f114f70803d6529394b64f8749e93cbfac0fe6c5ea1b7e6c14e8a46"}, + {file = "coverage-6.4.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bb00521ab4f99fdce2d5c05a91bddc0280f0afaee0e0a00425e28e209d4af07"}, + {file = "coverage-6.4.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2dff52b3e7f76ada36f82124703f4953186d9029d00d6287f17c68a75e2e6039"}, + {file = "coverage-6.4.2-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:147605e1702d996279bb3cc3b164f408698850011210d133a2cb96a73a2f7996"}, + {file = "coverage-6.4.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:422fa44070b42fef9fb8dabd5af03861708cdd6deb69463adc2130b7bf81332f"}, + {file = "coverage-6.4.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:8af6c26ba8df6338e57bedbf916d76bdae6308e57fc8f14397f03b5da8622b4e"}, + {file = "coverage-6.4.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:5336e0352c0b12c7e72727d50ff02557005f79a0b8dcad9219c7c4940a930083"}, + {file = "coverage-6.4.2-cp37-cp37m-win32.whl", hash = "sha256:0f211df2cba951ffcae210ee00e54921ab42e2b64e0bf2c0befc977377fb09b7"}, + {file = "coverage-6.4.2-cp37-cp37m-win_amd64.whl", hash = "sha256:a13772c19619118903d65a91f1d5fea84be494d12fd406d06c849b00d31bf120"}, + {file = "coverage-6.4.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:f7bd0ffbcd03dc39490a1f40b2669cc414fae0c4e16b77bb26806a4d0b7d1452"}, + {file = "coverage-6.4.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:0895ea6e6f7f9939166cc835df8fa4599e2d9b759b02d1521b574e13b859ac32"}, + {file = "coverage-6.4.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d4e7ced84a11c10160c0697a6cc0b214a5d7ab21dfec1cd46e89fbf77cc66fae"}, + {file = "coverage-6.4.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:80db4a47a199c4563d4a25919ff29c97c87569130375beca3483b41ad5f698e8"}, + {file = "coverage-6.4.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3def6791adf580d66f025223078dc84c64696a26f174131059ce8e91452584e1"}, + {file = "coverage-6.4.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:4f89d8e03c8a3757aae65570d14033e8edf192ee9298303db15955cadcff0c63"}, + {file = "coverage-6.4.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:6d0b48aff8e9720bdec315d67723f0babd936a7211dc5df453ddf76f89c59933"}, + {file = "coverage-6.4.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:2b20286c2b726f94e766e86a3fddb7b7e37af5d0c635bdfa7e4399bc523563de"}, + {file = "coverage-6.4.2-cp38-cp38-win32.whl", hash = "sha256:d714af0bdba67739598849c9f18efdcc5a0412f4993914a0ec5ce0f1e864d783"}, + {file = "coverage-6.4.2-cp38-cp38-win_amd64.whl", hash = "sha256:5f65e5d3ff2d895dab76b1faca4586b970a99b5d4b24e9aafffc0ce94a6022d6"}, + {file = "coverage-6.4.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a697977157adc052284a7160569b36a8bbec09db3c3220642e6323b47cec090f"}, + {file = "coverage-6.4.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c77943ef768276b61c96a3eb854eba55633c7a3fddf0a79f82805f232326d33f"}, + {file = "coverage-6.4.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:54d8d0e073a7f238f0666d3c7c0d37469b2aa43311e4024c925ee14f5d5a1cbe"}, + {file = "coverage-6.4.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f22325010d8824594820d6ce84fa830838f581a7fd86a9235f0d2ed6deb61e29"}, + {file = "coverage-6.4.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:24b04d305ea172ccb21bee5bacd559383cba2c6fcdef85b7701cf2de4188aa55"}, + {file = "coverage-6.4.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:866ebf42b4c5dbafd64455b0a1cd5aa7b4837a894809413b930026c91e18090b"}, + {file = "coverage-6.4.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:e36750fbbc422c1c46c9d13b937ab437138b998fe74a635ec88989afb57a3978"}, + {file = "coverage-6.4.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:79419370d6a637cb18553ecb25228893966bd7935a9120fa454e7076f13b627c"}, + {file = "coverage-6.4.2-cp39-cp39-win32.whl", hash = "sha256:b5e28db9199dd3833cc8a07fa6cf429a01227b5d429facb56eccd765050c26cd"}, + {file = "coverage-6.4.2-cp39-cp39-win_amd64.whl", hash = "sha256:edfdabe7aa4f97ed2b9dd5dde52d2bb29cb466993bb9d612ddd10d0085a683cf"}, + {file = "coverage-6.4.2-pp36.pp37.pp38-none-any.whl", hash = "sha256:e2618cb2cf5a7cc8d698306e42ebcacd02fb7ef8cfc18485c59394152c70be97"}, + {file = "coverage-6.4.2.tar.gz", hash = "sha256:6c3ccfe89c36f3e5b9837b9ee507472310164f352c9fe332120b764c9d60adbe"}, ] cryptography = [ {file = "cryptography-37.0.4-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:549153378611c0cca1042f20fd9c5030d37a72f634c9326e225c9f666d472884"}, @@ -2376,20 +2310,13 @@ dash = [ {file = "dash-2.5.1-py3-none-any.whl", hash = "sha256:1b9110bcde7559a8405095901d4f7cfcfd18054b0c9758ec8562b3977824fbfe"}, {file = "dash-2.5.1.tar.gz", hash = "sha256:343c802006abcaf71aadd8c3f55737ea7c72116e62fff465e980f9f1f304f1ee"}, ] -dash-bootstrap-components = [ - {file = "dash-bootstrap-components-1.2.0.tar.gz", hash = "sha256:4f95470952ecb9a0f2dbef34be2969353c5c16925817d48da50db51ade80f50f"}, - {file = "dash_bootstrap_components-1.2.0-py3-none-any.whl", hash = "sha256:d7fd69cb2b1e86f9cc4bcee4036302e5d534d0bf102d331b29392a3c355d776a"}, -] dash-core-components = [ - {file = "dash_core_components-2.0.0-py3-none-any.whl", hash = "sha256:52b8e8cce13b18d0802ee3acbc5e888cb1248a04968f962d63d070400af2e346"}, {file = "dash_core_components-2.0.0.tar.gz", hash = "sha256:c6733874af975e552f95a1398a16c2ee7df14ce43fa60bb3718a3c6e0b63ffee"}, ] dash-html-components = [ - {file = "dash_html_components-2.0.0-py3-none-any.whl", hash = "sha256:b42cc903713c9706af03b3f2548bda4be7307a7cf89b7d6eae3da872717d1b63"}, {file = "dash_html_components-2.0.0.tar.gz", hash = "sha256:8703a601080f02619a6390998e0b3da4a5daabe97a1fd7a9cebc09d015f26e50"}, ] dash-table = [ - {file = "dash_table-5.0.0-py3-none-any.whl", hash = "sha256:19036fa352bb1c11baf38068ec62d172f0515f73ca3276c79dee49b95ddc16c9"}, {file = "dash_table-5.0.0.tar.gz", hash = "sha256:18624d693d4c8ef2ddec99a6f167593437a7ea0bf153aa20f318c170c5bc7308"}, ] debugpy = [ @@ -2420,10 +2347,6 @@ defusedxml = [ {file = "defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61"}, {file = "defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69"}, ] -dill = [ - {file = "dill-0.3.5.1-py2.py3-none-any.whl", hash = "sha256:33501d03270bbe410c72639b350e941882a8b0fd55357580fbc873fba0c59302"}, - {file = "dill-0.3.5.1.tar.gz", hash = "sha256:d75e41f3eff1eee599d738e76ba8f4ad98ea229db8b085318aa2b3333a208c86"}, -] docutils = [ {file = "docutils-0.17.1-py2.py3-none-any.whl", hash = "sha256:cf316c8370a737a022b72b56874f6602acf974a37a9fba42ec2876387549fc61"}, {file = "docutils-0.17.1.tar.gz", hash = "sha256:686577d2e4c32380bb50cbb22f575ed742d58168cee37e99117a854bcd88f125"}, @@ -2875,42 +2798,36 @@ py = [ {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"}, ] pyarrow = [ - {file = "pyarrow-6.0.1-cp310-cp310-macosx_10_13_universal2.whl", hash = "sha256:c80d2436294a07f9cc54852aa1cef034b6f9c97d29235c4bd53bbf52e24f1ebf"}, - {file = "pyarrow-6.0.1-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:f150b4f222d0ba397388908725692232345adaa8e58ad543ca00f03c7234ae7b"}, - {file = "pyarrow-6.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c3a727642c1283dcb44728f0d0a00f8864b171e31c835f4b8def07e3fa8f5c73"}, - {file = "pyarrow-6.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d29605727865177918e806d855fd8404b6242bf1e56ade0a0023cd4fe5f7f841"}, - {file = "pyarrow-6.0.1-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:b63b54dd0bada05fff76c15b233f9322de0e6947071b7871ec45024e16045aeb"}, - {file = "pyarrow-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9e90e75cb11e61ffeffb374f1db7c4788f1df0cb269596bf86c473155294958d"}, - {file = "pyarrow-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1f4f3db1da51db4cfbafab3066a01b01578884206dced9f505da950d9ed4402d"}, - {file = "pyarrow-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:2523f87bd36877123fc8c4813f60d298722143ead73e907690a87e8557114693"}, - {file = "pyarrow-6.0.1-cp36-cp36m-macosx_10_13_x86_64.whl", hash = "sha256:8f7d34efb9d667f9204b40ce91a77613c46691c24cd098e3b6986bd7401b8f06"}, - {file = "pyarrow-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:e3c9184335da8faf08c0df95668ce9d778df3795ce4eec959f44908742900e10"}, - {file = "pyarrow-6.0.1-cp36-cp36m-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:02baee816456a6e64486e587caaae2bf9f084fa3a891354ff18c3e945a1cb72f"}, - {file = "pyarrow-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:604782b1c744b24a55df80125991a7154fbdef60991eb3d02bfaed06d22f055e"}, - {file = "pyarrow-6.0.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fab8132193ae095c43b1e8d6d7f393451ac198de5aaf011c6b576b1442966fec"}, - {file = "pyarrow-6.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:31038366484e538608f43920a5e2957b8862a43aa49438814619b527f50ec127"}, - {file = "pyarrow-6.0.1-cp37-cp37m-macosx_10_13_x86_64.whl", hash = "sha256:632bea00c2fbe2da5d29ff1698fec312ed3aabfb548f06100144e1907e22093a"}, - {file = "pyarrow-6.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:dc03c875e5d68b0d0143f94c438add3ab3c2411ade2748423a9c24608fea571e"}, - {file = "pyarrow-6.0.1-cp37-cp37m-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:1cd4de317df01679e538004123d6d7bc325d73bad5c6bbc3d5f8aa2280408869"}, - {file = "pyarrow-6.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e77b1f7c6c08ec319b7882c1a7c7304731530923532b3243060e6e64c456cf34"}, - {file = "pyarrow-6.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a424fd9a3253d0322d53be7bbb20b5b01511706a61efadcf37f416da325e3d48"}, - {file = "pyarrow-6.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:c958cf3a4a9eee09e1063c02b89e882d19c61b3a2ce6cbd55191a6f45ed5004b"}, - {file = "pyarrow-6.0.1-cp38-cp38-macosx_10_13_x86_64.whl", hash = "sha256:0e0ef24b316c544f4bb56f5c376129097df3739e665feca0eb567f716d45c55a"}, - {file = "pyarrow-6.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2c13ec3b26b3b069d673c5fa3a0c70c38f0d5c94686ac5dbc9d7e7d24040f812"}, - {file = "pyarrow-6.0.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:71891049dc58039a9523e1cb0d921be001dacb2b327fa7b62a35b96a3aad9f0d"}, - {file = "pyarrow-6.0.1-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:943141dd8cca6c5722552a0b11a3c2e791cdf85f1768dea8170b0a8a7e824ff9"}, - {file = "pyarrow-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fd077c06061b8fa8fdf91591a4270e368f63cf73c6ab56924d3b64efa96a873"}, - {file = "pyarrow-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5308f4bb770b48e07c8cff36cf6a4452862e8ce9492428ad5581d846420b3884"}, - {file = "pyarrow-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:cde4f711cd9476d4da18128c3a40cb529b6b7d2679aee6e0576212547530fef1"}, - {file = "pyarrow-6.0.1-cp39-cp39-macosx_10_13_universal2.whl", hash = "sha256:b8628269bd9289cae0ea668f5900451043252fe3666667f614e140084dd31aac"}, - {file = "pyarrow-6.0.1-cp39-cp39-macosx_10_13_x86_64.whl", hash = "sha256:981ccdf4f2696550733e18da882469893d2f33f55f3cbeb6a90f81741cbf67aa"}, - {file = "pyarrow-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:954326b426eec6e31ff55209f8840b54d788420e96c4005aaa7beed1fe60b42d"}, - {file = "pyarrow-6.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6b6483bf6b61fe9a046235e4ad4d9286b707607878d7dbdc2eb85a6ec4090baf"}, - {file = "pyarrow-6.0.1-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:7ecad40a1d4e0104cd87757a403f36850261e7a989cf9e4cb3e30420bbbd1092"}, - {file = "pyarrow-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:04c752fb41921d0064568a15a87dbb0222cfbe9040d4b2c1b306fe6e0a453530"}, - {file = "pyarrow-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:725d3fe49dfe392ff14a8ae6a75b230a60e8985f2b621b18cfa912fe02b65f1a"}, - {file = "pyarrow-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:2403c8af207262ce8e2bc1a9d19313941fd2e424f1cb3c4b749c17efe1fd699a"}, - {file = "pyarrow-6.0.1.tar.gz", hash = "sha256:423990d56cd8f12283b67367d48e142739b789085185018eb03d05087c3c8d43"}, + {file = "pyarrow-8.0.0-cp310-cp310-macosx_10_13_universal2.whl", hash = "sha256:d5ef4372559b191cafe7db8932801eee252bfc35e983304e7d60b6954576a071"}, + {file = "pyarrow-8.0.0-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:863be6bad6c53797129610930794a3e797cb7d41c0a30e6794a2ac0e42ce41b8"}, + {file = "pyarrow-8.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:69b043a3fce064ebd9fbae6abc30e885680296e5bd5e6f7353e6a87966cf2ad7"}, + {file = "pyarrow-8.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:51e58778fcb8829fca37fbfaea7f208d5ce7ea89ea133dd13d8ce745278ee6f0"}, + {file = "pyarrow-8.0.0-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:15511ce2f50343f3fd5e9f7c30e4d004da9134e9597e93e9c96c3985928cbe82"}, + {file = "pyarrow-8.0.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ea132067ec712d1b1116a841db1c95861508862b21eddbcafefbce8e4b96b867"}, + {file = "pyarrow-8.0.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:deb400df8f19a90b662babceb6dd12daddda6bb357c216e558b207c0770c7654"}, + {file = "pyarrow-8.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:3bd201af6e01f475f02be88cf1f6ee9856ab98c11d8bbb6f58347c58cd07be00"}, + {file = "pyarrow-8.0.0-cp37-cp37m-macosx_10_13_x86_64.whl", hash = "sha256:78a6ac39cd793582998dac88ab5c1c1dd1e6503df6672f064f33a21937ec1d8d"}, + {file = "pyarrow-8.0.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:d6f1e1040413651819074ef5b500835c6c42e6c446532a1ddef8bc5054e8dba5"}, + {file = "pyarrow-8.0.0-cp37-cp37m-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:98c13b2e28a91b0fbf24b483df54a8d7814c074c2623ecef40dce1fa52f6539b"}, + {file = "pyarrow-8.0.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c9c97c8e288847e091dfbcdf8ce51160e638346f51919a9e74fe038b2e8aee62"}, + {file = "pyarrow-8.0.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:edad25522ad509e534400d6ab98cf1872d30c31bc5e947712bfd57def7af15bb"}, + {file = "pyarrow-8.0.0-cp37-cp37m-win_amd64.whl", hash = "sha256:ece333706a94c1221ced8b299042f85fd88b5db802d71be70024433ddf3aecab"}, + {file = "pyarrow-8.0.0-cp38-cp38-macosx_10_13_x86_64.whl", hash = "sha256:95c7822eb37663e073da9892f3499fe28e84f3464711a3e555e0c5463fd53a19"}, + {file = "pyarrow-8.0.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:25a5f7c7f36df520b0b7363ba9f51c3070799d4b05d587c60c0adaba57763479"}, + {file = "pyarrow-8.0.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:ce64bc1da3109ef5ab9e4c60316945a7239c798098a631358e9ab39f6e5529e9"}, + {file = "pyarrow-8.0.0-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:541e7845ce5f27a861eb5b88ee165d931943347eec17b9ff1e308663531c9647"}, + {file = "pyarrow-8.0.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8cd86e04a899bef43e25184f4b934584861d787cf7519851a8c031803d45c6d8"}, + {file = "pyarrow-8.0.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba2b7aa7efb59156b87987a06f5241932914e4d5bbb74a465306b00a6c808849"}, + {file = "pyarrow-8.0.0-cp38-cp38-win_amd64.whl", hash = "sha256:42b7982301a9ccd06e1dd4fabd2e8e5df74b93ce4c6b87b81eb9e2d86dc79871"}, + {file = "pyarrow-8.0.0-cp39-cp39-macosx_10_13_universal2.whl", hash = "sha256:1dd482ccb07c96188947ad94d7536ab696afde23ad172df8e18944ec79f55055"}, + {file = "pyarrow-8.0.0-cp39-cp39-macosx_10_13_x86_64.whl", hash = "sha256:81b87b782a1366279411f7b235deab07c8c016e13f9af9f7c7b0ee564fedcc8f"}, + {file = "pyarrow-8.0.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:03a10daad957970e914920b793f6a49416699e791f4c827927fd4e4d892a5d16"}, + {file = "pyarrow-8.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:65c7f4cc2be195e3db09296d31a654bb6d8786deebcab00f0e2455fd109d7456"}, + {file = "pyarrow-8.0.0-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:3fee786259d986f8c046100ced54d63b0c8c9f7cdb7d1bbe07dc69e0f928141c"}, + {file = "pyarrow-8.0.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ea2c54e6b5ecd64e8299d2abb40770fe83a718f5ddc3825ddd5cd28e352cce1"}, + {file = "pyarrow-8.0.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8392b9a1e837230090fe916415ed4c3433b2ddb1a798e3f6438303c70fbabcfc"}, + {file = "pyarrow-8.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:cb06cacc19f3b426681f2f6803cc06ff481e7fe5b3a533b406bc5b2138843d4f"}, + {file = "pyarrow-8.0.0.tar.gz", hash = "sha256:4a18a211ed888f1ac0b0ebcb99e2d9a3e913a481120ee9b1fe33d3fedb945d4e"}, ] pyasn1 = [ {file = "pyasn1-0.4.8-py2.4.egg", hash = "sha256:fec3e9d8e36808a28efb59b489e4528c10ad0f480e57dcc32b4de5c9d8c9fdf3"}, @@ -2943,10 +2860,6 @@ pydivert = [ {file = "pydivert-2.1.0-py2.py3-none-any.whl", hash = "sha256:382db488e3c37c03ec9ec94e061a0b24334d78dbaeebb7d4e4d32ce4355d9da1"}, {file = "pydivert-2.1.0.tar.gz", hash = "sha256:f0e150f4ff591b78e35f514e319561dadff7f24a82186a171dd4d465483de5b4"}, ] -pyfunctional = [ - {file = "PyFunctional-1.4.3-py3-none-any.whl", hash = "sha256:50f11ea3c386c4330c7a8be92c0736b4b6a5924bdbd00b8b93f93b0c7fe6f2b3"}, - {file = "PyFunctional-1.4.3.tar.gz", hash = "sha256:11c313fe251b269c6506689135456a05bc5a6fa129c9d02b445f383d4f411e10"}, -] pygments = [ {file = "Pygments-2.12.0-py3-none-any.whl", hash = "sha256:dc9c10fb40944260f6ed4c688ece0cd2048414940f1cea51b8b226318411c519"}, {file = "Pygments-2.12.0.tar.gz", hash = "sha256:5eb116118f9612ff1ee89ac96437bb6b49e8f04d8a13b514ba26f620208e26eb"}, @@ -3183,11 +3096,6 @@ sphinxcontrib-serializinghtml = [ {file = "sphinxcontrib-serializinghtml-1.1.5.tar.gz", hash = "sha256:aa5f6de5dfdf809ef505c4895e51ef5c9eac17d0f287933eb49ec495280b6952"}, {file = "sphinxcontrib_serializinghtml-1.1.5-py2.py3-none-any.whl", hash = "sha256:352a9a00ae864471d3a7ead8d7d79f5fc0b57e8b3f95e9867eb9eb28999b92fd"}, ] -tabulate = [ - {file = "tabulate-0.8.10-py3-none-any.whl", hash = "sha256:0ba055423dbaa164b9e456abe7920c5e8ed33fcc16f6d1b2f2d152c8e1e8b4fc"}, - {file = "tabulate-0.8.10-py3.8.egg", hash = "sha256:436f1c768b424654fce8597290d2764def1eea6a77cfa5c33be00b1bc0f4f63d"}, - {file = "tabulate-0.8.10.tar.gz", hash = "sha256:6c57f3f3dd7ac2782770155f3adb2db0b1a269637e42f27599925e64b114f519"}, -] tenacity = [ {file = "tenacity-6.3.1-py2.py3-none-any.whl", hash = "sha256:baed357d9f35ec64264d8a4bbf004c35058fad8795c5b0d8a7dc77ecdcbb8f39"}, {file = "tenacity-6.3.1.tar.gz", hash = "sha256:e14d191fb0a309b563904bbc336582efe2037de437e543b38da749769b544d7f"}, diff --git a/pyproject.toml b/pyproject.toml index c09ce9b9..02fdb0a5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,20 +1,32 @@ [tool.poetry] name = "plotly-resampler" # Do not forget to update the __init__.py __version__ variable -version = "0.7.2.2" +version = "0.8.0rc1" description = "Visualizing large time series with plotly" authors = ["Jonas Van Der Donckt", "Jeroen Van Der Donckt", "Emiel Deprost"] readme = "README.md" repository = "https://github.com/predict-idlab/plotly-resampler" documentation = "https://predict-idlab.github.io/plotly-resampler" keywords = ["time-series", "visualization", "resampling", "plotly", "plotly-dash"] -build = "build.py" +packages = [ + { include = "plotly_resampler" } +] +include = [ + # C extensions must be included in the wheel distributions + {path = "plotly_resampler/aggregation/algorithms/*.so", format = "wheel"}, + {path = "plotly_resampler/aggregation/algorithms/*.pyd", format = "wheel"} +] + +[tool.poetry.build] +generate-setup-file = false +script = "build.py" + [tool.poetry.dependencies] python = "^3.7.1,<3.11" jupyter-dash = ">=0.4.2" plotly = "^5.6.0" dash = "^2.2.0" -orjson = "^3.6.8" # faster json serialization +orjson = {version = "^3.7.7", optional = true} pandas = "^1.3.5" trace-updater = ">=0.0.8" numpy = ">=1.21.0" @@ -23,14 +35,12 @@ numpy = ">=1.21.0" jupyterlab = "^3.3.0" pytest = "^6.2.5" pytest-cov = "^3.0.0" -black = "^22.3.0" +black = "^22.6.0" selenium = "4.2.0" pytest-selenium = "^2.0.1" webdriver-manager = "^3.5.2" -pyarrow = "^6.0.1" selenium-wire = "^4.5.6" -pyfunctional = "^1.4.3" -dash-bootstrap-components = "^1.0.3" +pyarrow = "^8.0.0" Sphinx = "^4.4.0" pydata-sphinx-theme = "^0.9.0" sphinx-autodoc-typehints = "^1.17.0" @@ -39,5 +49,5 @@ memory-profiler = "^0.60.0" line-profiler = "^3.5.1" [build-system] -requires = ["poetry-core>=1.0.0", "setuptools"] -build-backend = ["poetry.core.masonry.api", "setuptools.build_meta"] +requires = "poetry-core>=1.1.0" +build-backend = "poetry.core.masonry.api" From 65813f23303e24c9bb5d2bd916f3b777606fc88a Mon Sep 17 00:00:00 2001 From: jonas Date: Sun, 17 Jul 2022 18:24:55 +0200 Subject: [PATCH 04/49] :broom: --- examples/requirements.txt | 6 +- poetry.lock | 270 +------------------------------------- pyproject.toml | 3 - 3 files changed, 6 insertions(+), 273 deletions(-) diff --git a/examples/requirements.txt b/examples/requirements.txt index ba0b2e64..bec2f56d 100644 --- a/examples/requirements.txt +++ b/examples/requirements.txt @@ -1,3 +1,7 @@ pyfunctional>=1.4.3 dash-bootstrap-components>=1.2.0 -dash-extensions>=0.1.4 \ No newline at end of file +dash-extensions>=0.1.4 +ipywidgets>=7.7.0 +memory-profiler>=0.60.0 +line-profiler>=3.5.1 +pyarrow>=6.0.0 \ No newline at end of file diff --git a/poetry.lock b/poetry.lock index 5f833399..459bbfed 100644 --- a/poetry.lock +++ b/poetry.lock @@ -21,24 +21,6 @@ importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} docs = ["sphinx", "setuptools-scm", "sphinx-rtd-theme"] test = ["pytest", "pytest-cov"] -[[package]] -name = "anyio" -version = "3.6.1" -description = "High level compatibility layer for multiple asynchronous event loop implementations" -category = "dev" -optional = false -python-versions = ">=3.6.2" - -[package.dependencies] -idna = ">=2.8" -sniffio = ">=1.1" -typing-extensions = {version = "*", markers = "python_version < \"3.8\""} - -[package.extras] -doc = ["packaging", "sphinx-rtd-theme", "sphinx-autodoc-typehints (>=1.2.0)"] -test = ["coverage[toml] (>=4.5)", "hypothesis (>=4.0)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "contextlib2", "uvloop (<0.15)", "mock (>=4)", "uvloop (>=0.15)"] -trio = ["trio (>=0.16)"] - [[package]] name = "appnope" version = "0.1.3" @@ -625,17 +607,6 @@ MarkupSafe = ">=2.0" [package.extras] i18n = ["Babel (>=2.7)"] -[[package]] -name = "json5" -version = "0.9.8" -description = "A Python implementation of the JSON5 data format." -category = "dev" -optional = false -python-versions = "*" - -[package.extras] -dev = ["hypothesis"] - [[package]] name = "jsonschema" version = "4.7.2" @@ -712,57 +683,6 @@ retrying = "*" [package.extras] dev = ["jupyterlab (>=2.0)", "notebook (>=6.0)", "jupyter-server-proxy"] -[[package]] -name = "jupyter-server" -version = "1.18.1" -description = "The backend—i.e. core services, APIs, and REST endpoints—to Jupyter web applications." -category = "dev" -optional = false -python-versions = ">=3.7" - -[package.dependencies] -anyio = ">=3.1.0,<4" -argon2-cffi = "*" -jinja2 = "*" -jupyter-client = ">=6.1.12" -jupyter-core = ">=4.7.0" -nbconvert = ">=6.4.4" -nbformat = ">=5.2.0" -packaging = "*" -prometheus-client = "*" -pywinpty = {version = "*", markers = "os_name == \"nt\""} -pyzmq = ">=17" -Send2Trash = "*" -terminado = ">=0.8.3" -tornado = ">=6.1.0" -traitlets = ">=5.1" -websocket-client = "*" - -[package.extras] -test = ["coverage", "ipykernel", "pre-commit", "pytest-console-scripts", "pytest-cov", "pytest-mock", "pytest-timeout", "pytest-tornasync", "pytest (>=6.0)", "requests"] - -[[package]] -name = "jupyterlab" -version = "3.4.3" -description = "JupyterLab computational environment" -category = "dev" -optional = false -python-versions = ">=3.7" - -[package.dependencies] -ipython = "*" -jinja2 = ">=2.1" -jupyter-core = "*" -jupyter-server = ">=1.16,<2.0" -jupyterlab-server = ">=2.10,<3.0" -nbclassic = ">=0.2,<1.0" -packaging = "*" -tornado = ">=6.1.0" - -[package.extras] -test = ["check-manifest", "coverage", "jupyterlab-server", "pytest (>=6.0)", "pytest-cov", "pytest-console-scripts", "pytest-check-links (>=0.5)", "requests", "requests-cache", "virtualenv", "pre-commit"] -ui-tests = ["build"] - [[package]] name = "jupyterlab-pygments" version = "0.2.2" @@ -771,28 +691,6 @@ category = "dev" optional = false python-versions = ">=3.7" -[[package]] -name = "jupyterlab-server" -version = "2.15.0" -description = "A set of server components for JupyterLab and JupyterLab like applications." -category = "dev" -optional = false -python-versions = ">=3.7" - -[package.dependencies] -babel = "*" -importlib-metadata = {version = ">=3.6", markers = "python_version < \"3.10\""} -jinja2 = ">=3.0.3" -json5 = "*" -jsonschema = ">=3.0.1" -jupyter-server = ">=1.8,<2" -packaging = "*" -requests = "*" - -[package.extras] -openapi = ["openapi-core (>=0.14.2)", "ruamel-yaml"] -test = ["codecov", "ipykernel", "jupyter-server", "openapi-core (>=0.14.2)", "openapi-spec-validator (<0.5)", "pytest-console-scripts", "pytest-cov", "pytest (>=5.3.2)", "ruamel-yaml", "strict-rfc3339"] - [[package]] name = "jupyterlab-widgets" version = "1.1.1" @@ -809,20 +707,6 @@ category = "dev" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7" -[[package]] -name = "line-profiler" -version = "3.5.1" -description = "Line-by-line profiler." -category = "dev" -optional = false -python-versions = "*" - -[package.extras] -all = ["cython", "scikit-build", "cmake", "ninja", "pytest (>=4.6.11)", "pytest-cov (>=2.10.1)", "coverage[toml] (>=5.3)", "ubelt (>=1.0.1)", "IPython (>=0.13,<7.17.0)", "IPython (>=0.13)"] -build = ["cython", "scikit-build", "cmake", "ninja"] -ipython = ["IPython (>=0.13,<7.17.0)", "IPython (>=0.13)"] -tests = ["pytest (>=4.6.11)", "pytest-cov (>=2.10.1)", "coverage[toml] (>=5.3)", "ubelt (>=1.0.1)", "IPython (>=0.13,<7.17.0)", "IPython (>=0.13)"] - [[package]] name = "markupsafe" version = "2.1.1" @@ -842,17 +726,6 @@ python-versions = ">=3.5" [package.dependencies] traitlets = "*" -[[package]] -name = "memory-profiler" -version = "0.60.0" -description = "A module for monitoring memory usage of a python program" -category = "dev" -optional = false -python-versions = ">=3.4" - -[package.dependencies] -psutil = "*" - [[package]] name = "mistune" version = "0.8.4" @@ -869,38 +742,6 @@ category = "dev" optional = false python-versions = "*" -[[package]] -name = "nbclassic" -version = "0.4.3" -description = "A web-based notebook environment for interactive computing" -category = "dev" -optional = false -python-versions = ">=3.7" - -[package.dependencies] -argon2-cffi = "*" -ipykernel = "*" -ipython-genutils = "*" -jinja2 = "*" -jupyter-client = ">=6.1.1" -jupyter-core = ">=4.6.1" -jupyter-server = ">=1.8" -nbconvert = ">=5" -nbformat = "*" -nest-asyncio = ">=1.5" -notebook-shim = ">=0.1.0" -prometheus-client = "*" -pyzmq = ">=17" -Send2Trash = ">=1.8.0" -terminado = ">=0.8.3" -tornado = ">=6.1" -traitlets = ">=4.2.1" - -[package.extras] -docs = ["sphinx", "nbsphinx", "sphinxcontrib-github-alt", "sphinx-rtd-theme", "myst-parser"] -json-logging = ["json-logging"] -test = ["pytest", "coverage", "requests", "testpath", "nbval", "selenium (==4.1.5)", "pytest-cov", "pytest-tornasync", "requests-unixsocket"] - [[package]] name = "nbclient" version = "0.6.6" @@ -1007,20 +848,6 @@ docs = ["sphinx", "nbsphinx", "sphinxcontrib-github-alt", "sphinx-rtd-theme", "m json-logging = ["json-logging"] test = ["pytest", "coverage", "requests", "testpath", "nbval", "selenium", "pytest-cov", "requests-unixsocket"] -[[package]] -name = "notebook-shim" -version = "0.1.0" -description = "A shim layer for notebook traits and config" -category = "dev" -optional = false -python-versions = ">=3.7" - -[package.dependencies] -jupyter-server = ">=1.8,<2.0" - -[package.extras] -test = ["pytest", "pytest-tornasync", "pytest-console-scripts"] - [[package]] name = "numpy" version = "1.21.6" @@ -1932,19 +1759,6 @@ category = "dev" optional = false python-versions = "*" -[[package]] -name = "websocket-client" -version = "1.3.3" -description = "WebSocket client for Python with low level API options" -category = "dev" -optional = false -python-versions = ">=3.7" - -[package.extras] -docs = ["Sphinx (>=3.4)", "sphinx-rtd-theme (>=0.5)"] -optional = ["python-socks", "wsaccel"] -test = ["websockets"] - [[package]] name = "werkzeug" version = "2.1.2" @@ -2007,7 +1821,7 @@ cffi = ["cffi (>=1.11)"] [metadata] lock-version = "1.1" python-versions = "^3.7.1,<3.11" -content-hash = "ba58b87cd6434d5973701a3dc0886e2d1400a9c2fce92a3c48e87915480ad14d" +content-hash = "94fa3fdd6bdbfb8e8e51b34d8aaef8f5336698a8064f16868e92bd6b6adeb6e9" [metadata.files] alabaster = [ @@ -2018,10 +1832,6 @@ ansi2html = [ {file = "ansi2html-1.8.0-py3-none-any.whl", hash = "sha256:ef9cc9682539dbe524fbf8edad9c9462a308e04bce1170c32daa8fdfd0001785"}, {file = "ansi2html-1.8.0.tar.gz", hash = "sha256:38b82a298482a1fa2613f0f9c9beb3db72a8f832eeac58eb2e47bf32cd37f6d5"}, ] -anyio = [ - {file = "anyio-3.6.1-py3-none-any.whl", hash = "sha256:cb29b9c70620506a9a8f87a309591713446953302d7d995344d0d7c6c0c9a7be"}, - {file = "anyio-3.6.1.tar.gz", hash = "sha256:413adf95f93886e442aea925f3ee43baa5a765a64a0f52c6081894f9992fdd0b"}, -] appnope = [ {file = "appnope-0.1.3-py2.py3-none-any.whl", hash = "sha256:265a455292d0bd8a72453494fa24df5a11eb18373a60c7c0430889f22548605e"}, {file = "appnope-0.1.3.tar.gz", hash = "sha256:02bd91c4de869fbb1e1c50aafc4098827a7a54ab2f39d9dcba6c9547ed920e24"}, @@ -2431,9 +2241,6 @@ jinja2 = [ {file = "Jinja2-3.1.2-py3-none-any.whl", hash = "sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61"}, {file = "Jinja2-3.1.2.tar.gz", hash = "sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852"}, ] -json5 = [ - {file = "json5-0.9.8.tar.gz", hash = "sha256:0fa6e4d3ef062f93ba9cf2a9103fe8e68c7917dfa33519ae3ac8c7e48e3c84ff"}, -] jsonschema = [ {file = "jsonschema-4.7.2-py3-none-any.whl", hash = "sha256:c7448a421b25e424fccfceea86b4e3a8672b4436e1988ccbde92c80828d4f085"}, {file = "jsonschema-4.7.2.tar.gz", hash = "sha256:73764f461d61eb97a057c929368610a134d1d1fffd858acfe88864ee94f1f1d3"}, @@ -2450,22 +2257,10 @@ jupyter-dash = [ {file = "jupyter-dash-0.4.2.tar.gz", hash = "sha256:d546c7c25a2867c14c95a48af0ad572803b26915a5ce6052158c9dede4dbf48c"}, {file = "jupyter_dash-0.4.2-py3-none-any.whl", hash = "sha256:b07d90ccf38d4dfb04efd630a2b2627f367b79fa4296ee3912d0c4e21e73e9b2"}, ] -jupyter-server = [ - {file = "jupyter_server-1.18.1-py3-none-any.whl", hash = "sha256:022759b09c96a4e2feb95de59ce4283e04e17782efe197b91d23a47521609b77"}, - {file = "jupyter_server-1.18.1.tar.gz", hash = "sha256:2b72fc595bccae292260aad8157a0ead8da2c703ec6ae1bb7b36dbad0e267ea7"}, -] -jupyterlab = [ - {file = "jupyterlab-3.4.3-py3-none-any.whl", hash = "sha256:f028f4c6a4171785c4a1d592ca9bf36812047703e4aa981482cd3872eb0fc169"}, - {file = "jupyterlab-3.4.3.tar.gz", hash = "sha256:e2dcc40e94366dde5de4b19e8c43ee133cf041b852d01a3625a7cf29532da49d"}, -] jupyterlab-pygments = [ {file = "jupyterlab_pygments-0.2.2-py2.py3-none-any.whl", hash = "sha256:2405800db07c9f770863bcf8049a529c3dd4d3e28536638bd7c1c01d2748309f"}, {file = "jupyterlab_pygments-0.2.2.tar.gz", hash = "sha256:7405d7fde60819d905a9fa8ce89e4cd830e318cdad22a0030f7a901da705585d"}, ] -jupyterlab-server = [ - {file = "jupyterlab_server-2.15.0-py3-none-any.whl", hash = "sha256:0e327d7a346874fd8e94c1bcbd69906d18a8558df8f13115c5afd183c3107756"}, - {file = "jupyterlab_server-2.15.0.tar.gz", hash = "sha256:a91c515e0e7971a8f7c3c9834b748857f7dac502f93604bf283987991fd987ef"}, -] jupyterlab-widgets = [ {file = "jupyterlab_widgets-1.1.1-py3-none-any.whl", hash = "sha256:90ab47d99da03a3697074acb23b2975ead1d6171aa41cb2812041a7f2a08177a"}, {file = "jupyterlab_widgets-1.1.1.tar.gz", hash = "sha256:67d0ef1e407e0c42c8ab60b9d901cd7a4c68923650763f75bf17fb06c1943b79"}, @@ -2474,54 +2269,6 @@ kaitaistruct = [ {file = "kaitaistruct-0.10-py2.py3-none-any.whl", hash = "sha256:a97350919adbf37fda881f75e9365e2fb88d04832b7a4e57106ec70119efb235"}, {file = "kaitaistruct-0.10.tar.gz", hash = "sha256:a044dee29173d6afbacf27bcac39daf89b654dd418cfa009ab82d9178a9ae52a"}, ] -line-profiler = [ - {file = "line_profiler-3.5.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:409e32944176d4004df4308cc37674c1e48ea7444918c129edf5da68ded305c6"}, - {file = "line_profiler-3.5.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3d675732d221b5a4bfe48f57bd0ed2f759ad919e650890f4f5f1cf6536c1bc23"}, - {file = "line_profiler-3.5.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf3c88730d8a39a03c536d729f50d78d0947bf836c5809993781c8d730a7a4a2"}, - {file = "line_profiler-3.5.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:916ba4f353fe0c6edf44394d02de8ea4e6bd5225e3c8c876a6879e8c61fec36a"}, - {file = "line_profiler-3.5.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:4086531248ca399fecace5a2fb1c6e0723e07d72406b123f1f9ff91d0519ac7c"}, - {file = "line_profiler-3.5.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:7ee3e8df7ee4fa6ce12010adf4a5938862367b7d903614568abae307ffa46062"}, - {file = "line_profiler-3.5.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8971a6ccd7f0ffda45f30ca39b55877c455fc020308336093d6e468352436196"}, - {file = "line_profiler-3.5.1-cp310-cp310-win32.whl", hash = "sha256:ff31ae34e3db3c161321d714106e9d3b9755c231ef1b716539fefc49b2855d21"}, - {file = "line_profiler-3.5.1-cp310-cp310-win_amd64.whl", hash = "sha256:ee44195421ccb95f8f039d27d8ec797f3ad25e816c08365302c8b03963a798e6"}, - {file = "line_profiler-3.5.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:691c66477ed832141e76359d5b25db97d04cf9620fbce6a84de1989eb0d3a2fe"}, - {file = "line_profiler-3.5.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7fbe8a7e9f38721020ce3fcb73567dc862735c9a138458477840b4fb03440153"}, - {file = "line_profiler-3.5.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dddd5f2239d716ed471d9b0ae5ecaae612c051e53acc592331dc1e467c630366"}, - {file = "line_profiler-3.5.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1b69c751b6619d36f3870512840e0190b9d19f76fb09183ce3274c69544ab959"}, - {file = "line_profiler-3.5.1-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:b932246545b6108a3bf615d2b0e5a2c905b6f4a127d27608d996a0c6ea0f2b7a"}, - {file = "line_profiler-3.5.1-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:801fd7f357e8fc6910441e0b26fad311a6e81b2d34b90b64ba4be3bc366ff193"}, - {file = "line_profiler-3.5.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:9a26590d701aebc8ce80930e623596b16cf03db44ae6845b956559b51b1f4d8f"}, - {file = "line_profiler-3.5.1-cp36-cp36m-win32.whl", hash = "sha256:2d5de461e7ff4662b8c32a8328974e6e0ad433e1dc2e596c7105d8a8ffcc6dc4"}, - {file = "line_profiler-3.5.1-cp36-cp36m-win_amd64.whl", hash = "sha256:25930cb6d4a72f2f2e238bd80d0a875ec79ee98910be0aa969c7ca45ec68efb1"}, - {file = "line_profiler-3.5.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:71b939326d3d385372c5891900afc06e65eacdc108b28da006f59f4fee937c61"}, - {file = "line_profiler-3.5.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9ef1acc35f8ffa8b4027963c1f596bfd7b2b279eecb8cbb0c662befeb09fc443"}, - {file = "line_profiler-3.5.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17b8d03ab7f09af40140ec9e616d1b6dfa9b90495bd2d65a0a47052a147274bc"}, - {file = "line_profiler-3.5.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b54ff0f75736631f2b956c35ce7436519b8b8a40f99909106eb409140ed51190"}, - {file = "line_profiler-3.5.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:532604be45bcf1f581d9784350d7b5775b15565bf1355905edd4892aa601b40d"}, - {file = "line_profiler-3.5.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:3a5b6287752468c4548fae267dbb0f6ccb5db5d16c8828886e1ef29ffdfa9e2d"}, - {file = "line_profiler-3.5.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:4b680c3746a585df81e8200d28a94bfef9c7ae0748752012c90ddb4e5ca51440"}, - {file = "line_profiler-3.5.1-cp37-cp37m-win32.whl", hash = "sha256:f7f7e3de6dab51209ee1e2efe48e4c832d23d166a349dd37dedb6b0545931171"}, - {file = "line_profiler-3.5.1-cp37-cp37m-win_amd64.whl", hash = "sha256:93407999338a446b682cd2203b09d3c461e96ba5ab7b98900b3e43c51ca50986"}, - {file = "line_profiler-3.5.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:eaae9d4583160a963ed37850dabb564311fbe90a2a93add52230ece25bd861e4"}, - {file = "line_profiler-3.5.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c906fa9f8bcbdd3d2e5c8bd3245924c6b0a1563ca2134560d8ba3509723b8ed4"}, - {file = "line_profiler-3.5.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3c00208161aa03f220df57c5a7eaa734332221d64b91cea5c45d4405a1d1f056"}, - {file = "line_profiler-3.5.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ef1e3b30a26e7bc24b4b4f5d0107190a200bee19c0ee0074a06a3389ab578889"}, - {file = "line_profiler-3.5.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b5357eb328b425a4b5ba20e2b70f94d8c6a43b38b968dffe91fc0600a35b0a03"}, - {file = "line_profiler-3.5.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:82b02a7b18b307258bed445dd13ab351e53737cb7fe212a5670d98f4271b4b69"}, - {file = "line_profiler-3.5.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:3917f4d5a01ea297b0474898f51c5dd1a7df4806ffc019da28d04089fc4e0896"}, - {file = "line_profiler-3.5.1-cp38-cp38-win32.whl", hash = "sha256:13df519e1cdf63e16325ec6cab1f441c8b588dc6148dfdd92e99f44521dc74e8"}, - {file = "line_profiler-3.5.1-cp38-cp38-win_amd64.whl", hash = "sha256:8d2280cf96644137b8033f74e712f8bce80c509f08c8a64546b795905b035066"}, - {file = "line_profiler-3.5.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:861ccf4981867ee44e381bcfffff98c1572ba055903d52d7eb4b345c66e992e8"}, - {file = "line_profiler-3.5.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a9102ddec1008faf4861f72ed4ec1f7a338ffe7230ead5ea7545388f57cbc39a"}, - {file = "line_profiler-3.5.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d38e2e878ba47fc1f9a0e4194c0d4fa034c3c9eb9fbc954c0ccb4046672ed326"}, - {file = "line_profiler-3.5.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9efc10ac7ff16f8fe9f1ce6a0a783db2e3f617d5916f8d58a41f6afb13841694"}, - {file = "line_profiler-3.5.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d7b3cb1718bce1d35d40b3ab0bbbd528c67d930f9308622aabeaf2ed26f163ba"}, - {file = "line_profiler-3.5.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:e2b6cbc9ad958c3421df6b57e318bfaa78b9e1697528729e0a0b00d25b4a7c7a"}, - {file = "line_profiler-3.5.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:20233371c4abf160358dbbf0702228d9dd72c66682eb284651db3a76d6d1e9f0"}, - {file = "line_profiler-3.5.1-cp39-cp39-win32.whl", hash = "sha256:2ea6ce644513bb53047c3081702371869a54ffafb2cb523c6c6b6589da623764"}, - {file = "line_profiler-3.5.1-cp39-cp39-win_amd64.whl", hash = "sha256:0c717a9c08255da9f79595330f20502a32806ba96823716b8a42b26ee7b0f183"}, - {file = "line_profiler-3.5.1.tar.gz", hash = "sha256:77400208bfbd5d4341938a9a3a4fb5194f5af7fc23b2d496c913755f8310e8b8"}, -] markupsafe = [ {file = "MarkupSafe-2.1.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:86b1f75c4e7c2ac2ccdaec2b9022845dbb81880ca318bb7a0a01fbf7813e3812"}, {file = "MarkupSafe-2.1.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f121a1420d4e173a5d96e47e9a0c0dcff965afdf1626d28de1460815f7c4ee7a"}, @@ -2568,9 +2315,6 @@ matplotlib-inline = [ {file = "matplotlib-inline-0.1.3.tar.gz", hash = "sha256:a04bfba22e0d1395479f866853ec1ee28eea1485c1d69a6faf00dc3e24ff34ee"}, {file = "matplotlib_inline-0.1.3-py3-none-any.whl", hash = "sha256:aed605ba3b72462d64d475a21a9296f400a19c4f74a31b59103d2a99ffd5aa5c"}, ] -memory-profiler = [ - {file = "memory_profiler-0.60.0.tar.gz", hash = "sha256:6a12869511d6cebcb29b71ba26985675a58e16e06b3c523b49f67c5497a33d1c"}, -] mistune = [ {file = "mistune-0.8.4-py2.py3-none-any.whl", hash = "sha256:88a1051873018da288eee8538d476dffe1262495144b33ecb586c4ab266bb8d4"}, {file = "mistune-0.8.4.tar.gz", hash = "sha256:59a3429db53c50b5c6bcc8a07f8848cb00d7dc8bdb431a4ab41920d201d4756e"}, @@ -2579,10 +2323,6 @@ mypy-extensions = [ {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"}, {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"}, ] -nbclassic = [ - {file = "nbclassic-0.4.3-py3-none-any.whl", hash = "sha256:4b01076effdac53e775cd1b6a4e891663568b32621468e205b502a23b2921899"}, - {file = "nbclassic-0.4.3.tar.gz", hash = "sha256:f03111b2cebaa69b88370a7b23b19b2b37c9bb71767f1828cdfd7a047eae8edd"}, -] nbclient = [ {file = "nbclient-0.6.6-py3-none-any.whl", hash = "sha256:09bae4ea2df79fa6bc50aeb8278d8b79d2036792824337fa6eee834afae17312"}, {file = "nbclient-0.6.6.tar.gz", hash = "sha256:0df76a7961d99a681b4796c74a1f2553b9f998851acc01896dce064ad19a9027"}, @@ -2603,10 +2343,6 @@ notebook = [ {file = "notebook-6.4.12-py3-none-any.whl", hash = "sha256:8c07a3bb7640e371f8a609bdbb2366a1976c6a2589da8ef917f761a61e3ad8b1"}, {file = "notebook-6.4.12.tar.gz", hash = "sha256:6268c9ec9048cff7a45405c990c29ac9ca40b0bc3ec29263d218c5e01f2b4e86"}, ] -notebook-shim = [ - {file = "notebook_shim-0.1.0-py3-none-any.whl", hash = "sha256:02432d55a01139ac16e2100888aa2b56c614720cec73a27e71f40a5387e45324"}, - {file = "notebook_shim-0.1.0.tar.gz", hash = "sha256:7897e47a36d92248925a2143e3596f19c60597708f7bef50d81fcd31d7263e85"}, -] numpy = [ {file = "numpy-1.21.6-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:8737609c3bbdd48e380d463134a35ffad3b22dc56295eff6f79fd85bd0eeeb25"}, {file = "numpy-1.21.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:fdffbfb6832cd0b300995a2b08b8f6fa9f6e856d562800fea9182316d99c4e8e"}, @@ -3191,10 +2927,6 @@ webencodings = [ {file = "webencodings-0.5.1-py2.py3-none-any.whl", hash = "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78"}, {file = "webencodings-0.5.1.tar.gz", hash = "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923"}, ] -websocket-client = [ - {file = "websocket-client-1.3.3.tar.gz", hash = "sha256:d58c5f284d6a9bf8379dab423259fe8f85b70d5fa5d2916d5791a84594b122b1"}, - {file = "websocket_client-1.3.3-py3-none-any.whl", hash = "sha256:5d55652dc1d0b3c734f044337d929aaf83f4f9138816ec680c1aefefb4dc4877"}, -] werkzeug = [ {file = "Werkzeug-2.1.2-py3-none-any.whl", hash = "sha256:72a4b735692dd3135217911cbeaa1be5fa3f62bffb8745c5215420a03dc55255"}, {file = "Werkzeug-2.1.2.tar.gz", hash = "sha256:1ce08e8093ed67d638d63879fd1ba3735817f7a80de3674d293f5984f25fb6e6"}, diff --git a/pyproject.toml b/pyproject.toml index 02fdb0a5..7da1d64b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -32,7 +32,6 @@ trace-updater = ">=0.0.8" numpy = ">=1.21.0" [tool.poetry.dev-dependencies] -jupyterlab = "^3.3.0" pytest = "^6.2.5" pytest-cov = "^3.0.0" black = "^22.6.0" @@ -45,8 +44,6 @@ Sphinx = "^4.4.0" pydata-sphinx-theme = "^0.9.0" sphinx-autodoc-typehints = "^1.17.0" ipywidgets = "^7.7.0" -memory-profiler = "^0.60.0" -line-profiler = "^3.5.1" [build-system] requires = "poetry-core>=1.1.0" From 06708d0961051b853d5ed1d29a0403241ab54977 Mon Sep 17 00:00:00 2001 From: jonas Date: Sun, 17 Jul 2022 21:09:12 +0200 Subject: [PATCH 05/49] :fire: first working RC on windows --- poetry.lock | 2 +- pyproject.toml | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/poetry.lock b/poetry.lock index 459bbfed..16acb60b 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1821,7 +1821,7 @@ cffi = ["cffi (>=1.11)"] [metadata] lock-version = "1.1" python-versions = "^3.7.1,<3.11" -content-hash = "94fa3fdd6bdbfb8e8e51b34d8aaef8f5336698a8064f16868e92bd6b6adeb6e9" +content-hash = "9831166341393637749f5ef95fb8ff535ab3638ba9ea552b1f40756668a74a81" [metadata.files] alabaster = [ diff --git a/pyproject.toml b/pyproject.toml index 7da1d64b..83c1be98 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,9 +1,10 @@ [tool.poetry] name = "plotly-resampler" # Do not forget to update the __init__.py __version__ variable -version = "0.8.0rc1" +version = "0.8.0rc5" description = "Visualizing large time series with plotly" authors = ["Jonas Van Der Donckt", "Jeroen Van Der Donckt", "Emiel Deprost"] readme = "README.md" +license = "MIT" repository = "https://github.com/predict-idlab/plotly-resampler" documentation = "https://predict-idlab.github.io/plotly-resampler" keywords = ["time-series", "visualization", "resampling", "plotly", "plotly-dash"] @@ -43,8 +44,8 @@ pyarrow = "^8.0.0" Sphinx = "^4.4.0" pydata-sphinx-theme = "^0.9.0" sphinx-autodoc-typehints = "^1.17.0" -ipywidgets = "^7.7.0" +ipywidgets = "^7.7.1" [build-system] -requires = "poetry-core>=1.1.0" +requires = ["poetry-core>=1.1.0a6", "numpy"] build-backend = "poetry.core.masonry.api" From ed646d4b566ccf04a01570488ca2a220a5347cdd Mon Sep 17 00:00:00 2001 From: jonas Date: Mon, 18 Jul 2022 12:45:49 +0200 Subject: [PATCH 06/49] :chocolate_bar: improving dash examples --- examples/README.md | 17 ++- ...minimal_global.py => 01_minimal_global.py} | 12 +- ...p_minimal_cache.py => 02_minimal_cache.py} | 22 ++- .../dash_apps/03_minimal_cache_dynamic.py | 113 +++++++++++++++ ...ynamic_figures.py => 11_sine_generator.py} | 132 ++++++++++++------ ...dash_app_folder.py => 12_file_selector.py} | 0 ...h_app_coarse_fine.py => 13_coarse_fine.py} | 1 + 7 files changed, 223 insertions(+), 74 deletions(-) rename examples/dash_apps/{dash_app_minimal_global.py => 01_minimal_global.py} (88%) rename examples/dash_apps/{dash_app_minimal_cache.py => 02_minimal_cache.py} (88%) create mode 100644 examples/dash_apps/03_minimal_cache_dynamic.py rename examples/dash_apps/{construct_dynamic_figures.py => 11_sine_generator.py} (51%) rename examples/dash_apps/{dash_app_folder.py => 12_file_selector.py} (100%) rename examples/dash_apps/{dash_app_coarse_fine.py => 13_coarse_fine.py} (99%) diff --git a/examples/README.md b/examples/README.md index d87a563f..060a1c20 100644 --- a/examples/README.md +++ b/examples/README.md @@ -18,20 +18,23 @@ the [basic example notebook](basic_example.ipynb) The [figurewidget example notebook](figurewidget_example.ipynb) utilizes the `FigureWidgetResampler` wrapper to create a `go.FigureWidget` with dynamic aggregation functionality. A major advantage of this approach is that this does not create a web application, thus not needing to be able to create / forward a network port. -Additionally, this notebook highlights how to use the `FigureWidget` its on-click callback to utilize plotly for large **time series annotation**. +Additionally, this noteb ook highlights how to use the `FigureWidget` its on-click callback to utilize plotly for large **time series annotation**. ## 1. Dash apps The [dash_apps](dash_apps/) folder contains example dash apps in which `plotly-resampler` is integrated -| app-name | description | +| | description | |------------------------------------------------------------------| --- | -| [minimal example (*global variable*)](dash_apps/dash_app_minimal_global.py) | *bad practice*: minimal example in which a global `FigureResampler` variable is used | -| [minimal example (*server side caching*)](dash_apps/dash_app_minimal_cache.py) | *good practice*: minimal example in which we perform server side caching of the `FigureResampler` variable | -| [file visualization](dash_apps/dash_app_folder.py) | load and visualize multiple `.parquet` files with plotly-resampler | -| [dynamic sine generator](dash_apps/construct_dynamic_figures.py) | expeonential sine generator which uses [pattern matching callbacks](https://dash.plotly.com/pattern-matching-callbacks) to remove and construct plotly-resampler graphs dynamically | -| [dynamic static graph](dash_apps/dash_app_coarse_fine.py) | Visualization dashboard in which a dynamic (i.e., plotly-resampler graph) and static graph (i.e., go.Figure) are shown (made for [this issue](https://github.com/predict-idlab/plotly-resampler/issues/56)). Relayout events on the coarse graph update the dynamic graph. +| **minimal examples** | | +| [global variable](dash_apps/01_minimal_global.py) | *bad practice*: minimal example in which a global `FigureResampler` variable is used | +| [server side caching](dash_apps/02_minimal_cache.py) | *good practice*: minimal example in which we perform server side caching of the `FigureResampler` variable | +| [runtime graph construction](dash_apps/03_minimal_cache_dynamic.py) | minimal example where graphs are constructed based on user interactions at runtime. [Pattern matching callbacks](https://dash.plotly.com/pattern-matching-callbacks) are used construct these plotly-resampler graphs dynamically. Again, server side caching is performed. | +| **advanced apps** | | +| [dynamic sine generator](dash_apps/11_sine_generator.py) | exponential sine generator which uses [pattern matching callbacks](https://dash.plotly.com/pattern-matching-callbacks) to remove and construct plotly-resampler graphs dynamically | +| [file visualization](dash_apps/12_file_selector.py) | load and visualize multiple `.parquet` files with plotly-resampler | +| [dynamic static graph](dash_apps/13_coarse_fine.py) | Visualization dashboard in which a dynamic (i.e., plotly-resampler graph) and static graph (i.e., go.Figure) are shown (made for [this issue](https://github.com/predict-idlab/plotly-resampler/issues/56)). Relayout events on the coarse graph update the dynamic graph. ## 2. Other apps diff --git a/examples/dash_apps/dash_app_minimal_global.py b/examples/dash_apps/01_minimal_global.py similarity index 88% rename from examples/dash_apps/dash_app_minimal_global.py rename to examples/dash_apps/01_minimal_global.py index 33c21bc7..dfd79451 100644 --- a/examples/dash_apps/dash_app_minimal_global.py +++ b/examples/dash_apps/01_minimal_global.py @@ -14,7 +14,6 @@ """ import dash -import dash_bootstrap_components as dbc import numpy as np import plotly.graph_objects as go from dash import Input, Output, dcc, html @@ -23,24 +22,21 @@ from trace_updater import TraceUpdater # Data that will be used for the plotly-resampler figures -_n = 1_000_000 -x = np.arange(_n) +x = np.arange(2_000_000) noisy_sin = (3 + np.sin(x / 200) + np.random.randn(len(x)) / 10) * x / 1_000 # --------------------------------------Globals --------------------------------------- -app = dash.Dash(__name__, external_stylesheets=[dbc.themes.LUX]) +app = dash.Dash(__name__) fig: FigureResampler = FigureResampler() # NOTE: in this example, this reference to a FigureResampler is essential to preserve -# throughout the whole dash app! If your dash apps want to create a new go.Figure(), +# throughout the whole dash app! If your dash app want to create a new go.Figure(), # you should not construct a new FigureResampler object, but replace the figure of this # FigureResampler object by using the FigureResampler.replace() method. app.layout = html.Div( [ - dbc.Container( - html.H1("plotly-resampler global variable"), style={"textAlign": "center"} - ), + html.H1("plotly-resampler global variable", style={"textAlign": "center"}), html.Button("plot chart", id="plot-button", n_clicks=0), html.Hr(), diff --git a/examples/dash_apps/dash_app_minimal_cache.py b/examples/dash_apps/02_minimal_cache.py similarity index 88% rename from examples/dash_apps/dash_app_minimal_cache.py rename to examples/dash_apps/02_minimal_cache.py index 84128049..d748a69a 100644 --- a/examples/dash_apps/dash_app_minimal_cache.py +++ b/examples/dash_apps/02_minimal_cache.py @@ -11,7 +11,6 @@ """ import dash -import dash_bootstrap_components as dbc import numpy as np import plotly.graph_objects as go from dash import Input, Output, State, dcc, html @@ -24,24 +23,17 @@ from trace_updater import TraceUpdater # Data that will be used for the plotly-resampler figures -_n = 1_000_000 -x = np.arange(_n) +x = np.arange(2_000_000) noisy_sin = (3 + np.sin(x / 200) + np.random.randn(len(x)) / 10) * x / 1_000 # --------------------------------------Globals --------------------------------------- -app = DashProxy( - __name__, - external_stylesheets=[dbc.themes.LUX], - transforms=[ServersideOutputTransform()], -) +app = DashProxy(__name__, transforms=[ServersideOutputTransform()]) app.layout = html.Div( [ - dbc.Container(html.H1("plotly-resamper + dash-extensions"), - style={"textAlign": "center"}), + html.H1("plotly-resamper + dash-extensions", style={"textAlign": "center"}), html.Button("plot chart", id="plot-button", n_clicks=0), html.Hr(), - # The graph and it's needed components to serialize and update efficiently # Note: we also add a dcc.Store component, which will be used to link the # server side cached FigureResampler object @@ -64,8 +56,11 @@ def plot_graph(n_clicks): ctx = dash.callback_context if len(ctx.triggered) and "plot-button" in ctx.triggered[0]["prop_id"]: fig: FigureResampler = FigureResampler(go.Figure()) - fig.add_trace(go.Scattergl(name="log"), hf_x=x, hf_y=noisy_sin * .9999995 ** x) - fig.add_trace(go.Scattergl(name="exp"), hf_x=x, hf_y=noisy_sin * 1.000002 ** x) + + # Figure construction logic + fig.add_trace(go.Scattergl(name="log"), hf_x=x, hf_y=noisy_sin * 0.9999995**x) + fig.add_trace(go.Scattergl(name="exp"), hf_x=x, hf_y=noisy_sin * 1.000002**x) + return fig, fig else: raise dash.exceptions.PreventUpdate() @@ -76,6 +71,7 @@ def plot_graph(n_clicks): Input("graph-id", "relayoutData"), State("store", "data"), # The server side cached FigureResampler per session prevent_initial_call=True, + memoize=True, ) def update_fig(relayoutdata, fig): if fig is None: diff --git a/examples/dash_apps/03_minimal_cache_dynamic.py b/examples/dash_apps/03_minimal_cache_dynamic.py new file mode 100644 index 00000000..2daad02d --- /dev/null +++ b/examples/dash_apps/03_minimal_cache_dynamic.py @@ -0,0 +1,113 @@ +"""Minimal dynamic dash app example. + +Click on a button, and draw a new plotly-resampler graph of a noisy sinusoid. +This example uses pattern-matching callbacks to update dynamically constructed graphs. +The plotly-resampler graphs themselves are cached on the server side. + +The main difference between this example and the dash_app_minimal_cache.py is that here, +we want to cache using a dcc.Store that is not yet available on the client side. As a +result we split up our logic into two callbacks: (1) the callback used to construct the +necessary components and send them to the client-side, and (2) the callback used to +construce the actual plotly-resampler graph and cache it on the server side. These +two callbacks are chained together using the dcc.Interval component. + +""" + +from uuid import uuid4 + +import numpy as np +import plotly.graph_objects as go +import dash +from dash import MATCH, Input, Output, State, dcc, html +from dash_extensions.enrich import ( + DashProxy, + ServersideOutput, + ServersideOutputTransform, + Trigger, + TriggerTransform, +) +from plotly_resampler import FigureResampler +from trace_updater import TraceUpdater + +# Data that will be used for the plotly-resampler figures +x = np.arange(2_000_000) +noisy_sin = (3 + np.sin(x / 200) + np.random.randn(len(x)) / 10) * x / 1_000 + +# --------------------------------------Globals --------------------------------------- +app = DashProxy(__name__, transforms=[ServersideOutputTransform(), TriggerTransform()]) + +app.layout = html.Div( + [ + html.Div(children=[html.Button("Add Chart", id="add-chart", n_clicks=0)]), + html.Div(id="container", children=[]), + ] +) + + +# ------------------------------------ DASH logic ------------------------------------- +# This method adds the needed components to the front-end, but does not yet contain the +# figureResampler graph construction logic. +@app.callback( + Output("container", "children"), + Input("add-chart", "n_clicks"), + State("container", "children"), + prevent_initial_call=True, +) +def add_graph_div(n_clicks: int, div_children: list[html.Div]): + uid = str(uuid4()) + new_child = html.Div( + children=[ + # The graph and it's needed components to serialize and update efficiently + # Note: we also add a dcc.Store component, which will be used to link the + # server side cached FigureResampler object + dcc.Graph(id={"type": "dynamic-graph", "index": uid}, figure=go.Figure()), + dcc.Loading(dcc.Store(id={"type": "store", "index": uid})), + TraceUpdater(id={"type": "dynamic-updater", "index": uid}, gdID=f"{uid}"), + # This dcc.(nterval components makes sure that the `construct_display_graph` + # callback is fired once after these components are added to the session + # its front-end + dcc.Interval( + id={"type": "interval", "index": uid}, max_intervals=1, interval=1 + ), + ], + ) + div_children.append(new_child) + return div_children + + +# This method constructs the figureResampler graph and caches it on the server side +@app.callback( + ServersideOutput({"type": "store", "index": MATCH}, "data"), + Output({"type": "dynamic-graph", "index": MATCH}, "figure"), + State("add-chart", "n_clicks"), + Trigger({"type": "interval", "index": MATCH}, "n_intervals"), + prevent_initial_call=True, +) +def construct_display_graph(n_clicks) -> FigureResampler: + fig = FigureResampler(go.Figure(), default_n_shown_samples=2_000) + + # Figure construction logic based on a state variable, in our case n_clicks + sigma = n_clicks * 1e-6 + fig.add_trace(dict(name="log"), hf_x=x, hf_y=noisy_sin * (1 - sigma) ** x) + fig.add_trace(dict(name="exp"), hf_x=x, hf_y=noisy_sin * (1 + sigma) ** x) + fig.update_layout(title=f"graph - {n_clicks}", title_x=0.5) + + return fig, fig + + +@app.callback( + Output({"type": "dynamic-updater", "index": MATCH}, "updateData"), + Input({"type": "dynamic-graph", "index": MATCH}, "relayoutData"), + State({"type": "store", "index": MATCH}, "data"), + prevent_initial_call=True, + memoize=True, +) +def update_fig(relayoutdata: dict, fig: FigureResampler): + if fig is not None: + return fig.construct_update_data(relayoutdata) + raise dash.exceptions.PreventUpdate() + + +# --------------------------------- Running the app --------------------------------- +if __name__ == "__main__": + app.run_server(debug=True, port=9023) diff --git a/examples/dash_apps/construct_dynamic_figures.py b/examples/dash_apps/11_sine_generator.py similarity index 51% rename from examples/dash_apps/construct_dynamic_figures.py rename to examples/dash_apps/11_sine_generator.py index 92056ae1..f4c47d0f 100644 --- a/examples/dash_apps/construct_dynamic_figures.py +++ b/examples/dash_apps/11_sine_generator.py @@ -1,24 +1,43 @@ -from typing import Dict +"""Dash runtime sine generator app example. + +In this example, users can configure parameters of a sine wave and then generate the +sine-wave graph at runtime using the create-new-graph button. There is also an option +to remove the graph. + +This app uses server side caching of the FigureResampler object. As it uses the same +concepts of the 03_minimal_cache_dynamic.py example, the runtime graph construction +callback is again split up into two callbacks: (1) the callback used to construct the +necessary components and send them to the front-end and (2) the callback used to +construct the plotly-resampler figure and cache it on the server side. + +""" + from uuid import uuid4 import numpy as np import plotly.graph_objects as go - import dash import dash_bootstrap_components as dbc -from dash import Dash, dcc, html, Input, Output, State, MATCH -from dash.exceptions import PreventUpdate - -from trace_updater import TraceUpdater +from dash import MATCH, Input, Output, State, dcc, html +from dash_extensions.enrich import ( + DashProxy, + ServersideOutput, + ServersideOutputTransform, + Trigger, + TriggerTransform, +) from plotly_resampler import FigureResampler +from trace_updater import TraceUpdater -# The global variables -graph_dict: Dict[str, FigureResampler] = {} -app = Dash( - __name__, suppress_callback_exceptions=True, external_stylesheets=[dbc.themes.LUX] +# --------------------------------------Globals --------------------------------------- +app = DashProxy( + __name__, + suppress_callback_exceptions=True, + external_stylesheets=[dbc.themes.LUX], + transforms=[ServersideOutputTransform(), TriggerTransform()], ) -# ---------------------------- Construct the app layout ---------------------------- +# -------- Construct the app layout -------- app.layout = html.Div( [ html.Div(html.H1("Exponential sine generator"), style={"textAlign": "center"}), @@ -81,7 +100,9 @@ ) -# -------------------------------- Callbacks --------------------------------------- +# ------------------------------------ DASH logic ------------------------------------- +# This method adds the needed components to the front-end, but does not yet contain the +# figureResampler graph construction logic. @app.callback( Output("graph-container", "children"), Input("add-graph-btn", "n_clicks"), @@ -93,34 +114,60 @@ ], prevent_initial_call=True, ) -def add_or_remove_graph(add_graph, remove_graph, n, exp, gc): +def add_or_remove_graph(add_graph, remove_graph, n, exp, gc_children): if (add_graph is None or n is None or exp is None) and (remove_graph is None): - raise PreventUpdate() + raise dash.exceptions.PreventUpdate() # Transform the graph data to a figure - gc = [] if gc is None else gc # list of existing Graphs and their TraceUpdaters - if len(gc): - _gc = [] - for i in range(len(gc) // 2): - _gc.append(dcc.Graph(**gc[i * 2]["props"])) - _gc.append( - TraceUpdater(**{k: gc[i * 2 + 1]["props"][k] for k in ["id", "gdID"]}) - ) - gc = _gc + gc_children = [] if gc_children is None else gc_children # Check if we need to remove a graph clicked_btns = [p["prop_id"] for p in dash.callback_context.triggered] if any("remove-graph" in btn_name for btn_name in clicked_btns): - if not len(gc): - raise PreventUpdate() - - graph_dict.pop(gc[-1].__getattribute__("gdID")) - return [*gc[:-2]] + if not len(gc_children): + raise dash.exceptions.PreventUpdate() + return gc_children[:-1] # No graph needs to be removed -> create a new graph + uid = str(uuid4()) + new_child = html.Div( + children=[ + # The graph and it's needed components to serialize and update efficiently + # Note: we also add a dcc.Store component, which will be used to link the + # server side cached FigureResampler object + dcc.Graph(id={"type": "dynamic-graph", "index": uid}, figure=go.Figure()), + dcc.Loading(dcc.Store(id={"type": "store", "index": uid})), + TraceUpdater(id={"type": "dynamic-updater", "index": uid}, gdID=f"{uid}"), + # This dcc.(nterval components makes sure that the `construct_display_graph` + # callback is fired once after these components are added to the session + # its front-end + dcc.Interval( + id={"type": "interval", "index": uid}, max_intervals=1, interval=1 + ), + ], + ) + gc_children.append(new_child) + return gc_children + + +# This method constructs the figureResampler graph and caches it on the server side +@app.callback( + ServersideOutput({"type": "store", "index": MATCH}, "data"), + Output({"type": "dynamic-graph", "index": MATCH}, "figure"), + State("nbr-datapoints", "value"), + State("expansion-factor", "value"), + State("add-graph-btn", "n_clicks"), + Trigger({"type": "interval", "index": MATCH}, "n_intervals"), + prevent_initial_call=True, +) +def construct_display_graph(n, exp, n_added_graphs) -> FigureResampler: + # Figure construction logic based on state variables x = np.arange(n) - expansion_scaling = exp ** x - y = np.sin(x / 10) * expansion_scaling + np.random.randn(n) / 10 * expansion_scaling + expansion_scaling = exp**x + y = ( + np.sin(x / 200) * expansion_scaling + + np.random.randn(n) / 10 * expansion_scaling + ) fr = FigureResampler(go.Figure(), verbose=True) fr.add_trace(go.Scattergl(name="sin"), hf_x=x, hf_y=y) @@ -129,33 +176,26 @@ def add_or_remove_graph(add_graph, remove_graph, n, exp, gc): showlegend=True, legend=dict(orientation="h", y=1.12, xanchor="right", x=1), template="plotly_white", - title=f"graph {len(graph_dict) + 1} - n={n:,} pow={exp}", + title=f"graph {n_added_graphs} - n={n:,} pow={exp}", title_x=0.5, ) - # Create a uuid for the graph and add it to the global graph dict, - uid = str(uuid4()) - graph_dict[uid] = fr - - # Add the graph to the existing output - return [ - *gc, # the existing Graphs and their TraceUpdaters - dcc.Graph(figure=fr, id={"type": "dynamic-graph", "index": uid}), - TraceUpdater(id={"type": "dynamic-updater", "index": uid}, gdID=uid), - ] + return fr, fr -# The generic resampling callback -# see: https://dash.plotly.com/pattern-matching-callbacks for more info @app.callback( Output({"type": "dynamic-updater", "index": MATCH}, "updateData"), Input({"type": "dynamic-graph", "index": MATCH}, "relayoutData"), - State({"type": "dynamic-graph", "index": MATCH}, "id"), + State({"type": "store", "index": MATCH}, "data"), prevent_initial_call=True, + memoize=True, ) -def update_figure(relayoutdata: dict, graph_id_dict: dict): - return graph_dict.get(graph_id_dict["index"]).construct_update_data(relayoutdata) +def update_fig(relayoutdata: dict, fig: FigureResampler): + if fig is not None: + return fig.construct_update_data(relayoutdata) + raise dash.exceptions.PreventUpdate() +# --------------------------------- Running the app --------------------------------- if __name__ == "__main__": app.run_server(debug=True) diff --git a/examples/dash_apps/dash_app_folder.py b/examples/dash_apps/12_file_selector.py similarity index 100% rename from examples/dash_apps/dash_app_folder.py rename to examples/dash_apps/12_file_selector.py diff --git a/examples/dash_apps/dash_app_coarse_fine.py b/examples/dash_apps/13_coarse_fine.py similarity index 99% rename from examples/dash_apps/dash_app_coarse_fine.py rename to examples/dash_apps/13_coarse_fine.py index 453b4b86..6c8a8d27 100644 --- a/examples/dash_apps/dash_app_coarse_fine.py +++ b/examples/dash_apps/13_coarse_fine.py @@ -37,6 +37,7 @@ # --------------------------------------Globals --------------------------------------- app = DashProxy( __name__, + suppress_callback_exceptions=True, external_stylesheets=[dbc.themes.LUX], transforms=[ServersideOutputTransform()], ) From 9f4e17074b448c9b91fb382418d4524380ef4a32 Mon Sep 17 00:00:00 2001 From: jonas Date: Mon, 18 Jul 2022 15:08:00 +0200 Subject: [PATCH 07/49] :mag: --- examples/dash_apps/02_minimal_cache.py | 2 +- examples/dash_apps/03_minimal_cache_dynamic.py | 4 ++-- examples/dash_apps/11_sine_generator.py | 2 +- examples/dash_apps/13_coarse_fine.py | 4 +--- 4 files changed, 5 insertions(+), 7 deletions(-) diff --git a/examples/dash_apps/02_minimal_cache.py b/examples/dash_apps/02_minimal_cache.py index d748a69a..e0ef1630 100644 --- a/examples/dash_apps/02_minimal_cache.py +++ b/examples/dash_apps/02_minimal_cache.py @@ -31,7 +31,7 @@ app.layout = html.Div( [ - html.H1("plotly-resamper + dash-extensions", style={"textAlign": "center"}), + html.H1("plotly-resampler + dash-extensions", style={"textAlign": "center"}), html.Button("plot chart", id="plot-button", n_clicks=0), html.Hr(), # The graph and it's needed components to serialize and update efficiently diff --git a/examples/dash_apps/03_minimal_cache_dynamic.py b/examples/dash_apps/03_minimal_cache_dynamic.py index 2daad02d..10ebcdba 100644 --- a/examples/dash_apps/03_minimal_cache_dynamic.py +++ b/examples/dash_apps/03_minimal_cache_dynamic.py @@ -8,7 +8,7 @@ we want to cache using a dcc.Store that is not yet available on the client side. As a result we split up our logic into two callbacks: (1) the callback used to construct the necessary components and send them to the client-side, and (2) the callback used to -construce the actual plotly-resampler graph and cache it on the server side. These +construct the actual plotly-resampler graph and cache it on the server side. These two callbacks are chained together using the dcc.Interval component. """ @@ -63,7 +63,7 @@ def add_graph_div(n_clicks: int, div_children: list[html.Div]): dcc.Graph(id={"type": "dynamic-graph", "index": uid}, figure=go.Figure()), dcc.Loading(dcc.Store(id={"type": "store", "index": uid})), TraceUpdater(id={"type": "dynamic-updater", "index": uid}, gdID=f"{uid}"), - # This dcc.(nterval components makes sure that the `construct_display_graph` + # This dcc.Interval components makes sure that the `construct_display_graph` # callback is fired once after these components are added to the session # its front-end dcc.Interval( diff --git a/examples/dash_apps/11_sine_generator.py b/examples/dash_apps/11_sine_generator.py index f4c47d0f..2584b96d 100644 --- a/examples/dash_apps/11_sine_generator.py +++ b/examples/dash_apps/11_sine_generator.py @@ -138,7 +138,7 @@ def add_or_remove_graph(add_graph, remove_graph, n, exp, gc_children): dcc.Graph(id={"type": "dynamic-graph", "index": uid}, figure=go.Figure()), dcc.Loading(dcc.Store(id={"type": "store", "index": uid})), TraceUpdater(id={"type": "dynamic-updater", "index": uid}, gdID=f"{uid}"), - # This dcc.(nterval components makes sure that the `construct_display_graph` + # This dcc.Interval components makes sure that the `construct_display_graph` # callback is fired once after these components are added to the session # its front-end dcc.Interval( diff --git a/examples/dash_apps/13_coarse_fine.py b/examples/dash_apps/13_coarse_fine.py index 6c8a8d27..239080d0 100644 --- a/examples/dash_apps/13_coarse_fine.py +++ b/examples/dash_apps/13_coarse_fine.py @@ -13,13 +13,11 @@ __author__ = "Jonas Van Der Donckt" -import re import dash import dash_bootstrap_components as dbc -import pandas as pd import plotly.graph_objects as go from pathlib import Path -from typing import List, Union +from typing import List from dash import Input, Output, State, dcc, html from dash_extensions.enrich import ( From 9e85410f113a08fbd9c0d0c6f5de32762872e688 Mon Sep 17 00:00:00 2001 From: jonas Date: Mon, 18 Jul 2022 16:50:26 +0200 Subject: [PATCH 08/49] :pencil: --- examples/README.md | 45 +-- examples/basic_example.ipynb | 547 +++++++++++++++++++++++++++++------ 2 files changed, 484 insertions(+), 108 deletions(-) diff --git a/examples/README.md b/examples/README.md index 060a1c20..0278b059 100644 --- a/examples/README.md +++ b/examples/README.md @@ -1,42 +1,45 @@ # plotly-resampler examples -This directory withholds several examples, highlighting the applicability of +This directory withholds several examples, highlighting the applicability of plotly-resampler for various use cases. -To succesfully run these examples, make sure that you've installed all the requirements by running: + +## Prerequisites + +To successfully run these examples, make sure that you've installed all the [requirements](requirements.txt) by running: ```bash pip install -r requirements.txt ``` -## 0. basic example +## 1. Example notebooks +### 1.1 basic examples -The testing CI/CD of plotly resampler uses _selenium_ and _selenium-wire_ to test the -interactiveness of various figures. All these figures are shown in -the [basic example notebook](basic_example.ipynb) +The [basic example notebook](basic_example.ipynb) covers most use-cases in which plotly resampler will be employed. It is the ideal hands-on starting point for data-scientists who want to use +plotly-resampler in their day-to-day jupyter environments. -### 0.1 Figurewidget example +### 1.2 Figurewidget example The [figurewidget example notebook](figurewidget_example.ipynb) utilizes the `FigureWidgetResampler` wrapper to create a `go.FigureWidget` with dynamic aggregation functionality. A major advantage of this approach is that this does not create a web application, thus not needing to be able to create / forward a network port. -Additionally, this noteb ook highlights how to use the `FigureWidget` its on-click callback to utilize plotly for large **time series annotation**. +Additionally, this notebook highlights how to use the `FigureWidget` its on-click callback to utilize plotly for large **time series annotation**. -## 1. Dash apps +## 2. Dash apps The [dash_apps](dash_apps/) folder contains example dash apps in which `plotly-resampler` is integrated -| | description | -|------------------------------------------------------------------| --- | -| **minimal examples** | | -| [global variable](dash_apps/01_minimal_global.py) | *bad practice*: minimal example in which a global `FigureResampler` variable is used | -| [server side caching](dash_apps/02_minimal_cache.py) | *good practice*: minimal example in which we perform server side caching of the `FigureResampler` variable | -| [runtime graph construction](dash_apps/03_minimal_cache_dynamic.py) | minimal example where graphs are constructed based on user interactions at runtime. [Pattern matching callbacks](https://dash.plotly.com/pattern-matching-callbacks) are used construct these plotly-resampler graphs dynamically. Again, server side caching is performed. | -| **advanced apps** | | -| [dynamic sine generator](dash_apps/11_sine_generator.py) | exponential sine generator which uses [pattern matching callbacks](https://dash.plotly.com/pattern-matching-callbacks) to remove and construct plotly-resampler graphs dynamically | -| [file visualization](dash_apps/12_file_selector.py) | load and visualize multiple `.parquet` files with plotly-resampler | -| [dynamic static graph](dash_apps/13_coarse_fine.py) | Visualization dashboard in which a dynamic (i.e., plotly-resampler graph) and static graph (i.e., go.Figure) are shown (made for [this issue](https://github.com/predict-idlab/plotly-resampler/issues/56)). Relayout events on the coarse graph update the dynamic graph. - -## 2. Other apps +| | description | +|------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| **minimal examples** | | +| [global variable](dash_apps/01_minimal_global.py) | *bad practice*: minimal example in which a global `FigureResampler` variable is used | +| [server side caching](dash_apps/02_minimal_cache.py) | *good practice*: minimal example in which we perform server side caching of the `FigureResampler` variable | +| [runtime graph construction](dash_apps/03_minimal_cache_dynamic.py) | minimal example where graphs are constructed based on user interactions at runtime. [Pattern matching callbacks](https://dash.plotly.com/pattern-matching-callbacks) are used construct these plotly-resampler graphs dynamically. Again, server side caching is performed. | +| **advanced apps** | | +| [dynamic sine generator](dash_apps/11_sine_generator.py) | exponential sine generator which uses [pattern matching callbacks](https://dash.plotly.com/pattern-matching-callbacks) to remove and construct plotly-resampler graphs dynamically | +| [file visualization](dash_apps/12_file_selector.py) | load and visualize multiple `.parquet` files with plotly-resampler | +| [dynamic static graph](dash_apps/13_coarse_fine.py) | Visualization dashboard in which a dynamic (i.e., plotly-resampler graph) and static graph (i.e., go.Figure) are shown (made for [this issue](https://github.com/predict-idlab/plotly-resampler/issues/56)). Graph interaction events on the coarse graph update the dynamic graph. | + +## 3. Other apps The [other_apps](other_apps/) folder contains examples of `plotly-resampler` being *integrated* in other apps / frameworks diff --git a/examples/basic_example.ipynb b/examples/basic_example.ipynb index 5eb7975f..659e5eaf 100644 --- a/examples/basic_example.ipynb +++ b/examples/basic_example.ipynb @@ -8,10 +8,7 @@ "outputs": [], "source": [ "%load_ext autoreload\n", - "%autoreload 2\n", - "\n", - "%load_ext memory_profiler\n", - "%load_ext line_profiler" + "%autoreload 2\n" ] }, { @@ -26,45 +23,419 @@ "\n", "import plotly.graph_objects as go\n", "from plotly.subplots import make_subplots\n", + "from datetime import datetime\n", "\n", "from helper import groupby_consecutive\n", "\n", "import sys\n", - "sys.path.append('..')\n", - "from plotly_resampler import FigureResampler, EveryNthPoint, EfficientLTTB" + "\n", + "sys.path.append(\"..\")\n", + "from plotly_resampler import FigureResampler, FigureWidgetResampler, EveryNthPoint\n" ] }, { "cell_type": "markdown", - "id": "54079ce0", + "id": "45398797", "metadata": {}, "source": [ - "# Basic sine example" + "# Adding dynamic aggregation to your plotly Figure" ] }, { "cell_type": "code", "execution_count": null, - "id": "ef449619", + "id": "9c5d1e2f", "metadata": {}, "outputs": [], "source": [ - "n = 5_000_000\n", + "# Some dummy data that will be used throughout the examples\n", + "n = 2_000_000\n", "x = np.arange(n)\n", - "noisy_sine = (3 + np.sin(x / 2000) + np.random.randn(n) / 10) * x / 1000\n", + "noisy_sine = (3 + np.sin(x / 2000) + np.random.randn(n) / 10) * x / 1000\n" + ] + }, + { + "cell_type": "markdown", + "id": "fa10538c", + "metadata": { + "collapsed": false, + "pycharm": { + "name": "#%%\n" + } + }, + "source": [ + "## **auto mode**: `register_plotly_resampler`" + ] + }, + { + "cell_type": "markdown", + "id": "ffdf4c2b", + "metadata": {}, + "source": [ + "Once `register_plotly_resampler` method is called, it will automatically convert all new defined plotly graph objects into a `FigureResampler` or `FigureWidgetResampler` object. The `mode` parameter of this method allows to define which type of the aforementioned resampling objects is used.\n", + "\n", + "✅ **advantages**:\n", + "* This is the most convenient way to make your existing codebase more scalable\n", + "* You can keep on using your regular graph construction code\n", "\n", + "❌ **disadvantages**:\n", + "* all figures will be wrapped with plotly-resampler behavior (less control)\n", + "* if you want to go the extra mile regarding graph construction performance, it is better to use the `FigureResampler` or `FigureWidgetResampler` components individually." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b81403ca", + "metadata": {}, + "outputs": [], + "source": [ + "from plotly_resampler import register_plotly_resampler, unregister_plotly_resampler\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0371d1f3", + "metadata": {}, + "outputs": [], + "source": [ + "# by default, 1,000 samples per trace are shown\n", + "register_plotly_resampler(mode=\"auto\", default_n_shown_samples=4500)\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0f4656be", + "metadata": {}, + "outputs": [], + "source": [ + "# auto mode: when working in an IPython environment, this will automatically be a\n", + "# FigureWidgetResampler else, this will be an FigureResampler\n", + "fig = go.Figure()\n", + "fig.add_trace({\"y\": noisy_sine + 2, \"name\": \"yp2\", \"showlegend\": True})\n", + "print(type(fig))\n", + "fig\n" + ] + }, + { + "cell_type": "markdown", + "id": "e20add02", + "metadata": {}, + "source": [ + "❗ `.show()` always returns a static html view" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0f4656be", + "metadata": {}, + "outputs": [], + "source": [ + "# this outputs a static html view of the figure, which can be serialized within notebooks\n", + "fig.show()\n" + ] + }, + { + "cell_type": "markdown", + "id": "382d04ec", + "metadata": {}, + "source": [ + "### pro tip: `register_plotly_resampler` + pandas plotting backend = 🔥" + ] + }, + { + "cell_type": "markdown", + "id": "04a5919e", + "metadata": {}, + "source": [ + "The combo below allows to conveniently visualize large time_series data" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "88556465", + "metadata": {}, + "outputs": [], + "source": [ + "register_plotly_resampler(mode=\"auto\", default_n_shown_samples=1500)\n", + "# pd.options.plotting.backend = 'plotly'\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "08dc25ff", + "metadata": {}, + "outputs": [], + "source": [ + "df = pd.DataFrame(data={\"sine\": noisy_sine, \"neg-sine\": -noisy_sine}, copy=False)\n", + "df\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f10cd857", + "metadata": {}, + "outputs": [], + "source": [ + "# TODO: for some reason pandas plotting backend + datetime index work really slow\n", + "# df.index = pd.date_range(start=\"2020-01-01\", periods=len(df), freq=\"1s\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "67251df5", + "metadata": {}, + "outputs": [], + "source": [ + "df.plot(backend=\"plotly\")\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e5c7b3e6", + "metadata": {}, + "outputs": [], + "source": [ + "unregister_plotly_resampler()" + ] + }, + { + "cell_type": "markdown", + "id": "54079ce0", + "metadata": {}, + "source": [ + "## **manual mode**: Basic sine example\n" + ] + }, + { + "cell_type": "markdown", + "id": "3312e7e5", + "metadata": {}, + "source": [ + "\n", + "✅ **advantages**:\n", + "* Highly configurable (e.g., downsampler per trace, number of shown sample per trace)\n", + "* Most optimizable (by leveraging the `hf_` arguments)\n", + "\n", + "❌ **disadvantages**:\n", + "* more tedious (more code needs to be written)\n", + "\n", + "This example shows how the `FigureResampler` can be used to efficiently construct a graph" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ef449619", + "metadata": {}, + "outputs": [], + "source": [ "# 1. Wrap the figure with the FigureResampler class\n", "fig = FigureResampler(go.Figure())\n", - "# Add the trace using the `hf_x` & `hf_y` for faster rendering \n", + "\n", + "# Add the trace using the `hf_x` & `hf_y` for faster rendering\n", "# (this does not impact the resampling speed once rendered)\n", - "fig.add_trace(go.Scattergl(), hf_x=x, hf_y=noisy_sine)\n", + "fig.add_trace(go.Scattergl(showlegend=True), hf_x=x, hf_y=noisy_sine, max_n_samples=300)\n", "# fig.add_trace(go.Scattergl(x=x, y=noisy_sine))\n", "\n", "# Optional: update the layout\n", - "fig.update_layout(height=400, template='plotly_dark')\n", + "fig.update_layout(height=400, template=\"plotly_dark\")\n", "\n", "# 2. Call show_dash\n", - "fig.show_dash(mode='inline')" + "fig.show_dash(mode=\"inline\")\n" + ] + }, + { + "cell_type": "markdown", + "id": "7d828610", + "metadata": {}, + "source": [ + "For the `FigureWidgetResampler` use case, you only need to chance way of displaying the figure" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ef449619", + "metadata": {}, + "outputs": [], + "source": [ + "# Same content as above, but using the FigureWidgetResampler class\n", + "fig = FigureWidgetResampler(go.Figure())\n", + "fig.add_trace(go.Scattergl(showlegend=True), hf_x=x, hf_y=noisy_sine, max_n_samples=900)\n", + "fig.update_layout(height=400, template=\"plotly_dark\")\n", + "\n", + "# Displaying the figure -> instead of `show_dash`, use IPython's `display`\n", + "display(fig)\n" + ] + }, + { + "cell_type": "markdown", + "id": "9bfc2726", + "metadata": {}, + "source": [ + "## Advanced stuff" + ] + }, + { + "cell_type": "markdown", + "id": "b997c178", + "metadata": {}, + "source": [ + "### Adjusting the data of your plotly-resampler figure at runtime" + ] + }, + { + "cell_type": "markdown", + "id": "28f10919", + "metadata": {}, + "source": [ + "The `hf_data` property of the `FigureResampler` / `FigureWidgetResampler` can be used to change figure data at runtime" + ] + }, + { + "cell_type": "markdown", + "id": "d0ce62a4", + "metadata": {}, + "source": [ + "#### `FigureResampler`" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "499ac9f3", + "metadata": {}, + "outputs": [], + "source": [ + "fig = FigureResampler(go.Figure())\n", + "fig.add_trace(go.Scattergl(showlegend=True, name=\"noisy_sine\"), hf_y=noisy_sine)\n", + "display(fig.hf_data)\n", + "fig.show_dash(mode=\"inline\")\n" + ] + }, + { + "cell_type": "markdown", + "id": "6bffa11a", + "metadata": {}, + "source": [ + "now we adjust the figure data \n", + "**note**: after running the cell below, we need to manually trigger a graph update (by for example zooming / resetting the axes) to ensure that the new data is shown." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "94127fae", + "metadata": {}, + "outputs": [], + "source": [ + "fig.hf_data[0][\"y\"] = -10 * noisy_sine\n", + "# make sure to interact win the figure to see the change\n" + ] + }, + { + "cell_type": "markdown", + "id": "784d8464", + "metadata": {}, + "source": [ + "#### `FigureWidgetResampler`" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9a0a0870", + "metadata": {}, + "outputs": [], + "source": [ + "fig = FigureWidgetResampler(go.Figure())\n", + "fig.add_trace(go.Scattergl(showlegend=True, name=\"noisy_sine\"), hf_y=noisy_sine)\n", + "display(fig.hf_data)\n", + "display(fig)\n" + ] + }, + { + "cell_type": "markdown", + "id": "22687274", + "metadata": {}, + "source": [ + "now we adjust the figure data \n", + "**note**: after running the cell below, we need to manually trigger a graph update (by for example zooming / resetting the axes) to ensure that the new data is shown." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "850abc7c", + "metadata": {}, + "outputs": [], + "source": [ + "fig.hf_data[0][\"y\"] = 10 * noisy_sine**2\n" + ] + }, + { + "cell_type": "markdown", + "id": "f4130fac", + "metadata": {}, + "source": [ + "**pro tip**: `FigureWidgetResampler` has the `reload_data` and `reset_axes` methods to do this automatically" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "726f0c08", + "metadata": {}, + "outputs": [], + "source": [ + "fig.hf_data[0][\"y\"] = -10 * noisy_sine**2\n", + "# the reload data function keeps the current zoom level\n", + "fig.reload_data()\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8cbfca3d", + "metadata": {}, + "outputs": [], + "source": [ + "fig.hf_data[0][\"y\"] = 10 * noisy_sine**.1\n", + "# the reset axes\n", + "fig.reset_axes()\n" + ] + }, + { + "cell_type": "markdown", + "id": "500c5303", + "metadata": {}, + "source": [ + "---" + ] + }, + { + "cell_type": "markdown", + "id": "192a74fb", + "metadata": {}, + "source": [ + "# Various other examples" + ] + }, + { + "cell_type": "markdown", + "id": "c7962301", + "metadata": {}, + "source": [ + "The visualizations below shows how you plotly-resampler is used for various visualization configurations." ] }, { @@ -74,7 +445,7 @@ "tags": [] }, "source": [ - "# The example `.gif` from the docs/README" + "## The example `.gif` from the docs/README" ] }, { @@ -82,9 +453,11 @@ "id": "d2f83be8", "metadata": {}, "source": [ - "Note how the example figure withholds\n", - "* time-indexed data\n", - "* numeric-indexed data\n" + "Note how:\n", + "* The example figure withholds both time-indexed data and numeric-indexed data\n", + "* The power-consumption data uses `2,000` samples per trace (`max_n_samples`), whilst the other signals use the default of `1,000` samples per trace.\n", + "* The swimming pool trace uses another downsampling method (`EveryNthPoint`)\n", + "* The swimming pool trace uses `hf_hovertext` which shows the mean # of pool visitors of the last hour. \n" ] }, { @@ -94,11 +467,12 @@ "metadata": {}, "outputs": [], "source": [ + "# ------------ loading the data -----------\n", "df_gusb = pd.read_parquet(\"data/df_gusb.parquet\")\n", "df_data_pc = pd.read_parquet(\"data/df_pc_test.parquet\")\n", "\n", "# Create a noisy sine\n", - "n = 110_000_00#0\n", + "n = 110_000_00 # 0\n", "x = np.arange(n)\n", "noisy_sine = (3 + np.sin(x / 200_000) + np.random.randn(n) / 10) * x / 100_000\n" ] @@ -126,7 +500,7 @@ "\n", "\n", "# ------------ swimming pool data -----------\n", - "df_gusb_pool = df_gusb['zwembad'].last(\"4D\").dropna()\n", + "df_gusb_pool = df_gusb[\"zwembad\"].last(\"4D\").dropna()\n", "fig.add_trace(\n", " go.Scattergl(\n", " x=df_gusb_pool.index,\n", @@ -158,14 +532,12 @@ "df_data_pc = df_data_pc.last(\"190D\")\n", "for i, c in enumerate(df_data_pc.columns):\n", " fig.add_trace(\n", - " go.Scattergl(\n", - " name=f\"R-{i+1}\", #line_width=1,\n", - " ),\n", + " go.Scattergl(name=f\"R-{i+1}\", line_width=1),\n", " hf_x=df_data_pc.index,\n", " hf_y=df_data_pc[c],\n", + " max_n_samples=2_000, # The power consumption data uses 2,000 samples per trace\n", " row=2,\n", " col=1,\n", - " # downsampler=LTTB(interleave_gaps=True),\n", " )\n", "\n", "fig.update_layout(height=650)\n", @@ -177,7 +549,7 @@ " legend=dict(orientation=\"h\", y=1.11, xanchor=\"left\", x=0),\n", ")\n", "\n", - "fig.show_dash(mode=\"external\", debug=True, port=9029)" + "fig.show_dash(mode=\"inline\", debug=True, port=9029)\n" ] }, { @@ -185,7 +557,7 @@ "id": "28b02cff", "metadata": {}, "source": [ - "# Converting a `go.Figure`, with its traces, into a `FigureResampler`" + "## Converting a `go.Figure`, with its traces, into a `FigureResampler`" ] }, { @@ -193,7 +565,18 @@ "id": "20b9cef4", "metadata": {}, "source": [ - "This example first creates the `.gif` figure (with less data, otherwise the graph consruction time would be too long) and then uses the `convert_existing_traces` argument of the FigureResampler constructor to convert this into a FigureResampler figure." + "This example first creates the `.gif` figure (with less data, otherwise the graph construction time would be too long) and then uses the `convert_existing_traces` argument of the FigureResampler constructor to convert this into a FigureResampler figure." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4e239eda", + "metadata": {}, + "outputs": [], + "source": [ + "from plotly_resampler import unregister_plotly_resampler\n", + "unregister_plotly_resampler()" ] }, { @@ -248,7 +631,7 @@ ")\n", "\n", "# ------------- Power consumption data -------------\n", - "df_data_pc = df_data_pc.last(\"190D\")\n", + "df_data_pc = df_data_pc.last(\"50D\")\n", "for i, c in enumerate(df_data_pc.columns):\n", " fig.add_trace(\n", " go.Scattergl(\n", @@ -267,7 +650,7 @@ " title_x=0.5,\n", " legend_traceorder=\"normal\",\n", " legend=dict(orientation=\"h\", y=1.11, xanchor=\"left\", x=0),\n", - ");" + ")\n" ] }, { @@ -285,7 +668,7 @@ "metadata": {}, "outputs": [], "source": [ - "fig.data[1]['x'].shape" + "fig.data[1][\"x\"].shape\n" ] }, { @@ -297,18 +680,7 @@ "source": [ "# Convert the figure into a figurResampler figure by decorating it\n", "fr_fig = FigureResampler(fig, default_n_shown_samples=500, convert_existing_traces=True)\n", - "fr_fig.show_dash(mode=\"external\")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "841a217b", - "metadata": {}, - "outputs": [], - "source": [ - "print('aggregated data ', fr_fig.data[1]['x'].shape)\n", - "print('raw data (hf_data) ', fr_fig.hf_data[1]['x'].shape)" + "fr_fig.show_dash(mode=\"inline\")\n" ] }, { @@ -318,8 +690,11 @@ "metadata": {}, "outputs": [], "source": [ + "print(\"aggregated data \", fr_fig.data[1][\"x\"].shape)\n", + "print(\"raw data (hf_data) \", fr_fig.hf_data[1][\"x\"].shape)\n", + "\n", "# The data-shape of the original figure stil remains the same\n", - "fig.data[1]['x'].shape" + "fig.data[1][\"x\"].shape" ] }, { @@ -339,17 +714,10 @@ "metadata": {}, "outputs": [], "source": [ - "fr_fig.hf_data[1]['y'] = - noisy_sine ** 2" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "15eb48c2", - "metadata": {}, - "outputs": [], - "source": [ - "fr_fig.show_dash(mode='inline')" + "# We flip and take the sqarquere root of the data\n", + "fr_fig.hf_data[1][\"y\"] = -(noisy_sine**2)\n", + "\n", + "# Make sure to interact witht he `Generated sine` subplot to see the changes" ] }, { @@ -359,7 +727,7 @@ "tags": [] }, "source": [ - "# Skin conductance example" + "## Skin conductance example" ] }, { @@ -367,7 +735,9 @@ "id": "2ea4e74a", "metadata": {}, "source": [ - "This example is especially interesting as it uses a _background-color_ to indicate a signal quality.\n" + "This example is especially interesting as it **uses a _background-color_ to indicate a signal quality**.\n", + "\n", + "To ensure the consistency of this background color use; we set `interleave_gaps` to False for the signal quality trace its downsampler.\n" ] }, { @@ -377,7 +747,7 @@ "metadata": {}, "outputs": [], "source": [ - "df_gsr = pd.read_parquet('data/processed_gsr.parquet')" + "df_gsr = pd.read_parquet(\"data/processed_gsr.parquet\")\n" ] }, { @@ -397,7 +767,7 @@ " default_n_shown_samples=1_000,\n", " verbose=False,\n", ")\n", - "fig.update_layout(height=600, title='skin conductance example', title_x=0.5)\n", + "fig.update_layout(height=600, title=\"skin conductance example\", title_x=0.5)\n", "\n", "\n", "# -------------------------------- ROW 1 --------------------------------\n", @@ -419,13 +789,13 @@ " marker_color=\"red\",\n", " name=\"SCR peaks\",\n", " ),\n", - " # Set limit_to_view to true so that the peaks dissapear when out-of view-range \n", + " # Set limit_to_view to true so that the peaks disappear when out-of view-range\n", " # and thus not disturb the autoscale!!!\n", " limit_to_view=True,\n", ")\n", "\n", "\n", - "# Display the Sking conductance Signal Quality As background \n", + "# Display the skin conductance Signal Quality as background\n", "df_grouped = groupby_consecutive(df_gsr[\"EDA_SQI\"])\n", "df_grouped[\"EDA_SQI\"] = df_grouped[\"EDA_SQI\"].map(bool)\n", "df_grouped[\"good_sqi\"] = df_grouped[\"EDA_SQI\"].map(int)\n", @@ -448,9 +818,9 @@ " showlegend=False,\n", " ),\n", " # The most important thing here is that interleave gaps is set to True\n", - " # Additionally, the limit-to-view also ensures that the autoscale is not \n", + " # Additionally, the limit-to-view also ensures that the autoscale is not\n", " # disturbed.\n", - " downsampler=EveryNthPoint(interleave_gaps=False),\n", + " # downsampler=EveryNthPoint(interleave_gaps=False),\n", " limit_to_view=True,\n", " secondary_y=True,\n", " )\n", @@ -466,15 +836,7 @@ " col=1,\n", ")\n", "\n", - "fig.show_dash(mode=\"external\", port=9022)" - ] - }, - { - "cell_type": "markdown", - "id": "9280c129", - "metadata": {}, - "source": [ - "* The most important thing regarding this " + "fig.show_dash(mode=\"inline\", port=9022)\n" ] }, { @@ -484,7 +846,7 @@ "tags": [] }, "source": [ - "# Categorical series - box & histogram" + "## Categorical series - box & histogram" ] }, { @@ -493,7 +855,7 @@ "metadata": {}, "source": [ "This example highlights how `plotly-resampler` supports combining high-frequency trace-subplots \n", - "with non-scatterlike traces such a a histogram & a boxplot." + "with non-scatterlike traces such as a histogram & a boxplot." ] }, { @@ -507,12 +869,15 @@ "cats_list = np.array(list(\"aaaaaaaaaa\" * 1000))\n", "cats_list[np.random.choice(len(cats_list), 100, replace=False)] = \"b\"\n", "cats_list[np.random.choice(len(cats_list), 50, replace=False)] = \"c\"\n", - "cat_series = pd.Series(cats_list, dtype=\"category\",)\n", + "cat_series = pd.Series(\n", + " cats_list,\n", + " dtype=\"category\",\n", + ")\n", "\n", "_nb_samples = 30_000\n", "x = np.arange(_nb_samples).astype(np.uint32)\n", "y = np.sin(x / 300).astype(np.float32) + np.random.randn(_nb_samples) / 5\n", - "float_series = pd.Series(index=x, data=y)" + "float_series = pd.Series(index=x, data=y)\n" ] }, { @@ -539,9 +904,7 @@ ")\n", "\n", "fig.add_trace(go.Box(x=float_series.values, name=\"float_series\"), row=1, col=2)\n", - "fig.add_trace(\n", - " go.Box(x=float_series.values ** 2, name=\"float_series**2\"), row=1, col=2\n", - ")\n", + "fig.add_trace(go.Box(x=float_series.values**2, name=\"float_series**2\"), row=1, col=2)\n", "\n", "# add a not hf-trace\n", "fig.add_trace(\n", @@ -552,18 +915,23 @@ " row=2,\n", " col=1,\n", ")\n", - "fig.show_dash(mode=\"external\", port=9023)" + "fig.show_dash(mode=\"inline\", port=9032)\n" ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "336f4a9f", + "metadata": {}, + "outputs": [], + "source": [] } ], "metadata": { - "interpreter": { - "hash": "dda2a6ba7b442de4a5fc8ab16a06cb056e727f9750f1dde6d7d9fc5541ed6f4e" - }, "kernelspec": { - "display_name": "plotly-resampler", + "display_name": "Python 3.10.5 ('plotly-resampler-iCtYrdpr-py3.10')", "language": "python", - "name": "plotly-resampler" + "name": "python3" }, "language_info": { "codemirror_mode": { @@ -575,9 +943,14 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.8.10" + "version": "3.10.5" }, - "toc-autonumbering": true + "toc-autonumbering": true, + "vscode": { + "interpreter": { + "hash": "c5b971a1081d8490aaab9267c6f84945436f3f61058bbf7fd4d457e74012adc4" + } + } }, "nbformat": 4, "nbformat_minor": 5 From e6fc5dfad89ae474fef3060117d212d0e6ef6911 Mon Sep 17 00:00:00 2001 From: jonas Date: Mon, 18 Jul 2022 17:41:04 +0200 Subject: [PATCH 09/49] :apple: improving docs + examples --- README.md | 1 + examples/basic_example.ipynb | 84 ++++++++++++++++--- .../aggregation/algorithms/lttbcv2.c | 5 +- 3 files changed, 78 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 29fbcd22..5a7a5041 100644 --- a/README.md +++ b/README.md @@ -158,6 +158,7 @@ In [this Plotly-Resampler demo](https://github.com/predict-idlab/plotly-resample * When running the code on a server, you should forward the port of the `FigureResampler.show_dash()` method to your local machine.
**Note** that you can add dynamic aggregation to plotly figures with the `FigureWidgetResampler` wrapper without needing to forward a port! +* The `FigureWidgetResampler` *uses the python main thread* for its data aggregation functionality, so when this main thread is occupied, no resampling logic can be executed. For example; if you perform long computations within your notebook, the kernel will be occupied during these computations, and will only execute the resampling operations which take place during these computations after finishing that computation. * In general, when using downsampling one should be aware of (possible) [aliasing](https://en.wikipedia.org/wiki/Aliasing) effects. The [R] in the legend indicates when the corresponding trace is being resampled (and thus possibly distorted) or not. Additionally, the `~` suffix represent the mean aggregation bin size in terms of the sequence index. * The plotly **autoscale** event (triggered by the autoscale button or a double-click within the graph), **does not reset the axes but autoscales the current graph-view** of plotly-resampler figures. This design choice was made as it seemed more intuitive for the developers to support this behavior with double-click than the default axes-reset behavior. The graph axes can ofcourse be resetted by using the `reset_axis` button. If you want to give feedback and discuss this further with the developers, see issue [#49](https://github.com/predict-idlab/plotly-resampler/issues/49). diff --git a/examples/basic_example.ipynb b/examples/basic_example.ipynb index 659e5eaf..867ce159 100644 --- a/examples/basic_example.ipynb +++ b/examples/basic_example.ipynb @@ -51,7 +51,7 @@ "# Some dummy data that will be used throughout the examples\n", "n = 2_000_000\n", "x = np.arange(n)\n", - "noisy_sine = (3 + np.sin(x / 2000) + np.random.randn(n) / 10) * x / 1000\n" + "noisy_sine = (3 + np.sin(x / 2000) + np.random.randn(n) / 10) * x / (n / 4)\n" ] }, { @@ -114,7 +114,12 @@ "# auto mode: when working in an IPython environment, this will automatically be a\n", "# FigureWidgetResampler else, this will be an FigureResampler\n", "fig = go.Figure()\n", - "fig.add_trace({\"y\": noisy_sine + 2, \"name\": \"yp2\", \"showlegend\": True})\n", + "fig.add_traces(\n", + " [\n", + " {\"y\": noisy_sine + 2, \"name\": \"yp2\", \"type\": \"scattergl\"},\n", + " {'y': noisy_sine - 3, 'name': 'ym1', \"type\": \"scatter\"},\n", + " ]\n", + ")\n", "print(type(fig))\n", "fig\n" ] @@ -243,8 +248,9 @@ "\n", "# Add the trace using the `hf_x` & `hf_y` for faster rendering\n", "# (this does not impact the resampling speed once rendered)\n", - "fig.add_trace(go.Scattergl(showlegend=True), hf_x=x, hf_y=noisy_sine, max_n_samples=300)\n", - "# fig.add_trace(go.Scattergl(x=x, y=noisy_sine))\n", + "fig.add_trace(go.Scattergl(showlegend=True), hf_x=x, hf_y=noisy_sine, max_n_samples=100)\n", + "# Note how a dict input is also valid and how different # of samples per trace are used\n", + "fig.add_trace(dict(x=x, y=noisy_sine + 1, name='sp1'), max_n_samples=2000)\n", "\n", "# Optional: update the layout\n", "fig.update_layout(height=400, template=\"plotly_dark\")\n", @@ -261,6 +267,23 @@ "For the `FigureWidgetResampler` use case, you only need to chance way of displaying the figure" ] }, + { + "cell_type": "code", + "execution_count": null, + "id": "ef449619", + "metadata": {}, + "outputs": [], + "source": [ + "# Same content as above, but using the FigureWidgetResampler class\n", + "fig = FigureWidgetResampler(go.Figure())\n", + "fig.add_trace(go.Scattergl(showlegend=True), hf_x=x, hf_y=noisy_sine, max_n_samples=200)\n", + "fig.add_trace(dict(x=x, y=noisy_sine + 1, name='sp1'), max_n_samples=2000)\n", + "fig.update_layout(height=400, template=\"plotly_dark\")\n", + "\n", + "# Displaying the figure -> instead of `show_dash`, use IPython's `display`\n", + "display(fig)\n" + ] + }, { "cell_type": "code", "execution_count": null, @@ -282,7 +305,7 @@ "id": "9bfc2726", "metadata": {}, "source": [ - "## Advanced stuff" + "# Advanced stuff" ] }, { @@ -290,7 +313,7 @@ "id": "b997c178", "metadata": {}, "source": [ - "### Adjusting the data of your plotly-resampler figure at runtime" + "## Adjusting the data of your plotly-resampler figure at runtime" ] }, { @@ -306,7 +329,7 @@ "id": "d0ce62a4", "metadata": {}, "source": [ - "#### `FigureResampler`" + "### `FigureResampler`" ] }, { @@ -347,7 +370,7 @@ "id": "784d8464", "metadata": {}, "source": [ - "#### `FigureWidgetResampler`" + "### `FigureWidgetResampler`" ] }, { @@ -422,6 +445,47 @@ "---" ] }, + { + "cell_type": "markdown", + "id": "0846eaa3", + "metadata": {}, + "source": [ + "## Different downsampler & number of shown samples per trace" + ] + }, + { + "cell_type": "markdown", + "id": "2c0f1aff", + "metadata": {}, + "source": [ + "To achieve this, you only need to adjust the `max_n_samples` per trace; see the example below" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "193e26f5", + "metadata": {}, + "outputs": [], + "source": [ + "fig = FigureResampler(go.Figure())\n", + "\n", + "fig.add_trace(go.Scattergl(showlegend=True), hf_x=x, hf_y=noisy_sine, max_n_samples=100)\n", + "fig.add_trace(dict(x=x, y=noisy_sine + 1, name=\"sp1\"), max_n_samples=2000)\n", + "fig.add_trace(\n", + " dict(x=x, y=noisy_sine + 5, name=\"sp5\"),\n", + " max_n_samples=300,\n", + " downsampler=EveryNthPoint(),\n", + ")\n", + "\n", + "\n", + "# Optional: update the layout\n", + "fig.update_layout(height=400, template=\"plotly_dark\")\n", + "\n", + "# 2. Call show_dash\n", + "fig.show_dash(mode=\"inline\")\n" + ] + }, { "cell_type": "markdown", "id": "192a74fb", @@ -521,7 +585,7 @@ "\n", "# ----------------- generated sine -----------\n", "fig.add_trace(\n", - " go.Scattergl(name=\"sin\", line_color=\"#26b2e0\"),\n", + " dict(name=\"sin\", line_color=\"#26b2e0\"),\n", " hf_x=x,\n", " hf_y=noisy_sine,\n", " row=1,\n", @@ -829,7 +893,7 @@ "# -------------------------------- ROW 2 --------------------------------\n", "# show the phasic EDA component\n", "fig.add_trace(\n", - " go.Scattergl(name=\"EDA_Phasic\", visible=\"legendonly\"),\n", + " {\"name\": \"EDA_Phasic\", \"visible\": \"legendonly\", \"type\": \"scattergl\"},\n", " hf_x=df_gsr.index,\n", " hf_y=df_gsr[\"EDA_Phasic\"],\n", " row=2,\n", diff --git a/plotly_resampler/aggregation/algorithms/lttbcv2.c b/plotly_resampler/aggregation/algorithms/lttbcv2.c index 5eb7e9c9..ef9d9777 100644 --- a/plotly_resampler/aggregation/algorithms/lttbcv2.c +++ b/plotly_resampler/aggregation/algorithms/lttbcv2.c @@ -6,9 +6,10 @@ #include // This code is adapted from https://github.com/dgoeries/lttbc -// Most credits are due to https://github.com/dgoeries +// and implement the LTTB algorithm, intially described by Svein Steinarsson here +// https://skemman.is/bitstream/1946/15343/3/SS_MSthesis.pdf -// method below assumes the x-delta's are equidistant +// This method only returns the index positions of the selected points. static PyObject* downsample_return_index(PyObject *self, PyObject *args) { int threshold; PyObject *x_obj = NULL, *y_obj = NULL; From 03cf4bd718db6eeab168cbcd8e6504bd9ae12455 Mon Sep 17 00:00:00 2001 From: jonas Date: Mon, 18 Jul 2022 19:10:39 +0200 Subject: [PATCH 10/49] :fire: updating docs --- docs/sphinx/FAQ.rst | 68 ++++++++++++++++++++++++++++ docs/sphinx/dash_app_integration.rst | 15 +++--- docs/sphinx/getting_started.rst | 6 +-- docs/sphinx/index.rst | 2 +- 4 files changed, 81 insertions(+), 10 deletions(-) create mode 100644 docs/sphinx/FAQ.rst diff --git a/docs/sphinx/FAQ.rst b/docs/sphinx/FAQ.rst new file mode 100644 index 00000000..a654f52d --- /dev/null +++ b/docs/sphinx/FAQ.rst @@ -0,0 +1,68 @@ +.. role:: raw-html(raw) + :format: html + +.. |br| raw:: html + +
+ + +FAQ ❓ +====== + +.. raw:: html + +
+ + What does the orange ~ time|number suffix in legend name indicate? + +
+ + +This tilde suffix is only shown when the data is aggregated and represents the *mean aggregation bin size* which is the mean index-range difference between two consecutive aggregated samples. + + * for *time-indexed data*: the mean time-range which is span between 2 consecutive samples. + * for *numeric-indexed data*: the mean numeric range which is span between 2 consecutive samples. + +When the index is a range-index; the *mean aggregation bin size* represents the *mean* downsample ratio; i.e., the mean number of samples that are aggregated into one sample. + +.. raw:: html + +
+
+
+
+ + What is the difference between plotly-resampler figures and plain plotly figures? + +
+ +plotly-resampler can be thought of as wrapper around plain plotly figures which adds line-chart visualization scalability by dynamically aggregating the data of the figures w.r.t. the front-end view. plotly-resampler thus adds dynamic aggregation functionality to plain plotly figures. + +**important to know**: + +* ``show`` *always* returns a static html view of the figure, i.e., no dynamic aggregation can be performed on that view. +* To have dynamic aggregation: + + * with ``FigureResampler``, you need to call ``show_dash`` (or output the object in a cell via ``IPython.display``) -> which spawns a dash-web app, and the dynamic aggregation is realized with dash callback + * with ``FigureWidgetResampler``, you need to use ``IPython.display`` on the object, which uses widget-events to realize dynamic aggregation. + +.. raw:: html + +
+
+
+
+ + What does TraceUpdater do? + +
+ +The ``TraceUpdater`` class is a custom dash component that aids ``dcc.Graph`` components to efficiently sent and update (in our case aggregated) data to the front-end. + +For more information on how to use the trace-updater component together with the ``FigureResampler``, see our dash app `examples `_` and look at the `trace-updater `_ its documentation. + +.. raw:: html + +
+
+
\ No newline at end of file diff --git a/docs/sphinx/dash_app_integration.rst b/docs/sphinx/dash_app_integration.rst index 2a71c88c..4feb36df 100644 --- a/docs/sphinx/dash_app_integration.rst +++ b/docs/sphinx/dash_app_integration.rst @@ -7,8 +7,8 @@ -Dash integration 🤝 -=================== +Dash apps 🤝 +============ This documentation page describes how you can integrate ``plotly-resampler`` in a `dash `_ application. @@ -48,16 +48,19 @@ When you add a :class:`FigureResampler `_, we provide several dash app examples where we perform server side caching of such figures. + + .. tip:: You can make the resampling faster by ensuring the `TraceUpdater `_ its ``sequentialUpdate`` argument is set to ``False``. - -* `This TraceUpdater-example `_ serves as a minimal working example. - - Limitations ----------- ``plotly_resampler`` relies on `TraceUpdater `_ to ensure that the *updateData* is sent diff --git a/docs/sphinx/getting_started.rst b/docs/sphinx/getting_started.rst index 1bc94462..6ada4350 100644 --- a/docs/sphinx/getting_started.rst +++ b/docs/sphinx/getting_started.rst @@ -1,8 +1,8 @@ .. role:: raw-html(raw) :format: html -Getting started 🚀 -================== +Get started 🚀 +============== ``plotly-resampler`` serves two main **modules**: @@ -162,7 +162,7 @@ Working example ⬇️: .. tip:: The ``FigureWidgetResampler`` graph will not be automatically redrawn after - adjusting the fig its `hf_data` property,. The redrawning can be triggered by + adjusting the fig its `hf_data` property,. The redrawing can be triggered by manually calling either: * :func:`FigureWidgetResampler.reload_data `, which keeps the current-graph range. diff --git a/docs/sphinx/index.rst b/docs/sphinx/index.rst index f5821a22..0788302d 100644 --- a/docs/sphinx/index.rst +++ b/docs/sphinx/index.rst @@ -40,8 +40,8 @@ As shown in the demo above, ``plotly-resampler`` maintains its interactiveness o getting_started dash_app_integration - api_reference + FAQ From b6e5400b6e502a0171e5b731e80f8412d9302df5 Mon Sep 17 00:00:00 2001 From: jonas Date: Tue, 19 Jul 2022 17:54:02 +0200 Subject: [PATCH 11/49] :icecream: docs Co-authored-by: jvdd boebievdd@gmail.com --- docs/sphinx/FAQ.rst | 64 +++++++++++++++++++++++ docs/sphinx/_static/annotate_twitter.gif | Bin 0 -> 3873821 bytes docs/sphinx/_static/datashader.png | Bin 0 -> 70905 bytes docs/sphinx/getting_started.rst | 11 ++++ 4 files changed, 75 insertions(+) create mode 100644 docs/sphinx/_static/annotate_twitter.gif create mode 100644 docs/sphinx/_static/datashader.png diff --git a/docs/sphinx/FAQ.rst b/docs/sphinx/FAQ.rst index a654f52d..5b6b529b 100644 --- a/docs/sphinx/FAQ.rst +++ b/docs/sphinx/FAQ.rst @@ -61,6 +61,70 @@ The ``TraceUpdater`` class is a custom dash component that aids ``dcc.Graph`` co For more information on how to use the trace-updater component together with the ``FigureResampler``, see our dash app `examples `_` and look at the `trace-updater `_ its documentation. +.. raw:: html + + + +
+
+ + What is the difference in approach between plotly-resampler and datashader? + +
+ + +`Datashader `_ is a highly scalable `open-source `_ library for analyzing and visualizing large datasets. More specifically, datashader *“rasterizes”* or *“aggregates”* datasets into regular grids that can be analyzed further or viewed as **images**. + + +**The main differences are**: + +Datashader is able deal with various kinds of data (e.g., location related data, point clouds, ...), and plotly-resampler is more tailored towards time-series data visualizations. +Furthermore, datashader outputs a **rasterized image/array** encompassing all traces their data, whereas plotly-resampler outputs an **aggregated series** per trace. Thus, datashader is more suited for analyzing data where you do not want to pin-out a certain series/trace. + +In our opinion, datashader truly shines (for the time series use case) when: + +* you want a global, overlaying view of all your traces +* you want to visualize a large number of time series in a single plot (many traces) +* there is a lot of noise on your high-frequency data and want to uncover the underlying pattern +* you want to render all data points in your visualization + +In our opinion, plotly-resampler shines when: + +* you need the capabilities to interact with the traces (e.g., hovering, toggling traces, hovertext pet trace) +* you want to use a less complex (but more restricted) visualization interface (as opposed to holoviews), i.e., plotly +* you want to make existing plotly time-series figures more scalable and efficient +* to build scalable Dash apps for time-series data visualization + +Furthermore combined with holoviews, datashader can also be employed in an interactive manner, see the example below. + +.. code:: python + + from holoviews.operation.datashader import datashade + import datashader as ds + import holoviews as hv + import numpy as np + import pandas as pd + import panel as pn + + hv.extension("bokeh") + pn.extension(comms='ipywidgets') + + # Create the dummy dataframe + n = 1_000_000 + x = np.arange(n) + noisy_sine = (np.sin(x / 3_000) + (np.random.randn(n) / 10)) * x / 5_000 + df = pd.DataFrame( + {"ns": noisy_sine, "ns_abs": np.abs(noisy_sine),} + ) + + # Visualize interactively with datashader + opts = hv.opts.RGB(width=800, height=400) + ndoverlay = hv.NdOverlay({c:hv.Curve((df.index, df[c])) for c in df.columns}) + datashade(ndoverlay, cnorm='linear', aggregator=ds.count(), line_width=3).opts(opts) + +.. image:: _static/datashader.png + + .. raw:: html
diff --git a/docs/sphinx/_static/annotate_twitter.gif b/docs/sphinx/_static/annotate_twitter.gif new file mode 100644 index 0000000000000000000000000000000000000000..228c72f55def1ba0ac935b578cd9ea67fe49fdcb GIT binary patch literal 3873821 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

f_x=aC-Lq;T{ zjKU4$P~?!FDtsJ{y5%`yr7l+2b~y{9=>~AKYiNX7qElIW@zpwPVX9WCED5F{4tuj1 z(~@sb8^+26ZifkrEKAan*X2AKW-}I___HcF)P?Sqj3ixqrQ(ymXDxD`D`Yib>9u@f zOhu~YZoHg}8Y^R?sGcv5P0fuiJK`Da7SsM!!&`XDAUpYD+78N5_#M;6EK}FWNdDB45s z4pSk8NoA>*FE4#mFti_q6KXvB78-KeVkywiu3liEmt)CRe)qnK?(HQLAAVtx-bDtGI!Rp#e2 zwm>fP;&+O!vKt3%na?ZS>0;m$)D|4tG5GSi#E^#Y31|3@(3K>ob`rfCr`Ma8zSMGW zDT(mOXa!%%KkX$YYV@@h-Br)zOCH#-3xJ4P4`sh#?a*m>RNy)TCS3MhYEO0y@!pw5 z-(xMim<+XLGlM6u-508Fs4F8V{?k)A#6bVigFijvHPh6{YV}t|j=ClwGgfxQsqTG~ zfO^8DGlpw+H;(c>;N7Q*TH!VIKvMrsTIQpnm^;m?-}o5l=_khIHa(w%4`?b)IU)|u z`H^~uaQ3gNQ(v0DMP<%)@V+7dzh3%gJ=T{-`C)$c+omhV$Lrdr*ldIt7D2pJwWq-4Dax@%gT8Jv6j#A2C6bNqf_yp>?{%D56#- z;q9cU1f4F7HW56{_z2IFF)NVNsqwHszU+D^WAk{U=2Gfu7vf?iw^w4u{3cKTEsDPN zxqUsshZAP%fRxE!^CkB(-BRxy%FOT1H&t5}IHlB~X~DmS6&|-dReHL7hRr4k~`!)K>pPIx5i4a1(5pmP0zlh>cQs@=ckdNJ5*&F?;z44>pE{;SA;c) z<>{iIg7+UT^FC*2SpTHZcbEE=*f<}QQ4+u)Nc(_K8zZTUC|K&f&&s?ptyL(t+p@#5 zwhI}&%-A{)zOEOo?!JkC)0<%(`9Y{C=hdwGH}tFekCYnH!%rI063P5i)MH{tepC4- ze3bDrN{M|9u`HwfT9K;j-?AHB}*n<2}>wi@wLE8}3@$ z%-_I+WsJhULOOd%@R(Jhl-t6O57z;j0riC8ENwv?m%Nb(J3AA8j5#CPE7XGhYq-9vV!(3t*7i zCAhVIQMCOvHH#(>>fuzeny$$im-!Gr*l6PzV&&H3i(%f5j@!EXsgobSKR z*8chU`NWb@>NsZh9ktp`dfQu>32W`^SGTP<57e({MO*~EL!L+QUcU$>th#5446!~6 zA<7nmX2aHhhZ2xu-&4h?oJ4Wd{a_hkuB0zd$pT~Uh26aqP8}=A;Y4QX7|t!ld{30b z$v#j(Dx!NbT-+~03l*Lo9q3USLDwn5luat{^i;8mN~McQY2%67M@p^Oi2bF=JYNAh zzn88!QMz3MlCh*leo=I3QPlfU9;nD~ccOnOMU&Y^Tk;4x5t6!QNAKB10hgj-wp{9W zp12c+D6__#qWoWM__!Q-g#u!p#;USMV}gc5qY1sj{RAA46!cxOvFs84%b>SZ zdZ`zbXH8VWYUG0>G_g*k0fd6_Rl?ctNejA!(%}@vRn$q@g0VP`)~NW}D)O1^xDC;G{PRSVQj!3B(jpCokCecM zlQvr{9VbG7%_zQ;N4=>s2&R&}nQe7nFJ!tTIsHiz<(-t1^Mn!4WL-b@A5~1hHo`|g zkiU^inRuO|MjFSkA3)fZa;%6{V@(kuM;;d=Phu%xaROg>TuI{Yk@+)0X@w}eMSC~Y zn5!v>eDBdrkblimqnAdpx2DFD`$sEbLS^<-u@jqOJZaL3X>7%5&ur6T#6kwb(xisd z_~Y)qHKiNkf+=q@KEF`otfo}cpkt?H(7P0P8>(thEu}>($mmRIc1i!}Sk1_v-`Xsl zQzVmeCF6;-0QM!-U0TAWn$pu*%7Bp9dz$Hmh7?{_R>ywYdsHgA1!O_Q8+gn>M*XBI zS~KV}bGb_>VKZXrTQ-T8Ug~sqwv$i>k-*Dt;hal>?R$cG&N&IgU~#=nDpG9pj){#k z^et|BH7(ggqLbFtqX#a0+TuU zT+R7@7hwx#WZ*8IuDIv@_Hm*Z&jQk=oJ^7Yhr{8F-sLY|Q#8SEZ zV!!aQXR-99sAW7k?Uk*052dwvx?=!S^wUyz&x%zt1COx^Ym|+!SVaY@jLxoHJD&8p zCM&*1C9<_rjkSm_tP(Rf68e(246aaxEzN}nNO6O#zKSM?01C@1S%s^;NDgv8iv|$W zGLUPsr)x?UUs)1_7rh?|hzU@wcA>5KM5P$Ym^7nOYff8lE?5*mzJspj!po+O@M(3S zy#7_|pG!0FNkps1RWO6LIUuaP2R2e8V1^JY1qr^2zZbuyGguAFjPKdPOu+=k?w8d(XC6@-a%r znipznH88P?R=4K3qfpqiFcE5xT=^vNn3wFAIjJyx`>k+0zNa1v-eK~Ggpv=&l>j5o zCD(W1%qljUpn#j&LMMe`2Est?vsS7*?a!8=8%-cp1(?{L5z2?pUa08uxaKJjKC-adZ!5M)xge?_)` z3j-thE==;Xsefm?fA>%S9{s?9?7)%5zUHT?mh$mc6$90=Jb%ZB%&c?OT6FQ6Ix5!R6Z6IQ96610Tdd~ut zu^c@>OvMIGMfFV`*aXVg31H~Nm9)oTsIkE_-1fb(Eu3+GEtt0(&=}ci1P7w!W{fR? zMf@|ReKTI#lMUB1O*;TI@k|lJjFa3Cmm7Vp{Y&Go`{-?+Kucqs$$D*qKM zc>H_fq8;&kWZjgw<$N6f{JQ0o+%?Iz8|dke-mz*;2O4F_lj2TvlwRmjd-1h_L4gysi6ECAQo0Pa2OZ$JRtg~8nj z@Pm4g#{E|c!Qe(@=N*B?+8|SCxWwqm?+MlkcOF1>0nJv-Fw;63q`) zvw*Scz+MYOZiQb@k&X3xzviij9cV9iJaP~#q`9kT~%RRDaJ1@FMN>)<4ldyA?=pkD~$IJ!05 zg3h`jNIM+7vq)rsgD^yZyWR|Fe})|2fZSMsp98nMVc0KdcO#g1Eg0OoIN*x&rd1fc zySGdgv}+o?+M)wYz7NeBBOELvj33gW7S_$Wugmawp7!xPvH(K;dGmh0_1T%vSrar- zVM~a#TRCj2v2;syYfA_LDPvg%b%1sdZ}YSPW@wOcepP4xuHG5Q!v@e_4{B_BtG2l2 zwg-XNfp$VcaV~&bG~{>ihUy+@M*&c`2iZX!sUg5C!LN=U1JC4u-AJ%147h~=8zMVB zhOl4FyWLRGLmi+gvI^slAbykt+SZCyB!P;3}d1U%l>o2EbHeoQ+00f=oPv?B0+_jRgW$ zhZsVE*Z0>K^VZ>KAX)j9D&ch|HK0CxJK_ue{Q_{vJY)xFJQ%YU$Gd0@n|#~{Fb&;) z_>wq~Y2Cmb;4R<%vH@gt2UGD?i3c%<2z^bPA$}F?`2;#<}4O1M%nE;U58XOmJ zoHKA7kHDOuF~>_RpAM1VsNMIo&<9lR`+V;E%X`NO3g3Sokrc~AxWAArDQqSvoE(-x z8`;PU+gf_U?$9*p-Q^P#m4~I}!}^E7RtQM=*=fNXfHHsm7Dl4+c}>p-pp|s)hK8Kp z2fCvnD>?w5XMoAocc11VKXd?|FL!HjfH;-AyYLU4&sLS;8?FP}WehVO$lag83k@ts zTd*^;&|NECXc}MtrU3AAVc7@zswVWE7UGr60;Ev^fW8KWxdX=BF6(fBro!unI6yY* zZyc{QvLe47Dv$C%`bMd{-^~0+;(>170Q5vycddyq(FXE;4=VHOkI?O(EQKEuB36_b zVOU&mIqw=g>--1qFayvQv^98BAxs#71C2O4#ono*kAL+>fp*V6>>`f*1Gfu8FXHHc z;yNFm-Tn&2TplHV?reGsEdZv;@8Kwxt%(@!z!|-L`PM0Tj2X;xN_f}DEIZ-O-aw$H zz%x@ibN_LjZ4r+u`0A$>4T}aIJ!8*W%JF^DDCG*_obV`!nxuBqkxa!BE1!E~GJM#p zmYu@nXe_R`%xSBqin`9qMqDDdMC))OZo9|njd+6|ZIa`dFUrt4oBeD5N6eU({mf*TVDQX2D5JtGZ_L}OyU<04`&PRhhPWl%zrq& zF4qYu6gm9ivKA58A02!lVf%E>?d|i-`E-`23+kv`3BK%B`&Lkm=*UD~4U4pf*LSXz z?HUvPM24k`vrJNUhR6OV4jkal#6z)%dgGe*+!2k*q<3oSmz9%@YNXm8L={i)Om@)c z@PK%Jq&=iGw7P?JU5WQZ<>9vbkArv_*h+ZVg6zgi?-84`fe8sB56ZvOtf&rE6vn&M zR0-PZq3YH)Yib)u=xX_?D|5K&tI9_7aw+SlJy9wZ=LAB&6S+@RS_Fysn}!#|$O_g# z5#b_}6=R}WGe@+0 zN)xC7q+>uuU@$AOzKnD z0l0jyOG+Ww{BYEWVK_zQ&<|+KB-lOV3FLGx1qESDNn7 zX}0h2(I~1v2QzfebMvJ2&P|CkO_LiAeiF6NKP|R#+!bAR2|~&Ix=Hsi1SqKx&xtH* z?d~VpZ*?Ur(s&G|RXGe3P2Xj+=iA_J+wX*SI}%#x^U9u>VwRq87$+;F(BDI&M@-zi z6bV6gwgnBn7O`C_Pp(f#w+46u8s5dSC;N{RqSXRM3GdR?8B%)m9Y-IxyjiSp<7>hR zpl=d#Fazbw{CwivGP`W@Pf{7kF^zd{qdyK^HDP6M6pee$@?#&*B67^l0sg5XEuoDe(9%- zv8_meZvP6A72+Al~fphnU?n{1lsgNR5Kc6$X9AlOhEGp;B?FDi#5JQ;z}( zw9yn+#Sk{aWQ1d!s(8(QhG(p>nk6X#k#_=3P1Jm$mBesai^ zieT8KEo^7TVEVHV%6ESYjjFR9ja~bx1r2y+-x`3#O8b?o@321L2V7Nrx}-; z5XKxPc#2|k{d>d)eg|}m---fTnj*R2XRO*o*5pl3fZUczDZzCaQBk%4)-%$B8BO!QJmn?Fex+Rl@jX5CNhhtWagR(#u>LK>MC2> z`~u;^uk#Z*2BvelpD<(Ol%bva!t!@L9H66i!P6fK_W=F}lVZrjeIVH-?fm0S` zW;4pvVM#4G-?Nzz&(8Q2enR2BzSfgU)KISS%*9BHOX~4NrO<|}^@)hZ*6Zg?o#W5k zeQUQjzpQl*0^Jc&vX%!29Za_$>pk9zK%KrITd8 z%!g=J0nR7mtY>%ItB@crdeCChKFM^BqLcmb3SUQ)5n*2BueeoE;*b}?VyTI-zf zZSd`PwfeoaK8XLkDeAtP9rgAGBJ;K8Itp^e6ew{5f7YrJO!8FATzEBFOc&xE^Ux=; zVsVkcuICR)h{BVd4P)&GE&*~rc8~b!J_|y`6}$=jwhfv0zliq}c@vIQ$v!`w(C!B= z2!}r6vZIdpWk)7|7Ds8B_toliuj2ffiqK94tkQ2nqQU;DsYre5(y#Jv4q5*M^SAVm zzIJ=RK&S;yPrdGF6^~{7KX0Dk;DGR2{?|s6l>7hJ&6EGwX!=)SkPK4#mkBJ?{u@wM z>Tm4YnqTpEJh+)^?Jmi?quc|wzY2rjt^O(u?gni-9WP-`VD5pr&L^vX6$T%lOZH zgAc8j#wQww6h=yCJ=aIN#|Ow4lw^*Y9PcAJQLGsi?-D6=olG3dOfP09k!q&qXKoT@lz57O*qt8Kn5Lr17zwE= zzM91smN_sTf2=BdNXZoA7Y}-pEUZXyw-(c=N?EJe8gTT!%n{^l846yb`}82ns0Z#W zy4Asfmh}4QQgxC}^S+7sXAOC?TZ41CLgm8~WF?12oO%N_?G>NWP7R$!Wcj~LVE?sw z^1qwFxUtyvA11K>SFr1UF@gOPyY{Cxn!lK*TDOXML$UF9VURH`>DQK7HP!^CDN-u1 z4sx z>ClxIsQ_QEdK)wEw; zT9q_nb~YRTy$S5rhx;Fe!T%Ny2AQ9QyO{y-Nh^nA6%K=-cZwBcrm*(|q|FALh$yLL zB#gf#id&qPi1M5aS04~9LXj;3w;Kst6lTY;&F#NNNO5QNoiVFXu&v>Hj%tZV#%dnX z*yD?i^Uy)#jsI?*s1%3R*uVLB@D_ePz!j^<*SToNzCf7DbIl&*=95le-l>GH6G`@) zWB0FUN|XI0;`Z{I-E+^WP=WUmQd5kBZM{O>QrZJ`B2sGf1xKTzz8RGPFkTmS7P481 zN-@}De`ty-#NTwlC#qrFeJ>@VCsY683RG(Q`lEaXR_FbvFF8ycBz4N(j+ce%8`P0OG9i9PvFtbM<~1s(;n z3`TOLK`;~c@ zb0=!cb-OFw>|B}^rSYY{oMY8B-0$&TB#v5T&vc{JzKk*R`NDNlz-(dQyc8Lh3? zn&mZT5jARSbTqRI;C2xQO#Q?|EsBjoct_}dZLeE`;#Tf+0W05Z4>jKMkLtfVU*2`M zW=bG?)8blt{+-V&tYRllPH_XJ&9uJ4T1C>X;Xf8>~6I`Pg3-50>Owaqcf4SwnvUhz|bf-UE zmVC?J>q|mjr{7@@M}T}1{*O`;o#R2Hir3i-Z58+a5};fyqZbZK>ecNllg#-mt<0;H z7@Lh+uWZv^wg=iJNth4LuNjZF*DaTxJO-pSHR}W^A{@@&qO0{EhM3pMD!cRpEGdU97sdD=6eJj zk^=kO8Zcc`7_~3y@wJhMCuz9TCJSEe9ep9ZA>DH~WX>6pLGQ_{Tm9X4n{rdq_mZv% zUTuGO$gi#aX!qmNGJ2u#0;GTJWqTEnUpUA}-)s1@B7fC8+qX2O@+aVi=OEHbT%V~T&O?IgzIU_Xm-E}@-w`BDR>Yq#U!OcX^AP!NoyiiQS-LAwbMNNS z7pshoSti$9g0^TP0l~5`Cf2q=P#)w1n6rg3B6To-df(SYBm0lTf?C4&(g&#H2h72P zZ|BZGV+w9ceQ$qRlM7uqrjD<5;Dbzg!6=%Jw}Ccr#(;WG`;I2bHD1|$!`&H zQ*q|0d%_z}ama0%^djv3Dk%yW((aCh8f!cjd0@0S>p{z zN!HPz?-YPhZde))(X9e3}4U2Uh;XOq$Kzo5_-G{Ws^?j&`3S;i{;EoC>tciKV3a1Jy(i|G8J}>BnkNu5bi{3tbo1L0!?!= zOj&@nO^|PUP#fAzJLyatGm;qfOebtSxI5ErGt-?m^HPE2>lyS6LF|UiFoOXf?WM_U zV5e@ee26GqIy=%VJK8@xoG3dqE_(zHIrD>E!AX>g6FhA*eklPaFF;!e;{45=Z5YWn zVUiN*+%mJ=QZtfEmfW9BxpkYl4VSr1w0S>ma@#cWu(#gth`et9yuR+dfw;U@qWoP1 zq+l~=0+BzJlRv3J{6QGHa7nUiqX{A{STQSj-3?pYWRb+IL_Pa}i%V+KjfT_-7ao}v zzV|Qum{a(vyYO_g@XKZ48Ep~tJpXnt|3@6D+D9hZF6o6c5+WMS?mZt}8++Y7fc6l; zGXo9EMc*ROgt=(#MYJ{?P2qxu1{C9Z7E{uh5SXJqjfz=7CAg#|z$+3CnG%lN61q(Q zo{K#*s08o{K>ieV053hTDLOo3mdq_8UxJ=B7v&w|Vj+{ZFz`-15H1WnnnDHnCK{cU z8NoCqf0gO;YCP&GA?PVFuPL9TF?r_%ET*Wib%9yTAo%DsO=?iiJt$*Dg>7#6&)CGH zi!ww{iP0i}Jd0MUr}9^KsXQC$Z?mGWQlyyEy=3`#vZRkRSY+fPS*C>q?!dvOG@2^r z<=U`vI~gEB4*+fh6!-*~s;Z_6NF*>X2fEm2^^|B|mAklv5j9uPU6#Mtsxh#s)E+9q zGf(E<0{8?J=YB%te=0M=sj>T1jim%RdZkh|bwL3|QdkykUi9m3{RL;LO`NyYd)#&e zo_ZXtoHHqDJTS)U)z?vb5RW0P8s~by|w9t*6NY$e~kiegw zogv7DdFxLkSdFDx4Q4XQTXl;j)crw1!3kR)BBVwu2F%n2&D6=swn_6s-?Nac@R3Ae z0_wkRK)t22*!=s-zmxG9IS=gNmLVWp2!OyAz@4RV(uY`oC_^7!_NWI?b4GGB1X4Q# zHJ&w~=zxt_&V=ZEH3PzLB(UY&P> zuZmQS4@}JW2k679)zFZ31i(zVnKuxiW;4==R7p8Bg+b{o(8QKyM~U4}`N+yQjC}4ig%T&E30t zs6^kVdzznsCe$#5EWzhV$LCbth?dccLPzuQHbj^v=wVRaZ4)%>>|p{wt$vsq`BV0Qj8>TNK5s z(=9=1s^}Kd{>g^W<%ZCnW8NtbM9&stnb%^dCMYovJbMJ2{p|{H--GbG_TU5&D>nfi z=&0t(04Ig33+Z~k@OHH;0NOD(Bze8q;KSIgoXYqK`yqVZ{LP!MiED?~N+(0aBir+D zuODW?pr4x75{3xZSYTgSpj$yiC&KGrPf7f4pd8W+=W=@A86G@7!#-d(_#(t*L(82w zKse5&(X%uLAkiG;3!TZCIk=c_Yscn|&>Tn^E1g)}>O=r0AS)^^J7Sh)lTOpb?t}Xj zBfdQYsZ?Zz2Yp|IRX0i^2Ro1BBV!+UbB@lBgXYWG092(X*Y zw7bHAGX)SoanO{ukY@8(4L#H#JbKstSZs%g<_(d`XZ*75Mo@G2h3m8)0(HjQRI9KV znl~tx2x=7mw8JudyZ3Ri15gX=M9%=~EWvfX{LtldLjh_)&Fcz>= z?Ap4oX4w#J4y+vrLJoG{V^|=wfLANE(9NMaYsR*(L4C_MkO#8s&hiX*285d;N#PpQ z;~!wfNboj%&~Oi+61<|1CHS?y;jV9Ri&u!`w%^!ffo~y{?!+Nl&~u#R-rk(nx@lsM z9y2&*qzW+tW$bZp>MmeG;E4dKAT#OL!_fPitv4X7lxa`Br!{FL_j;PrJxdjxb8Z1l zq#x5ROfx*|A(o$3pTVcjdNVmY_k?)eM5Cgglza~LiRohVFP*MgTt75r9+xK@U(&c#_8Yt83{Vdf`ezNE5gt?kg>T)irg^VdsAO)_6QHSc)#=I@Or%O2m)yN?6! ze6<<~zQF()Oway-UHdxf=I++)vgAuS?$1PtNVuP7=Xhmg{)Js7dHh>GB<4sb59E{n zaCuGRI53T?aRs<3##}~AgfcRvIxP729AfYi~CCI%aWJ7byG^aAD8b`A-d5VaL3Z;nycB9Ei z`Fxxz38lt>`>fAXk@1CilVZnC99#)3%kjDnoXjt(!I|aIGToDZH@+(5eRZWKUn3T~ z3VgrhF5)}BBc?4feC?$zHr~grBe5*ks&nscnHuxGlVXfozRQ;}u~#nWrPfCeZUN$p zvM=mwnf&sV#r0IE9ozKO?tniC2Qx|VsdsY>iyP>O6qFe}dH|F#)KjC(&^LIbB4K1? z;@ECvVjU%6Z1%L=cf{XiT*Bn>i+AlN)*(PiQ`;EM4paLS6-hJ4EXNMBr$tec=FXMn z9pMrS{rCk#^Zckf=)8Mo~ZMvW;Pmma_XNcAePJAkcA)neAL7{lFN^rPIP4aQZOj zs6=-HuGw>>{DC>+UfoPXPJ*1cNsy_d^kF-GmPk9Yv&EJ#(I_byM5@{Rkn#>sj>A@+ z(zRJjQAv7(e}PN`m!^}-rES8;kmss0ZcV`z2`+}S#w#@3_4rh|C2u${R$Dd~Hs1A~ z?Ph&dT{WirOngJdV4^6#BDe2iAoc%sjuyFo?UXU z;6FOmKfruT-F1E(zp{=gu$%k3CH34L5BL8E#`fFc-VLGum@fItXYvOh-0|h9Lrm1) zhQsg5vqoZpfAz$i8%iGiHC-ZM`~tGBqMam)Qrr@4?cSa5?es|P3i;P`iHXjOzotuu z@l&wVCB7XILd-o2{j$D`teqHU;^^sZn}v6-?unD z&#^Hcjj^{{c+jy&;S`iK|JHC=qI)nTu@I}g6mwwwd+L3DFz2SyN?5#3C!4M$oi9le zV1Lx!{_uj^G8t4rm@4nj`GMsL>99H~4WH6$(Wi@2wM^lHe-oZ@<8!>k!S39>XeHs8 zvy_2bJiQoh^yD3<>IRO}kq+Eg{DUGV$cI~rSFoL1!=GyNscuMsp_6)yy!c0Dm(27k z6O{fxI`uhu+1GWgxb1u@BbgkZEiN_2+;raV znx{R04;Z|9ur1T}v|Yj|Pf+(X_1Bk&>jRWu^uF^3u5>fJG!Umi;?(531_s8Z5m0m&SRtL6~)ilE85<_T(Q$7{q*-A z>W)dM{u0n3TMYHBU9ONTuK&3u-A$93{4YJR|Clb}`CqE6i1O$EG`6pc z2mE(q`~MT4Nu#Roe>S$u=3%a8gJ1nMUGlc;&)=%7p5>HVjN9?6e@&PCWjOpFfyIBG zF2Sm@BED7E(vCR;i?z_9CCT%&Wv*l*`-GiOTWOh8a3udW94d{T)y7~H!5VDDhe1Up+B3vDEXs12jaViD~t+bZ$z4fc%YX zseA>@Gi2mn(hjQPv zBi<_p49`unET;t<63Vj!=*I7FqpMB2 z9wlSauS~51CR=h>Us?sbexIO|^=?|IFAqzy_{h7SyVve%6QHJiRGe3vDcDf<$JH7f zzA-NIs~MS8=a3*_)=j&{A2dHj&l`Aun9PAHA0qIF!U8F0j%)*XVt*)^kEUcBx3zm? z#?-B3rB+6Mv9|Qr|MUu^?Z?;?CA!re$rW3m2|K$gnm;=w|B-6r((6`<`~oOX*jd*} z?^)%9JZ}Zde$fBWvhybK#b{pNd1|K&yerW^Z8G|k`|rN9Bo0-)v7HOMFa5VR;c}n< zNMB>-TUk((guy4Yw-pWan-Wb5IK(o5-WL_D?{)(&wQRw!J4SwwH$~TO8z%EygfSai z#oQya8_1C2BcC{kgJj!%@AmLDr*+Zj`qf^0s?K^j)tt=Oav1W1hKIq3aNSGWK5183 zT|$1^;PF3X0=ebU7F=qN@+DYHz$5 zXCQB?Ai{ORP4}Ous1+b-| zbGG)GOK%1u^lzq&H{95J>=;PMc2aOmPSBYhb8Yh-@_T;g4WZ@_UG9td(0cIMp|j_{ zc63r;^gKPcvqSf8xz3Lw-5>ss>rTJWp4R;MvY`alQ>!}|_4u)!6O0^|eSFMU5oqKQ z_{0B2&<{&#dM{ayLyBbClav=H~y zSiR??vPlKp%$M={fTDFY{AM&%wN>-lVPGDfI;tmp%rnxNmd;fyLZ~M)$r{n68s&i# zHC5)o(Gzv~sZyT@BqUlY!a?fP)4@8cDYFALsO~A9JC{x5l;5~Yi7aUlvZ5Ie> zY<%c@iiCSGEG4F~q{%?4AW+!~RA)jT`ogp@EUebPmJqVA1Bw=I$b>TSBuktch9!2I zvK!k^KO*cZANPJC@{td8!3sv>4e_OsFl~;vHK87zhIpDt$Pdo4Mw{dd`TR-zt(ZL9 z4Ef{n1Pi5rMe5UJHm;N{MYIS{_$9xTdtt$;DJduE1oSwevK1*(3n@x%R4PXlYgU1% zJ`^$~HFG)Df81A>4w53M$M2Tf{fV|?EZUqHu78x)C4?uI;q-cd;YN&8Hb~NOFfDg9 z=@y-e;EcG@8f2Fyg9>f$d&E+|K*q%b)HPmC-1gbWwh!kH9N9UGqSn80FKy+H(B-}4$%`XSd=+v5 zcLCoRYnTplpr<{8zG{SH)|A8rf$25?D??DD^VilOpT&XW2+HHh{Pn?n!tH!KG}miK zV05Nnp)zV^4N7ROw`7LZqaX%?`Sj>Q@}5EhNWs~BA+B{Hl)eZoA*k}G;D&WnElzf7 z2JOdI2=xm7cV!4sb{<&Bh;SVM{DJi&3clmwvw5Rlqhhj!BCxv|*Q0`9*1Ss5947?8 zli7Ft|q@vL5xF8v5P}7%v1;aQPu{X7BMV zJy^+^8{Vugu-A-bekN}mlHt3Pv>#Ap+#^*yrbs7-QX571sRPEx6ikkpr$m@?0`!e< z^Y&EM#OYfq{@=Jm70L+Qk!Nn9G`_q-n5>eI&Sj+)!Mqia(NY>n1t4EVf+;3Plc<^l z!6tx9ec3~GfNBUR<^>GsA){M--uRHkt~ z8h_I2^(qQX8Dl5`2Trz;ZxJP?n$#T!a3{udCl+8g(RBnr9|GLfEj`My=l1u z0BAr3*=PwFnjp{5=qewOH&6J67XYdO0bW2AResyQ8E-U5%c<)6o(0`Fun4Z97;U5! zNHO9Zid((WLiTzlVoqT18{#oLO8)oUf|Y7J;8Pf z7}I=am8dk5*JOpk?I8=|tn=oDa*Q<=H&!pI*PIEGwMs%d*lTgO8^JiOFH&g;1E>-M z0O!J+?iPTt`CWqug1IVSUxFZ~$&CCo7IRi_M$$uwASuNSZ6OGKD2)AUfgj>Ry1>@t zKE=^Nk^oK)S9?2P@-&$Q2MINM1UyRD)$2rswoXC1-iJS+bho@RR7|(steTTYz+Y`J z(sd?Bkm_9x-mV6Uim)zr?Pn5(l{AVO+U|Vzb@sLR=RI6aCldE*?`X)@RZ;VBG0?N%U+)E<@SK6Bc(Zirk7>^ zJA-8J+bZ32po#)REhl}Z z)U{{1kX9GQg-z-(y;oFd`eO#E=o+f0(HD=B=?WuX4T^v?Mu!yP^ri9q>o!zpgHF4{ zbT|Tx*RGDRjbdV-<`U#xB>X{h`H3*qMV7x^lY@8R!i7oPKm3|lz=ND4;ol&)azTp! zZ;@O7J>Y@aXf3%O@e~4)G~)m1*R;FxO8nA!O^%s%cP95AfCsPDml3MPqUXT6bHIaZ zm;ZTFIKz~7j7N*=hi*?2=6q3Ko$%*yZE{yB@$F9|M-u1fkKf6yzc+>dB)3ZNvE?Op z|FBCNoqJJGmnW_E9dFX2Pv`OD;FMvzz5>N#A)|Sf5%zm5YKwv*+DJTj`o-X$lZHZ7 znp0sbc{&+<_+6HJrYX_t=+=CB2JO^hkF!17TZVkGR7;4<&D^8$q90B56Zpsw!#ZN- zG|Se;lUU=WG_?qOGJ=sOB}GGi=6w|Xm@-F-t3X!XE&bEd6)WCbgT-3c^}VdK?%e0O zooRI^np4N={q5y!c{tQsKmH;0C;fop-H(R)`)Zs@Ca-qORiX%+Sc8SA)jweY5A9`z z$^Q#->mNt-^*{S6S*lH^0TlVBreqe$83Oe+4}Je*UPg zM*Kl;Z7bgZQjb=Cl3THLuKukx@vg@U)RJ8<*Jea}u)sQ>eE-eHmQMe3a_jdmR69R` zbE%0Z-=R)+H`!)ODGXFttB`vzi&i#|z zdS!zU3@`2dttq@n8TPX&91D1W8^)JEeZ5s5b-HKsw&pfYAL|eam5LVyRr2rT*1tY~ z*sKt8t1NmI|EVeb=lKKM6t2f&o7>O$Pc=D2SaK`7hDIVKCb(66E0n4yhhxlli)mk0 zs9YNwP4wS=;4RLHP&L0Cl)PKQt;HjfQ90IaTlCxc0}HI1-7OP)S&0Qa7^ALYi0;a23;l?fM#OhWUFEV%(69;lKba<)8 zL95z)YRI@5TEe7vXTa=CXsT(5R;I;-1c|+WKBNLs8gGOT8Hd*+jS#LWZ&7bW!`t!P zuT#>lT1N4s;q*1bcjXyoFMm^j&sIb;;6DmSPLHu%{?QLB+%iO;DuQu^_~k7n(Tnu# z;A-6gKEv-iqVatUJ`xB8T66&enSLg!nb;mLa_ z0)c26#lAt3o=b`z+c%I7k62s~U(<+t-*VWq(Xg)|9x!4&9{4D)gNFS*(l+X;*4&*W@EK&0YuvEYE zRM71qi*$I9TZ_0`dG8^LQ_ckbT@v@vS>@m%Z%GG%HTz9A);q{eLUE8;cr~2@tvyLN z6D4$$zPk54VV0ZXBh<#%Gz*qIx2FYX+s=`uJ}jzo-|9~<%K&I?+96{1yvUam zfbs+JH^gh#ddQIE(Sp%^&9BW|l-e10Z& zwV(0P_9C>WzDaOjw$2aNrfby^&3%py(_40vBCJnOD zTaY~mud+>HqIPFvGw;ROp{ug#`gVL^=ib>oe?~dr6{>3;b80t`l_5|(FM7xf@`;v= zbwaUFgdb83?<=r$r;OW|ykH8&U9btp=QP0+$EUlyPgxbQ&l>xd?d=M9cSVw5@@(mw zBt2*~PVCL_$c|jMtbroJSz$#soawM-U194o@Ph`w_5y8)RvHCsPnPQ-yDj5}8IAK6DZSZ)4gozf(Du!e?MPj>LxigWxtw{c9t#l@bgs<#nE2?OCxc1*>cF|uT zzA%GZ`7?qu_5W37*FPHie=@tUE8RJR_`kqk_+VwfR=WHjCM-Xj56=~?zhHJHHWz4M z8N|P>bYJQ3|7oSeHuV1~g7f41;m*cy@R$F)%q}*!cR#q7QA*T~%V-2=<#Mv7D4jJ* zTZLyO)zH{+CC&794hQGPOIne7jDn+HxsMHD3zi42fwX!+Ul@U4fpIy-+WQ3_43sO za9U*vb##?}b=ms=b7t3nUg^GeODp$$eSCrF9R5P%veg6nmk17~<*ns^S?S{GV4Q!m z`S7nJIM|g=UqIf5$}&n|f7SBNmFn|C8rPLX*V|@6uyeS23w}$#Icy4j)wBN zxy8906@TzAN?v3Xgwes^3ZOw?UJXt|EL5V1hz67u!jlG5mKDEMB#>Wu5L>}Nz+;oF z8dkC$P<;|5C50TsYggn{>Fk$Yl}|FHe+Eecm0iXvTE8)fv5M9pwxRzYD_wet(VL$Q z{aOQBXsO?xxBoxqvHz30FWTmc>8D9N=NBH?ozqH;y-hP&mS3Lp3*YH_PQC(Urwp}z z&O&(cY5(lbtJx$2oxUzT%gu8hyVq1b#w9a`^i_V=OP0H#uOqOt@W)-R_$lH`vYCt@ z_r6S?gRq!0=~TB^9z6Jfi0#C-z;0CCpg@25g4|ug&cY9?-l<1>Tv&b~`PFGt9BOuu zVw6~)1(HC8yU4jLVq#vtr*n4>{*nszQg)Yn*MOU9VFwT)J7Oaf{>bQnx{n_rwh%9} z?lYy!sgA#-aSdw^NP?}HF8yg1t}UnN{+fjxZr#__*kjFu?j^a`s)eCtn?r3W^dx!r zZ)Qk8^qY`&gof+nq7&Gbd|kUsbZ&en9yC+3HHNLqM_g?D=pC_wg|`QZhC9fj{6z*4 z36~m8wBm1{->PUwhQ`%GHaG0X7#8FKb1w zF>n1M9ivs+hiy*b1FFs1UfL=zyzw_>J`*+Rz|2xT zg{_fr^pS1{ESlhZ;=g2M*W3J}2liavmZQEb;?bj!$B%AJ1eZTDy5zx8r%0F4_o$H~ zEw}bX$=clL3&HIoj**7;72z%^My=#A?a599S_2`n^zkLA($yj37&!#|$4zi1`DKX3m7kKOHE{4ai?2OaszZ~Q_v;-o)j;osmF{%ag@{pri|SqNUj zZIy6_{HE?}Ej}iYa1$X5EW|!<<>#rMhg2|F5nRLa3t2gkOr-Fea=$%qxiyQxCjXIN zC`VchME$1jTa8+orbhRTU9@#2)U%LZE#@t(Q1l@j%uyuW4g;N#Iy>zk1q1s{ z^8I>fN5xdGF7_vo=q=F~J!L#8)PuY6kJA_6J>T{n6$ti4T+nxLy6(Q4hps6jiV?Lm}UZfVQM z1AX!Mf+Kfmr`n~KVs-6oxAdlAEfvxkMb+cvv@|a}Wm>;v6>s*;qM1_P_%?M2u5jYy z!~;eK;w;hIo>|x*176f7g6kqK!v*GKd zl?L9k#zFD#ejOn)&kFH>f8OpE0SOgKTbXW#B!xN8Eajrg+aUei}?!Na^x2ro%2QXsB-b9IQ{RDX8sG?^uq zwA}CYq6J6VHMP%oZUtWJ$LtN+br<7YG*`+QU8g5}yq{)u=iPV_M$Dx80(m`BK%t7j zVDRy{aMTX=9k$p?&`WL%j<|lU(nm>&>7}?rWQyn_H0Q97TN@Lr*StX0Pw|w_FD#C4 z)fyS^^IVMlRvhs+J(e$-MV%4!2>;xrUGZD&N|)Xx#gQhn31=a--^WLsVZyl@1}TkS zUD-$<9>Fn5eE|>%Q#`9#*_uorw)3Vq%^v5Mx!gb(;u<7;w#fg9OWs=8>jpE!`X=h? z&e7}rX99%@8}^(C?~%zJ&s8TZbzX!!T?k$*Y+7PzKuA~Kl&+^a%W zYdxM6X`o-!NN)6U+%V4kN^Un#wLPsfk*KbpInG_Rt>%{MCu;B`5Ed|VWiuGYz};yC zdXIeRyg9q9-F0fDyzjBu21=Tpphh%25RkQxQ?f_~`Lr6~lBUdiGa>g8?V(VMm~fYO zYN09}M7UpbJH6j09jZQhOa^%mR%BVN^+6Esn3wu8xI+_NAG%9y&|I1e&%O$-e!=)O zvV4582+m-f3ZajGk>Sh4b#LHQL+e}eYujb2Yi&a&@6)0h0s~Ehju6fAbQv!KdD_#t zqwl`j$-is+L58AzBq~uH3ul8WaWRjjyxJ{$&Hh1#Abm7Xd#}o(YfThOD{1LQLEY}N z8mQ(@Jx;u$s!@t>^HpqSs`j=h`hk_)C$+R0nYQ}zcaZ_z9q*S29m&4_U@=R|dv|uD zt%tJvOH}ZlE`I*uExH@LC^?}D&svTNneOfA4ylhM3P+b5gLb^Wccw=KN{xn{?p9nT z?l*XS97Nc?r>q&EPfq%M7B8Erkz!%vPH_9Xoo=u0vmdL-n@WpQ><3ID-I@}Y#cAV| zkA&rA_Q97RYhjE6Sk|3N&8ZG*@uSS`H&IcZZOj)xDdHb<&^y@MtznPLl9`UFYplQ?Yk_YA)@%9F07%-@}a`E zp(e;sNr>H9VdyClB~vElMOq{?H_HVvrR;R=yCz5#1tfo~%<93la#j4xt2AZ0973jH z=4c;@=`fkYFq@vhqa~zVrcAnVI6{okCq3K^9xezc(4>tRBW|1#?p@(?S;iwnJOfU>1iLvHX`J+Oykr&;ta3&oD~`>Eyj|R#2NI) zb#uder*->jIr=i8Pg*T&2S~`M;$ZMwAI9TJITBhk*}0VyW_)SiAf)N9$$sFzL!(Uk z{hJ-^9?v8S65$rbE)*vLF*WK9k(N)wO4Xf4WJEO3@3b_t2MlLDB4^ud_+}`%pKb%< zHh?4xN_NC~w#CMZ3C6k&4i6}3cp$e_uxqJ!7Qce4szO7$_zTdM7W?L#7g#ACQIH*ch@|( z=cPSoq^!m0`rQnC%+d|VGLmtU-Z!L3<7a*bWE9s7cuW(5%mia*GOH;wP%)`V;#uOx zS!$V44??pL16k=a0`6$?V`WpvO4|UF>^!`Xd@P1U*P?4TOA(Vjr3!ojj#+ZyLU)U8i=>3ey$Fg~ zu|Y&Ignfv2-Ynf`9()EAoMD4L3MQzALZQk0vycnQW_fzOH{}}(h@xqm+-)#DVFC8} zNg_bhJ~g&|(m{w!%L9MT6wshG7+EE7Xw1Ne7vU}tDkc|$aiT*qDK}<9&53T5U7&ka zNaj*lAb|A^$b}W5A8;*{B4$Xu{mZ?FNN#MAToxpK%0i$rkB=ORJ1vxdfD0)VE6Szg z%<;7=FWag>&5{i363v8w@U05g#fK9M~Yt+F- zTUD=6J}rf`(Z?QAV4BA$5XIJo>mvmBwyN*B5nW~h#jvP)co7wafZgaJCWnt#{i$j0m1?BIfgw7upmH-8QEw zdt~K@$uqJpC)}KyqXr! zxI0$FyI7C$GRL%sVy+0?b%VM*PQUJ^K*Qk-8@WI>37jUo(O!=Q(_b4PyF0h7C@dzusKo;fLsQR5EXP{AuHB(oz$3(RkWi_Sp_a@3kl93rI3BG2BoMxwv<`_^W0!{wj2*Bb~wph^il8^*<9!L?noJR{O zTaMU3(P5G30pxQuVp7UfnAgS3!0Toj6;cMlMAvu)Gerb}Uz{TB zrFNmWTS-H^Z})WHZQ}XJ2enM4dIx#zFj`=3{g{8b`)C(iak1h z`gL?XBff}0&9@^#e?v#+HmLghIJGyV~9L^WGnA|7$;q(;K4UW{f> z$6`Ub%PM|iK^Nl$pZ^&Py1%-VBuB{e=cD64J+%Mo=y=;6C;z`59qsiT-&cP6MVuz& zT`dg%b#z=Uju%~D#p=ksYbB}wMMu7rUT*Rr(#g8AtZbID>Mo|rN#S(*gzZCh;~S@q zhpn}%RazY%*Ei~n_raU>SRJ`0tM`5Crv3}24(kRj`3;xoF$JT~jWRc?Q=6x~H$Jz_ zNAmr5N5_->f7|=BSRb6DQl7_>+!&HBB$@?;IUJ{|j!VkF57_iHi~E4;y{$k=@jt->{(n;h~MykptS_ zZT{4eFaLUI5B$^7@h=|Qiqq=hqG~++( z$XMbui0LK@OPrpI0miY!>D#s>XN9;5*(1*druB$*LvX2DyMX-t+QZUad{i=s8m_X?E7Z+a*1CepqE8FLc zk3yrhy>O|fm7ei?xZ~TIV`v_Wg^Tsm#ol010E$2aJa5!uy&`+*Tu^fYCyQE3KUg6Y z{i{fsjVG@q<`xK$h;R}@^%2Q%Tq?{at5|T4uH~D^qA8XLK1+OE_ zI!*(GS6r%8mB#l-3Rq@i7O+*JIqeY;kO)f_SaD?4#7Xep+GQ>~R2A*!MHwbzAKE{4 zWb8xxURY{iwdE)EsrK1PW23I>8+j|^UAU3Af+Ez-MnKcGw0rV@+dD*c0eYbYH~**5 zRQe~OX+ACzV7mnS4~VV7WV(MvY`rd!`x9cTv2wgz_ksGq&U{9V=9-xXhrgQn?_T`u zdcz}O(rm@fdd z^=syzsVeVv?-IP8=?|nJkk=MN--qEEHRUlYJj?KgAIT_*sDX;YrZ{%h8O!l7x?pu&)lOGS(eGiH=eOD zeM2EZ#UyU5XygWTJL$%5aWuYrj64D-#|-ETc~)Hj;phzyUQU9;E+!KENF#f%XC1`6 zn=dLMFYVsSpz!exI+uP*o115YN)C>hq@Q7B@ZiPS zLoHhq=#b;MbzG$T0_e7=z)RycL7)#L|s{YXXoq7E?|WsJcz{mR6s8$wFy_v!mwUL zhy=ox6gjb|n2`>ejzzgMd{Ji3UlvgQ%*u)*yp3P7mV_rQEDSZCvODy#l>YzT&x`mX_iQ`n8PeLKPJcW2`^wu$n$>10kA zdkd$8nHgCda;KKX+dW-(0@EE)k;tlU8PCF8s>q!9QIr6z=*MnQNMkh?leudvFV&`| z$F!ch4$1`Go{{C;$5Jr^`b6IgdGrvY*dnNa!fNJ~%NbFd6=IpT++* zuAWzFE=)_M%j{*$?oJL%AUA?YZ46b|Q1Z^1TiXy!m7YzR&Zb;>WXVgFjqJ;xO656o zF{jF(WiOMsSD=$~WuovLRIgQ~pD{F1Cb3tkn^0v~u0B~Ms>>+W;B*kEM_HR3?tDs@ zW1KdI5DC5KU&m0L^NP2tv1;$3Tf(o6bg$O;Wi_~|VygT2YQ-h#<(sO$3AqSP)y>2H zC)&u7o=00$x;RxfXV>CiY~6d5`I4W2r`+s{3NIt6LX_&*_Q0#`{YI|FS9W?YX5X}n z^38{wIvM*t1(WSBHhnh}aAjnD6H%zxxSP}K8Wkrk2Tbu-iPc*9ODetN^$F4Rt@Wyl zl&-_l#;32wN=lBs-t<4{fL?q#I*s@KvP6rim5{3+b7oU@MT_iP7X#__1>>AfpV$V^ zxO;rUNx}7(w3A1Om-#6p?yaaW+g-0yB9u~U=JoBu+^-MkI^ibD(Qbvks*k)jY@H+6 z@^p(wK%NlBak)~l-lJV0`iIARs<4y?mu{1Uz3ZEjYjb?g{cL;ZIfKpPRnu#jaj*i% z_cs(2AN$LlU6fPjY^vs3WV_kpBJ5~anpxTgO<#T6iPx0#ys$Mij3&2it+A^JF$-+* z?>|*q?#W|I_2ae=YRLVh`AOpK(JTEp;d~v$?S#%E5#4FewFym>8R&Q(OLU;!AMx6k!6J4A72gFYvBi;yZa!mH^g{AW=$HfzNrS?R-BT zHb*`X0I+8Dx8P;bJ&*v@Z*_Ov3GaaHnHJzdlLX5CanEu3{yY2^sG0Rw*DUKEe!Vc( z@!`VM0bi(cTJY1+c_tU3u5K5)zzD1Cu9y(D0?)Ldcw| zXnRDbo-lLKsE5!cx1$3r`@}~l`t^#Jx=T7Q?KHH#B>46%D+&0>A(X`)DYF}8WywlU z0}|97``~BGz^+3x6v#8ySaF$v-herJ(g*sXMs|^zkBfL2m!C9L?EVHDk7i&N?(V!F zT_3cculJ_F^Hv}C4usI**eJ-IH`VLQ3xnsK!X(>ER5J4w3Z08IIvG#vMz`aWI+s0f z9SquUO{DAStq99fB`T)9%vvG%q*|9#pMsTsbh-{$UZ)`{g2HNpyEg6brq+FLx=~Z! z^~LA)_xH}WJKYrBTf+Lt1$*{`UcT;a9edZRRJo(Do87w{;U`-tHutgvwTkl7g3fi% zn~ASZyAPwHT+4iKzI$S_cSzxTx_7zm$Cs}%`XfwdV?71DsDZ{$PfOo_pL*U{D8x3D zc~u%~Zk)q{>MNxA2UIWnQnLCg3n3`+5mNU>sjr$+3RlQEt3sqrA*c*5O-iUd0wCu^ zT2h8@Sy`)sn8axzlD<&w>2MtoCGA)^5Xx>sE2C!$QJs=X6^yhob17YQJH_n2N@>RhbvaZC&9pBQioRnTeR_O26~9{~$&Kr41%&Wmuw3=uJtyq7-HRJkbe~xPA!P%B22= z;Ckn#sx%Om;-0jV3He%hdoP+ATS-YTgd%iwJ~FT)oYcX&k{su)IE)6HS=MslSfN9o zj6n~y;sA9#1im{!pQLf!U!`DbU2 zAZ!L2VU{kkO%l5X=wLY5Lw#;S$m|X&Qq&-=Sk-DiQv<_c#RJREqCBY=W5vX2;$+!= zLuY|#?CBupzOo7#*+hQS57m+a(v$Q@(~)7RBJQc}N1UBA(Or&IP4(1|>2jw=WmUF9 zt7BBX2hCsjslJRcf9 zAt`P*_eF|ttI%-@r;8qQUuu8Aa|MrXAYiM4nblzQ8N&qh+1(78T1WpR2SqIRm-UE|IW_cFP%6L?w3 zswsaq=&>#lT%r7)ImjD)<7zwgssAx<)**9`DN2EYP$)zQ_*6w&KIDt&c>6ITO=R0Xerzd2D27&6{Y zV4(Z(4E69#(CNWP=hfI+OJ1h8vk%D^2tKSkWm93(&e25KiC!m(Da0S=t#)xEJw3W}v8Y~5 zuTDs>RQ?Lt=QIeP4N%tD%Zm|hM6Use+(g(_T$-E3n)F@+6b7p}Q25yccXNlAjl13*zDDP8j< zogs}Da`>|>fR-SzaE+?s0`=(DBRN(;?=7lxtDdGhb4CsW+HNVUPb)6RS1%-+V`(@8 zT2KU3@!6C|PB0yH&KueCbZd3kW=+p*P&S``7tmgu)79@J#!~yN@Cj5P>Lg6QZvjatnZOI@2q3`0x@*J< zXi4kp60EO9k<5mGwX6sXtO#1>3q&@UH!L7uJ8m3tKo06@s52;|X8?#4WVa)@+pUK- z7yBW5RZljHvJC16&!W0dW2nkNw8Vktw;P^7F*HtKN=0?xwR#ABO^+i3`OX$uo*2Lp z2s0W{;OVT^-R@26>5X^q@jt$haJ3y9vE%|Xk#^M57nKLi*Wz)v&#=_cusx%aFMG^V zPYG}DMm>|TexwVpv*d@qbuI(o&uurEUa^vUbhbElLR?S0%=szd*MO8q{Q>c?({uT6 zBuhrVFU+W|4AO4XdGGNgB$7WS#Q~s>rKAW%$DTaDr^)3QH<*=s%VuuS8~U{Hgrit$ zNLzoXEO)4?bEsx+sMgY{>}W_6LjGuuGdSP{@STD->P6?=i|&&aj_rXaso_4$;pc(F zP@zI~Gurbn?$TJMvrtQ$bYRUSN46cLK0Kt$K;&2@uy_c#u#!2oE18WTMGSnZ* zchS2?;uX2k=`lntFd7WOc7!g|tWfTpXvh-3ro=?0GypMAUQzc@fY7`<*u1FVjp!{} zC)U?T@suy)h2OY4VMd4BY}5fQ*gawTFS&4=h1c9g-1MfxjcC|o91G7i5&6bVs)8&Q zR$=BMz5A@!De>PyulZ#42dBoSX!17xq&v==W7anve>;M3o+_pK2L%M1Ds2rO2J`-n+spqz z4g9;e7mb$McOAYv>x>H*h-IHy&F?YL)f2b@nB;C*XYBI zSDoVTK3qy;OZ(D^jUZe{mm3b7e%Jg;R(WcJy$lCVd>A3}mpvF2(cAvG^djl&Dcd1( zt?3QdjlgcK7NZhwewU($NYCvcG~7}oo^Ewi1V6F`6^3K%P;}$_kCX+?A81%Yz*w{h zi#o%Nb2Tu(#4L)Zd37-^ngg^HFY?A=DM9RG#ZscwFEx-oo`1-&L}{te)HcRlNPGk> zEKXb^d|ORz{N}!;>VnE9MugSWB8cjv&PSI$4mF#x=@pcMc=|`fLQG|9TC==^eljml zbD8QSD4eT6{l^TDN}d+8te72o=Lo7EQyo1u4BihzCFP_ah!0uh0N>;vJ{ z5$Iv%b{Ua@y2OPwXH7!tYh^ZNWX)WvW5j%8O$Db*cjSk}TW{A6>%;DJY?YdSsbAIJ zeb|QS<}=G(bX}(C<`HVBi3HSUF1PZFtE?(H_EybyNv%2S-K1-)oOm+QOp@aGEpfuV zp%v^^X2Ec&#!B-DqqLSu$7H#MfEJX^!K0CVc%Qy9t!|83g3a z8hsb*(Mdkxcqr>oYBY26{imSw2!hh+2c=6yPjJC8!G!0fTHN&bSGGcd_;cIs&s6E} zYM}pC7|TDYfxp{s0Y2QoB2s&YWLi;nC6S+{TED4*zihV;=If&oCkv32|5gn|Jin5S zRRc92(Et0}%kg$nh=|QMT(yDT*5`pF;)t7sbnHab9+BQvCgDtKgG|y-BGZOTJb@Bv zEq|z7!>wg5RpKnt7!O1yI|pIMJYTC5ax=6lK&h* zFyG0Z{ux1d>FU3e!=7uWY&A&bfgER3j#CNV&z0_}jI${UzrMW0)yQx1hvMsIwsB&0TW?Y z{+_FSRW%EJ>{aeV?4-SWO+!1UIw(Q7ti`^_yJ zhyu44Z{Hv9w=|}G)(nz=`jq&fbwCSiyL~bDQQ|k-ZMnv~b@PMvitBx{{Fry4ZYC_V zS}~{He84woU*~k!1CHMB6rVkOARp}R-d4Qn+zI zy$sEmuVca)eoxrHj5ss``CShEfPeQ|FZk7>ph~aR5AZY>?WMZ#?HBI=`_@E8!mo0K zo2{8FtzG;w?iUT_-XHcVc>%ROAIbLZ~B1>IfOfIm}Dj z1(CeQKFT;O;yPNs^NhoD&NC<3elrXg zi6>21RjM{;wY4a$6TePvU2Y43fa>y+UT#jADvkJuhtx9aFQ4(}2qxAdA2>d>&$*!0 z#KuSB{`pEogqUR1l5st(=8#O-Ky^|+*j?I+gXjS{)8NzEf>5&1s7AO(LJ zpZJKtQv2myk}fvxud$OC+8T{KC-^<3F4Q;{?YOAH2&&zv z5RTmt!e>Dd)*)^Xg0Cwu**t>j?W+-Z$eDB!`S6RFcBK`?BSex;pU;?a2*P`F_;h#qM(ug>OMP@2QPe;$~M(ZefpfV|gOd&C*F(Rx; zJ@=S#4*L%%MS**)HmjOS7buGmiVs?8?nFb&x#^nF6S~K;=^Mo+d zq+9aD)b6eqA!4{O0?ClX?N$_9U%2`LDo6s?Zpk5Cjc} zADVmSA~G{s+<=mcjs}15G&vuZas_SoTI{;PljvZjl-|a~2b?L4hKcGpAQUF~StCKR zP&`;9t{;?oeL00DTK}4wl5U%pQ5(!aT*;&jrnQ!O+K{G#PP4X+x?x5s6{BQprsQ9d zaE40fha~qbr6adBn@P9#ZLXV?uYB~L5)wq+=8XSnv@Uj$`Rv1dvbr`k5Aix+45 zg=X4iMMTnRCTA%r9Z6r^j#a}+PjO^?Ii1zin3zp0Z#I&qW0)0zljR7{Cd<;QP@}bX zgt)aaN(yC1qtS&tkg6E!f~=fV${fp{EXD2YZZp^uvxqad+-GzO4KdKLqKp^BN-zDO zUZA|uLC$Z5>I$cRd2?&2Z)Wo5*WBhDQ`<%I_*!!WN^)7P^SPq)9T)P2d-I{70=or= z-8NM2j9rv_fsJ(mYCBm}K7VZWn)8!_OHkqyj$HYod^)|t1#x;X7)BcVf4FRo z_%KGQY#b1jRRoeQ4C_v(AmM;<{fy+NqO!^(d<=pK9>O0S3O2`@OD@BuIap;u>K8vq z@0TZv|CCgBA|+pjm)sONXcrk|72h^w56yw@@^T}Pwz?I?T}p37AjA%QCFG%@c24B3 zhj9HvIV>YprwX`{ik#2@qt6J$GzRg&yA&3vpd(a@^6e=Wy(~#SEqR1lR_`eD(w>4b zAS|d|KP67VnKwyLyWA#Fnr9iD6{nMHqQK;r@TP(hLnN%&iJUo4%8RV@C{sc4r%82| zTHOfg;~3cHmkO?;$~3)7F-QePPvwPl*}`sROepY^pi%Kb9i7n4LOwEDuTx__ry9J9oSRetKH%-*uBB1g?mPxUc}+N-SK6Oown zK#eaPq}{mh%AFKYKi}EFYT0MfvEGvb;-QJLV=&`h)WF9V*gCO41i#CX5yt zL(y1-cGHSnvz%)1L!0LJMa|wl&D`}!dxB)g2l7O}NC0zkZY$E>vC=-&ElJuLB*ySy z)kfuCkOeA#K1jj~0!HIEtyrFs8g(_%^lfQ#`3VH1squv|o{8m~*6Gx(x@Y;hM`?wk zZ7gpRNZGA9P2s3bdRcA>5JcMWR%3>|Fr zx!nY$S3k)HRwNsP=rgF`L-Fm>a~-Rwj24HU40UANU0kQwSw{deEdCe6hUxq5pl1N9 zGI)KvxDG?xU^b4kymvIiVZ55rVJBB_8v;^piAy>B02~=ldmY5E*3(J5Ng#gRqrHTxQ2FkkH zT{LTQN6<&^UC+;{H20?B_b6odfwY1LRAANY(QgWl70B{YQGQRCpbH$+g;u_!pWL%w z4{ep_1dDgbFO14QqWh=~`JhFU!L;*{-U?xA)%|3Q>}dyi`!7YIp^rZK{q~&XJZj!* za|0DVA1(Ag5^h%b(hOo045F~>nuZStWkLr%Fo!6D%N5>kwM~RkZ()m z^oHL@4BuwTWw}T>yO4YR=)1Q$FeX1j$uy#T(7R+iA_5o!g+^Bq!nIc_A9%;s5yETL zhVD=I>xAaCRE@@5j8XyyJ^3h}jhV#JkHI3UsthP*JjZOOhwrD3b(_hnmyh9!cB7t+ zFTJX@?-`v}1Akt88q@QB;<Plm2l?l>WWPA! zd=5k6MdK1-p%t4Wxbu_wzZCI;$&A%V(c}0>dcj1U>M-GH1^P%FTIk6tS)}tcYvYuT zRbz${#ndqw9T!>jK5F{@ZD3JdMw9Mg`F={q#|6h9oLynUNATi@gy1cYz{*}hG`WJay~!`lj6V!P zleO@A=`d4j=Vi{CzNaFM|~T4d)!+!H*+^`Q>c3ocx+DvUN-$fBWViXKrv!RCxL z-vUn~&OKI@pz3)IOdtq}%pC)VRm1i3Qh2Mug169M^#U@#N&31)=W&>6Ezr#XsHPRZ zwXm!U23BC$9U~+lJ;WPv657%$SUu>d6`;x$sCi0aDz@4$zUnQ6pNJ?+tX&=ZygG5a zIz{_=93_t5I|aVcf@yQj6KDe;ov0SweiF_H+=Zl;1_MJDmRHTa)aZa~QXUPTiBDlC1=wMs03&AEX;Bfbf< z*i1Ubn;QrJ_V}!tKWC3Q6?pks#dCQ#80ZqS<{|`ClLDOF5@Xz3{~o-~BeBhIf&P$u zogzNyCf>0mL~{?Z2y`O_h)!}bYweI|;6N=l)z)@KgE!~Q6RA;Bn!gnhAq!So%gWMQ zWx;FJwqH7l?MG zJPN*Up)8jJYLMHs4D} zIHq+xKuqLQd*tkTJXX7qszrq8B27kNfWep$1fF@`!58!J92C(R?;+r7y(H&w$KvZr zH*uxJihGxZm&U#lYJ+nMr%mG9l=|ndr~5QD;D!?@RyJ(ow}KHDCKk`BkJjmP3_Kq& z4jJz$HVIw|#)&w@9}R#kB%UlwxILyo2c`B3QCr(?U+LcMRn`z6X`#-KCH5iRSm2uT zkJUTLQr~z#tWqX^YgY$1bbfnSR}@7L6!`#-bCoygT#)&&(9=yge2-9;=Tgz!R5Y}k zT;^_z38QzW(=m@q>5+gUk|>SOX{U z-o*v+ZZBdRRJZw95`4}@N>2AA+m)QI1+OoEMiYP^_3}FEe z@4K}VI`GK`amc-mhxyNlrIL*86{yn1VkE)D@4iG!!qVJNCQ$b=tM4w+=jRHipmix? zfW%KTp6wF!&pF{|#rn_cyPx%LJCGNfO`6}dYrdb3|GGI{e}wrvGL(`$FVN}oiq&)1 zv2!7c1lDXeU75}hbVF7jQ6iJ zE^ZkquOEz7Sd^>G(3)Kvr5nr=E9RK}seoi3?H*kToA!2+iHAr{(WzxD=684!3ZL7I zF>nR#ytP0lO7cGqr`Wq{AIz5dG{?fqAG@G#Lq|$Z3W{@*If`PdL=eJRWN&MvTm455 zv2a<7YGnx0oeSGKe`f9UzNpm*cVzD(mHBs#~-6Rd_xCW~GBU zBAD8Vo6d`6o-Ej;j30qaY>(Qd+|k^>>m}7;MY0NzgYR^1pJQOgFYii z?jU6r5^VI)6_6`kmAnAodyW^IKbAMRr|T=tK5$NOsG^wB6_Dyw8y-0(>79+2Dl`Th zzF1XXKF0Ekep5&g?E2DhzKridWA%dLTq)%S8!VWB+QhwIyPP@IqC)edHtdDtfbf2I z`w(Ta3>1a)b)#o1bs+cUDkOc2Tv+s*bz<(?*rT{dP|Dc?6$xSIBD+2%4lMl_j~%je z`@Y|QGpHcy+Vm-`$TbW9g86O0s%QH^N&di_gGmqAjT1xN!tzkjDrQi{yAL}m6)JWX zD{T$b(PCc1o>8W5y}4bY?&G2t6imK9Sq|O1vA?;WRuW&$FlX>)KNqqyYj5-UWMBHZ zcjUcZ!1kEnKJ8$qEY|05)5=LiR8?BapVaqs*6j1B{tBp%*+)r! z`G70={A`e1HSlowPUTnlq}+$#tM%w7Cl)2|Bwl2HYLR?-d$s?v_om_C{=_M{6c*W3 z2rj2;77DCXi^)9|N)nNW+9H!Sd5j1n@3+KpE|nq*91f!eSmDVF$>6>~M6k+Pf%Ngj zNo%Gec_OR`7g=Sg7N(*E8+8agTV?P4n2NTW-Xw_>l4Bs7j!~3*3QcS6H|z|LCD7Z* z>gw-dHJFYw?0-tu)GE*IJ01T7U`_E+Na5k@=~wo0)+fD4;>PN5&fgn~)vNy0^<|8S zR$kV|U-OBFrafbBl?tx`q0Lcg%!CM0CVdcJ^RudP;No?5zRj#HaojbcC5 zzDcqWU`y|g$mONSP5PFW5VbuN$7#x{Zj<;fV;+$IXhvZi;`QwH5@)=iJ?q2+U~`!TPuFh2$-1qLC;Z>*BP5QNNsK@k3TBj5jd(UknQq@ncx1q9)*k?&vn z(!XP8Z?Sls1+oIr`cgC2&_DGS^#3;9BLHB?HanskGO(be)^vi5abx7<_x-Uf*B;Jz zGKOq94eL&_k1XkE+g^1K7R{YN2+JQ>RO`Ns{mXR~jPkwjBGz$pp?ljzn{?0hChmAJ zO1P}6^Pkum`l1Pk?f+Bk>|YRszq_vfpGFYcmjs*lDvoOCKKC5G>&AKT7j|}BH^fqT zTt6mo^zRXb{}?;lj+O6Lypj3Kb!7(t2_B#N63U*Qz5&ZwdPp4m&TeCoe)^A6Yt%6~%;JpS+0TmC{2{)wGg zuU%-DrlX7w3Q5dV6th(&N%xmg(tnaw$jzGxZ=7wU!NkHs+2ohciT0n$pvicL4@ zWt6nAAR*T~53Jcoo2MQ|MCQrSUQ6NYsD!0=+Klq_>8MNq8;C2yjdZ%;%<1aC&qY|cY$DyGN*rspHNjuqa;b!z3j z6=yoRQ^(vOU^Ww&KD0uGj*zS@+yJvDTExCi7sE~wZzkM^^;*;xQuR7l`IXO zXSX^lZfuywMa>u;nbzno=uTEZP@Et6ud~9)uU$Ra$sop5q}lLFy|w z03MZjWWoQ2!;eOQDVA$UiWx*3FY+|Cj%Eb=p`CGroHXxdAL^|f`?B_|MJijB>&Y07 zS43#lqZnv)xZE4Zi9ir@OQb87^bUPeS$!mJuD-bksrFcSNi!RK^h9Ri!0f%$oQ=o< z_XigdgiQZCp_H<4u~=ubAY!KX1fJ3UeoC<~m1HvJ;Of$^jCrr%Kj<#=2F*A;FeB%# zykffDeXdM33a@)DE7Lwny^=-*jP)55dl~|uz zs&4{hChEAEG30)@kSj9*dv=c`=RhHlWokL>7Tf_OSJpkbU zm+7-@6d{?*=gA>r0_(p-k+JEOPJanevy6@}@H0=5CSl>|hBa0f3#P1C{=sOwhcXox)QPj8zsEmZ_fw*t`Y{ z$c4=@EAhNoDwGN&W_qO^9;jC)_Y5cPi{wiYCd&j6m&uGU^R&`@;a%$@IAz8&3w9sp zu?d{cEt6E2o?KpJJ{c7Z< zAnS^`0bI(n%MnhFw+&Ki{o562l|66m?s%iLJiCoielu| zmopjD7dvtq)|XHE7>h}libO(3QWn(wvS$;mIuE}8??sV+nC+D6XB!dZ_hvStSZl7g zUJ0(z4kapb|6#UYkS)Hp|Lft|MY{KY$?rA(*TeH4{NCT>X#btxn`D^&$HTKTEXMs? z@Uyv>JQ8-BN$#i{3ZJAGi@Wc>p&XSz@JQO9OuFBhe*bTx$p3_parkiUIVFS;jhO_^ zq{;_)(6L4s;}M-V#>g>77HhA|&XrP8Z1qRow;39>JjaT@tQA2C9mf#HGKSC;*ht9W z7g$-8$-U-BvT5=&SVmEhb4)PB8N5>FuV;ghD^1&pZWib+F)D&DZPD%Q!N{+g#!;v3L!zam&5pmD{4eeW>_ui8tZsCgy%|*pT#EKQn9CCIjWn zP>S9R@^P%S0$sT{jj?C4MZb-vOT^f_7&f$%xD6?QGwnl60v%kB) zCw$v4(C+V2v9^>C0a&EzKTvY_`Kg)}$?fbTZdN}(_Da6*`#W5Z)yh~Z_bM(@#GCHz z+@fs!U`%XWeF4+(q%^{I`hs%U30oRhab~(VG3&CZ^1Zsa_Q=fZc36P61Juc&=ewQo(sTt?dAy>AjIh6?jf&Q6gP$Inth~84ba5oU}``Qh|EohbR2y z^W$O#m6k-6MygNst5SWcZbeELYhp7!gUjxH*Lq-bUuQ0+7XI+!T5(Q7FF{$UApPg5 zon9iZ;aplnZOrYUIjwR;zsQ1Xbg;`b8{MZpqEv6S>MA=y)8u3*lL%ouLIgvF;f>b< zm;Lyxqv5CtadC6gw|Eqr%gbeI#6&Y&W@oiq5Tnv-EngPo=ju-jbgNrkk6Ow{^(E}L z$#$r&o-+lZOKa0B`{Y%=@c<2pRlCZh!bS&V-az2WK*O;Ah39?^w)lzJC1t z#P18_dSKBvks>x&lr{2~3DXDE_`MLmzEzCUAyOQT+A#954II(y9&jR>kWt$NO@8lS z@d=L<9NXO5Yw8=YmC=({+lne9XF5kb4OJc6B747-$@9b9qq5~Qg{tibfr+}r7Qgt| zF5khIzxWXAtZVevY|rynhM!W`?}TMEH)veA=pl*_&y8+IhtpVb^@RsYtg!Uo8A#JWWsesxVE)zLyp#Smm%Z92wPKy;rOA! z`=9f*iA`0JL5IdGSxTA{%s%ep&e)pmi`~S{wQ%xStDu^t0Sy5JZk3DSdHJWQ#OB5x znRQ1~(P%5So+ifh6IXY(kJ`h{&7JCQ@yW96^W1)$Gu2-~&aIzMH74THABOv{{ObDN z)zY;>sqf~2Gy6;89#dTWwi)j4jmgJCy`$o%k)pph!Ru-jj2dTgrX4)nIoH~D&(9L= z)9%q|mOxQ45A~A54Upl$*u%2@~L&Hz$h@XYF#QX0WXvHfyK9 zy*>LUQVMH+pS0KttWL9Bweb)g!(nJE@JZ^kmD_J?&DC|(K@80Vi?F3fVjI@Q*B?pC z&k~cwjGuDfjC|Yae?9r!n71mRQmgACkEHX9qUO(;^!9~Z%$5T~O|`Oy99c9W6ZMoQ zh;Z71D`laz`qOyk%HloguB3a*hSa~;(@U=hN(0VyFNqHrUf#?HNL&V)n_G;!{9GcT zd03B{y!{$<_iKaX!^HN@jah}cZ#w(~`rtPrlmfPN&|>xrc#itc>VUc{`*5qora5aYCs)TS=`Lws;6OumKmCD4v{+R>}d z@tr46pENxhkAeTC^Lo$<=E{Kb#E^dA4JVw2!5E-M_DXtJB%(2-=rer53>YjK?N*Br z(tbqO6QF_&2kC^!N{6zAT9Z$E3mB5=EkQIK1fDOFYB>lZ7DIHAk(Sq#PbmV;DPU-! zmn~kPy-*Y$YnYo7tp`Q)3}1A;btsQrbX|BfcS&?@Uo_WVG&7PQ6g$S7jTRLpq;_@% z@mG|Nbr-}3bjc6 z!7>`CP{O;0m<2qDse_Hq67@Wu4V{t5x{$Clh1Dd5Q}wlK*_BSBmY>rZ(I-5}O_V;7 zA>_nA>1!)j&tt-!0W9YYd@C)yxag#lGXC9GC+2RfqHf$6I>enk0*9Tp#2+52Blg_@ zi+Ka9C{i>HG1#iP}-ybY^gR~|E5Y;8L2w+pTA!Tk5T$yAiyX0*4fG)n&k4!06r>Q^K zl3k{ClQvUoqLA41DLxy9*avCW7pBT>sgF)ydkABP@e=xg35ksH5FvPhvFN`pSlX!} zu_@f;WNuQFQH~iCV8Z8M!YMk!#DfgXr|Ea6hM~rpkGB1wd`Kn*KvOp^-f3oDEQVD# zZip5x`YHyB$f_t$Co;@@eFL-Dpa^IpZkpD-qf6kuAz`L_om?1EwT%-41_e@r`YAyv zy!iG3Z%4}C1`2~~ZnBgi&&+Eni7lzx$~8Q1USpzS@p^01l55ig9ql7M?C(zB%HP1y z`mf@i*Bw8I_eDMBcQza{`mOUnYnU~sk_6;K>7@m%{Ng!IpaTGb4uYj7O7u<_WyQ3^1$ z!19c&@~rmqx69=@Kg;u|Dhfm@NOy}pXUk;Ch>H#>A1zhX{H$>CQLh!LY&5BCUany3 zDXna;>{zbsa?-@Rh8{?n0JW)4J;K{Tq{GV)Xm7-mM-cUC7KJ+X<{M+fI+jsg6+1Xw zyFO7RUrS}0O=f`uhp$LXn?3`JbIt=7%Zho!iFvyn+;4&ecsdnISG8f+;6dLpSdpmk zQT#sS!_F_2=JSr#=h`qS;PvG7$%C0OlU>hgV6KqkkdpN{kxp7w@I0=Ivhtakp{6k< zC10V3Jb=|RYhzi}GOw`W1d+MYzais!0J%2`=eeqtJ!4y}tFQgrzW+baY6CGaFi8Jp z-`^AdpV+?0i}U|;+t+`%?>DD#{oB4jp^bPOyXxf?2z^V%tMSsfPmAys?CfJ#6#Cdz z%xG76D12A@&zqy>s1s!h;G_+G?D|IDl&SN&z$yHlRa|01JKFpr4hi*HMLhnB)@rN% zI1yp79zdURlTD!A3N53;3&DR91C7(8BJa`W7r&gDdX%Z2x@j>%Vq?QaPpi45SNP_K z(cJrQuYW~f1t^5?q3$=ly_x=F`S|6}{2v)JPLx3({OQ%pCX4Rb~^{jF&G$TFH| zaqPkP-DR4uW|0J$ zvIqqKnoVYks8uYp&Tp>oPZYbf*|mJbn2K~L_@F7fl4fDv==VxczwKLI;zkQqt5XL= zF8?OJy;c|%%s!uboSn3QK)9_$CWN^Ei(HCC(Mn#_OsrP1#hGuiC=LApyUMKm$Xpp2 zdzN-C=}lc~ISSt@ioAk3m^~$vrTk}VHQQshSvtBi6=bdE^vF~W9sUS=B{t@{W`>~g zFsxCI?>)3ey^SjK9k#7sxqSb_oxK8W?O$ni8j(Sey1P1MXp?+0!O_Q73w5)wrQ-l^XPIbkKk24 zwEPJkI@OLiS1znM)}sF^VB#z{GO$yxoMs?4Gmd*U#w5#3ZjvCMCNfgo(*go=s4A`K zol8-)@Yfi~it<0!nOW2OuA`n5Rp?DJ0@^{pIo|4+n@TP=eR6rX;xI{BZP zUmmpj?q=9`Whq%Z{qxv0{jbD;g!X<#i-qt}%^>%$mL=l6M$y$DC2*f2vU?xNptK`E@?j z5A%d(J%C`+D@k|ARfqj!-@nBajJAE*|2M7nx5e7OSEC5+mHMcbPtnz=e>ryb;a)=G zI(>|h0W8)M%k)3Ek#kY^CV-yxK^Sfs;{zHb(8D-Zf9zA;M7Dii_Mnf5<7=Dc84i-zICDO|crK?VYNHeL4lqP$W%TbN_ zZ_}Ne*H$?|4D-&^DTn1CV@kYWQlx z(>kSNACP0I#?U=@&6~-5SI{#QyWvF_2Cz%VsUOvyNI?2f8u2`SsBTNubCUl?E!E z8U_6d_Q(S*T(9|fyc9+Y^>XR@k@?0K-ve}MmGnm|V9DGC74AM3X4V`!?KJ=+@EN;O zs0ku>4_K6Sk6+pCICW1)R?-CwGNf~GYm;ubia zd5Ng^ScwLHx{}?6pn8vMJ-}>Ly^YV5Poxck=;fs>J=)V=xm*boMxGk5e8@gtk8;k^ zpCWF7)c3o;6cySF9BBVl#5Ie;4E?H;{(dcibFDVWWR7#|J&rWb`jG16#>0pRVx4bF z*n?}n?TfO9`dqOUT>T1ok?fW)zAFXOqx;4W-N8M-oT*bplV8(z9^Mnj+g)%KU?8Z#+Qkd=M$Xie-Us}eE zr(mUWsxgcbXg#S^F`OIiJ<*u|6;`D-qx3T;Ob$j=;k*7Cc3m!&wSNxK<7D zn(tV?1vxXXqbz(bk7+%MI0GMba|^9J1$eA7nQhuSBRiO;v!i8yZ&6HMe|U`Q3%)zu zh`|;%M&;2`1}UdQoPI2^Hym9&;6R z9uEB@?Wt_W^+_4kU1wj;_9Kiyj;l-2BCA7@!( zp7c*3sx-Or+)Axh!`Z&a($N;KV!S6j%IRqn^#$E+1eqkd`F7vj)`pOa?Gmq(4t#ig zRxcW8K$8R>$wiz%wZ(v3;@V>bgXeLxu49abKJRvn8w;b&4m_2-fV;HMs_(09UK|9( zc651Fev6wKm+>r9Wx2|+!{6}Z5!!IK`|%N~bPNIf%$)1;8CZ|oV+?yo#5PC0#r7b!yYgmg^zcTVltT=$Vi2r?w3G zJ9}`EEyd48(uV>tTqQanh3fAs@LOCm)@JbcDJaCBHpEZt+dd^xMlj7%sDie4L!W86 zANl-#sM;pJem)^fYuJ6SAWk8{$*ZtK(r_Msw4OVhzVUf!WcWii`Ym!n?j>mzDa!t6 zddf1e{8D)1vxt(O@Zv`yEd3F_Op(R(ri?M<1g($WA|q6c$c&AkdP3kQ6jB-vVh)P} z)czv2U*Oi)f^l_G`+#U-KS8HxZ7(4)#~7&J5SXM47Q7T0fs6*7M|0E1?9WA^46R~J z>i|5sF`k7nMxkMeLohQ3GK5fUMhqk-BbG8hJOC2)_6yA-c^obv?nOAzuvIIa&4acc zP~>11Wfk{sh^EpA&czVl5JTOh1aCEpY-=^GyCw;`jvu~=>Te~d6B6k2f7MGLi*p%1 zMv(w+c{O8{u%HB$_D|Srh=0+Su#6Y4aFsx@m7ph`NVXR%gc*52mS}D5_(@3Yi$A@y zW#S|zJ{+FpE1f{fm$Y>q8u=U+WiYSB_z~Bq_Csn?oPWvME;WSl?GEGM~O^-bdsGVk{oQ5ZZ zpDG10kA=;jrcuzpmO~~Zv5kz7X-;jg4clHj#WL6!!>r07p6oC?N3dVHtk{7tY#*FF zl`ery2VlBO8fS!%0w^1C&h0Zkr6AoSGsf(~Uwz6*Q6^)y6lkAMJ!DKFO3P&J&s3t% zl(q5{fH>O~X6|EWsTpUQ=Vvw1$Cj~^3EvphD8mw!A$6Hfxx$nRQZcf~H6ZXm z6JLq*W(fs8sW+&UC0r1YS^91%SI;UZEI$e5q9Wi`Ar#V1sneeHTt_s*Shy&YF5W~s z<)^raVT|)&Vp>X>o@Y5%T}Gg1{EOMBw2IQ?kx=q$8C8r5hMs2+0-`?aR_y8*Y1gIu zZC0@Kl*7)MOPZ7G<20L1+?rDEOW0L(TfgjPgJU{~;8Va>kN~M$K=o34 z75(cpk8+q#0Ci)9;L8QrXdo@kA%tB7{F0sW$OPX0Q?N4-V&#!3s$DZsAsFdIo5VqP z6ZkIRQJjjyyRq5l1&1+MzevA<=oO`EM+CKPw4!fEBq2eNHC9vnqjwLXHHpv4_EV!t zO(Eo>!XFN;Qu*rhmx?K@>gV(8g?do+fx2cGyY)i!`LqxxN_ah_pl%?pfux|p4xs?`v8(%UFp@ zTF2MPV`j=ZG~>%OE%p$ZnM$|>(LStfwtLj_z%eWFS)&7=Cuw9$O<$vJ5A#7Fx$Of% zuC11szB)hYM$w`cdYecaJK99m7D-G~GR#&7NI6QWEs(y6fUk{PyHz8!O)K9&&spFX z6{K3R-QKhPmmj1?Rp^pRSS_8&Q#9Yjszto${bTK9(P$TA)d72sz}uey)6|p&qtKQhQa`^= z@~5WwnD{A{NjUk0DcxzQbc8=25MHl*KvjM~QFmhzcBf(xZG(wFfrvk~cWd8}RG)TN z=RMOLCceWUIprm}&?2EaCVm$JsyOX=Cq?{2isUk+hwOPT`KummB#DV<*-<4OsiWAr zDE_Db{*)Hc$`sK`ZJ*d`pZF?~R8^mZSif9HpY$=22xq^fS-(tGzx?lht+Bpt-jB-| zgkrymjNX1U9wS;tyBkhFE^iP$6&tWo8~A?8c#5!z*zV%d4jT){>-E5mJgvpb_dRze zMhoS25Wv4hvr&Jd*GZF+exX zSnt?KuNY7u6S$K{`K@-?tj;#q1xn_u&HEECp9ZH$3~arDmuyMgZ#mXNINGf?+9x); zNi#YXG+|&lR_!v{>H@5C8C!ckdM-BE(=oBHHm2(_`gIkcZ#mXBHnBbixEupuS&d;? zb+vGgVTn&_g^W48CC0KM-lHRtWB|j6NTK4;5d7k@+2IasHY+K@1+_uuJc?u~{Jjkn z{(u_rW_9Ar@0nx5*`I_{=dUI%jwkn@j~=c9)OdkYzsA)uCb`5X@vEn*o=A)Ei~@! zAF&4LFc-~c?xgVNJqVQYW{plk6_$WX1VLLZNP*7osU1*n1E+;q=}q<~T5GcrVzS+t*W zh=BFupppcTK0#G$?dC-HP!g3`jn4k^_Y|0}V^dMaB1}TPskm3Z!s1P8sb9FET zMC-CbG(NwD(4XNYGG=ys%}ay=6B)~|f%Az!(Ggt~5l?ro(a>kbyeD^5hsKSHIs}0| z#7DddS^F)Ci@-oF%TcXv&|o)CSsqbK_oq~GV1z5ML5rkG3V{EDprxCrj1D+*0ve$O z>Ua?Dco53*0*z}|X(TqMjzH&hTS`*IJ77D#Zk)B#t;q3-y5O;qThOW&fdWP+aDoUq zPUH>-veXcf=B(CuY-WRjO}yLgmH^#aP`Wt5juruWUEa0n+3TR0{DCe`EZwoi3I;;pfdIs_B1ckbha5(j9Yg$-+` zCy<+9aD%8w0lrwT7?`L$`T#d+7gq-1HjI)yJib?8zHaQ7fIO$4lN5EV)7)kqO9*Wzd(W^XxH^eh3@ zi2(Gde7PG3D;O|{Adt)3G_W{Q4LJh20lpo8w7Wqjs2wbNI~>$uhAUt=0r0fsYunU8 zvMa#$#op^$;@LdB^gQCjQynh}qE~eLSe~kG5^G0!-;O-ifcJ^qx`=IGqg z2VoWOlBD`h1A?H*1E8R>Hzl!Wlm{A30P5y_Wk4M-hXC3WH?=J{TTZ`rb@gd$Esv@D zOPT|FSHB{~k1Di|x?i1i;xExmT)KyBw&W3&b%UHF&kZ%c)};ZnxWPmTpq}xs(bYt8 z)!PkUr!K#eT)n_2d;aYf^^Nk5WT6|6yn8QlaxdMIsQU5RD-XjsgubU6(SpYf7TVn4 zjvxEv=6;R|L_CXf&i`Fp^_k2&I7qw?o3;38&of+ z-+~N?mn=wr=z9R_CvaA!E>zclRZJ3n-XIzb0l4w(yd6ANCF!guhtRbc{CwXqKWc-DX;8<%>@wN9~e z>g?~^U%0c$V@P&S)SrF-)Jqu3yju!pk6)~wEfGnjkKXrdEb5{6{YM5h=AHl8_vbPg z!$H#~a*?ZK(Eczr`eq;H01^Kq3R$sN zr1)eIN_*CFc^#oP^p?&+kC0X-^$X83@W(OF=J7Se zelinE&Tg_lSv-m)qx>{?kSxETja%xIVQhm`qf>$#)Edcv+>Q9c8piL*K8@9v*&a}0vR5X88^i?xMFievBnT{5@lto056<&@ZMpjIrIfHziJ^iSj1E#kwJ z=P?q(;Oqtd+eBZy__V3@@}2?V?oL+LWbS&gVF_lIeOQeqt3oA@*w;L^Ed_LR8u^Yaq$S+X1$g!XpwCQW~@bZ zz~Y3*xA{9YH*cbu(Qpu}haddAd_<^j+5mq8f7Oz={@w;}za1BoAbj+T7aH zTHWud_!%60pJBj(Zb-lX7^`87u<(At8{=A#TRd`Pe>D&f<=Bg*$ z%b$|u*{79QsYSkXvM?X@L-vVq)S7iGA0feLilhQOKZ*Y9YsS^7sJH$@><;(_iLtNV zn4qWXxGK8L2vBMIv$UWk_(qSzi|2INT*wA#tk3zUTS?P9;p=L~EeNex&&D zGDtJuXfDBq8cXvc#AO^;Hu(Kv@-Dm{uDbo$5xqDhucsnd0V?=VL4twEO;1Efza*11 zzI*GIPUm|dBm+?^Xw;)&%sb>;ODE1@ly70=kpi#Ji{l((A^2XAK9R!)^l5W{i0fEj zL-;V0-G=+26P7i+Y6=k5k#C+n9QNj`o`jk>AE>8Jx3i)a5LjJhahNqGIrlMQvV)$W zaZ|%)U`U+fBM(D5VqRpbPRVeX-{q6jQDMV=}~syWMMc>$TI6c^4+001p}Fc5o6 z>CnW>y%Bm?4#P8-VpHgm0zSk`t<0M;hSwI?D_xU5-JMDQKxnp=iaHCEZUwU?Uq=f^ zZA$3VV8hitu!feIz-rR@8gl?+xFpLsp8~1Px_6l18!hUj$352jzn26^71$8@4R6QAPKLc5RDHn>UP}^b(pcwG)zO!yFNHo`<*u zAe$emK6rAI1R8qC-u;}6TUw-m`}Dk!QQlESk_pkqp6Iy=9TAS%oMgG?%kwDOdko~z z^zXgMb_-+Gn4hnEwxyfZe_;f7YuC+*Qp-~~Vb@#7uV(_yWgCj`->bA7=*_ghmu03X z#QQTvVZ^CQ2wz!$Du2YsWhktMaD>zIVR-v(AB1CEYyAZSN~a^LEFBFPTDCom&tsaP zfJ#L$At+&)3iJe0G+WlpqQ)Zz3-#&2$2Pq0jVDZeV{(D64)PC{M-vws^Aj>2da~(_9I)+*}We zhv?uv1^Bg^N|!uMdu~&%w|&3!R9>%u#+FFl7y|A_YbMT#-c4b@vzUlZ4~JUc!;XYX z{D=$IjI%2>lI>JsfAnTojm*x&A!;XX)9`hbQHMoaA#`&UQ@Qo`zrhB*~Ch@yGLd)%13DApsw~t+!4F&x(t}kN8V5 zD`Ny(mKVNvHtA4=k`h*lEPl9GUW%z|Ye_E3nZ_%i6`1W}C%E!y@&#GYS5p^-M%?Aa z0pI4PPFFX-m6dfIzt)f9ZeHmttNRLm?em>(zNnU!&)=f_I(Eg~0~c4;t_J+NzIVF6 zxLR4i!}0G1N_d2kuWsNf`uD=SJfZ|wH%X%X`(^HH4d1PWw8YpPsV*weHXZv80`L{G;TqgBxr7N?MJhO zc<_@A=>@^!D@+o+{}9;0$H2oN!LIxZ*!k0ms?9^+91}fM`QKalCCt)2^GDME)yhv= zESWAY8HKOXNHes29m_nx!)fjxohZZ983B`d;U7;vW&RM#xgBY)UTN{z zu5W5Lur+V<+dcf#40%#wdm&Int!<`Ki6UkZplIG*N1NePb2|K-EEB@4YJlxRHg+R_ zg3Rj6Zu;dw$u}%U&5wE!tT<@J^cliqqR4GL#<=^!>FtX7LJ}JLx#WsqY#R;P^=nqbH@=ifz*JM)l17TbSEm` z!-h##*Mxj8I`27w#bc{zJkKxFF)*&{P00yfID_JY^LdImxsMc#28;3^2@S&dVK@m) zn3-C>yyI-(1es-i`uh(f^Yf)O?II@RLg-R$lEtiyr?mE50(9tw91pgojTD3t8Ss2c z2!moWd+HNifgLKDS$6&@8B3?sj4U3cJ&SfU;oNS9rtl|HKGxgJDedPUKS6TYKRcM_ z7-KR!I)=x1m&@bzUXT>KNP{3LjOLWcd~E&9ZONE5{x4A|n(9MX1=9q-ex6HR=5z^3 zlSxXM>M+&uo8+?!$U9M#P_k;Tg4}@36e7zkTM2VTtFh^;&xCZlOw^IS+`_(dGZv=& ziRw1Ah)sR~)L0`uez?P4mVELBTutUPs{3ApkLq46kDU#d**;EMb{RIQDYz4=l6oo^ z$)@TcbKuhdO|oR>@>JF7(0OJ6FBJb=8TZj8x13V@0drfE`RZB=CoY^iF}x1TL4i*|XlkmJ1iw9x^xM7teL446oL)B3dv(TM)1Ms4^X+z5 zD#^VoayJ(Z?EKG8)JrASZ-2$<|NZ9J5->XmIlK871_j_x8OZg#^qykU_(kW+%ZB_a z-5+5E$ye9QkS^(va^PHz!6%aGotdkRR2x>%d5ZVe;rdhx^+G$p--E4u6j(1C~ zAYU#H79AtnPvVv359UbhkJD7%#83Gm&GrK=Nj#u~mn!u!7)NL;so{}{mdWzGz{F;i7!#;@mWIB`|BywT@0m*cTGyE|K29W4vQQCg9kO zlN1hPV$}ER%ki#%pfOq`P1ZKN5S+|a%2|A+gRfLotCNPoq{VN6uVS=({<`(wo8i>{ zN1Xm2W;o?cN4Bl|uQB$edf5^3c}&R^^rcdB%3xow2LIRBB~o1hu`fBp-kzgu@c7w; zAxC=s&?~c2(RJevS6Kg>qp?!)(Rr9p)@*h>FROGyw3f%7Zh;+s95YVEL;{vx9(MMZ zZyZ#Uvo`$*x=#+GL|fAy6?#Sej0f`H8q;EyNeg*_Iby0JnqF@144KJ8ilU%t#GalX z{5JYSV!;&my{6PNekbAFiqZ6Y{S|uS9A615^oKnQD@TtbAGU!ER;o7XT7*cb=N9o= z$G7flNqy!R5_#Kj&!$>aR}QdYw>T->^Q_Mj&%$%Yp!FkMq7sAFJkl<Jf}7*7`cv znp3;7`h#Xj0}H5J3TZhX{L?pnF&Gs6zKEcKuvPCV(M z1a|;XFOmZl8hM)9!ana%qn0Su?pCDV*|5nS6evpbr8k}a@iu`FuVDQixQFhCYvS_; zVM|N0Yv%aYkTBf;gS)p3i?Y%CwE<^vk{LQhKm?Hxlm;p3a_AI6It2_;rMtUCx~03M z8yOk|bwH#+LTQJ6dEaY2dp+^)cYWBO_D4Prad2GM`TL(|Bk^qvb0MJw)M`iJaj0yH zGj-6zAf>xY6{A_=lu-jgzh=H3M(DF%qcRF;?fy(o-P7yIv2TN!LY_0su_iYXNK-J3 zQ_$~qhQ?Rf$c)4su7V2F=rUt}7?H8wOT8h}ml=O})%+J%st0stU&@^U8v*z#Uy;e@II$FBRzV>nO)H$K^$~wu8 z$m>9>M>(g}HW@Ys!$c0xz42k00+J&3uQN09?zAOKeEmqCL|?mx-yyP`K0ae%^`Kb?$cj(G6;~naLFCygIoT*V!U9 zAVJsjqi4VFjAM!^`oWNv;z156=kP7kgr6q8hsf-Jb&)&LBkZFG?fc~qHc82+$pe0F z>Rgyy0BZ2Hw&%5lCc70-#Q0L;#@DK|odu%JCHl*g0R`x{_&4p}xAJzTDNcW7R7iG7 zrk;%)oAlM-KTy`Ns~ti2C5~(G{M!n^O5OK$kqgVqtA3B~YzhgSFR%{_Hja$L0HN^HI*JC==+*7QSvV$E47*ysc zABT|QgHyyp2ZRF$D8uzhql>7$-0y&>eO&;M%u03HxTBR5{OG8&jOd5xcRLYKA5RP)HaXGFbu2YO^(+Z2V zjG{O~brnXL-1577eC;NI;LGpfV>RJo$9iY$;ur3e6UP*|qiE`J7i0675%QQ{*)a!m zVr%La#HXO5I2$?H_R95DDqlr?}aK zH{c$mT)uT(z*g;EF{LItF1jY}ZdE+rV0;RBf{d+*Y$_9peu9fj0%K=F$1MS|c_F$J ze)Zyn=AUtLvC+Cp%)f4t>nl-+6jPoyBvKICNkhmvXeiB6Db1Tibp}mO6q20x6Q!$? z^vB&?=c%463Am>w8xlsnrcGhwOY{=~SY?WjdF)9|a-FXeI4?zDKdJ3yyz!OP5RcT9 zd5M>)siy0`i0)Jp7axZQsaf-sty1La#iV&2sqC~NM1*cO&r%xtQ|e;VLK^w&=f&^N zr76ayQ!S92@c7>+28eH$=h>T06lz=iur(}@3B+X4_5Uo;;b5Kv=4LCQ} zg=PjBN<(b|Rvhn=X6ir4P%+D_J9w)voq7L|X~IKb(;y9c_uWqe!3B?ZzxBkAJl>gg z1LXL|8`%h@jdw+p?~Jbm=Z9u#9nvD`MB@oOF@%w?V#5iVBPol;Ga zp6N2y%+os8=cP#(->!ms5q~dTWZ-`RNCLflp6tk z048*UF#5}F^frO%GU5BW>@{*byqRtBH{cOP{_@#3i$R)hZ6n0q|x4l<+JVXM} zC6F1(0Ri1_L6!wka(XzK#hGBd6e5$iI#UYPRr&B%diCv=YOV!*HMbFRgBOM4t>HJS z5%j7NPOlM7uc_&POANz1Ff{=8JjB%glB(U4s-1U+101V@7rx?LtDu4!4nd#9Jgyn9 zQa-HGy8%tWBKw>HpalnDh?Unrg3J!X_5FMGj`R(#VhzvJ>n&C4-GFQ_yuq%ep}!J& zAcczJNAEdTG-THm_KQX_AplFJ{$8U#FIbD9DZ!{I$*U#tHTo>B%?1NZ$+xZIA+~ zVnI6CcAc!2F?;Z?*oU^Q_FtSIcFtS+wpw(UK3pucYgM-I8-4h_(7G-50g};isq*21 zzV$S{MHSmVBGyT~*fPS|Ns`n#vDHGp2y*agD^fxKU_$A@%S>QUKs6SEh4y6uM8t;y@Uf<>b5b&mR z|0bsY1p>96MY84GkAJa9nSYyc`J8NjUDPr74`Hs`f@O;Vh!0+73MvCQV+`D8+jImD ze%gU7Cji7>0P#ZBWc4WQ!Me@iFzosDBWI|D&quk3#*3t7_-TmtXC^vEBWCxbj z#Wci=Y`s}38a)nN+c@z~>2?f$!olQy#_gHFPEsSfIMedIp&BWWg$k(j6+)KDq__=) z*_&zHo}S16-+T{q+9OtR27U5z0$}C_99%Sle2O>ygeYr?+nSP5o6H|=HAjLqDi?(_ zz{1zSr9O+A1j9u*U_(1t%vG-v)2qugTI39Jx^>$N@To zL2YG0!kOm6vml5ajbnZw7(eF0O|n^uB_hFiI*v`^`Xf5jCw-Q-DoNmxbvFs4atuT& zX~hOR3f?_I)V6#s1Zc6(LYD&~S#>e}f>`(!0sznAPN0M`q^!?y+vtcsW>9wrwkWl@ zvD2m10kha5e)MQs77ey+S($nSjNij zM}0eK0U7u-JUxe5&^MV%OldQXx;d=^IQ{O}#kRl7oN8Z$ic752imw=S zAj=4bVmQClZ%uu2Ml5E*6wuw8nEK5q*c1BgZ}`?7_)rST_^C z$TcK0+RFd=8>SM*ruKzx=aYu>mxipB`L>_(QlKq~DaH}xlK5Vj)aFr{DNG(`NHa$= zhQn9lUO6JE-p!^)ZA3HTzG!bG<12sckt8|qft)t+OsF4B=;Z5UZ|%_c(7CwmgBag~ z;+hs!UNmraR>KPk#t} z{;AmR!5A|C5!kx^r!;T>B`@x`@jCIlS%Pf!&pC2iQ@_uxmBvNizzR8PE!5IjR7l>x zH!8%$3fM}OSLah9Jh_Fxf%Lwi1lC;v|6jGsamo;*{ND<>Y4Xn{Y8$|l^#Sg z%B5*@bUCX0lN{?@lBV9Z9MsLYR8BDE<3!|`3)RyZ^hSon3N1P{Egx+vzSq&nlO4?2 znIY9#wCl9&ZJ-h{FU^};UL8sl($kFkJ$)1gd7@XbT)h+&1`&GdfN{Y;`&N)d!KRhM zYdg_1WXI|Bvw1>y7{b}qK-T>V3=TKBg^6M?9|=oLZ*RbS7VTS264a}sxs+JloU6Je zk5=)0l1AAeBtIA9fBcj#H)N86ulnVkv>X*8D4lFbb#{*JD)l47sR6p4eM;)=W9u|H z?BFW%i(c>r@!GtQQ{^dICHlV>FQ7dd9F64h3h&5u_Hk>;Ttx}057 zbkqaF6NGZ4EJgKlI+l}kJahfR^z;YqD6dtmYtTE@iV!Du7kGiibhE^tgmLAClT;Wj z@-9=(FE+}B3DrvH*OrwO`3Z0)xtli|Mx{93;V{X@YYKg<4r58a*)Y$>tL-YXnk1Bj zc5LpLlij5f^fdpTbgMdjImxK@j7R*C#W0xE_QIF_h6;_HMYexWIOduyi^D*!3W6>= z56=}U&jd0f+nBZ`im059D)2?O7qZM1gUEuXosmzX-)}y!NVcq>w+hw2Ng-Pxmxs28 zlLmnA!kF_v*^{RB#^20>+%3IVs6V>MfvxomdtNM6{Yb#y+pUhNrd#IGtMLGqaNbm@ zd#1kW1dXs~y}<5>FXQ++-U>RY?5Hd&Vz;rB#rvV%>+Ab%MB25*wd8Nlcv{=K{= z?#y=B}G65#HhF*P!K_I%n-WO&hpR7`n3EF97sc+=prTU;^LvP@m-%}Y`+-56QpjUhdK(+Fw` zBKaYjEA+AN?x#jJo2fr zx!{Vhg7D0Elxu`M|7MMXg!&DJahyC;+kT|`NOw$7f&vSW{&f*1rmx7}$e^Q$Lnq6j^gDti`{dqSBc_TnxaNF^nbd6!Y$ck#5@Rc@zT61ahuY@b8BOGR3!KM9;KxjeUv~?o-_;avc-Rs(q(yP%Qt7 zMk+<{dFb8F>zcN4lMo1}ugPgY4SpZ;o-Utgm1_%Ck)~VEvzJNbT%xQeN@0dTX2aEF z)mLKi)AfCTVa=dwPJ-*GsQa}Yd1&%Kv zwUNE?QZt(INT5)3iA#p7WiADiRY|Q=CNLvPo?AUrPe0KwshBiotp0>7#s|iI<8(g8 zD{1ll$Grv;Vv$|nsMPqoQD{|fWG8-Giypp~k_Y_l* zZW1t+)b`_duvn;|o>~x9`|+f@}HGzr{@7Y7@27;O_YTA;|CEvpG(w>R&&PV zxqeAv*O)+Y3l{460R?&2xWrZqHlg{!he56hp+|5Iz4;-%KG&qWR!a+xE!jWlO zqZvAYvI<#YxGOC?h$ppG3=f#N-z~4cxlQh}qKl;x4B9$!*{J_?<3$(`AAb-TaqLv~ z;KA?b1{yo$dWQ}(d`)$#t~;`}dx)7Tled?yeh{uKj(vH&({}(hm`E4ju${%)3xwdFL7eJ%jo==M>3;(^NS_y~S$u}kQ z&Ky=t0W=B(%P%`y=I#K|tUoTpjGuC&yb%Bz%@EeFd?Cpp$)$?rlOIR}(C9UhXF4Cz z_h+@${?aiC(@4bMkUisOP=;G8z_&tXJ!}vIyu~uM7%pvXeOVYxhDN< z#~SAgq5Jh&>59pUrXO1VmYJHr?Q;_^W`O~WKUna2$b)8EcZ}~fbs(A*mP$1dd=-31 z_B2>Z55s?n&BZ><)(#kZfaPapyf*?VK>L8^LzZI|)7)nDDQkTo-Eu)=+UPQZ+^|8l<$a8$d#)omIOtX^CtQBe; z?`Zl zD={I$N!kpenixhrZiLQs+Eq5ONEMCt1ji<*{?)Is%Y@u%y5T!#N0jY?iX{)tBAYFY{D_I&;X%iAZp zMZG*p`Mik39GZSbdG49=2%BxK|0$X!3*9&v=$H5xmm%FjOo(b83-v!-h72rSHhJhd z$~<3T4TVdpWwB69T%=h5OWnm`dSGC-c%kteT>l%*Sfy)X2^We>#iP(4vYv1=&C=TL zG+p~sbtn-=@mBgkI&TTFX+OwLh5z9IenF zf{Bnn$bYb}DrlxW=^i`5*HB!n{_J23z4NnBCi@m9Sa~Yve+-uYE;eVFc22jc9@2SF z)l;gIICRHqIXt$AyX^Y{Ys$y4_N8nARfqfUjJ_(*Hh=mnSZ;DW1O&@}1z2B{nqEDh zT+`@@3wXaIpq~RV!{7MRE|+`Q)!bvXPpM8?Vp&KxH@b>>m-)ydN+!KuI{#bzI!;kk zR0)V?Q3a1|;J59q#nP&QXqF4ZRTo`fqbthNYSla3vIO7J&+~Nuie^#Y<(i;*Kso#i z?lMTTxNa8%g=hP{NH%)3g(ttif@8F4IG1_t)RVOmSiC;5)lX=f)F!pP`w6G=H9kUs zUpXB*D6bWLF(br&oUbZo*JD^Wl65=IxkO^>>?;>3p_+?hWx!1`9MtHfOD~L9ha_T& z#7o$Z0l&vw$hrKR!kvl0FfMV*SO4CxoSs*)cOrK5v9!aLHQ(x7C<+U{K>(>lqNDLW zF@;a)_{M%c=sr;*mke#BT(@G>UDPu!OBTi^|J4+3*NL1JPOnOG9UfTL7gACWsWph1 zdJR5pcEl>%y=hd#>~6+dMvBm6YV1=QHHzIbvAg6JRNf@dA32svh`o}pKbrbL!sXlSy;_HH}IOHDE?7~2XFecYR7I(FNK}-87=F< z-u1+`oDJJ>+l4RnOpV=+V#Esxi}x9XavyVdF+4Ez-Ii}?T~m7+HQ*l9k-6WtHTvWA za9&M{w{oE6gc$BoJ_a@z+(o^M4!Z1Kl2WRa@hsf)dpE}xb=MtpM6}<;AM=fYI;0mb z5Y-)?D@%4u!9&@{jLdqhd;Yqw2j+|uc#G-H!GC>V^u_YmH!ByUmoe^YWQ&W*Ka5%Z zH<~n6)!Vqv&o?k@VIB|rH;RmYZ|Wb{H`jdpRqpkB%QV=$s+)&dGyXHnf^f^H*e9!X zEw5C5WaeUDFdR1|i++7su_<{Y_85K8_QMAyF2~D#!sTahyG5D#3w+Eyp)q^^%Yvvb zE9!KJvl^82fob*L^+E@gd{FvY>$=L~)|&u5U!Ci(HWbFr(ivKQRZ40#Bv+rzEtq_% z`_Q%()1aD*4_$?jN)940(64{_qX;*6SX=%Cz6g_dlGuJXu3r@-x`PUP|+GUDj+qX4)gL zybQkftB{h2#^(xzoRZ9kBwG+V;E;g!B^7=HkZuYaGH{CIvqCD`5XPDY1NFk|*KSda zzVKyRcMNdz+W~^*iW2lEimORv0X6bNvwngn+AS|dC9Movv2>^;wj1vLU&#YPeZeyB zRI=^?3-JNUa{-diz14-SXRQ?Xu98nv2kL7E2Jt!Q&jp&C1e&^2VQ7LZ^n=JJylt&s zzF+dNrFd#}67-Nbc=6E3bx!oTesJNl;IO;sSv`+0vy`4G3adH9{ys1ZoeO8zjOY_)9vFDgnG*4_QGRX0 zY2{g`>p`-rAXFT)9H@q%+gUV+0TQ>Ij{2F+@6cl{!3i7--td; zA^Qc0t74)7ag`Pe8Px#gIIr&|Q;e`QbQ&A2#COy51vz9eruYh(*DA$j%FWBGQPSCw z9NB^BPM>?V5ptce*Op?L0%CW}W5*P!SUlp+W8>Z`#>r^FZ9DOC8S8Pko3uu0xYqUK zP2=J@_`)LG;#<4oS$iUd_^(K9u*rzX$SUcS+0ZC)$f(#NG>#K)F2(QGB(S+80Ed$L z20FIKqzp%XQB#S$3E}i?=-bDd7N;^+p*Qz=6R!0H#k(Lp>ynI`ZoX8ay)y%2A`Qd* zGCGY79sEKS1k_{;a;@6=9dR6#h##I}pyBrE%X?2$d#Xr(2 z1)pj&6a={mK)D8Jrt3)?8sEZ}AW^!Ixh|t!S~9;T3d&{JRX#)+_0xC)C;;-`V!%db ziF4y2-$R|^F%Xm?T&P3*Je2fXF%4TP<+YL%4j2J&b_29DW_WX%u*n<*DBRkN0S)mcUNG?*)LtalUUhp~@?7Cg9wR9xOzW><fR&D<3*G^ zUy(Nj0We3w6g136mQ7XG`UFU-cxe!T-^ReZc@asZh-@_CqZEwGxpWo{>z0Cz;1IJ? z09gqP+s`EOwF+HId%5P}AO-XDA=v&@UfM%M>`Y+he6>`&0)mE>pg}#i>3&Kj?V{fq z#^!>Sa!{ahx*p;>DHu!z2Bw2$`#`SxK)P{=?5s+Xt;!-?a<(rFRP}6!07Vckv13v0 zeiJN@25U%xsyt0CeDHbWFeER14R5;p9I3N9UjcHwz&yJqqK70_4)hT7B4w=@f)*$hw ztFw{X1)59Pl+i;OmCh8UTNj76&=+Hl!9e18{nLk=Vwk*D!Edv@nu|&))>50x7Mjb? zn=9#Cs>NDrjaurxS{l<^koyhmH4XN`jm@Qur^2m0My=SSm$vnqg~y5e3wyrnLG&b0^|QB3YWE#rY~4Q9x%ZYoVRp1Qnq8 zv8Dfol(IbzverV{f5y zscEFePg&K@q0xEXz>mvjl4DUNMUaEL%)fu@L=I9F40KxfQ{6<6vn+QCue50_b%k~c z9+P)ZJ?J*}?d~D$v1aZOUFw$nd2Q^Orjc1sTMveyr`t!i$COy#M>r$e(I~7pZyA zd5$UnK*~LPK%Z4%QSnhYXjk>GYQg>2Gr}{@-(ze^5BG)7-i?QpYHfW1#LGV-lHb6u ztN(|1IfIR5dNdahFSm(~ug%l|;^nrMzedpEKpqX&&R<@_yHvtH23 z1YT1?57n8;L$X$)r2JHMvgMc=UkpfHKlsU_NSk)3CBV5)Gp^$Kw(+{FfK&2U)5*5U zxk%IAkeF!kjYe|uXpvB?g9K&M=Lm7N6V#I%yZjNDO*xy@)M<~6o9ZvJXp<9C12@>z zF>wtAS}({fZYuNH%_qdANZ%OGTqabeOL~^}e9iW?;Z=(q8G+5O51gBua$ID}+1X`# zq&&49Ge7>aDauQ-voj9ODA_D2tKZx##aG%1+LhJL*loRU{8KGxJ=*;Ltrpnzsn##P z+}dgQ-_?R=PT{-F+kSaYEqi^OKU#mz*#BrdSuF+b&5pKyd?2{WwbucncKEAWu-AnE zss-I>1+M)b3O$GYUK(q_#mV5dz2DCi%ylqOXsLWKc&o7NKqK5wY-{MU9&tEgHKckt zx?LA^_%ZX-$Y0e0e(6U(EBDIppnwsys?g5=G=dh8@AKw9`u`9w?>wFGT5@c_=gx1m zHPrEVPqSG4Q2#C@9eDZTWHE~0K7Ty$ssJJ09U_x5#SKMKyGe(Z^is*9_z*$c<) z46u9ZC7s^)cqq}eZaeHa8>b-KI^Y1ed>Ry7X0<3FE!&6!omajAcrOPWFTBp;OUT#wyX>|6=R0| zD3tMwaSq(FXGT~Lkf~tzEVxxMeon$4kOzNN3s@e3ZuPLx@v)Z*=a4NVh2Ip_xLty# z1C0;|e&_Fe+!wN}SpX5@me63LW>F>*tJREe1 zD$J27-c^k2SR%FajtGHVy4Uf#^2G$m(ld;wD4rmzU&m=qBZkPMiON|*wXo(c1z5$W zj{YKQvNWW4g}&VcG65^?0ggA{qEw)=@mH=WbeV=`XuNbaC{ze-OE0fRg$=;EEgrXlTEf2py0~_jdpyVCnd(AtT!~_9a#N{$&mvS z!ZDfl>1ToPXtBCmNo-1WVU9Smq@xu_^Xw=Xs)@E!@-C@zFjmQX=Dks?Y{!=9=ah4r zV;4|XqJ3ziBW=8MTQfpAx#ISE)|2m12C-V$cvFG*G-h@RWm39!N|fc#yf{s6vgk!_ zBpPus(HgoPeq2H58ot^YH9Ia5bRs~Pc%7HxsfI}h?#94Wo#`!wp3H-v%!KVt4O<1C z%)im`<N@jz~DV;{| zVs6(bTL}hC$4%tsPF86t^H#BZS><#YGB&W!yLaW9S4u2;qh50F`F(7iWLZ}5&Sv;2 zVex)GHo$v{lku2DwaqT~hlXFv!ltL2HNF>$IqnVkh(GRllY#OJ9DcMU{1Ol^JL@KR zb==mM?xOkV++XRH@zw5oCtZQA<^$E`2#qoW9o*calpv&8S$<;Bi`HT!&$+lxg-BTo~bM*a`V6xYD^XOmgG;oQu^%b4`E{$~X< zG&ymbbi*gMUJ`Xp;#@qpugSF&K^5*~_&wI^O%?jkS4nE2wmk_p5~|{HtuQ9Y?HI-oiAiL{c^WK?z#%8F0QIKG`smZxm@%3{4$-t^~2{~ zkA?Fux~qci$H0^RTHrXoV7PAl`F8&HoN2@N-S!G#1dY~(wu{8hQ?~N|o-sdK240))vQFy|cYpdcfuSoAs<6u$iuw8+| zuk-y+9#7A1#BJAJj%@Eu@RWYqrF!R&*l~In)vD)rY<6D??fOooVn;4i1(m7m7sg-y zRpo1Mc+dVrkqfJV-|oRuCO4ADhP>CpV@JL6zL8(ZRY<*ayJFb(bA;-5|IA0tX|IT1 zMS6vMTa45;KZ65rAm-cD(Yl+_ll9InTIahwVNa~xaJDz?358dxY z%qKfuy8in7=8E-eq7>K94U{s4e81fU!Ht5UN2DFp*O0(`2sN)`JUKIF!Gkt zR65~fzZB~Ln~-}QL?Htq_3q)E>mj-!fr-MQi6`M^>tL&p(ALV(Hf(6-0LgY1@+~ho z-#zSy4@qXDKZFofX2Zh+p>0d#HJ2i;Rtygf316lOY=(rktp|3bL>UgiyHi3wi3xI84{{y|Ud@7aAE8vTU|-OvNl3snO^mdDB&fQbeHzjl619F3_-HN-5GhyErdMyIh2p@<&gqSV@OdVbst9VA2?b16WqrUs zw%RVj(F3U&Kd{R0AgEIURD=@x{gm>A2gjtyTOyaYvv`BIpF-bS_e#e(CsixP12UT~ zQ<60%lFg>f?fJ~z)6BioOuu=OCCocu`+TDG?xgA6#r!+G!Mm^Mtgju&MRe80!=e1gr}+t@ z#ZO@5@d>GPy5NO5zaU}69Snpgt6;jLV16|s<4jq*qqxk@ENHMeh_=+t23$#3s*8)r zNN{{#Qd+(l0U9qU`czVu=15dkS}R)GbB3ydmDb!Y)A1?QvMn353#(6aRL2%kZ<44g zMep)Pk0?bR7DXhU<%3*`MY4;-eal~5c~iGp{BDyd5KRnMLab%MFfoo8G&oOD4;an1 z4i$gAl}MnHacl=(WKRq?%=jHv+8bJ`?E|hI0_iX%rY0!$G!=y#mUWesKL3S!VOXlg zTcuA>iLzHGRLL-qsyZ#IK25<_Bf^2jts`$bSi`xB!X7+ph*E=w8F^+86-ChJ0G5`T zeuLuW68}LZ^dU`-f>$a-6(~p^)S6cIP2@^9L4`K~vWK`z_fiz z=LH@5r9hY%aD!Amu#DCAf%Ru0YXEhVGlFCYS>%)QV$1(@9kvMQg9yN-9St8L;HSWt z7FP-;d>?!YvruXLXcGo(aW9<_Pxnw7doaNK159l-e40R>2!X98z$OH|Jprbz7MyXK z82F2Gm_iop6HXr#25Vr76J97fw1EPP*(8G2aGZ29FLH*UHPRGH-CS$Z930UBS!ar* z3CkeD$7V!`v`sJ}WwvS=H24t@3KXt_>u!T&Fm-xHkU&ge1ydbyI`~qm1sKORr^EH7 z+RHydzethTJA;}q^=ho(U@y2IXNFP$tPTehf?-jCun2ol1r`bH6lDm&6)~t9rbKxR z{D2p}sDQ=}6^Dy{D96A<=qt-r8tjMvXoCE<{69(MYT9)IqgKCjlmp-(!eTy2feyoA zehUqPpPIkyp{l(=ZQ=Pp01FUrt740fhcHVC-!Z^dbY`Zi}L6`L4Qd~y`rV(&~ zecmIMcLw!KkubKZoKDq8w170Sx)%!@0Y8oW9|dBKYLlUgz0 zOwNorqb_y?xC~fEONDhufK{+X(YHV1xISLuPFpK! zID=^&6r1gZk9l*?%A0-k;BNJ8gUEzqo@JnEXzsvIxTZpi%IU_x(IU)v5qSjaRky*P zItq%UNSd(FrVgZ&DadV|*mAf-wxh!UJA}%JX5)%OdBKn1;PO%A=MLoC%A%ZCL;T`! zdn0(aH(Y`nt+O$hX$(h1z+~~i>q}LJo18&qz|h=u;S0fHTq|;+14+9bVX)U^AT?F$ z+)_Bwkv~ifRD|h27pOBe0a@>jj+v!02&QbdYYVkyI_usBIT7#mc0`_ip7VdzI&l6q z2|J5ar*VCc8!&2(ZX;FlCuD2B<6f#vc%uqr)_xPcd`}8thK4=20qNfdt79euw#F}| z1{XUDQZ1(5Rz??@PE1{N0Bd#c;lc2hDJ`ZJ_wx_OVvF*Xot7#e2)yBmDy2FEbqyG} zCLR_X4tr`hg-cM{ZbP;J)RhVR{T1Q{8yLYbqOteE0T&VO2^w334s(GuKR2r2I_`XF zS&RYO-bdO09>wE3dZ;TQ9RaPY2;3H?6@Pzrd2#F&cdOrhWG}oIS2nvG(Mm*$3;aAM z!--1cLBUEks7O_(%?QDK^X*mGm0q57FHk8KTJ{C%hk^LvpdUM~j-ZhXIH=I;Z{<=S zWC0^sM}fyc0R}ffavRmh)Y!*0SX%k*Q%4K&aF;A#pJN)uRCc#5=?w9 zUzEL8U3OLw8V=5~A08fg6`ls9&qutyaM(7>&y*s?+yzoFD$R_>IO*kaP2NLWI zk$e0z&=gVOd<8!o@pOyW2}pzU0Kf1ZpH1qYa&~;-h40{rwXz1e)xd1t-}o57PjTZt zBdhnfVf#4$?ueAmk>aOeWvQ%95b-H(MsQta_oWK(=XTVD`&5U8ibr)Qf8GV0ecTQw zF&9ljJbS0gJ1fH=`A1))VEc~uKjN{o>m#_t%>Cfk(9Ab#-rRE?Vy$6JW3OHjCS#UX zM9t^y(P*Z92)e_n_i zn)r?sabCxS<5sh!edCi%B}dNHu+rZ}1f8EBH9MTc8n6Yr&P^a#eeN*w3l2H@5!T=g zD(8;VbOOmcf<79SD)4?F$c{{mz13twYw#jsp-M;|SRWT~Q>Wi3i?S69+EnCmT@-IG zaoBqx{tu2fSf^Ojv2UwThzE^iEL+t$9vWKEEmO^Axe2FS`QSg?el1;)>NZCI z^WC@>C33dUsSeg;VrN4fDo3#rC=f3Jk+<5$W;pOui6}45iT~NK% zP9=7g0>ZGef|*oeE|t|VWohuY92KrA?4S~9{a!QrS}T^2lpQf?fVeWOF3Ts^#Hvvx zNBzBeOODn$uS%XC^&k+EXO*&aovMpKHiZ{0-BV+yOWFlxFCQif+es_(gRM^FI*NTN zoBJrtnOB<=#uk-Z4CAgEAyZ+`ydWFB>*jSOZ1GFLAT7yG2#&OuVj?0!IKU z--oZgx&+fq*|4;*U(dtDh9fMUEDLm%SV`seZsg^uDS!3$=P{2I04g`pg?ZJ>kqT@c zD^V(V8uQ3sxjdH1_sdhB=;1xCUUPGLS@<@?<#nNT#v44|Bh$l&t=Y!y3!kWM{%7B- zcRcZYDR*A4c{bWn+2^t1+770h?ehwbL>;QO1Wq>Iz)lM{Wl~}ZVwLbNta^@CO~3+e zw@>V8+wp44(+`(_^jTNg#rK*Uir22c#G8Hk+Vsh+aLui+p!Gn7$6$MNh`l9`4<>l^ z{!i^|E&+1&pXHTnfrH03U<4g}|K+@S1>3b3IfU$PpWW8l+`hiKD&hV)-z+ayoc+2 zn@~CtSy7eiO9e8Udy-HO3BvHb%+7-9q}(@F^Ha=O#=$Iw|ITA)s9M7ap$tGCTN3mH z{h#vKvTTCD2s+>iMZU1y?f+9An{K-C?-6u>Im35h1yNuG?bc(?_^VMt;%Gej&2yG> z0ueMtka-+?S|$rUT0Le;My zPo~Y2frhp@;|TMz`;GaLN=yRR~N@+_cykw}rkgyfZy)SFM3I^CCEz zC{`bACD+*sDKx2XYAQ%|PX1|}BTPsq>Wk0MDiXbQowyn6Pc)TjMp4*uGKVeKc%Dt& zYa@T-8&Tw^TTK5pmO=qoD)&FI)E_s`)&DX;mC~g8pRrVRzAQD5`A~*ZO~GjTf5KA# z9H3f4pZ)Ivs!q{J`9HB#ci1(R`m*^No5_+#!+&EbKRMQhzpE~P+&sVl^$HxQy8L5+ z`WKd>V3*EZ6r}qe6Uy@kO93%$t8DO<_xBJRlh*gV*n*k`G@FaabGTN{zGhdg+5N#% z$33DUqo;zmY=nmw`T#di`2oq{8ul+N)eH? z--&5ggcV(I{vWVZdSulcqP)(3zn}W$m4gA+WQV`8)DYL>k;t03IEB%n{82Kcc+kD=t@cq$$ifP~9rFgl9 ze21U)j2>usFZ+|*-OwG?zVh`SJKz(!F z{qF&)h&}4^;+@iRYv}QnB+1_w$6qJ#c=-S9=E(xF16az54gN~`Em&wk+RLocp9#Sn zP5{KTsbT`2P2iLy+$_isRTqa- zwHrx;UO)Fb#7t$M!lnH-?_~yI4hQ7<#6Tt+moN069!)skQj>5mezYoMxE00Az z8Odcl9^kpL_a8BB{W zn#iLys`FgQMTkXM2mE}UM-nO{ESnhznRtfcZsoP)^E}8W>96#^cD=Zt$0G;D+CI)v z?*O@vjg@dje1as#zx z>B_v$3MN`#4oRK6o63amv{uBKsrwe?CWB84buyV6+*xpf3BrTih+N1aErm#T!XQE! zDfw2ni&6GoKH zCcu%V9;ZwQ(}t8#&UGMl2d-wJY=ip?=7c!;nIY2H4cT#}2vMmU)UyAJwzK|c!$Nm9k|wfag}mjAXpc(T!eOz z7dXKSwGzuLdd)5;N=?t?pqRknf)dsGpU)N&A?=OQ&QgTPZF#+I2c4s8kq1c^_`z@EInh{iY++5m8;QulYpG zC`q?brJ#Hn@Q=vQZ$A^Oiv{T#twZkH7RLB;`LJ=A&`&Gk(-#oo+kV7=$Gz6B4f3qr zzr&4~zGx$>g+A!x&KKDmui{KgiRpb;&GPaY8O17drygwkr&r5V`w12RI$!xb{xaRGm|9dboQ ze6kz-g2C*vH1`?t$%Wn$v*wT_F_weKhd-Bw{~6Oh?NU@P`!l9Z0gqAtdrW(ft*C=c zL2ig;{sXTlYi}PtElZj4hxlWu)=~&Rz7!%$ttNQ=JM_Kf12xBY%tO&p;N52A|@)pC}wz%P9rn$_Qchm}Om=MkgwY870< zzs|4@=zE0X=;Nl}EuLhr!d)Q0AJ(j{K>1!X(U)h(0NHUX-;J~47ak!Ov(7r5J(Ge! zqcM}5ztoQfB$#}3?mdCF*^Ccd^O4L!S1Yt3-<{e z!lfW%3aCJuwcO{v&QzZ>kas#pze-<_Dh-EAa^$V}j`D4;M4{xlSEC_CpI z(Je*8-Nnc6wu0kR^ki<18(DhJR?a0M6CMoReGQCsr`l>L4vC48n^C5)%H<9|4H6M2 zkL;GK$U%m$-ND$sx%q@PonWcW>_P0&*F1$NUWkT~sJZeanvTkRQ~Sd+pMG83b9zck zg9sPO9OJjJ6$P1EZ4iZ^a4jWWPT8H&ZRGW5S^7m$^zB^$-m$W-&3K2B&2+tgn%9fM z_s(k>fp@M^HFFk}9WIn)md45}vXrl6W~QdB6%P*{m&o?vz83jJO8nS;JI!CQE`NNW zg2}kE22EKgsT5X2C4AoA)t8Alcu8m~qV0)oM-lY$2ux?_{n;CV3h*(AT zi0evO%ljID4rP5E9PN5m+IBb`c-{baKf!={cn?O(f%(F;Km-Y^7z{5mb`oftl!8(I5?0jW!9zo}u6HqgIrr=H|pg;yxA0Evq|; zDSvr3AiRAeZ;xz_?po4yU*DPmC&$z#>v@^Rb4oXzHj2@FwM|}j5#2Fl#f4&bES2&h zI8QNlh68Nm7JJRnhfBwahYn}krSX zzj(4@8AzP=5UDVb$r&psw!v%nxQ_cLJ$d0~D0=t~fd)DUK9m9rkgc}O1kg~tX4qeLG!Zl1t2%9~lvxJgf znKGs!i(_5$6FIqUlnN;;mqKlldgMWLHZuF>lkOhvx`P;ONH*^)U}*dQD!pr6i+cV( zM~IL0cI>qkSFqr+`l!qU->zG%>^>5>>5B-T2 ztM5M5h?zJyT-!DUXogqaNq8U~2T#&osqa1lr0YkTS z5kb#BE6U})Qb*pDvNQa_cl~acho;_eRK9N}AqBCD+#}{l`RsII8NJ)))skP*2iRg% z236k$Jc=WHnM=gGrE6?4N;pl)Dd2qbL9`25-W^j(j0Wmao0(R=y+eWT#gC&EFdH4rzii2QfXSSBuj5@^1^y}`?KL}xdqCEy6%aJ}9iEcdZbK3nZf)rrxXe)< zR>G`kcUfvU8`O#Jmm{>tiNVrVf~_>=xDq;tUr6#Rq|P5 zDDN`)RHdKCrHaA|wOrfCJ?e**O)hT6-%m zNPV^KKite|6RGX&P@%m zH>UlfS*2IKabtxJ&NJS`Qf1D=)t!a+%=kI3LTxU*08p%d`1~fq)?}O5Wko@@v&68!;d5us8{* z^{wLWL z-R*kOvX8EsZrqY+qa(kyt)^-UJgeEQr*>i*f+ zuJxai^56Hqm)&`Hpc7zf#PPk}UV9&yY(KfYgkABxWcHL@a>OUQkBjs8>__oF!{fm) z4M>M(8uT9F1;q#u8h4}i$d#Hv7oW%=5_Vxyg%OSH1Q@YoK7Bg}dG2Tf^ zeldg~tr$qq`_U^;!3gtKW(2p6H`Nt);Sn>X(wB@I(&FpvE9>rs6+Xi0_9`nrcB3y< zb$sU?-Ly|AbaH8xxaj6FzNEu$#*+>+uYA4z6<>7usAGMdqWzwq_z7fz>a*Solewau z{GDjYVmRn{Bs^nAqv;yMGuoTyBW;1T-)A;N*t0kWD- zflfaiL`y-a+(0L!zvh*IG@T$#n}D221bw_*Zanf?d=Qp4_?`y7rVqH4L%v86S&{40 z6CJGa%=NJbetj2v0#|T{lS+X!Ozm-q#mN&Zrx0tTbCapp&<3hGp5pnF(CM1c*~!rP z8hNji&|VO6*%OC;Kg#=@VOup}9$jI3iZ&%DVFzO29@(TnoWhN?g_LE2PP&xaYAA;% z!tup1`xF?^6#a!029hTioJ$eep&Y1V1g*ssFKI_Eff?HEHeO-GuXxem!x?L-KIu@> zi&NZiCS%d1gc3vsBE9I5!~(>)C`w%les1Q+PGn2MQS9#>*#jSN5>wyZyf!8F%PCYYRye|M{|YNI8_Q@!*bIuyiMAUN~A7LLdqn`Pb9%W$y6-K4`h-H*!`uJl00M> zjy9wdl$5`Vkq^{T4B>Q>_PCJ|pqMzNT6I=hcP6WLlHIIj z-!)hIreygo&Gu(^3U|ea2>TBg=5jV8jFRse^uj!H#AJhqa$FWN>NjO)9^7OQ zOkvlEl5Kfzv>bPjC5J7-d<^ZwtYP(?n+AEpC~9~8mj)Zq_mKYs2|3mwoD<2NUEoi9 z?q8zVC|QzlqMKC1%u$z~xe@>+#GI?5!4^_U5tc}CftEsR1uBELmkh5D2_tLM7qSss^6jxsQ`eP&ykPf(Tv0-pS~rl^X%Gd2}I9yA=!_@7oePUxeObbB2&|%%okav=|#`mi;{ec&{v9A zAY@}4h+5+Z%k<<0F3E*5Ssr7>%-@SE+AZ>{$edo4G$Bi9$cy|mvb9S~E~ZtMdkUij z!3SG3_&gNLob=zZMUdfi*_Fgd8`6T+l96c{dKJ2LG4fn&>D}Q{e6lcB9vR(k?9VGT^`(0F04`RRT?k7UEcT}`!b^3hAdk?iK;Lb z_G+qP#i!!I;|ejBic9IrHwyHZ)Pc0rga$oNOn53y&tF`ggz22iQYPt{S!*#RPn(kUtpYHhS=V`957g=azunPHeFL_kzaIP)3;o^<0}OU+K#`5v zTn*iPA2Rwv} z5KG2Rn+E7l#Ql&Q&k$|+KB7IF2XT^Cjca^d&F@o3649QdLb`?r?6h@-%yxxc{GLmT zEQCg@c1QMg#kzGTHgty?bOXk#T0GJjG-;MWPmWa&ppMIL=qa3qen-KVXd^kc>o{8Q zO@Wf40D=?VTlmnPNMZqU~Zgz?6GJq>+*vwZ^>eM72!_$+-$H0jS~7<325 z|E2SCzpWyKoE->X7l}=p@ht_28ykAHnGsiK2li$Mb}j~hxwId%1KLBN@ub1ykioMZ z(6}XHk8zNwVG!S>anEY#sA2F&$k2}+7x9aMaTQQ4x}UwE|J#1syc8MCij1z2$%|AQ zAi=z(2fUl{pL-6>O5+G$e8g%Ejc9oe^7ElD(SzgqU|7l^ap=%3=rB>=Am6SFU+9qP z;s}WB3;xoOs=&~A@Tk)3FYvjMN0?Ey{V%GEgQ&aUTXUo8cZU>3Bd zVK3*JPrLg;>Em;}0ejeCa7({c<2am=nW<5WJVvU>yut2lXX) ze56@wR12?9a;57FmvQiHl3ENm0|JY;5W9@;E>+%`AYOm6~P@Feo=rE{EnoGVJex=ZY8 z2q+O@ZHQWYGH{Cv$Qn)hp%5aEddrgnPGhVE^oX-)fZZUhmF}+GU(o(t;R1wtlO@KB z<#>HmxNe|vTDu6vy>0|rLV(#Ywg{`EHj4k9ki?yo!hQJ?3n6qT1!`e{elWifiYtO# zgqF$!19H-Bg{Ve24k&UFqSC%zqS_J-Nus4lfKEW|A|XV7_P`iuJsEv5CjK>M6l+X8 z&J6uk39E|u5K{kq^xZJx0;bIm#Vt0QzZ-w`wCs!8+?Qq|tEY1c7OqR#2;5%LL;#9t z3{7e&{Q|;J_J$dp76PdT=II1tQHbT~v<0Ayl`GvtUIfkEhmc!0TBku*79lE#B{RlV zvOP#lDzJ10JwTBlr6;FR@IvY3wAagmuNxKcdM*JIBnJFZ4Vqw(ZPV-mO`bxcfdxIbUJY7Y4MAUt8(Iy*5&vxNB^)|?2um^ zR7gFz_H9}@3_Oh@NmT3M4hI)tz$%E{p!vfRzpeacs3_weP_Y|dgoof69xlS-JoSI~ zx|rASHP#L1R}(eXuik-VCy2yX9qdgQM`^J%SELZE_ZY&^4S(ou8he0bW?H_ zhLJh{tQyzndQ+3GcN=+muNHjoEzj+*y3;x~m73W- zl)RDQ?{J@1gY>DnJP7p7%Z?XxZF6mH9u)NV7nEt*UYiMq^h6ufqOLv4zR5evRajH& zmC=K}W33;)a`viSvDoFHQ1F>)KT{^jRWg3fkHw1;??F^*yk^{u_XF8!u3gad8@U8> zT_*IJZ7x(eY^tTY`Lkcqx6PL(fFlKDz%-^DG0-Y-ev&JIAlp1oxa;j;a z+i}kz!%qcSA>Cn4{6#mcyX7Rd3fl$}xA~xuP!FAp9a`Hi;@$SDR#|q%${dUhgY|Mml_+c2N#Si) zc~X_`)|!NDY;W4M)#vcE3@?aiqEIXJbyceugm>QQ0fb@>_%8o8d=shXK zg2A>b0-yJ>b_nZ$;|t0{QJI4Jfk*O>4X4PR7Hvio-(RQ<-uL6@Hc!EgN3Au658-6i zZ=`%A?ruU$=y4{i#2n>5`N*WYVs20rQD;$5A}^3+Ojw(_U3|JofRMhxV~apysMvO8 zs>nb=yauMX<8J6UvqS1LF&aVCb|z)FXfDq!6S3GvAt2yd@lhwnp zJ6&4Zre1*wIJSdwuK8TKZw3~`)zky7D}FJp0izr=tfmb*40nja@e9?n7S{x{bu>D}?dC0F9}Q^j-3djV84R!H)tuP_-xK|fCE6~gX5FX57!&-Za@(!L&r z8PpD%V7-?tzFZ9*0yVl3Ef(nzF5~w~YRe(|tj}}w3^=@y%}F<9kAkQ+NjM&Ld70X_ z_^~&z&{&@+KYwcDQ`fgFl-t~TpNKYrmzM=%a|K3uPcu$mu>~ri7Y2zClc8^1GWF>R zyPp-2;_TT@zC!oVdEAEV2vhnvE+F83Axo&aPUhpQ=3fX|JabJV;F_yg~oXc~t4?GR*NJ>+!lwkJOe1#k&;( z4u4ZNpAFhUqL!%C<91HftWrv+H1F#_6t4AlER zvkCeVxg4!ljV5@U^BlhY!-_2YEI_Kgwo^Rl5f|6@d1SH#CH|`2yI|~1uN@@{7t^r< zqdBF!2NAG%Y5a7EgAW_q63VR9SBP&Xj$%dPN!|EZvEye;gYR{?C!dbSO5A^K7;Vz` zTDsdyTw<Im}Ac2rNz6UDJxxfw{EoKwS!nz>4A+h7@TXGdW4Uk=XHyk8(iq zu3m4v_zp>Nf2-vJUiKE7*7ZX!&X*4LGx>)8)lFkXFWUr&6?0?AztVO}x0f2&a5*v$ zzUH^fmNeYbuC84(?ks36HQakU@z-3M`Q4l^nMl%*_K@BlfR^Qk-kM9>=La(mUU0Lta7uB)xm%XP3Mx^-5x5 zYQV9>F7;=_t9!>&gC2zTY2dqNvNY2}{_^%2RE=ixqSM3Se)d`Hcg+#e0a3~g38JEj>z$B)t zaA&av?tbxgR9zo%4fatuP|mP-e$py)Z4GyIYM*M1yz_7`-KtRLoM|ejyUO9NnT*)& z2!kUfT7wbz$wKKl`<8nk=c50fN3!?3y{bTx;_qZ`_xl-ziNWK^h5;^zuJn#4M0J~Y zqud#n&6`pK#ZG>~oh~2DUp9-jKmYYDVJC{?*V~0wZg(?G)8MQCvG?8SC$>L+N`Bgt z6j#6b)JAWcmP_~>Rq1B19dYRe=e=;mkooNZao1TcDW+;IkMHCe3wE0|t<}{(N$t~b zoNd?~rn;(3@*icYKa78>uw#{hxV`b{N1gv*{BIPztRNi!I#%{~ znMx5U{`tz?^XTH4dZ*uv|M3T%qPM;;p|jP0GGA}@0$R4Ej^9IPs^WqbJ%2I&Yev4| zcVX|oj}$Y9#c{gdF%z|4pTIuv{Zy-GzLj2Sr+og?@~@$@zZie)606ZbL+!tmss3X8 zIdSW-KaBq*XNqI!D5LF>WhYH+_`KQU6SJ|2g>T}l?{KcQq8KJHG1rh7`L9=zLh>v` z)b9w@Df{G4maZoGI+;`?dR-dpbjcQwwIS4KA;OOXMbe2PfU%$C2vyi+C=H+;EB`eSn3Y z=}FMu<3XGIYl^1Gq9r!7L?g}7jVJ;Bm={5y*Fnj-@f@qKWMR}uT0?%>1hz07cZ=hu zQhCQ&{&Q^`%B6hxSltbbdOlkvs zkTsQ`W#}y(Tcg~-Y2TpPeXNeD)K1+b|M9@1s^Ji3C}FvXZ*&=PJgL8xjY;@~ygNAQ zcZ)OI*TUT0x&NHfLnF(!1gE|(X|Ib5SSWci>#@vaXCZW7{`3vEU!7Jq*W)S-Lf%u1 zVBrcoLCbrkeg569F_4mWb1G@JH=;I84p9S(U_|CRnSok;SIub>6*SN4qn1?=W`ruWF_8k1e}vec1Q{)L&W zwdCCj7Q2}D_8-)!S>)CAMli%I&)Gf70#tb0!@@3B#$ihGT3Z5ttkdi!(M;+!76p40rJB*5q9zWFsat~j$;*#Ttw!2HzAwFam_D`j# z$U8kwS<}n{uTqr?fN#uGq$5%c9>jeKT{aey*g#I-!9=xq(LNWa2+iFJ!B&W^nntra zBr3&ujYQ`cXYx8;zC6B4V>*Fg=ru z@qpMvktNdYw&eVYPpO?0(o*?GS5+ok>5s4hyUWTHy^|dhckDj76`FEd*45H;mPGd# zz7#E}c!RDodYsn!N|1AMxxu00$4x$qC)>Bv%I@S#+u?RC&snFZrjM#>`d`bQ^a$PP z(tZd0|AO|{PpM)pO%P5oGYF)Z4rCF{>G?Hf-g{sWM@S=<|60c(uy?L~)`9QK+@OR1 zxnZTx@r~+@J^LP>BGhT0b;F5Vmq$7ehAR}0@fS(IGBozX~t^;L6W(rvh;r?rQC zp1uc{qMIMF3WSSqpb3&ih$P!vHL?V>PkI+&U;E0g!%737JM(1zS?y)IJLrZM!(WN< z@r*B(6-sqxnm(VDt4o*8T3;+*KiA=z`BjSXS2gX~ujx{pSHzUxUSyL@FjR3bixzG5 z;?_Uy(Yd!wE}guZbLB11gqUqyr|MdOSxsM*>~EW&l?d--bpM$iesTfDl=w_SaL3qG z0%yTjrlZpVPnbQ`Dht5)pA7}1+Io4nE+Zuqu@dhpva)&Cl=RO=;s+{nMElJ}Jj}Rb z_9}7})V4J31V?k!MTTiDYPkqphJE?>(G6)^dLJp<7&Co}0I<*S{A{B6UV^u`npFp^ zezHrws!Sy6>x(yqwdxV&<(zX?GZ~3i6EIeWoc>*F*ZTU->#wVtc2{gjhG$*v9Mpz> zJ>U+o9$X0nY<~T8j$h6v>J0`yYMC1{j=pTX8*lt$LFQ@ z)on?)pSy^<&4&&gd^U;wo-EyfVffIMgySCn&g1X4%;jj-^W-V^AyZusZ|AD*b6k=H z!%nX-({eXS{r=CDi*?p*XN7w(`_Dv;mXfzzD5KPcGk6*Wu^dM22U6?7^1n8Z$|S}G zZ+!3Tc3#tmyV}(8zxDBI+(DY0+2&dc5dG@k1U;{Bqi%?Lnq;>kohhllXL4M^cx@Me zm^av1h`&tj3wSNS=dTQ&bi5M$9u`isQY{aPi%hQvF9b~A_>j;mmYLEx?h zA{Nr|d+Fq77Vl?~>nFlR`V|4oN%N82Ao;}X{}xasbdfwH^3`7i&miCc`+r~z0~TdS z$3VCMGjKUGe8UsQeiDG0gh3P$MrO31IS~RhLUg&pk3kv42uENn$5Y|SZbjfk{ih&XpBY_|(i zC_qw;CMm`XKQarSK>=oFfFTb*?+X7#1pln#h5sJDg{Ea75R@pQ#|IeZj2H)LO}`C zi2{C>0eg6Upi_987@%Yz&O#BTcKr@5WSyOX&?0~~&Xz6?EgtvQInIeL4hSvuCgWVY znvdS(b)-MGM$IQ(>9+q`&}(>SN@c&BdP2C+CjMNn;X4CzVCH-V7n;$#>zvJ~Cx zBvX?jt2wSrN&6BTr>6ssoQezWjxSeA2qcOx?e;D0 zj#p()0%;`Svm~gZ5w_bSWdw@qL@gKxh#+r2!>^ zCvC#EHeumiUM5P2Z``p$9jW1F2ot&w`*eZpf{As9+U&-Ow=ju9(uBUAK|pj-q8t5p zbb(2tqe_xSbO^rvOQYh5-06;(e)7>d$>xOD;?^9~Md* z63UgmEs%}Bl)BsO#S2UYp9K64FNOjjPB~`u0eoU1uk=1#C*$vE`B3>Nu>+BIPn@tF z4Qt1M$xq-KC;%`1om}8J@*wgEy^;hlC*AwMqKmo>!dh%ZB|7)-=%P8TWHGlK6`f)c z`B!w|m+$*mba7qJbv7^=h%TaBNR!U;f#@R4RYq&Bhu@M0Bo{u51M!BlrgpW59B@07io0XDHiDQkSBh5QV>9?!ELFCe}6d6q3uO6W+Kme-BnV+`-a5~?od=kCLS@Pae& zPk4d%8imQ9X3qT`USK0*^L~dHiTS>NhZmT<-CSHz!WM)n=|9qo=Co;Jcwuus*H(UI zp05Hr@^^aC{1G_vb8eUZ*YrY8yp)Qe3`j2$>Gl3jFR+o6+W_5MwoCgYHsN=AF?UuJ zgf9M_UKkhum0oN~Q0)c5TDrV;xU;uv|4uK~yHhuqF=x{O7t;Yd#5LPNAF93}5^{yU zu>0MJf{~hFFPVwvj7i5<(*H^?5~hhYnJZDKWG!?Vk28>703TX6i&vS?oB`Ky9g}zu zP~}$WEcu}Vq%mkLKW07+io`22s4owQPi`^?t9wFbjdQJ=p^~l@L%1ZcJYJ)fK)5_} z{E$TbN6XAFLBFF5*1E{@;6Kp?wvy7afsMT!h%V}Cs{W2Hh7|rp7xKTO3v4m|_mVY> zV%@WnY+>m5e#yvCa|oiEG^F`zH<9j5#7157+jb&S123~4&kW})T9hFKEl(c3z=hT8mzDb#b;Rax}a`#eI~P@ z=MB9po{%cByn)30VrKBmoes2WXJ;DZu$NGe7F@Co8S066X9ZHgkbI`^Iz6unl`0NS zgB>$BZ|8U6yA?y{hH%n7SytVl7jUncp{xrSi5cv&UhNAO27L|Pr%%i}f-U3aKEmQa z#Zm;9ry9N}q87Y*6c!}(tNs~J!z{WXp@*{%WTV$0}rE)(?^%wA`2dwT2y0mIgOCphH9$Ij@67ud!C@=hgx%msG zD>y`cd}zqclz0mZvs3A|Z0|~FXrba8*-7hy^eM#|c4OvX1$W^-+c|SrT$OyFfEBcS z5t53TN%bU}L=XnxLHj(RZP-zjl$mL4LF3KOCH1c#F_-cv!B+PBJ8|{1x%=%<{UU(t z|2|Y%uL6>BB~@o^kbFHRG&3qx&|jPwuMk}E%dM;+2&{5ONETday-4VGHkTsZ&g)9* zd9A-#x*|*&Hp2|&)ps@GDQ~rGxWv{6zDt;QX<{(wKDQ*^$Ad?{hLRd~!wtKm-H0g~ zVa(9s&3yTwb2u}9aS)<9h7qP)2V25U-P-HMvkgqpcyjj!YzR%{Edj2#C=JQluK?5`9L^J}y^xZjXf@p;Rkw+5($Na8N*!P@Zx7NmQ>vq1 zRkB}t)uq2$8kT|v={L3&U^5MM5NfU+JaphUD3UvgBsk?@U-kOYQOWXG4Hc~+!N^`h zRoyyGbZ`n@xF%|C$gYaytqfWL;)@!rJ~7g?ELE{U1v( zPf^Tz1t=2In4)QPk%ndfZ8N7{DnKgYUN5RHSmNYa+Jq?N6LF{@h+HNo^YO zY&zWUj>Q&3)TXwZTgdvqU*2rNc@~ovHpe#On(=L_L4wc`<6<~z4TW1V1-ykuimZyC zPUN8^_%mV|d)T4~@!B70>c1L=2OT4VPA(Fspb7Mfg1t8P)GvSRS!bY{Qn+;xs(4u? zdP$Xm_4z|sJ)do&0mpJz2UAq=V?xX$Bq5RDlc- z+X(2=795`kv86+KA8R#KD^9KaFlB)ZI#{ zd~@FX?QDDsv=n&ZXrAKm`kNk3G4*J%=*GP%d=|J7q{flqgerZw;AszE06&4&SM5je z(EEt2Z>IqQz#^qLFiQEcDUBpQU0E3{u6=prae4CL^6VQZz48)wd5O1#T$J_#L$C8r zW)aK!sTLZWWDF@5dmJq0wQUT{-4DBV*BwNs$)*rOtB{IJ&|FoH)Y{fAHf(eUZ;SL!@V>}SFUj+hFihwKz5F?l z;?Bjx$VXw($@l%Q8=l#3jd2q*(CkgT-J5IlxXSeO;$(m4ptkr4eBR|~cgEZ2iC}7{ zTgUDKO$cq1&8PEUmsimoS)@^>w zJ^aJ?JF9jMIv8vEGo7nDY+iSe_pMZyNi$KRKSwxolaf1J`_4#aSd-I=dislt>>P3$ zpcNL)7crruaGR4&-^l)s>~q#^sw|5SmE1GYRjaWa=u5^4SyLuPo1z9Z9^bj< z$d(+c=VLdnSDY_K?<1VqW@1d|0yFYt!e2oAr4*r}; z#2sM#e?=cZU&hD(UozBSJO~~cLGFJr)F*GQwRjW#)7f7mqcZ5g9#H%KVW>aL_WsRK2d~`tCx-gZ*}t}I zwfC>HKfq9@0V6(Yf7HH@3ZC{$RMw0GXa7{SnVqcZnm6*j{q>GLGq>N7FojJs%Cv%Z z$=qJsm!Q^ILn%J+$I?1{9_fDB^i-3_F{mX0M~&}lws`c-X-tF>?#y^LzdwKL?k$E| zxA4_l@UPuJKV9}7{o-Uc{WTQ>6rt|r%q#41QyF=^%07JJi5P!j=u7*O%fQJ@=9o-U z3GA)yOcSfks?2GTyBx$@wUHsq{fX;qi14`C*HE#w+^=C$M;l+mr3txLFi2|il?X)u z^@>!z^WRY~&D(ANI^v`7w0M&v{`DvB^FIx1-JV51xd84Ah6(Vk13( zBYz_!&Pe#b$Q=%lyrpA@fSn=KdOCE zbBqN$Wqo(Hcgh{<33e++wJmomCtnuq{;S$I^qwz&4gUqry3V&^o^Q{;0|E$LqgC^g zhv)O#w^V+~Q|>o`3WU)uL=^Vu)^L8t*fvsy3-WgI1!Ws!W^LZh&j_2sztz6~g`xh> zv%kNeosr0xF`)Jp7aA4nm%hsP;LpO+zR2IF;W{ z;D@GAA!Te*ezKaxsclSM*7SExTy?%yjhhV^;X9gpeA-|A!M(`Sik z$-{(pc0RoMH;q(e<#r0Oa<|-zLtX-?7mas^NSz=zkK1$QF0M|A>+E4~M=7r#0wX?0 zrGf6GS#Iq_74-6cVGgWp{iNI<5s-u547KJo7r;;-@QW9!5^_HBRguq+O5Gu14;JZ( zDSBwCNeZA|fl<`c0rv_+vQq~*d-dGc>=CEFeBXRj6dbq?ef}fL5pE^jfzZWK5dBy$|HMsu~}UG3-@F?hJoGtY=Kbj;I0&&YCx_4C@9 zw{@TzRb}zN&;IA~<5S(a0t9>xY`wa}XTdqit1t`y2YUU*cPhV#<2bEe@$j1iBR-F; zc&(c4NphcYAk!qfWrodIZ5w^1tfZ$eZ;c7cW%!G zI8(jod)uTRu^%aVXYKB-;lu*TFZpo>w(o9bOp?@}JY9{)(7kER=UEWATWKYa=sIs$ z9mi+TDbP18(CVKys=A0%h*S(>~Wm)uDQIVjZN#WP)=O=(-t_*Yc!7PhHPM5s8t@w+=6^=M! zkKYXS1pX@b6V*27vZ9#kzU;RgFG=(fiZIYJiEjm_lO4y5Q}fZF>XN-;s3ZTF&3YvG zgO4tY9!G!_>+Nvz2l($jPn9LuK&)05-o^57LB2)@LK$33Oe+GNsaNrRMd6@2ME9W{ zb&-L{)UmdeJ6g6UnOxJ`nOPs@E8WlneUH2np*G1Z&X%#c_m-Z8;;m1yWB>Nk-)H}$ zpEs@p%acBBl#Pmj+PBfl=)`MkD6FdDW0BR<6}-O5d3>O|<15<00#VELs!2j@bM&!Z z_qO)0o9MUp@(f^t_fSWD7Y37&Ap0**(r(*c+i^diS!hc|ij=*#vf!DSB79iW8r4s7 z%aoz-ahfbud^5=AN&5V9nAgoJ7WiAIWNDCf`{36N>CR4>d26rc`tgC3 zE8GYfLh8OW5YI=S$xij7vZP--W)IIN(*?uOj3>Kj$AVk*d%kt9=~VgqRrAL{dQ{&F1^+`TFYenvvO&{Y!LL{^CfiavwFs#(?1O4Sm>w`}R zac?d>1TIhSJKX#6vBYij*23BF$4}BZxbSyN{m-s@AJueC?82N-cUSyh1=JBgbG;&n zyKMuD+5;b)pe|Ame8oBi1B`t+P#2eYw4Rj5aC{w4LNU)vQJa-2PtqDs{1#8(cz$)* zlU(sVwbOej?R%ON50yp85yE}l-+h(FYoprzs)-lTusdbC7nO)Pv4+R-M_XTv(8Vio zN;~M0fDJGt^3%^I?XeAQC#ZDITRPsGnBDrPyv?#a_>r-_(i8B?OB)oeH8m$fQ|Aq+ z)c&!KuWXGkY6GNS<9SC1{OhWprlPf}DcCsI_ZO%2ul4ud)c3!Cf$gFE?Zmw7biBAu z;JdH9{1sGOY47h`4cPW0ua4!w=}3bDw7J69U86ejpCd@W?n9Ue!MdJcK&3lJ4Zg>P zK*oVJF*frZfvUNIB`?9Mp5S>uFbef%E*4yr8|2~-o{a^Q(b#CB1H}Uj3Eul&E(X16 z4l*|bqnLx^75(P?0vWObb(;ep83zJ4$-B8hRq=tqevK~b%^NZJjwft;KXfY%wm=ki zoil7f$5rd2^`m3>))0(^%l})NwR3>`^=jB|uKy;OLC5$?w2iF)GxHx4uEz?5?b5KU z<{)+GxdLEa zROEDX1k{<7qZ=Y`96jq9T`=S_KLpK1lja+POjjVY0kGIKq6oWKG=b( z&YF!a-F=N$z&$s7-^_oXHhkrY|1NVFuvD}B#Osb0K4u+1ij!!~GZJJ&(g|EC0wKVq zI3Ew(<^-xmz_W~h-!^kSA;3KnxrhP0nKMUl5g*9xB1BCg5jaL!p@7>g;e9Hwt~6}r zk1I7F0iWL|d^ROd-RV=<4Y6vD)bLEuMw5vANJ#I77A{6iqd}E;iG{i(R=8$pi8P3Q zGo^qyvG0VCR|!;!^?0u2w1bWX&d)o6aY1QByWG$NT_==3Y>C!EGwy>%9PEp9{2p`I zl33anDr{@t{)$ETQVr}V~E&w56p5J}U_$(}#q zW@cbj#iZWu#MXpBPGYb|TE;WV*aog0e>-w=pn>GEGM_lK~&b||sh6B2s@XIBClK8}3ToJ^nr z*1;CdNPuh6qz`+b4c#DZfo!ch4|d(iV(AQRbaEnHjBYofYM}RmGT0!IBrFhY)&nxt zb+Xg@2uM(Y`uq=b;&(R@wh12t%83rlp@7a7xXlI)iJfcv@uX0vbptzTOQjA1W+h1{dWOWNbnddXksd+#Z`p z3MgkD^(2}oDWq*eRuBp7;v_VQIe?+J8c{?ek!7_ARqF;}#K9`ei6+Fp3qjy(I&j;3 zBK!PU%PFF9H2lsD@%MC4#5SB>vJAS7jd)aQar$9-5=&tT{r&)pkSu$rS9Tl_cA`io zxuEnKQ$zwAMr9p8uN;5!I;#o3CUsZaiN(T$s2>uk~OnQl^nZ;IVqbvJe zNQwcKDyDIkIXT0bL;(-{VTzclySnKAaQBvPQTP4YHZcRt3%yM6angy#v3W;1wi(4RV+)jPxqpYa(#t z3{478VkxUmhDAS7aB;fKCQ6F|k2hOSyz=5F9yW#H@iBDsf?Tkml5~U>zTnDvkiIfN zeKliBDNnYlyy&PLbLn_PQkA(|ljhQTQW6t?mJiY<*&#BE98%Lb66OY=uQ}6J1jRLx zu=Ag(nn!W-2w+dYtw3sN2S#tMKgX>aI&0F5GYbUYh|FU$9b+8KUT zy6$8_y@;u~VHkGqiAQtFLe|V!)g3Wt)O-~UM95ib;OpwZo=4ML>ljRKx6(g|t3iCJ zPFy24bdG@4M8eKcLWxpc?j1ww>wM4-qU|HH9ZiCG7+wP-;n-NFautAl0E#RFFZx!l zE7g~wBs4uCbTo;zhzicj*goD&nr-kBhC7=L#f&akxAA60AhoN|{(%Nn{E@P4JQfb1 zq7qEVlW=J-uB`pDWNLj6ZGDA$R+A)AtvaMW8$6NG`t33mXQ@tK(OJ^~;yct&)Y^zm zOG)>-ih7TT!h3+atm!pJRR*NmffQr8er0nwE~;7kF#G!GDIp3GBei?A+O% zbX4(-hGe>h@=S@tT-Da+4zJYI#xdC+>DeNVgkqLAqR@%#>^0t$6cyM6FVtTIx*VF! zOdEX@R?N%=LywKC`ORmw66L8@agKZ?6rE36A95oxmAePREDL*PGT{p8`X&G})e5ox zz?RSeWdEXUU!X7=nINWT4;KPXiUV0a9o?4#Ci}znG05E2MT)2P8gqc<%w@^OK=IJP zZPjtIw?T8ymgjAOX<>2pbM1%vDFh2dlUNJ%jL{?uSrA5IF(_xNEgL3~4MAYXBS6h} zL1VsI$1P)Biox5Nvou+=ll#Fh8rRG())?E?maLbd1ipWSBF)X$7s}T-E7o6a`E1gx zn@`P=dUDh^H{b7P>bTmB+2R)L^D!t8j_6hmGqqBg;xcL40a{a>9{R z?7@F#eY=G1zPWy@{K<`vB?B)@9)uBCoe!vh=;xzO#$U#^9 z*mk%ersuHNZM@g}Zm&OUZ%}Q|hLULX$aNLwHgTZ4>p*GB=6D?yrzXO7}!K&=uyf~y!MQJFX&UbP)wFCU`Ohgf%xa2_8W z9$WdJ91wKq0zMxRt{f4?i0b{IA@?J?;w6*(13NR>o);h(Mexe_84+|oSNFrBzb{B0 za@33yOCYmGpUZU8q+x+1#gptl`8IVRvqVby94>fsRBviZAjK$OOFNoyDE64$zl!Z| zUD*G9d=#ci{4e;Z|4+r>4}L=a!DVE7n{%rRLp{EFO~pOe{63bR=x)F8Uzws%w~@<( z?$L)DlL>6M_$WKo8?6x!Hrtp6^6xJjX>Y5<_06vvd2;ts`z&5Qm}_x6!r-GwUEjrC zwQt71-gUP7v+A_H+J%k?S)w;TJ;sQ^pU0%R)UIEd(W-xYEJd6j#`-;B7vilj=50f%ZnnGnjAA9wjqd-K87bS25^78 zSa~Mn$F326ze5=ISWuHCFXF$<6mi0FSy4=Cpc(NV?37GNkft27*_A#>J4IpXlvU~? z12pG_A62{VZr+yv#Q4{Kk-n^8NgHbRMj7h z2hS#2+j^xNVip*c!nA)Go3I5Zr7NPY_t3iXY zFrp;`^<)UNuv9jT@Xny(&*r6`N|wPU*z$o|~hgAp!lKYQxA+)#G zul-*uzW6*BtAUt}&{`WIwFvS5ju<@V-NFbBi<6YgM!wntplQJZHmfNA5F~Pa>Jh6}n|aHSd~v=d)P^5y%6hX8u*%68Ur#;9lpmHvD6|&Ab?au4%Tq8l-}7M{19-mDE4?*JPjJ_Aca(r95qWNlt)4(4`BEg;KC+ z5J`ndObY@dNM6!1BpOgi7KfvHfqAW^MR7$(A7cljK#=Vs#ogkG)RKxc!Sf7dFQ=ZU z4BkRX=+4UE3uKOKMS-DIi3Vw{*7zTPUiwUb6=La%RMgIc^P(NikSPo|C6ZT_HJrtl zUuSuHBgqh`u7BegSoiV^J@wF{R_e}7Gmcfc>pp|=Y4fK~C+C&E<;q6lq`G(R`7{4v zL3*3iF}oOPDSjd-GfLq6>VHx1`aI#8>5}5tLC^h~=lX@OHa;Rt-1iveR9Tho@z0Gn ze|#wHjlTC(kjs#XI_G1fZNGINu0iJ(S4O$&klmZoR$0Db#uGg?!?2;nW&U4HV{&B6 z87)fl^2u&9a>EXlyXAW`KcI`}OO7{u5lO*MIfuk6HHb=<2czoE5T^G{hZL6DM9f%D zMk3uvYXg72+ic^Wuk`7|T}2Hia(B>IC5q*&;yt>+=)#V4lsHYhryDT3uqsTZxJL8@ z1je!m2vHELN5@gwpZbP{E!|xuO*Zd|HN{bRrm_(o*}xo;_T2w9-7@XzMc)&d*H5GGe5eF<3)dsj+*|;R#-l$J6eZypv)$e-OrQI4wETHC2nf|p66EL zKa;(uZ!7b1I53Cz;~$7!OA@V;eq?tesPMq0k4E5!YeN^= zn}$U1O%r9|!ODV@#HZ!o2EO;cYPNPv_JWX%jo6#pwpo~+8r|i6j{0)LfA*gCEer07 z%UH~)^FBx3wmRF^L|$8C&|`WI`GV$|J1Q|{q;8Xj#4V3LWyb49{6U%v?F>YCjQ`zL$Jk^0hJ6&RG1_rr~51I$**3aHpG*t)=l~+ z7Xj+%laxwteW$l@9G+!HSK{G$NSAGWpoCag)BXd~sH{m+IjZ;XjXhs{MtDvxTYvm~ zvEZeB7M+mwR$9Wm*Z#@%wk@5_hjL+f#^%*&+Pce#0%0s;tJk^g=L?qNcb*lPOflB- z^Y6hci3@UO^vUIgm(?r9%f34qXNBf}Sg`qi@75k(F5F=~`h0VIxKeW+(06nF)G_>} zHdg<`@fjLoN?$OQwsV6o_oQ!qtxp`#ua4QT?94_~%FoZy1v}YU$InkB$M0y;tC+>V z{(&b#%D>3mze39w-R)nx;eQSYC}9rRJ|mix4j^je!L*5gQ5s4h1q7)AW%UB@=J?B} z1m5Gn6@#l$If!2b%9{EGb$DK|1u;qmT~vD;cLmmd5Av=zTMi5&WxjQT@@{9adUpr3TUmBhS3^65?^ zESVW4YoAhR(2?;76pviRXwJU=r3NZzSI^yoyGWdOOs{P^HFA^r8%AA~~CQST50V*=uq9)GPuZdIr6C(c*`m z1gzUAZO-AsQswGh_RwKcI6fd}6@#$A8ZH|Y_jqy*| z9S1%p7+aB`=4jXl#0>`&5yDB2OylyLee+oC3}+J7Cn0o-a0d>Ut{!Dqm5Xp8g=;EY z2LVHTO#UvD{Cd*PR)d==2l_@P%Ft0KKiK@hl_TF6>f_gn!sdx(=yD{6L6y+VyD_{Q{*@ph_8kFJtZj$g`Xpz zM{T5u979)l$@pi}C3Dhc;m$MUy1#XN2u(8`(55MZY?&hC0M#SnwPNzgK8h|jxGl_K z`72CkOk<-M#vk-l!vaRH0}K#>{o00)vTA*6f*oEGKbUzB#ePb3oGR;QjX$1h#SHX( z3d?U2`cVk0`bBIctn*R<)))r7snWf)g6TWtkZ1sp@m}yJ0+U@5069*(zFNRoR+oMv zYj-%eC<_>@%5DIkUrhmygH#Y&ed@8K)mqUYV`4rv^5bk+jwj+c^}!%4N*9(i=7!*l zt$?TR1YjK%=fJS@amPV8run#1j+I9lG>k9kMPUY;JN@gF{nO_6qo;~A$*&$%)qAD0YB>;a7P=i-MGX7ljikQ1T>X`uydR27Cks7$> zL6}3dhJb~`NNzR)0h+PME8-ifq8o?TMpwkqR>e1nV#1t!!s8gc*n)fy2*4Cb=U8y# z8!Bu8#a-jt5`dM|z{7dV=xSWrnNo4!3f^P1CuaqAqOiIW=!SL)SfdDM+o!p>?Q{=lG&m`|0#v=DbKY3>K#1Un3(O#mKGwmBzmOeHVidXk9Z5W1-m)g*onID`Sjgde6P|7dkgk?%r0sP8=eAT#onlD3gA3ZF3NLzqOxNDj#JpEw+Kp-Jw z4X$4RPIWFWpK8A+3_cILk_=_XC+>4$dP|KL#0&a%Mm*Ax%E1LIO{>}lYT6)*RXBMP${f|YW@*hMv(c-7=h@;?~DN>PvotXlkW zkXKKx8y9Tih#15N?P(@yGNe1WBEF*x*I%z|@bap{zYRNB9Sd5)?~gg&TXDen@!*YU z#ZR!c2DN7R&JlfwA*@X51JPUzt|KntaVw7Fr`b~2^dp$&dD~$`&UIKD06UAsG=2BA z{^3F!-dZjA_>=k-G1f5rtRygp10Kf(I~#(nS$&2wzJM*hhcD(KIzCgbwG*zP@CK8L zIbdJw?WaMVOs)FN*Tg|Ooo0faoUNTqxkw9;lUn6XujB3fDOPDb8A+`zOWda{O zPxhEyv!u)MnBEOU=AqoJ)Y`4G*!}PLsNwEb{hrdgj^j5OdVk}iGNKS+WZOr2_tKz8 zum~eMk{;7~;v6E8nqC^mRv{;1kZGilR9|J|OFbdxb7q2%EoWMi*coJg7VOUSK7b}g zM}x8&c^OAB3hl07LU^#^6Txm$-rUNlMdC@=eR?grz(|@u4g|&Z@Rx*~eQ?_X(_RWK zsa!(xOsRf7jloBJ1FWXRmyAexM%p*JQhKoo%M-;$@W`Kxyy-cW!lpy1=HyK|eVZG@ zEn+Yym0sdn*dQTm9i2oUb`*|NC86WU3+&M~S&pxfR35MCzqRsj4~wr4Q|ueTm$~7S zDkR6;LW9G`zjdWB6X}ImYFR(;|KywaZ@N;hY&pgw%?~a3&7U`CskXSPzB28R zqkozHuzJ}V|HCx2)m_CcLgxib^mK+}xbJ)A&%Y0zGggH1pQQdZkxtJ+>j#SKin(VD zemQim5Nx^n1>X;f;`w-mS)=cFt$ZeXd~-v3SxD|>yM{;R)CGA1)`Ahi?CYT{NtWwj>;->!rPd>Od;gERQbw-Z|Dh}OJhR`}4>3(w!d+{-=p_*$XlW{CfiWB{mPvg7xXdOF5NZ zBZHC@h3=hCv5oo`*J;0}eyG3y@*flFG{cw?Bel(_XVzb>8=_|6c0xNu{yqBrd3-i~ zn2GeV_g}fjorL8?T_M?DrXG_Y2_ggV8Av&z6?n|o<6i5!e+&B**_3cpkqab{lP)HGN$9?^tYJZruEcUlMxNu!`Y87L^?Entvy` zza@yLTM5z;5XC*r8%)*A`1eFwT?s9EZE9qK(V)+Bgvw*E*t1XlS(8G6YAoa5&~G}} z(fDCJbq1s>0dib8g%QSeP^zw5EH>ZE7h)JJwtK2T&2f}NB=a1W6q(TAakTsJi!;Ka zJ_!#;H^1 zH)-MzYW@Vd7GQFX%0>(pTQgMF8-v9jfXn2@CY4vS^1vI0rnhR6A6~Lz#>>N|Cm&LJ zry`*7AW3un(^Lb;0*idmh;>p}+RSi)V8g(Oo#$U&snXCfR+~W%Tm1)x!UMUZw_T|t z?F8Sz!n^Ffr&$;1;li}};xDE-wePW@mc|zx>P4ti1;8_7X~^JM@Lyf2_w3A9NkL}z zxnv_n#2QVNO4iy>;v_>^q^(n8XG#q3$jwS=4NfGXZ9lO7{&C8^Gx1Li`ru^N)mfqB zAH~XBOYla?$x@l_e<*24ay%~;JLlBvQ<|x9 zvIy+1-HC=NCL5o1GGz%9_?>}3wTlzbK9JVDXX>mPt!`s{vBR=5aK_{7qG1fp)$wnauXPVAGgWHI?+$X6H*7@@ zQ4r3o{Uz&Fi_Gd2)y`3qOHI*^~@&&BdxUwd&hbquc3}jY4%;4tjg@8>L)4exn zCD!C+^CGZzbf2<46QE!4ZWs=V6iU~VByfZC;d+hcgH$gRybCoKKGm&x?9a_e%`6xP zZ|Ss=hjEk2N5C$K0^4z`rR8_hVblD2jHmRW0o(jBK4lv=rg5E2J)6*Tn!J`jthhlK z9jFghTsNy)r4-B?#hpRL zSq1^y-I5hXD80Lz^+MpHzQIx-~47sfW{F-qpg7_WLAV?ZW53jBIh zCSDV*M2vZS(NGY^%f&D~*Y8AsHxc!H3)3{(z{)2G`c9x}-Tg8Z(R86ujBoZsqU-Wb z@RCb8t^ZVz`EP{J=*c~Bu1VBcDq;)KpIj~lYhm4-`;`=NXG zChVcoxNzP(1WH{;#8NDQWWC6X$1@`b7jYXi)Z;vRWTS6rSM-?Xg&U+ps{*4aH^+c} zog_BTVjRa(2Hzg-7p{G0oUSj>7u+ty;&D_Uh)mk|j&O=GVmi~nuf;b@d@Gc{N#GIk)>#D> zr!R4O6^oQu#E%(cjuY~R$y43c2$fbb>jszxHL@wg0PkA1iUeqAi`34cTr6L3zq%?D zayk0L#>w3j2)>i&J`ZMR+Qq#Tlacs*SpFc*d^*)RlLFmv*OM^tJRXI`Bu_J8*R8{( zTZNNJ9uw5qXS|&uXLIZpec_Xkj%a_?-$2SgWn}b~!1<#OUNyMM0pgVm^LYhhZSclt zg!ca=p7OPS`Q3NI$>|2mm1NC=kU(l%9oFefuqN~_steBDYX)&NWOH{WKh`7s_=@7# zWZo4v(Eu|GhVN9}qp*bCXQbuRCI({rlQ`0hmBaEjgu;DyV+0wA_ zp$XK~guv8aQy8XU>D(|Ki0^0(jt#(*;x}uA-)oQ!MG&sDftcN4+ioTbTd-q`AeF{f zu1G^yf&jQw22MhL9&h0J;nMy_oTeL?JT8p?JYn&cj4 zNA@y@O&Jl2wILMfHV(y1q_Kehn2}1fq-Gq-NC0jw&F6W8MLbXWq#Fyw7)0QbNILR^ zw51;|L7;+iphq1MY#lSUlaqJ3Mh-Sc4$ooYjL0#nsEJ0AK;eH(r1vNNK2-aGbBL~2 zNtZ)mzZ`<+m<>|`mZ&rhlwJ;sAdKo~0mJ|J>IGwwU=iYG{o6v|->+U&c0@3YnSqSuzqxvE7Xts& zt5=DfF}LJ@di4&?TIRItGBzszXtfIK+o7lSyRKgIR(q{u2YW;d9*xHv{TR#ID4)=2*~jBuiLJF z?jC-|!sq^nWla=2;HVP=kUi>xwcyKjlReF#V8Kj%z}vDalM<7TYo&Z-iHLuWp}C2Y^98C9cLdiCP( z+RT~+Y&p*bS*!l_n)ff3HMc&OC3oUbrN6FTp)|K!FwpI5UgS$I@t*g6P1{&S9~A(IHlo> zy-&yX%8eF_>DKMlONgXnR>jdI1aLHIIJ<>8M;`?qzj)(I4G5y=k) ze8gF+m1XB5Q9247<7Wgv?f+W_W25J4hU}sdjRXr`e}OyocbS=D`=QNo%!om~C4X7g zFs%LF!cG>^V+$!NGI$oUCr-9H{4W^{6~kSJf0w}&$c5f61j_4VIXRk|(u z34qMF{Y0ceK6SWSRb)tkXPbjjsS6M{ zclpACrO&u_eDNOK^&^Ow*~w~H?-R}SBaqd0PX35fTz(N7WjTcnWBXL}^a2h|?wrcQ z^(Zp!=Tj&cpDN(H7pk_h4vDZ~W^nSB6NQpMEn5YOaEkL)g5si`x$}!%*kv6FHE|f4 zPG9M;BFR&Bw96>~FNQLdHJGVq0?Kgj2n6BXQMn>cIEevTWm;>@(*W72ItX50Cc}r`o9L?Uxsv;nRGqf8cDBOS|DRQ#LG%%G%uL1d4$PkyjiAvLXGTuXqEf zy~j#el)^)$^=3)UsVX$qO^KdXVMFvnrO9U0AtSn7#CL5isq7qZ`G*me^H8Nnn`@Y- zt+hL94$`oEU#N}nbUx;UBFx2Gp)Q}f$4Rd94&($$PCC1DKwlNO@tcgftnPoa`um?1L5g|p4-tB;x)RD%BZF_ ziQ8`sq*%p3x&)t6oM+$bN0If`f0s_lLqshT@IwNKoK0fs_b}kmGQ=y6S`)&hi$3CN z-!36N^C|hJGc^iRn0BGUY`V3=BX!ki{p%x;6siW%>2#lm6z ztY>UC+C#XsCA`wGxO=8AoMpiBRXC-oCV_^V(vQF5tyX;aIl0yL5yG;M*Bd;AJ zi)}=wsIWh>@^&5tm2LPOg?bF1cEos?bTtQscC*kif=sn#rnMzmg*2UP9~gE|UG<+S zx+re=sISxJBl^dx*S|%YK9~gL@J)K~gq4>p?8vhXGBZfMD0h2?kRxtU?=^O(Li>DjFV?+?oY)pryBm{$e` zD$R9P4e6QDcQ2^Tw$dt}V1|)if)7yTH$8ZnZ;tI^K9l3U5^nv7IeSK$Ut2ak=vrjX zyc1}r?s*>dm9VYdD?yAmCju~G-T1+gog!tfegBY zB9L9KR+@~%C7_2BCQ&IE`1*0Xr*<*&(`^^375wtG{Pj`5Yuye^7s~PRI~^|MTO4Gh zcb-8$mr|#@bGb}pf(wy_fZ$oN9c_+?F=NoMc8FBKaSjTh4_!|ZQH*+&SSa;LAC-N4 zS-NC%eA0cGP53&9Q|71U*}mq|s6a|f7^~5E_m_L!$8t1}&yJ-F3dl~B^L-!kn_md3 z#I{fk)!mapPhDZ!Qe|}G!nl_?iK9ICTSz-dm+zxvVtJ$=?pV0Sw9cuVEyM z?SlcOa0^zS5IL)mJmvfz6&yG(V!`{ct>q*Y8le>rB(#t{;_GU@tD$`k8w%! zQp7x@JwpM<1>&Dgzuga-l*NW=#dK+W3+L&~T=UhGf;-0WdZi*BamI(wPQ~1JiBny+ zd=s-|&g7?Fm%F+*kzPZmE#8Flt}b{{ z%EHKCw$H@i!v^ zyan=>cOQL{#mlYbnDV0?Tb;nUxV{zx5dE-n^e-({?^de3*d^k_q)pRP9fS06Mg+7=w)XeyZ z^GWOe0uS{F1bc6Wg9O*l-aUm&`!2UR_<^6VFJrr}A0ZV^(kg-ee*Vg_U{Nc?PjWPT z)}+BMPjwenCAIjv70mXkgQzzy7(dP=V$WHH!Id{4f8#4rsj$r1^ik-5;)eK#i=ICm z_d|eUYXftXea9Wr8i@9_BWVED%|(!vgM6VUFLggss>W*mn0WH5B8lpBpKkd?#8;z> za-=Vp?;*|iqNJWCxCTGkOd9g-)Fswi9&!0>hdFt!_y&Wl9rfR#D&fXTf zhSuMU_sgdqF)2~NlRIr2>!qOS@^e8a-<=VbiK-VnJ@yWi|x>ko^ zZ+eMsVY76R9?zlz+W6UV)ZrMMd0Y58-^3{=JA;Mw$%;^CoZWPr7|UlY`Qb{T$A~KA4{M{9J_R?lUHx9)mtf}2EPVc@f?NQq z&0ZBqt7d_FDZoII`1%>smJ4D*YTb1p@;A!Sesz)bK}_=EpSRLPk91E)+dn#9`aH0=VRiC(jUB$v@dehpXn@M@ zKt>?$q&#(IVmZ2oBWQEu_o^H6;)d^C_U1Ao?|0izdRtO0+dJ%D95+TYls6X;^FN2g zgGcZC-X$Q{Z*KZvv#+cv*MPWEF3jU@r(lsmRi-_mMh6F-63X<3qD=?3W&FiX2DFc6T& z^gVq(7o_$u7!~XCGSm^R9q@YYuAMNo<3+G@qhy1doX{)MI}HZ*t|9lQLE0To(EMP* z@emh1S@gRUhu_KaVMsQmEgO+)&nJ8#_OqcO1X31T{H*PyaiNd&5JAbpr0I>MnU*wl z3P!1Vl*yFjti0i|RJRVi8%d%krtZ#8;WbE7-Qq)!IWnl7ki{wv)P$D2 zcNzDvBU(g`m}?sugdlWLC*T#ay+MTXDY8A-ju#k*oc5DO2q(N?0zLW~|2Wl2wmRWa zU@+<|LEZ|s;b6pc{8Dfrp`A3=j5;<5i(srBpBQxCdOM!!f*1)R(f*YvMVe@`eK)?H z#eINf3=2vFf@07qo$Z8UtAqtdOh>4AUg=~VzZ3;Oosa=ZjO8i_ft{e3>f(^UrJA>kWPJ93b%?+VQYju3v6br^#Uh?Z^CqFNd>@4+#UJ@Z!!l85UJz4CLO*G+ zWlqX!F$|l4deBlT29ar|pXrML?F?k%GA3(w_}}EDh<6B=g(SlCVLzM5C!QwTGQ|me z_1kDddi1|i6NzKxfPMQ)apNI@pY{qwg9lN{w!@BF)yj_F^pxVuk>H4>l#Q_8%J~Dz z{lIDXu8>@lQlt|_SjT9~CJf98#)s7VA^Bt@4jyLjz zmGYAU^Y6~*yUT|f{>Z1$_6*LqJ>mH9>&*vR(?oew*P3yRI`u<6TY}!am}rBe`8<+L zzhJMnz($nX7fEe*Rq*{m;qXR`D@i`?S)e9go`|1uDY7skFaqrlMi`X9BNesHMD@cA zim;}NqIVvnb|~Xeb{ry)v_0f-6=~yXihoZQTRLYM%t~eN6klkShy%mH-7&#_CB8TX z1=eJTRrVD$rN>&OAJa-5{DJ_0T$hg0W&@fYsFh^F%BP2~-$QU~Y5r+x@WY=u)@oQitom4U<>jqC*Hqf}3ENO~2?qw1>%4}eou zk{8~%j@-14#mv|`0AaFaAiIhuDeG^zo-UM=iiC?6P023Ip!I-pguhk*au+n7Av&Hh zph)tm7}lc?Q<@lRFbro1X25!Yz-V7T< zJleLr+VZ+E07<+O6GENf_-$q4{!+4)Dr3xRM&K2^sa?F5i=;^i*L)26!+~gN47!49 z2WLPD7u$)h+hN!3zzpbjNum`P5jdlRrnQ4^u>(X$gmK_8T|-x~h!(Jj7|Pl=t2((C zJ9%0=m)oKId(iuPo#(!hYU4zeN7=FCjS#=;=(Epv*Fejd-8Kq{%=k|wCZismYdj1j zwxpCSFdaewq0VsqNS~7v$I{KZ23qn3s*eF|7JF^V+#TrpoW%N^jrv@@``+yJdM*OO z8~WZ|_o01T`yA=|?ZklZ^@v}Vk)h`k%v;GH;FGkZh=*~LJdgx0XZPxw0R0zxJ<9qr z7yG=20Xpiy)QkZw$wA$0cP)oOovOi7!oKpo!5Z$NyvRY_s=<1(L6tE;!`@)3*x&~- z*ZSe14zZ!KjG<=2p&}z-$Z#`&k=W0gEc(3Zkd&C|n2mx%ES`|$0Wa=?385Aiupkl` z9s<-G8?><*-dY3%8v#|0hCXHt4quOcuo*4Q82Bj$yzm}AGy>X}4L8{g4o3oi84Z@I zjOBWd0mO$Ow*B9|N6Uu+!Fz+m<-HodKn;g+4fV0%jGnTEXM}6RH&_#JsR=6635XPW z1P4A6>qw{_H_}l%!Z1G4yV_H84X%U%OKzQR5QVBibu3_>H=t>CQoU+Yd;~9#5UB4! zSXniAt2JxKYo)`?8lipdV0nkBzHDeiJD&Pq8}&j$U|BZC0XO_1a=3aAuK}~yKf+r# z8ni=U+-MMt=+G`|8e@XfmITVHLohg544svqa8@4+h*8%0X96{jW|pvKWqBbX`_S4W zFoyWH9tp&NN)Hi4NtoTU2`J|iLBU(lZWU2-6#-`0P0C1w+1Tfr!*@{7>iv)WOYjFK zguSS_)Kl0XuU7E$rtPsHKer){?0pR=P-*|@;9MjqZl&4PZL6QU!eUy^mX>! z(|vILYR{Lqknt#t7>w{_6{KDcJsty*s16;g05LeyK9j|HUx4J&R9_~f(id?Hccy_#9;xP#yy}UB%N`o~}hLl{&0tqL#WbO28vL-Eyc|bhoT3QE&EF z-BIYl+u{1wK^sD#eS4cS3b+TED)pT~zu@_fVGGC~;bD-KDz>wWk^oHy!g>>cn);yZ zC+ITbyX7Z<7GnHO+YcMmCUR^`KayzIWWE@>gT|Qm+SkztC}x^IRz@^#0^&H>L1z=~ zO3qjC?AA-{M#2_$5zqn*3_ck@ur~Fj`W|?y~?QKXDq}~D2l>HUMcd%;%xMV@ZmR1@ZX6LfDo2sCT*QnJw^n)$hJV?lY6F)@ zj-_R6)k^|YM%r>0(cACa2y!Y2($@9qT!wU7WRGaCEOvv>< zcoAzxUviOze(rb`oGH12G`aYh^~*2@tosgtM0AN|LNu!;i^f2Ut3ULj_Af>NPY-AH z+M)H5+jog~-jA*)NuHMfIVrItx_@(8q&6RpfX-yESG-$S`b-9Wj-6856jk--n;@m%(${B2>J}0>x5evo<+w-w?Tb3Yb>w!h|NL%HqnT6QEs&F= zz2zag>@WAEKRq;VyX7I;JdMk54}#t)7=j)z+CH&pIvU!xVXLPb(+*rF)w)KBnvkN&*ctY|RH(|dxY zZx}^-OKQovptSs^6V!vWc14j63UW)KQh{ZXSHb>4v`Tghtpg8!euq+UD%5=&bZIdZ zAGvWTakCJ9YN=$O!&kYQEOWL^fHrUv)Am}}!RxN~>$DbW9uhsYSh5_g|MPcJY1g2=oxS0~cp1>>A` zYG@*RRX-Cr6lHmkq-GN?9nM{|%C4+qecVf)f{^}3vBF&f9A%C!MhAty=$8V9t>(Ia zi()M*%?0li<>5rUz$71GLdI|{+WUvEc_?F=yEMV4Gbvh{NxDt^ka+dj^%TSTGD}dD zs1pT*K7Jl zz^<)vk5W)GNIHAphPf}@C>qtNYOA+qOYXMzb}R8~wA2Mfam8_@w>CR`Ns7MHk;?=O445na(&22oH1;uWZkoE`ccqg z*K$*Wx9HF_9TE2?xcJi8);9|W(nTw}*xa4UO9#;@I$kG2IXUk@1BM zx1Ps&pu9ad{4U@geM>iaj9GYw@YA<0J&Y?EZ+4{1KUn1MD*n*HA6LT9Y#bi2-t(DI z?8{iY(PYagN&6-k*-{w8Ww%q=C*igKcG%0l?bJz@%2I{vnj$!xO5o2=TZrIutQ6>g_eC9g#A6{-JB`T4N*~Ba1|5f z!@N9*6H~GZw z&i3}6Vcn0rL!>4wau^A;O|GI-B%_0PeEn;10JL*}z5XxZC-G+Fm5p-uoE$CnGID8s5?((-d z(z?l!CT|ugi0c(o?%KxlV+2f1ts-hpNvC%nmO;2bDI`;Ovl0$U4zUFCJyB9O%lbto zFMi(i6D*bFpbKTr16~RLyPMl>76q80kd|UNM|kFRCy` z;Oq}YPTN5ibsl+Xg44xMf=YzGu0^+QYA5YQTAM_pbUp)OOZ}{VJXSi=sY!A{?zx8; zrAShi9-Q4lU4#gah(4_wPPld886OIwbz1TR7=8bJ5*I%KHkD63VR$p~f1<9;J$n0zAL-k8ZH`Uht;CHzG@R#JvhvZVa^0od(RfSoXXXUg(cMs znpgPq`y0(Gnj$a`yeUI7n1A+qW6@l#I7#p0&hc`dF}XUrM}@%~PS6^jwL!IS`{~C@ zfF^Z?q16v3??M2he5b4fVoD{H-<`f=^?m@^T!gsa94{0$>$g8odXxW(s_4-V50cm; zDwIAPAS ztbLUM_iR3)Eq^7DUGxE&P&S^kw1%!PKZY-CgAVtw`tG`+m0SL7Q%GS35p z-K27V<~^5DnWk~Ada)GQ3#kTS1f~58%K}lSed*0Me+Gl70zk>1aoe2Ko~N&_NVs=e zEL)qnuVKTAB!MKRHq9zx4CJ4MTMly_wLOhC??EreGfTemG@Ne6A)5m9CC=Xwi!I>& z_&Hp^3<;H3#THo+F<8$qzG?NRJ0=kA4N=pVn2F_qBa$N$CES`7?v|}S!)3`*b!k(e zUjQDu4mV^S>HPMmgta$~w(xfRNd=T!g?LxpU=!~f7$L1IRWz0*>_?3eJWii?fxJgC z_)}>5$}C^c6`jvQ$U{EU^M}t(-4BA_&Kemr^f_)P1u#|9yD(f#YZ&+Cuc(TKS7ir{$9&rfXG~4yIyVksn%ogm?W1k)GhQg`nTlog~ue$(rnjo3cB`grH-am zbQ5)V7G>4{!`WN5#o4A=+ab7D6&l>#9fG?Acb5<(0fHsCySqbhm*DR1?jg9lJH=D! zzI$eRrsv&y-ao)rANX)w=UQvuQP`9Mi)KXHw?xXGM85V2^tK3Jees{xWx03j{#Giq z^;FIk-secFbRIMO&W7=gKg=06pT5Ay>^H2O8z!ag-YZwDq%vwsTN7 zrt>v9ww*r(H2d>bF^selh_nJEoG?a*@SrLuH*Qg|M-6V_(9(IVeR-)@A&cT_{sM8>Gk*-%)~k7#0AB~Wh*`!`^2@{ z#Eq%q-eHX$-`GuA;cec;W9`IK@5J-$#LM;s_i#yn)RbEQLINy|R`A>tGWRk?Yx{8+@lS$e8L zWU7Tnxp||pi6^W{+8?Ja5kUaHc0b6n5xQ0wM&Pq@6VH^`P>3vsNzIX3EMhBC7Fra|7P!s|ha$_?v8hWSYrraM3Mj}+a48eA=+xmWpoU8% zFzGVR8T_6Y=^gfd^KS#a5g|bDlK;|M>H6dG#2no3>JEC#D4Z@`Wg-#uFFl@mLx1YJ zL<_9GGBp(asc@8@vmyUHQq!kmN>`D>)6el!y&C*(=U^PRN6NPUD! z{8wH1-$!c53*BSBALmkAOO<{Kx5Gg}x6$L48#V`H&>KMNx+--+w7YdQ3go(BA74;g zTN6K;79|q?1@tED5Jh|7wUz6D2vV?zg4z3)GL(;2$66ZB8cI18JRdr_R=3iO@u-{h zF3%wF9l1;cAgR;}mhR8zl@)WR!$0sHVnu_G#xd?y9Yw*nmsZEG_x!V-ZLKd)_g6=+ zQkg|MmNdnEqoUWa4QoCKR1o+Mw0zIHA3&03!(Y8iYU9sHZ6h$3w0k4ye;KKL3`1LD z{{!^4P~l)kE82?EQHK2+&^wiV`hq8kMKTA@NP2B&H`OBb9i>PdAy7;wUaP?IQ;7OF zw~Ff7s9LbJal2!IdR*;wzT{WXCp4G&2TW?EFKE0do-aTgGXBXO%NTbXkr@nHz}P#| zYzEfl9T|^6ia~K~Y?#cT?TD*=Kkr5g)TAFW`xd@2t7Zmz);1+{31mpic>!R>gyzW% z8#aXflJ)!=rHqM)PG#Jl2O&&sw4*PyyCM4}+9DO><_H7UDb7_rjmGW2)%w zO7UrzLH;C};pUqqr@jdHkAIQ;+hc>V*v^N+eNy?(Lem4=?viu9FZBDm7|uAE%= z2&kqj;`(Im9G6w#biXM^HGaP(!NtuZk|k>M5T+l_;neM9_y@idw`a7Umt^yJP*eyR zscCriJpNVJU9frjJACJV_Ez{+W@A^IY$ljrzg;Cbv554vUu%Y<6w1Y9{rSkZT*s{5y@eNX?_12T29!evY zAz>x?5(W|U7q*TiT10xJ73t?x?kYsVO6PZ&NZz9N0Q6~pf5PH*_}swmrdrzoTQ_49 zQv|W4DF739S@ys5cz!ThD^ZR>XT1oKcbCRY7{oPv?hREN&B1OM>|=8M&$@2vMYsi| zuDg*R?4{Km0qv42yu42vGpJ1BL?KPIP{buSc|mOLo=diUFI9H09GSS7E6mh6P-EFc z;&@FivTwnhzQP&}+tf>mw^T0;zKBzi$rCwsMpWLMici-|qsHNtDa`^!!ph{)7^V!@ zl-oy|W`|KhX%Fh8o+Nt5f20%;6%M_xinm+YBJoJ6HxP|Y;p5*HOGg}oo>qwpEi2$s zkEio9vJ;J3F&kT+MfH0lmc9>TfB|P8;V9UP*|r}*>sb>BY}&A&1;;O9eJ}|%Og8yJ z*VFHNm=f(~hJYomEDpmYIT?Gti4!92FXR>%K;u7^C8}*9NLsHL%g>R6@!ODRx>s`M}Oe05LuLBl7P6+_v89E<$WGU^5 zq#8a(B8Hj~EbLW>mMa)%A`2TL=Vshmq=JB~RrQ%b5hU{c(+#=M$zjKR^;BR6CwXgd zsUB%+u)+Bzv3*^#W>CF?X6kGSop`&?fm47(()3pZa`ubzqNa?A{~8D?Pg9Z)$2E;Bbo{*~m3!FSULkPKmy z$O49$mRegItBd37H=G*I#URV=T^X(G7 zj{avPgJ%bBRi$8OU@vN3TaS)9{hsu&oMy}SJKk|dJRr)n!tWC6(mlorn~{(H#pOv< zIMOWS-)<*Ihp*a0wRYXX6`pNw~fD6AusY;jwr6Y5%We-0VM)NtOT&w?7dh4GX|E0&p~P?i`(A$ebm08(-GWOiiq~Gjx9A<; z`^415mOLX0dgmehrk;(HU&q8~Q=-;}0doc2>1xKR2r_oWn+cFw?1BlYgHDg{F2v9o%KGfV+hsV^j1e zg{y%3mfb>a-f^cNv1w_h3!?14L;z;iYSZi-eliR|rGo|O9G!Iz%U&KxKmVSFBBNyOD zA&}j*tn1c0k!tAi&nxI`5Zm9cC1`9`x>cL(3OfH%UZrr6Qd(pFKGA$tnvB}wj*hb? zHkGt};^^`qC%uM058rovjyJM<5Bn68MdVb6jyS2!F}f!Fn-y7e=;Rx;e%b8S{aaeW zM7E#I9p0;qvB84IUa#my4g5?M=0oKx;2ihj?OVEeY{@(Iw^8n`l)DP5FD2E64<-Ds zg4ADy&MJsGe7Oji4$WPkX*rna;0(Ya7hZ38yy3b$`Nci>Oub?DxsV1Ocl z1;qT2K<+AjekD16#Li^=!R!z4m`zETBKLm0s{XedevjGya^x<;TmIW!FrUS}gveD@ zaEUU!aaB6~vpxoZe0>110Y{qwnb3hJ+`f5!fdVy#_c(Aw2m#WAe9X!0HjONl!Q!U( zyLhpHeUlfXbN%wQTEzkOVbLZ;7y5>fZqN0=eu*&%4q zJ~cgo_xAn~2z-#8+L^tTrk$6Fs!t2MzkqVkv?NEk#rrq}%u3wQd$zDT)iBN4Fq!PI z+MX~$yRaWyp@LB1HRRzx5y(P=>HGIF(h<0agE4v=iN~Y~7w$nb7NH2S5z`wHD+okU znpo=yggcF(3EaqsD!9F(ND_<)^sdNKzi=6tC}iU(xOwS&Eel9^cTPGLEr0Ur~nlu;(~~pK$uG)O%_;e zL@w<7&`)ZyJ^i6|Ghig9CFUVBifddh^b}n7CE$iPC6L>5LOm|G%7YsbW24rC*HWCT3|O2`MBo=LJqDVHwl% zKS{BQ^2V}OyA{^JNs%P|m`RKfPbyGVOrUWlOyP44Nx~FGW4}Wuqjf;P=YvT|_SXhQ zhOosvB$KhGnA9XghlD25a`_y*Ck@8*YXWFjro=aK=4z+zD2FylrxwZJlsv@XsKkG& zfpeGP{OLkmUCdOLV%nR%>wyOytfS1y$=c+lDQhfAADeiZ6+B+L4m|gRl@H>gDEq_H&5$qFqcz$l~c@~h(wmFYnQFvl@1im zy$s5w4&@TY%iGG%rA|dPs)>Lm{~{M9FH`qF^AST4+|<^`H_ThIQlIsGJ0H& zuX(P1RX(Y4K8A6@j!%KqMjmo*frLl_$wt9pezrCePFw-5?IX~BIO^Lng^Ny}<8mSK z6a?pvRph%2^dBbGY6iLKu<^UHg*20fmApS1l8VD)?YAUON@b2IVO^Z&N^5q^#ItR` zF2vl(tM$`rvYF)3yxZzlfaVjWcSBeaA=S%#@D^?hY1>of>-3L^SyNnsM| zU+olyZR{C&k@bDItmJLH7HPRajPo@KrpCO1UJv>s3$R%fAg_(N5sDF*57=o2T&)A) zhtXT761IkQj~}sT`N%MkSjV041X8dj4{>jXftI|0FYk!#iW6U#aSvQdr&%izTGEk! zRiZsrQmU5G#2COMvqml3n0CG;X3NTBEfKu(K%DxCPpWz_SIm%`cL`q&M_LUC1Cw=> zV9})ku<$vV<#0%3I5@w!7de#u-e=`?!(x38h@caO7%?hbX5RTY!w| zo08=?pAPY3beU#TngACy*Sv_4>j?K%O|voe_jXO^w0hh6-lCxRj*^0F4< z3}oi>_2x?a7QXFz9T=0x$(EnG&E(w$$mFfs;;sEpEl6*}9`c*TvRl!?NBJ- z?LabarxLS?Xkk4ZVLo23QhGvv^8t`qP}MZd=78TNFw=qLgLXsGK8W8E_XdHTr1L|X zc+L|u_A|N`3oN7#a@ULYt_A$ZNWHU_eRg+!VV8o7=X@=j^OfS>)FA!7!1RaNV zj;Y9#F~e)E&G&mo@MT80e5O1aT32aS*A`aOb}A^nJGg9W)f?@gvWH&}crlEha@9s4 zGL4V{uzu6e_A+OPI%C2(&n7>^ML7HZw~j^Iyj8?ZE6bE*#H?)jtnD$0C$(*VH z@R=nq1gIP)u0|!=7v(TXX}gG=F=@JvIHlRlCNLjOxD=^0&*?GqWo*VeV@U)R#b6RP z<7CO;+}lwARxS%^6@ZRdwm|d(P}j!`24LfmEFy`F(P0cKIZP_{y=_MAdKE^A5Ln7% zSn58Rj`x84jHT3!6~3`0^fypgN>CxTb8ukg*ghTz^loRqS&9_-z5+Wve)V0d6sf?7_%GN#*-tm4lg)yXoV*ZoiMGnf5H& zL3}ocZ!fkI#EA3^j3EJ5O+N{0;SQXb0N*au=-HAHSc&_qt`~*g4BHlWy>p9Cmm|F!oUjHUG zIsJ8tkB|viJmrA6aoFrGBP%YBRxeEDXO@(9ggti1(PwX__w~W+=#fZ+!f1>_S1dwv z>_Y2joVTLW$MBJ$_splrCa6doS5L->Vm~=wD$zosk7P2BVucZ>8t&ZJ?mS=byov9p zsQ)-9WH ze9y+{$wl}u4t{-}JiC`3-mUDaoV4*^IM7}Y)R&{UiBYBBNB4vczt;e zQl%NsVBdV6FW2wrN77{KLWX-g7s77#dyNmazgP`m?KT7Gtz>Z8euA5ArhjfwyS!A*c}MKRuou_AC;i z;7vx+`{hy=$%xHT=4RCQ{(Iv8=&elCV+A=Ph^`sMSBc_%@wcQydw2-yA^C^Lb0pvY zfjZrbBX$^v@n3s9&;F&yQ%MY6fSU@1~a%3YGwb-l4&e=Vy1SCvL zn5X+vx_LWeEINCu@!1+R_g6u;ot(UgngC3ZEjm9=k&}pNuWtda-A1NcQi*0OytkF+ z?|DyNIvtIqs zUH11R;C(zpcfDiAW?x#v&$SM3Xs@~!>ulKj4nGVTyFT(z9*} zWI^sm-N)ZYzAUGx2bbS2&;FJKAVAC@7CSMwhn($gPml%cNTy;(`^$9j>CxImM~7GQ z!wdDgHw?DL`rnfP$b$7x65y=uI;4z{5CEDEG7co5sx|&c5}-95RO=#@5#o@-uI7p< zdW9MKZi*%;LPR@wD_s3#5CAp9887@fh7spS6d{^DkX5RBKTk>xOGW)Vh?IBTSg}An zUsFS{Z`(@$GmlELoplpPK$}Z^SJEtT)+|+SC@)X#3snH6=C^KIK!i$QKDCI`vx^#s zI|4-Q$=k+DqC-bDyB|Te6SA)oYgV)`n-IDSFBWBRrx+aW;6R9>5G7At1GFrs^!T^6Ow3<`a$}c_GFdKNtdm zn>j7>jNExWr@wG@i~l_d7+}0xaa{O2hW|fsb$>AYiaPT5UivCpg&V}ItEO8R0^9c? zK{MQiQc0RzO{o+}nl5HEk{$%EX-;a73WdL%qWE;KWR(u}E^&)h_p`U(`rKs=JoZ=O z&pyeQ%RD|ERq(0hU-a}8DsW7T*E}r@9o6zkcD-^46nKK06ga+-Z1NAKeDwNp@G%gx zO=-`sLEtSS!R5=Ihd%mqz-WbHC)BABdPhU2cJa{(^s)h=w|~c-N0}=?#xUeFwa}6! zGaka5>6gof4sTi|{@XZ`x5>txNUpZj?`BEhZ|+3y^=$Ru#%Ccc*hPVS;e8n8vXOjh zMX;OC18EUAP=Ekb{LS+qF!|I5np-eB3El-cTY5GOuqu*_=pqCUn1gf#h_DjA2*uTw z;%d$kCsAf4VC>DorOt`8wN(z`huOkizy&&&ee{+s6XTH{?B~PoeTQxg$DU^w4DHN{ zH(t4g?&T9Bp1cVN=J972wR;0Q+x4zOE|?j{MM5>%7bkhUj|^iy$dJM}Hj5#c7Hl^p z{wx~R7HmqRX^A98)*V|KPAS4Pg!~8G(KwpVc4h0yPZgj8T?3vzA=BrErB~EO4nKvLBVdI)e-#&+IFE<+T7>5x5RrZWmnmuv3 z8buHN>s*}QG-iCCm7`PDa>-sIIp4vc_y8`6+k=Zgn9I(jjyrN(X(fcKgkp#;LvVE& z+e0N?Gnwhvxf0*-rQ`Ux^CJ6GKD=m3=h8J4t6tLA`HajmZv2VDhuSxKWpNT3r3jMmJx#S ztUl7$Do%g@T+>!=<3yjia(N3W-dETL$*pYq!qoN4SJ)?wtnBh7iC;QbI2OwN4g_R3 zFOHr1(v(8fo{)!JEbaSlxz)>5rxa`JLAetNwW7(bwzrndfB{`BnZ*XugHMC5K$Q`M zIdb~<*0KsoqT0smc|?yqKYa@y2!AnYeQfur>?2^8z-Id^)t;)TjyxV2EjTV16CSC&zMY}qNN4R`UJjv1-5n9 z2$O9>r=ca7>@C zKZ||y@wpEr`1O45rSU{5dUt>K34F_q#toAa_Uso6Ygpw@$p($h1*^L5bsGzf08v?rPo{|1RRD4gR8ez4jFR(!Pk_vRaBfnl5JE`hMo>JCZ@d~} zYrHE04PLBdSR0RJ7f*PvR(QX2_+WB4c^BI7f8gqPBFKSi9Ql^>$q^8Kp|e!P>V3pI zZX_E9>L?BFCID+MI0V`U&DIH1Q5tyN>HYTOH%fK{9}3`TC1z$Yu_08LfzO+}dra^> zdXR0f(uO9$Ct5`$YSkoK0mPBe=1&7TwcUlX67&S~`%pW_dE zQqC&=oY2VQi(e}@dj6DY+<8zSCM)ta2{jgrN~0rR$!}$~l~UueVqrv#+E)_8`~u{O zQh!WVs>TIs<$!CQUzH9=tEfx*J>P(q3z!P*_;M?)5cZvo7J=+0N(^_xcx&^`(R}es zu0g{4+rORANfz7%cYjP)Qb=tjBR7x}y3n$D>tPHNeJbA5tNrygq?|%m1llBTO{8nz zG1v}HL@Y{F3j2Wvr69RqFLnc9+`2RtC z79S_H)Zoz8PX@)Qh&-$bbhc6H`e1(2QW22!DN^^suaioM7x1yfwl^XB7L^3M(X*>( z24;^8o-fy8&k7HLwZT)YG0O-*mTF$+b&T444P;c)~%wKBpnI$)vLkeO}?9O(e5Z1XVP z3@`DjMA%plKaL|(`+d2@G6*w}x{kG%z8=#OpGnO7Ls)tP zXP+gN7ZGbyg-B;8eYEvCw*RlzYuWAOH2fP~;5R5g{NpxNm;1P{ub3ulihZHIm7SzR zX{U~|sG%C&?CK~915_Db5_%|WD62bw5s#4tsj}wPS+JxgX}w)3y}w&A;O8f*Kj`x( zP8Q&Oi4`ht@7Woh0&+yEkAZux2Q}v4r-qG9A{Zf>6anCZybJ2|?1oF?iImBi`1fSF z7`CJqZUX1{T@^`pV_0J}u45ze-O7EV6jG+a#l^pboY2B3Ie;MPUNXFkU}*^P zIccz$iti#sDLeuCvSiGAMCq-n5>3m zf8gdOR6^Ky<}xEX|9nDs{xMlK3Cn>n1ew$ZZ;&oA_1~$IOo&Ufp;xyjN?>qB`%7~f z+mEF?swPj1rmHpg0j216(g=pAoMA}3Kbnk^qmUu-!DB?YihlYb34SoU>@2Q{OTMCv zR^F38e?a${r@({DA4FbRVqdn)(+Z=P5zS}I3h6GP#{PuFU8k*-%R0+{7OvlA^0uSA z6(JQb#X>@$TPVa7GoNH&l3 z%5dIJq1BIZ!)CUniJv}@*=%Z~_;S@2{W+mWmgk#q>nbC_kqr+%*vtGdgZa&m26eh|XtxO5NrLINDMW&W%<$ z4fv!dIH&qhRv8% zmFCf%&(`~SU|KfqE((U$x|%~XpSp=axeAdOM%#V z2wg)RSruNmiME@6fkW|p9_}={Mf7w(Alx1k={HJlxs=Q#wyW%nsPY4&Pnxg;ONBW$ z)%0p8VNgqO*i=AkN8mfJU?f#T>|1n&XTnX`wW(>D$UbIwJfOU5s(vF z$0r?pEwx5+=nNb4CGz`7z8qs7`3hCPz1cTBUH&GmFSc{lX2Il=CP#Wnb_?x9l$8>y z`$lO|di@IZMuVMdpHyR)#&;IU5G#Pf^BmSC-AXeWu1v zkCftE0~+&4X(Eco3|e(-0&NHI2vi>V4X`GitQOZRB3Nv3B2wg z7dR=OppTx_4VdW3(BT0WG{zBI|crr4gv*FLWppXq`L&xL(2q5Skk{3JJ6 z*e%@RC;bF){Uk4)aO^(aoBR28`36;ew>I`Sj`0xi@vjVU+UoL`*!V8&9FVx+qg5oT z(-=UQ=Z^ww`w`0{AKTa9p4jYO!yH@AvItk)h(c{&r_~%O3^OQ1#8ziE(9SfdEI-J$ z7g8(}@x%@GcNX_+BnLlg1cxFJt=2~>DRb$v2LefXRVmqaEt+uX|0IMId66IhzgOgUA>e4J}ryx8|r; zw0LYZk~U@wE)Ghu7?zkn>N`1f?HCFX6fOU#!*8^a3BYO#P&-Q~YmA}P+h8G|Sjs7r zIU^^sjaWw!u+s>Tdvb$}%O#FE77)(@c;5sNNC63@U@C5qk&*fv08xeR(Pf%2=7i(r zHpqw{FceZSp4a(pjpIGVV{v)02oQzoimml6Ep=V&_AT7&l*C_o5(Q!t6R{GL_ORHB z6Q?ke><@5ArIWNxW21tSwDOX&v0^@TC24Lag;OU}LQv`A$=|d&LPNB=DM3d23K24r zHi#)PG9Ti2C3hF?a)9VAv=V89e4l+#ekP=t)JA%kr^Xn2h0$V`JfNR1qzaM+@`*=R zKX8fIreUe2kh6ZP?@PnP?Mf&5o-Uk>_NB<9pEg4r zDq|5W@+AjoFUB9z+eGh^006}5{S+=BctLt$ue=X+m15!9hW~Hl(b3<$c>@dm z7bleTQR@FB1^PeX(f^H4M7f5F-z_2w8^1^Xq%2-NZ;1ZLjx)~Dgc1t>COamV`|dzOpNPyi-4H2I{fR$<&7Zx6)7V72A2P8*D$=syKW%yf748(&>6`P`0~v@gKSuogU6dN>ceAG)N^p^kNxXm*1g|9%Kr>j%;u z7Ud;={)0y^IV>rwKl~dWo$jctcK-8GdE@^k1sdf#I8Ls+L_Sor_}(lfL`PP_5X5>HCI#5#?hzt9=ze zTOEL0v~y^qsApn?X91HPik-bCQivCxaAusJoMem-gj`-LD}MFd*>}&tcq1c!r3XK& zYR;%KCt>IwHE-n9TQ{c_WPK~~F|JKv*1UmHdHHkA$*ne&xq5@0?bWdqc+g?Ov7yBU zqta>GBkHC>JBXXY+sx&x?fb@$N8BaBr)TK9Rd)xxW%xdej`!hds|$ipuXv;ddMe;Tlj;I3~|)G>hwno_>pfEnVJ2x^((yM z9zZ$D;(EZBh)?L=F$%qRiewZ1YEZ1=<@R|cd_tg0>WVXeF-d6gA->S?LFle1cl_yc zKtt_W8fx8g->Dt!BH=A7LSl!YB@E3a;e#HJ4voLxSt-ooJ(EHLOYh8uY7^~ebhQpI z!ovyt9OZA!PP(I%EQ(dl;!ovHiXuQGRvAD}@Y)FZL=5i1`&^Fmw@*Z$eKB1^Sq8BW zCd{E)rR0&HLS*&GFwA$vYbfkO)vV+IRnnz&_N-qFo^xiqX(aPws=`IyljGSH^m9g* zMcAz15^PCV@lH<}e{Uu4xh#?jxTuOkcp@i3r5Y3gl|=_);*wz;{FGpeHH>u6YbT?X zaaXR6y=|tTdSOF(u&|H2e59cHO*yPwT^c`N;?u-5h@+OureEQ{(_}(BVn0=#xYbNq zx#|D6l%;5zlBJ1q)VQFJ(BV>n)u~TTm1!ns;q$IRh^B3uYsw#)~l zrUfTJQ@K8*7+?^ZjV57{NQTD{49?BQ3{VqgM~aWY3(ft0VPPqbG)={}uZCMED?4R5 zn!p#9M>&?$pf@a?+|QxG)pk&7UoxX{QCnF0SX?kD3`iAJDM}%uQzG`A%}*^UR-nXE z%cU!Uq%3NXl%=q`L|p-rvZOxJX|YnySLu|LndM{Z%+M9rRjcQIrQA*49Hw)?<1};% zNYp>Tn{7&!6%H6G?Sw%l>by*#^e~8sBC?MrLn8a zg~i{Ed;{^Upc}h=jPWVWc)_Z%C-RXc_#NdYki4X~Z0o#A>6jM_AGzni-mc2%1K~S( z>B!OQ3qnhNLc*Dtf$0l70w1f*SwPcZA@3zGV))-vmdNVqWQHBOSGggLdKAcaVG5&z zHk$#M&9s}rY`|S*@Dtn>SZXqTm-T1Um=hm+`U%AzFLm+wcR^4}O`loF?z{ToLw3Bo zXY&pKWT+|5p^f#TB$+ukmB7c5cR*&jir^ZNusW6xZ(Vlb%QX`hh+Oow=7247nJXWA zt5|F-QN2covBHy6$As}qjRo6W9rm{he2F9du>6H~IOIydlLTXoH2QwQdX3m}t1>0f z!kEHHO>RbprRo#kyd+kwJRya;weDQE3f-5K@|3R*kIT+v+|9W;Hlxn2wI>D*s+){o z3np7uWt+J3ijKuRww`Ffk_a8I0&(7uOE!BGqZ_9})q!1)3C&5Pml&Mrq}uu(yW{>l z+iFJ$^~e?;`a=U)nCUV$&7C%bziC)={4%lSc}LUt!LgU|D)ra%F3-e6n>3SN#?$j& z-bPbd01*`?h;Uz8@sTCU0Iz`lcdMiqAI-tmb*S3QoMw`%a&^T`#a8%HFkH*R0GO@n zb?Mkf{7DRsh^zjL-9A#;Eo*2MziGyUijt;FXxY~3*M0^`%>Gm9gF)9jTO?q}U=161 z1EDLoAJVki5yBQq>irdvKw<_pMtGKrw3u}Y(m zJom6AB)E^`@o(Q(|FXq`LB~L2o!t`zAP0+4D3jHM{tFJfYKh(u+m?hMvn~-oNln4n zKebo_r0t6Ssl{T?u*^{~>jziySBu4no{N8Q^fN@4)Mh3>QvdfoD{M2D)$b-}h;(tJ z;j4t5C+wFW5>T_~0 zX|=T}d>0^e^&eKLMqk^eaAlwMmA`cxc6^8EXIh(Pe*KI4>Ti42_ST0h$P+yASNFE= z_3{nFgflROUDMrBgU!zlFF7#%)Sb588*G^0(y&bN!Coj_$!Xq)lv+T4O6A~b50IpU zap2n}RX`9W5C_$Qq+VRllUSPvCzKM-Ihup+5HnY7QWy(0R1R@^!57^pklfGPR6N{I zpiY`XZgOlfCQ=Ct6cg()7C0L<_3mLqO2HF$N4@9ym|51tc0>%jli&-}|4%Y-mIlq~}h;&g+6QZBdGB3aRTZ_en#EbFvuNKQzN_~&<&l4`EjrRZ2V&MkM+?Dn0 zu6nZvDRLZ%J1DLP)9;mUgm8oL$PlFCQ28HuY?{mh07?Tw^(GDvdr6i=@;qLCHhiWq z_rM6g!UnL`p??e0lSyS9@6$y^YCiIzL2XT6SBjOX*gmYb);A?~&L%YmQf zN1xW18pbz|&K*?Nf?jLJPuGw!J+(Zd45Rsul7E88jvFRCJ<zPL)r3S2#nEF= znq}wPBr%Kq-npj_l!E|_la3h$$LIHp2b6?g_;L&MAv=Q#QABm|C zg7%wkmOOYUf>tyrB^CVg+H+!Jy+I_(Zi)_=LWa@%+0*AvF(L(V1~3Te2OpT5#{c|` z^r$l%G%}qF@f*^PvUDO8@<>2OLkZAu7{G>L;*Y!Mg(1?SNOo$GE?8ZV1{FcGBf)ol zBj}a`Hc8o{48{VdfTXM4*}}<-;?$SQq#Y0OMf`)lto}YI0Y*^o20PzUYhQ%_QuY=B zO#;5fqp8i2qo^odMb!)JkO8hiG*Lx>ZzLuTLHowqn>aufI#WXN^hr0JVkw^@zbN+0 zkBm5%w4N|QUL>J8PCga7;aA)0g$@Ct-^X0%b!^B&+=85KL4YHtg6EGq8sdSJc(0>mi z?(!p%C=XCBP^*h0noD4QxL34{k0&`5x8z556qrDeqF`4c0gZr0zJ1;%^4eDW{M|AJ zy?OnOU-WQR2Ok#jt@S(;XamJ06$nxiC-BkY;#9~^oxaCYX zT)dD~b4(#wxA=Dx)A}`%K9a?Yx8^)O{_+tt&hBA?HwUyrZA}PXFz)%cZ%PC-VkdZo zu0gs}LsBfH()JN5@>~~NwjaVPUKDtv$3ESWd-TK?0#ajA`?oVcDWAB==Zt%KFQhRA zpOJu-4Ru3ui>WB0zIq(B<|ZFhgLuWCtnh@*KHR!=C)tkxR`&U_D;5{iJH@d}x6E^J_xVI4UEzNJ=WL20lnS)UhwJS-Cl6{`#K z?sxV2YUqcnFnSZ&xscwLR<#ggR)oxB?enZ~1%$_alx|tw=0J~jtw%|+0dNy;x+JZk zz|;Gk2%cg)j6G;@%yHdhT0{>1WcEf&ZV{Md<&+tV*2D9*;VwiP?-3hv2hE-4r_8;7l9 zJQ3rd8m=5Vr!Cj++wX+!S7T?MNK%Kr)>GCSwm>g$;xL5YZv5aTh_&+|J*Lo`hTGj*vkil*v_WzH# zw{VL)-?ue`1eYq@Jvan+cMldE5`qK>fk1Ew!QGw03GVLh?k+)t2X`rMkxuvSv(I_< z?U}hV^FQ!V>-YJt_gycpi`|-z{Z9DT@4U~u7Z0Gr!kY@wt=T`ze5Z?P6g#Rp%dW>kC^HvTblB@pX1SPA5gv%wUx82_ z&)Xfh)+{I5?POJ4JKka*N1%%sIp5EEl3aU|?T`)tyr{IisOtz$9lhx5ycp-an6EK) zkGD2R_;xkA2_q_!(>YAw+wbUbC1b_#svLSvvVyo3UD5b439B z9i04~68v52{N3jK>3O^$_xA=?|H%nAFFZRi2Cd7nL%5_<%o2Dp9{7elNHNkfgNHNC>}AYYP;p;Cp%#-Z4MrBNd$kkYtUp%u zEZbVHe{Diwo0d;|-ODhZkdunwo<6e8h+m_Nc=)ZfE;$}tU zlsT;w^}Gv8Y!qX@Ghi-?1s@#E#v6UF8O7lo&65}%Ok>UGOeZiO9eo-t(vR`7{sR|p z%(hL8j5di}J}W(2u>3sHH4Ok7H+F-KO2e7hizOC2!&3DdzS>CH*rS`IW92~oZL2R#T3xy#^cA$P6 zwFAOQL{9^-83Q*}hsNp$_C1h@fdH!?#FGX6S=pS92f9d?RWO1LU<3j*6JR(MV7;`- zGlwetWc;xR8h3gRa3qJZ(eSeB2iBRz?@iHv+^hQ6xK;#Nrq}ana=pH?Umtb<0Zjm@ z=S5{!#{Ju-=(a+fkoVtvRd#FtyjMkdLZ_ebw_cThwnHW9kT+!_B1BSDZd4cqWYc{Q z;UNYoSt-BRun%pFri*(^2>_m&RvvOwj4U9dPBOt@$kE*4#4dyAo@?={kxDyrPw=tMn#9J^1rirIse=vO^NpI5Q_V`2zrY z#E@SEyX?_pPov}47Rb03XK_3bdYaWPOcEAgO#1b)4L9}6<7zyq^m;6np~ z&s$kip)6t_WZ$|46IA`aF&dP_zO`VM9-f$Bs&c7!q8*0Dvr^n*S?gkqkgipP1kU_| za1Y(qf-$rTyh<3FsJ1e+XbpF^_^G7C3sEjPs%(idgqu-FE#5iORg~b@xK^jGCo&y? zJRbG6%(f5yO^$ZBZWym2^tCp1fVe*(hC0&z?!v9+i z<`?Z9k&frzB0WTyr@o2!kNHz_oFh;v+JOxlc$I09Z3X~Bu-$wSISp)^=utuKonv7g z)fT%DG=X2mKypoPJd6!OFAGoTY!+2OKI)1Ujge-4)~xy!uGOyp+ee)jHJa3>*bn21 zm6=BMQrhW4YsE(l=TSZ=>CFZCkLDJIlC7!gqf*cJcc#OMvv*fz#+C-HWq{#7KvsgX zPA6i-;Z#2a$0&SW4ahBjAn!+JWe+PittXo7pyXCZki9*)pz@y;i(wZ-Wp5e_r{p)A z8-rQHJQtU{qO_M%RXQEDr4U?Je#6b%44j1Vd;nC-!DB1-8+PKda_MILj4(-ykOS0z zk~e0Tih17j30%v2yTK)KMp0Fks3CguW7*+7$F+EH^no!4q-!?TL&XUW|3UqMic76^ zA><7Q^W*Y${MdcyxTkbSh*8kz_px;1X+kk<-RRm5 zn~tI>)ZxXFFzp1SM?Yj`I&>w(+>4CA@!=KYHTY_LiH6Oj3~J(EF_Y-U4R@Mkd;=@= zZI$rrxfU5F6=N5>kC#7;f%5Es)c|C!GCcL+Tnn+8yDP z)nikJs~p3BTPodnX)8%p}9wUCd5yHX`$;o`3okY83%btD=qPS~_~Wt~QqSmsEx zCH9#;JJyYMF_j3o!d*3nje_(o&h zJYeidlKE<-c_;;SH}{26XM?Ac(u>|A89pYloE7t2_jmOa;-X%hnaZCmC;}5oK245> zo2fbBGX_Z1I!aRkgDhYp^JLWN#hN^+oQdtTL|b3_oWrjdT_5Ny|E?fnm*G#bd zNjkzV*rh4I$F<~U8x}4~oQ4bk)~k98FaN7o6`)x34tG#1FQobjO)&aI4?z=Z7{)UW zq&QsXM_l6Q#U6^xq!)3HlZ`X7Aib*4j`?Xwuj-96xaa_BLT7H_vIZ-k$;xq1x3{YF zFEoMRs;=)(G~ud#Ec6$eu%~kcK@&P7tnT+SxqdN}Il^*#FmW7hAfw3Z{HapymB@u$)Sp?(`{V+N)BA$?W28E#P zZw8TU>nnW*M_xw8vxFxOiTnV8wwtKAytRj{na}uY6t;z|X-UtY5vQIJq0-*njIpx8 z(Iv3%aG#MAbh5x@(xi}o+3Xw$b+05{SJ+ib^B+zC_a;fF1WY;IPK`p@bFi>mD8gV* zmz9JTa86o4Q+ddik8qlqDn@8n-*z{kIp{hq@3UI#^mRabRRPQUMjKl5W2VTJh_(n+ z(sv8$TVaYlfJ3NqcEvh>|? zq_vAREXf!`T^l^}aocnv>ATna^G*6cU48g$9dlvVSYwW{_5n>%(r$%|AzO z2aoC^#UHL3k*{!J@0az!uw&r=-QFtFRR-qAkkjjkc+Rq^gC%B!QplwA$b0wgMRVy{ zZpqVyzPF-j3FL9%8hIL!c>N=ba*jgXD_<)#1lwuG{|hXuF~Y3S zwx9D2oEI#V`=X|kE3W;I_wLEFfP-E}Um74!?w3<|U>+_~0WL}!07;mZ2W5>yH z&mj-fSuZQaN|Tqf0QO#6e>*QOJU>=V=M8_rO9XEwDf?GzPOh~6qVeA7o-W|dXFgYs z1b4Gv%X+D$w0th(eB|&{u|+9*G3ijpeE?IyQA$bkb(DV5H+kyLrCt}sEX3k7*3z|Tv>Q(34pv9o&J{j74t+s5X1Lf11k3?ew{f2_&4Q2o?Rjae$!G(x z@~mU7(F5>MM##|HBffT$p^tbVgd+q-(gq%j5r2opd4zl-@?g6p1m*O28P5d`eX=d% zA$IHW!pifXH1odoV?%fJbj*|L(F*aqMrTC;LLmZQAh_f7IiAJ42<)I7uX+anP|n6e zSG3-*Y58o-0RgIE7@`En>eSGYVNH(w9nw4`SLjsu*!wch&??Txc;GM5qRu;9PHc15 zw3D`EGq#(!)?B^8&sjh3#d~?1MYKBwVQa-qz6+t9jOnNKf{bn<*;sScva#fSp3Jk$ zh(H}#ML7qcWOt$*j)f{aGUC@qA{dhjM=4u41BafGkBbL2f2K`lrL#y>pwz~>=J7;Y z3**9bLW-y3qVwkWvKp#j>tut7cKqGULhem*7jl)IoSdN%y`Mvdyl4Ud?OvUE2`Cw{ zhi$QM=pfT0amI<^6}gdH?@Ssgh#qtC96yUIK_{WwgjMfK|DYxsXHAL}CHAHR)a)ji z+qgTgF~&*zy$tm8;3l>-Nhs=jAHvPZ!~I3v{EMq>;%06{68Op=zCSFCjO~#9TIH~K`vvJV$VQ|8Gar#_Z_zoIn*NNa*Gv)cUx;mN zM`ymQ;0OGq{hS(TFW|x;I{R6~F`K*3mhZitgd;}Yw%t2=dMO=!5nisoe#bM1pt_12 zHU#XuI)L={7yJw~edk=Kpxgr0od4FVa>)w-+TgiBsWKX8_vwn?YhjgMp3 zgK*pf&HWt_)EUN51lsv=Q3+o`p^nv(9N-}6<1w)K2npjvr}Vl3a0M#8rw8205k}Ah zvc^j1yGvmB-=E8s$#|lz@8Q80mMvVA0fO~0lJuh3%b&}W4C*i=GN|G)FcUW7sYPJV zkL9ypl*bKle0;A)%MZ?L(*VAVM4eiwn0j5VZ}W00!-GTDfO{1OvAS|^y3+gv!!Wb* z-lS4Q9#hbcJFC1{f4uy?ZCEqU#MJ5OcHV9e_M5wHYWWb0nG>xgOcxm&l98@2pQFMMTpQ#IUm)A=w#L94O z7%-`LlTiZS!xg#*S8|8TzK_HBsqvzrp=l9k*Hf?MuF-THaflqfjGwSmzR9?}Y3M`N z$X(MRe)EKG^9!(Ov)^--8P`0>zE;P6b5?A_>YetSE3Pz4%T7}5)E@HK>y}+tj4

+
+
+ + I get errors such as:
  • + RuntimeError: module compiled against API version 0x10 but this version of numpy is 0xe
  • +
  • ImportError: numpy.core.multiarray failed to import
  • +
+
+
+
+ + Plotly-resampler uses compiled C code (which uses the NumPy C API) to speed up the LTTB data-aggregation algorithm. This C code gets compiled during the building stage of the package (which might be before you install the package).

+ If this C extension was build against a more recent NumPy version than your local version, you obtain a + NumPy C-API incompatibility + and the above error will be raised.

+ + These above mentioned errors can thus be resolved by running
+     pip install --upgrade numpy
+ and reinstalling plotly-resampler afterwards.

+ + For more information about compatibility and building upon NumPy, you can consult + NumPy's docs for downstream package authors. + + We aim to limit this issue as much as possible (by for example using oldest-supported-numpy in our build.py), + but if you still experience issues, please open an issue on GitHub. + .. raw:: html
diff --git a/examples/README.md b/examples/README.md index d110cba9..de8bdcb8 100644 --- a/examples/README.md +++ b/examples/README.md @@ -3,7 +3,7 @@ This directory withholds several examples, highlighting the applicability of plotly-resampler for various use cases. -To succesfully run these examples, make sure that you've installed all the requirements by running: +To successfully run these examples, make sure that you've installed all the requirements by running: ```bash pip install -r requirements.txt ``` @@ -22,6 +22,12 @@ pip install -r requirements.txt The [basic example notebook](basic_example.ipynb) covers most use-cases in which plotly resampler will be employed. It is the ideal hands-on starting point for data-scientists who want to use plotly-resampler in their day-to-day jupyter environments. +Additionally, this notebook also shows some more advanced functionalities, such as: +* Retaining (a static) plotly-resampler figure in your notebook +* Adjusting trace data of plotly-resampler figures at runtime +* The flexibility of configuring different aggregation-algorithms and number of shown samples per trace + + ### 1.2 Figurewidget example The [figurewidget example notebook](figurewidget_example.ipynb) utilizes the `FigureWidgetResampler` wrapper to create a `go.FigureWidget` with dynamic aggregation functionality. A major advantage of this approach is that this does not create a web application, thus not needing to be able to create / forward a network port. diff --git a/examples/basic_example.ipynb b/examples/basic_example.ipynb index 87747fa7..53a6c549 100644 --- a/examples/basic_example.ipynb +++ b/examples/basic_example.ipynb @@ -326,7 +326,9 @@ "To this end, the `\"inline_persistent\"` argument was added to the `FigureResampler.show_dash` which outputs a static image (of the global aggregated view) when the kernel is disconnected.
\n", "The example below illustrates this behavior\n", "\n", - "**note**: you must have `kaleido` installed for this to work." + "> **Note**: \n", + "> * you must have `kaleido` installed for this to work.\n", + "> * The static output figure will only be shown in environments where javascript code is allowed to execute." ] }, { From 606a51c58d8e982a0c605bc29e8750886cd671ed Mon Sep 17 00:00:00 2001 From: jonas Date: Fri, 29 Jul 2022 22:04:14 +0200 Subject: [PATCH 39/49] :monocle_face: --- pyproject.toml | 24 +++--------------------- 1 file changed, 3 insertions(+), 21 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 1777590d..08e66147 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "plotly-resampler" # Do not forget to update the __init__.py __version__ variable -version = "0.8.0rc8" +version = "0.8.0rc11" description = "Visualizing large time series with plotly" authors = ["Jonas Van Der Donckt", "Jeroen Van Der Donckt", "Emiel Deprost"] readme = "README.md" @@ -16,8 +16,7 @@ include = [ {path = "plotly_resampler/aggregation/algorithms/*.so", format = "wheel"}, {path = "plotly_resampler/aggregation/algorithms/*.pyd", format = "wheel"} ] -build="build.py" - +build = "build.py" [tool.poetry.dependencies] python = "^3.7.1,<3.11" @@ -47,26 +46,9 @@ memory-profiler = "^0.60.0" line-profiler = "^3.5.1" kaleido = "0.2.1" -[tool.black] -line-length = 88 -include = '\.pyi?$' -exclude = ''' -/( - \.git - | \.hg - | \.mypy_cache - | \.tox - | \.venv - | _build - | buck-out - | build - | dist -)/ -''' - [build-system] requires = [ - "setuptools'", + "setuptools", "poetry-core>=1.1.0a6", "wheel", # https://github.com/scipy/oldest-supported-numpy From 380b0891124c715e2cb0b80e8f9152466e5980b1 Mon Sep 17 00:00:00 2001 From: jonas Date: Fri, 29 Jul 2022 22:16:04 +0200 Subject: [PATCH 40/49] :dash: adding yep --- poetry.lock | 13 ++++++++++++- pyproject.toml | 1 + 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/poetry.lock b/poetry.lock index e2ed1c78..ab3a3532 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1837,6 +1837,14 @@ python-versions = ">=3.7.0" [package.dependencies] h11 = ">=0.9.0,<1" +[[package]] +name = "yep" +version = "0.4" +description = "A module for profiling compiled extensions" +category = "dev" +optional = false +python-versions = "*" + [[package]] name = "zipp" version = "3.8.0" @@ -1866,7 +1874,7 @@ cffi = ["cffi (>=1.11)"] [metadata] lock-version = "1.1" python-versions = "^3.7.1,<3.11" -content-hash = "5888ae343ddeb60827c221df99e4426949467ce760b771e6eb89c3ebe0327a5b" +content-hash = "617c058b68325cc069ebcfc7dfe07d121c3a08074c3b61de6be943582eb24f05" [metadata.files] alabaster = [ @@ -3047,6 +3055,9 @@ wsproto = [ {file = "wsproto-1.1.0-py3-none-any.whl", hash = "sha256:2218cb57952d90b9fca325c0dcfb08c3bda93e8fd8070b0a17f048e2e47a521b"}, {file = "wsproto-1.1.0.tar.gz", hash = "sha256:a2e56bfd5c7cd83c1369d83b5feccd6d37798b74872866e62616e0ecf111bda8"}, ] +yep = [ + {file = "yep-0.4.tar.gz", hash = "sha256:41eac9625335b41657bcaf87083edf9e1a95178ea3c7ad3dd6d6ff5e2fe7ceea"}, +] zipp = [ {file = "zipp-3.8.0-py3-none-any.whl", hash = "sha256:c4f6e5bbf48e74f7a38e7cc5b0480ff42b0ae5178957d564d18932525d5cf099"}, {file = "zipp-3.8.0.tar.gz", hash = "sha256:56bf8aadb83c24db6c4b577e13de374ccfb67da2078beba1d037c17980bf43ad"}, diff --git a/pyproject.toml b/pyproject.toml index 08e66147..d4683c76 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -45,6 +45,7 @@ ipywidgets = "^7.7.1" memory-profiler = "^0.60.0" line-profiler = "^3.5.1" kaleido = "0.2.1" +yep = "^0.4" # c code profiling [build-system] requires = [ From 9b275ce6c73ec038877c098c8c265115814573e1 Mon Sep 17 00:00:00 2001 From: jonas Date: Fri, 29 Jul 2022 22:43:15 +0200 Subject: [PATCH 41/49] :thinking: updating efficientLTTB based on benchmark heuristics --- plotly_resampler/aggregation/aggregators.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/plotly_resampler/aggregation/aggregators.py b/plotly_resampler/aggregation/aggregators.py index bdd25332..0bad7d2c 100644 --- a/plotly_resampler/aggregation/aggregators.py +++ b/plotly_resampler/aggregation/aggregators.py @@ -282,8 +282,15 @@ def __init__(self, interleave_gaps: bool = True, nan_position="end"): ) def _aggregate(self, s: pd.Series, n_out: int) -> pd.Series: - if s.shape[0] > n_out * 2_000: - s = self.minmax._aggregate(s, n_out * 50) + size_threshold = 10_000_000 + ratio_threshold = 100 + + # TODO -> test this with a move of the .so file + if LTTB_core.__name__ == 'LTTB_core_py': + size_threshold = 1_000_000 + + if s.shape[0] > size_threshold and s.shape[0] / n_out > ratio_threshold: + s = self.minmax._aggregate(s, n_out * 30) return self.lttb._aggregate(s, n_out) From d74cd4c740db93a75bf0ed0130eb29bb64d63484 Mon Sep 17 00:00:00 2001 From: jonas Date: Fri, 29 Jul 2022 22:46:52 +0200 Subject: [PATCH 42/49] :see_no_evil: removing yep for CI-CD reasons --- poetry.lock | 13 +------------ pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 13 deletions(-) diff --git a/poetry.lock b/poetry.lock index ab3a3532..e2ed1c78 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1837,14 +1837,6 @@ python-versions = ">=3.7.0" [package.dependencies] h11 = ">=0.9.0,<1" -[[package]] -name = "yep" -version = "0.4" -description = "A module for profiling compiled extensions" -category = "dev" -optional = false -python-versions = "*" - [[package]] name = "zipp" version = "3.8.0" @@ -1874,7 +1866,7 @@ cffi = ["cffi (>=1.11)"] [metadata] lock-version = "1.1" python-versions = "^3.7.1,<3.11" -content-hash = "617c058b68325cc069ebcfc7dfe07d121c3a08074c3b61de6be943582eb24f05" +content-hash = "5888ae343ddeb60827c221df99e4426949467ce760b771e6eb89c3ebe0327a5b" [metadata.files] alabaster = [ @@ -3055,9 +3047,6 @@ wsproto = [ {file = "wsproto-1.1.0-py3-none-any.whl", hash = "sha256:2218cb57952d90b9fca325c0dcfb08c3bda93e8fd8070b0a17f048e2e47a521b"}, {file = "wsproto-1.1.0.tar.gz", hash = "sha256:a2e56bfd5c7cd83c1369d83b5feccd6d37798b74872866e62616e0ecf111bda8"}, ] -yep = [ - {file = "yep-0.4.tar.gz", hash = "sha256:41eac9625335b41657bcaf87083edf9e1a95178ea3c7ad3dd6d6ff5e2fe7ceea"}, -] zipp = [ {file = "zipp-3.8.0-py3-none-any.whl", hash = "sha256:c4f6e5bbf48e74f7a38e7cc5b0480ff42b0ae5178957d564d18932525d5cf099"}, {file = "zipp-3.8.0.tar.gz", hash = "sha256:56bf8aadb83c24db6c4b577e13de374ccfb67da2078beba1d037c17980bf43ad"}, diff --git a/pyproject.toml b/pyproject.toml index d4683c76..95da8e74 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -45,7 +45,7 @@ ipywidgets = "^7.7.1" memory-profiler = "^0.60.0" line-profiler = "^3.5.1" kaleido = "0.2.1" -yep = "^0.4" # c code profiling +# yep = "^0.4" # c code profiling [build-system] requires = [ From f892f566bdb0bf033fb691e9a77d4d56a8ed180b Mon Sep 17 00:00:00 2001 From: jonas Date: Sat, 30 Jul 2022 13:37:20 +0200 Subject: [PATCH 43/49] :test_tube: adding tests & fixing :bug:s --- plotly_resampler/aggregation/aggregators.py | 2 +- .../aggregation/algorithms/lttb_py.py | 35 ++++++------------- .../aggregation/algorithms/lttbc.c | 14 ++++---- tests/test_aggregators.py | 31 ++++++++++++++++ 4 files changed, 51 insertions(+), 31 deletions(-) diff --git a/plotly_resampler/aggregation/aggregators.py b/plotly_resampler/aggregation/aggregators.py index 0bad7d2c..07e30fc1 100644 --- a/plotly_resampler/aggregation/aggregators.py +++ b/plotly_resampler/aggregation/aggregators.py @@ -21,7 +21,7 @@ from .algorithms.lttb_c import LTTB_core_c as LTTB_core except: import warnings - warnings.warn("Could not import lttbcv2; will use a (slower) python alternative.") + warnings.warn("Could not import lttbc; will use a (slower) python alternative.") from .algorithms.lttb_py import LTTB_core_py as LTTB_core diff --git a/plotly_resampler/aggregation/algorithms/lttb_py.py b/plotly_resampler/aggregation/algorithms/lttb_py.py index cd1e6367..3fb10dca 100644 --- a/plotly_resampler/aggregation/algorithms/lttb_py.py +++ b/plotly_resampler/aggregation/algorithms/lttb_py.py @@ -2,6 +2,7 @@ import numpy as np + class LTTB_core_py: @staticmethod def _argmax_area(prev_x, prev_y, avg_next_x, avg_next_y, x_bucket, y_bucket) -> int: @@ -53,7 +54,10 @@ def downsample(x: np.ndarray, y: np.ndarray, n_out) -> np.ndarray: """ # Bucket size. Leave room for start and end data points block_size = (y.shape[0] - 2) / (n_out - 2) - offset = np.arange(start=1, stop=y.shape[0] - 1, step=block_size, dtype="int64") + # Note this'astype' cast must take place after array creation (and not with the + # aranage() its dtype argument) or it will cast the `block_size` step to an int + # before the arange array creation + offset = np.arange(start=1, stop=y.shape[0], step=block_size).astype(np.int64) # Construct the output array sampled_x = np.empty(n_out, dtype="int64") @@ -61,7 +65,7 @@ def downsample(x: np.ndarray, y: np.ndarray, n_out) -> np.ndarray: sampled_x[-1] = x.shape[0] - 1 a = 0 - for i in range(n_out - 4): + for i in range(n_out - 3): a = ( LTTB_core_py._argmax_area( prev_x=x[a], @@ -76,33 +80,16 @@ def downsample(x: np.ndarray, y: np.ndarray, n_out) -> np.ndarray: sampled_x[i + 1] = a # ------------ EDGE CASE ------------ - # The last part of the data is not a complete bucket - - # edge case 1; penultimate bucket - # calculate the mean of the last bucket - a = ( - LTTB_core_py._argmax_area( - prev_x=x[a], - prev_y=y[a], - avg_next_x=np.mean(x[offset[-1] :]), - avg_next_y=y[offset[-1] :].mean(), - x_bucket=x[offset[-2] : offset[-1]], - y_bucket=x[offset[-2] : offset[-1]], - ) - + offset[-2] - ) - sampled_x[-3] = a - - # edge case; the last part of the data + # next-average of last bucket = last point sampled_x[-2] = ( LTTB_core_py._argmax_area( prev_x=x[a], prev_y=y[a], - avg_next_x=x[-1], + avg_next_x=x[-1], # last point avg_next_y=y[-1], - x_bucket=x[offset[-1] :], - y_bucket=y[offset[-1] :], + x_bucket=x[offset[-2] : offset[-1]], + y_bucket=y[offset[-2] : offset[-1]], ) - + offset[-1] + + offset[-2] ) return sampled_x diff --git a/plotly_resampler/aggregation/algorithms/lttbc.c b/plotly_resampler/aggregation/algorithms/lttbc.c index a85751cd..e295d67c 100644 --- a/plotly_resampler/aggregation/algorithms/lttbc.c +++ b/plotly_resampler/aggregation/algorithms/lttbc.c @@ -1,7 +1,7 @@ #define PY_SSIZE_T_CLEAN #define NPY_NO_DEPRECATED_API NPY_1_7_API_VERSION #include // pull the python API into the current namespace -// #include // pull in the printf function +// #include // pull in the printf function #include #include #include @@ -109,6 +109,11 @@ static PyObject *downsample_int_double(PyObject *self, PyObject *args) double point_a_y = y[a]; double point_a_x = x[a]; + // if (i >= n_out - 3) + // { + // printf("prev_x = %f prev_y = %f \t range_off = %ld range_to = %ld \t avg_x = %f avg_y = %f\n", point_a_x, point_a_y, range_offs, range_to, avg_x, avg_y); + // } + double max_area = -1.0; for (; range_offs < range_to; range_offs++) { @@ -151,7 +156,7 @@ static PyObject *downsample_int_double(PyObject *self, PyObject *args) } // This method only returns the index positions of the selected points. -// almost everything can be sucessfully parsed to this so this will be +// almost everything can be sucessfully parsed to this so this will be // our fallback method static PyObject *downsample_double_double(PyObject *self, PyObject *args) { @@ -199,7 +204,7 @@ static PyObject *downsample_double_double(PyObject *self, PyObject *args) // Access the data in the NDArray! double *y = (double *)PyArray_DATA(y_array); - long long *x = (long long *)PyArray_DATA(x_array); + double *x = (double *)PyArray_DATA(x_array); // Create an empty output array with shape and dim for the output! npy_intp dims[1]; @@ -291,7 +296,6 @@ static PyObject *downsample_double_double(PyObject *self, PyObject *args) return NULL; } - // This method only returns the index positions of the selected points. // x = np.int64; y = np.float32 static PyObject *downsample_int_float(PyObject *self, PyObject *args) @@ -432,7 +436,6 @@ static PyObject *downsample_int_float(PyObject *self, PyObject *args) return NULL; } - // This method only returns the index positions of the selected points. // x = np.int64; y = np.int64 static PyObject *downsample_int_int(PyObject *self, PyObject *args) @@ -573,7 +576,6 @@ static PyObject *downsample_int_int(PyObject *self, PyObject *args) return NULL; } - // ------------------ Boilderplate code ------------------ // Method definition object static PyMethodDef methods[] = { diff --git a/tests/test_aggregators.py b/tests/test_aggregators.py index a397554c..550d322a 100644 --- a/tests/test_aggregators.py +++ b/tests/test_aggregators.py @@ -6,6 +6,8 @@ MinMaxAggregator, EfficientLTTB, ) +from plotly_resampler.aggregation.algorithms.lttb_py import LTTB_core_py +from plotly_resampler.aggregation.algorithms.lttb_c import LTTB_core_c import pandas as pd import numpy as np import pytest @@ -640,3 +642,32 @@ def treat_string_as_numeric_data(x): FuncAggregator( interleave_gaps=True, aggregation_func=treat_string_as_numeric_data ).aggregate(cat_series, n_out=n) + + +# ------------------------------- LTTB_Bindings ------------------------------- +def test_lttb_bindings(): + # Test whether both algorithms produce the same results with different types of + # input data + n = np.random.randint(low=1_000_000, high=2_000_000) + x_int = np.arange(n, dtype="int64") + x_double = x_int.astype("float64") + y_double = np.sin(x_int / 300) + np.random.randn(n) + y_float = y_double.astype("float32") + y_int = (100 * y_double).astype("int64") + + for n_out in np.random.randint(500, 2000, size=3): + sampled_x_c = LTTB_core_c.downsample(x_int, y_double, n_out) + sampled_x_py = LTTB_core_py.downsample(x_int, y_double, n_out) + assert all(sampled_x_c == sampled_x_py) + + sampled_x_c = LTTB_core_c.downsample(x_int, y_float, n_out) + sampled_x_py = LTTB_core_py.downsample(x_int, y_float, n_out) + assert all(sampled_x_c == sampled_x_py) + + sampled_x_c = LTTB_core_c.downsample(x_int, y_int, n_out) + sampled_x_py = LTTB_core_py.downsample(x_int, y_int, n_out) + assert all(sampled_x_c == sampled_x_py) + + sampled_x_c = LTTB_core_c.downsample(x_double, y_double, n_out) + sampled_x_py = LTTB_core_py.downsample(x_double, y_double, n_out) + assert (sampled_x_c == sampled_x_py).sum() / n_out > 0.95 From 12457eb0f171386352c426e6e2a4e77c0cd9e359 Mon Sep 17 00:00:00 2001 From: jonas Date: Sat, 30 Jul 2022 13:53:03 +0200 Subject: [PATCH 44/49] :muscle: Fix ipywidgets dependency #109 --- plotly_resampler/figure_resampler/utils.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/plotly_resampler/figure_resampler/utils.py b/plotly_resampler/figure_resampler/utils.py index 8913daac..5b39266e 100644 --- a/plotly_resampler/figure_resampler/utils.py +++ b/plotly_resampler/figure_resampler/utils.py @@ -4,7 +4,10 @@ import pandas as pd from plotly.basedatatypes import BaseFigure -from plotly.basewidget import BaseFigureWidget +try: + from plotly.basewidget import BaseFigureWidget +except ImportError: + BaseFigureWidget = None from typing import Any From 9777d516e3d332bb68d005e97ed057b0030bafab Mon Sep 17 00:00:00 2001 From: jonas Date: Mon, 8 Aug 2022 14:05:05 +0200 Subject: [PATCH 45/49] :monocle_face: print build errors on fail --- build.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.py b/build.py index 6244dc9f..22cc9c84 100644 --- a/build.py +++ b/build.py @@ -49,15 +49,15 @@ def run(self): " Unable to build the C extensions, will use the slower python " "fallback for LTTB" ) - pass + print(e) def build_extension(self, ext): try: build_ext.build_extension(self, ext) except ( + DistutilsPlatformError, CCompilerError, DistutilsExecError, - DistutilsPlatformError, ValueError, ) as e: print( From d5d6080ccbfca2e5d70b1aeaced21bda18e2d3ef Mon Sep 17 00:00:00 2001 From: jvdd Date: Thu, 11 Aug 2022 16:30:12 +0200 Subject: [PATCH 46/49] :pen: review --- .gitignore | 1 + README.md | 2 +- docs/sphinx/FAQ.rst | 14 ++++---- docs/sphinx/getting_started.rst | 4 +-- examples/README.md | 15 +++------ examples/basic_example.ipynb | 32 ++++++++----------- examples/dash_apps/01_minimal_global.py | 4 +-- examples/dash_apps/02_minimal_cache.py | 7 ++-- .../dash_apps/03_minimal_cache_dynamic.py | 23 ++++++------- examples/dash_apps/11_sine_generator.py | 8 ++--- examples/dash_apps/13_coarse_fine.py | 5 ++- examples/dash_apps/utils/callback_helpers.py | 6 ++-- .../dash_apps/utils/graph_construction.py | 3 +- .../aggregation/algorithms/lttb_py.py | 6 +++- tests/test_aggregators.py | 5 +++ 15 files changed, 67 insertions(+), 68 deletions(-) diff --git a/.gitignore b/.gitignore index f08fca10..1ce3074d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ venv* .cache_datasets/ *.DS_Store +file_system_store/ # Sphinx documentation *_build/ diff --git a/README.md b/README.md index 5a7a5041..86686432 100644 --- a/README.md +++ b/README.md @@ -158,7 +158,7 @@ In [this Plotly-Resampler demo](https://github.com/predict-idlab/plotly-resample * When running the code on a server, you should forward the port of the `FigureResampler.show_dash()` method to your local machine.
**Note** that you can add dynamic aggregation to plotly figures with the `FigureWidgetResampler` wrapper without needing to forward a port! -* The `FigureWidgetResampler` *uses the python main thread* for its data aggregation functionality, so when this main thread is occupied, no resampling logic can be executed. For example; if you perform long computations within your notebook, the kernel will be occupied during these computations, and will only execute the resampling operations which take place during these computations after finishing that computation. +* The `FigureWidgetResampler` *uses the IPython main thread* for its data aggregation functionality, so when this main thread is occupied, no resampling logic can be executed. For example; if you perform long computations within your notebook, the kernel will be occupied during these computations, and will only execute the resampling operations that take place during these computations after finishing that computation. * In general, when using downsampling one should be aware of (possible) [aliasing](https://en.wikipedia.org/wiki/Aliasing) effects. The [R] in the legend indicates when the corresponding trace is being resampled (and thus possibly distorted) or not. Additionally, the `~` suffix represent the mean aggregation bin size in terms of the sequence index. * The plotly **autoscale** event (triggered by the autoscale button or a double-click within the graph), **does not reset the axes but autoscales the current graph-view** of plotly-resampler figures. This design choice was made as it seemed more intuitive for the developers to support this behavior with double-click than the default axes-reset behavior. The graph axes can ofcourse be resetted by using the `reset_axis` button. If you want to give feedback and discuss this further with the developers, see issue [#49](https://github.com/predict-idlab/plotly-resampler/issues/49). diff --git a/docs/sphinx/FAQ.rst b/docs/sphinx/FAQ.rst index 945f8a78..674a74fe 100644 --- a/docs/sphinx/FAQ.rst +++ b/docs/sphinx/FAQ.rst @@ -20,8 +20,8 @@ FAQ ❓ This tilde suffix is only shown when the data is aggregated and represents the *mean aggregation bin size* which is the mean index-range difference between two consecutive aggregated samples. - * for *time-indexed data*: the mean time-range which is span between 2 consecutive samples. - * for *numeric-indexed data*: the mean numeric range which is span between 2 consecutive samples. + * for *time-indexed data*: the mean time-range between 2 consecutive (sampled) samples. + * for *numeric-indexed data*: the mean numeric range between 2 consecutive (sampled) samples. When the index is a range-index; the *mean aggregation bin size* represents the *mean* downsample ratio; i.e., the mean number of samples that are aggregated into one sample. @@ -44,7 +44,7 @@ plotly-resampler can be thought of as wrapper around plain plotly figures which * To have dynamic aggregation: * with ``FigureResampler``, you need to call ``show_dash`` (or output the object in a cell via ``IPython.display``) -> which spawns a dash-web app, and the dynamic aggregation is realized with dash callback - * with ``FigureWidgetResampler``, you need to use ``IPython.display`` on the object, which uses widget-events to realize dynamic aggregation. + * with ``FigureWidgetResampler``, you need to use ``IPython.display`` on the object, which uses widget-events to realize dynamic aggregation (via the running IPython kernel). .. raw:: html @@ -57,7 +57,7 @@ plotly-resampler can be thought of as wrapper around plain plotly figures which
-The ``TraceUpdater`` class is a custom dash component that aids ``dcc.Graph`` components to efficiently sent and update (in our case aggregated) data to the front-end. +The ``TraceUpdater`` class is a custom dash component that aids ``dcc.Graph`` components to efficiently send and update (in our case aggregated) data to the front-end. For more information on how to use the trace-updater component together with the ``FigureResampler``, see our dash app `examples `_` and look at the `trace-updater `_ its documentation. @@ -78,19 +78,19 @@ For more information on how to use the trace-updater component together with the **The main differences are**: -Datashader is able deal with various kinds of data (e.g., location related data, point clouds, ...), and plotly-resampler is more tailored towards time-series data visualizations. +Datashader can deal with various kinds of data (e.g., location related data, point clouds), whereas plotly-resampler is more tailored towards time-series data visualizations. Furthermore, datashader outputs a **rasterized image/array** encompassing all traces their data, whereas plotly-resampler outputs an **aggregated series** per trace. Thus, datashader is more suited for analyzing data where you do not want to pin-out a certain series/trace. In our opinion, datashader truly shines (for the time series use case) when: * you want a global, overlaying view of all your traces * you want to visualize a large number of time series in a single plot (many traces) -* there is a lot of noise on your high-frequency data and want to uncover the underlying pattern +* there is a lot of noise on your high-frequency data and you want to uncover the underlying pattern * you want to render all data points in your visualization In our opinion, plotly-resampler shines when: -* you need the capabilities to interact with the traces (e.g., hovering, toggling traces, hovertext pet trace) +* you need the capabilities to interact with the traces (e.g., hovering, toggling traces, hovertext per trace) * you want to use a less complex (but more restricted) visualization interface (as opposed to holoviews), i.e., plotly * you want to make existing plotly time-series figures more scalable and efficient * to build scalable Dash apps for time-series data visualization diff --git a/docs/sphinx/getting_started.rst b/docs/sphinx/getting_started.rst index 84f96299..9d6f09f7 100644 --- a/docs/sphinx/getting_started.rst +++ b/docs/sphinx/getting_started.rst @@ -56,7 +56,7 @@ Dynamic resampling callbacks are realized: **To add dynamic resampling using a FigureWidget, you should**: 1. wrap your plotly Figure (can be a ``go.Figure``) with :class:`FigureWidgetResampler ` - 2. output the ```FigureWidgetResampler`` instance in a cell + 2. output the ``FigureWidgetResampler`` instance in a cell .. tip:: @@ -120,7 +120,7 @@ The gif below demonstrates the example usage of of :class:`FigureWidgetResampler

-Furthermore, plotly figurewidget allows to conveniently add callbacks to for example click events. This allows to create a high-frequency time series annotation app in a couple of lines; a shown in the gif below and `this notebook `_. +Furthermore, plotly's ``FigureWidget`` allows to conveniently add callbacks to for example click events. This allows creating a high-frequency time series annotation app in a couple of lines; as shown in the gif below and in `this notebook `_. .. image:: _static/annotate_twitter.gif diff --git a/examples/README.md b/examples/README.md index de8bdcb8..bccf0ac5 100644 --- a/examples/README.md +++ b/examples/README.md @@ -1,12 +1,6 @@ # plotly-resampler examples -This directory withholds several examples, highlighting the applicability of -plotly-resampler for various use cases. - -To successfully run these examples, make sure that you've installed all the requirements by running: -```bash -pip install -r requirements.txt -``` +This directory withholds several examples, highlighting the applicability of plotly-resampler for various use cases. ## Prerequisites @@ -19,8 +13,7 @@ pip install -r requirements.txt ## 1. Example notebooks ### 1.1 basic examples -The [basic example notebook](basic_example.ipynb) covers most use-cases in which plotly resampler will be employed. It is the ideal hands-on starting point for data-scientists who want to use -plotly-resampler in their day-to-day jupyter environments. +The [basic example notebook](basic_example.ipynb) covers most use-cases in which plotly resampler will be employed. It servers as an ideal starting point for data-scientists who want to use plotly-resampler in their day-to-day jupyter environments. Additionally, this notebook also shows some more advanced functionalities, such as: * Retaining (a static) plotly-resampler figure in your notebook @@ -30,7 +23,7 @@ Additionally, this notebook also shows some more advanced functionalities, such ### 1.2 Figurewidget example -The [figurewidget example notebook](figurewidget_example.ipynb) utilizes the `FigureWidgetResampler` wrapper to create a `go.FigureWidget` with dynamic aggregation functionality. A major advantage of this approach is that this does not create a web application, thus not needing to be able to create / forward a network port. +The [figurewidget example notebook](figurewidget_example.ipynb) utilizes the `FigureWidgetResampler` wrapper to create a `go.FigureWidget` with dynamic aggregation functionality. A major advantage of this approach is that this does not create a web application, avoiding starting an application on a port (and forwarding that port when working remotely). Additionally, this notebook highlights how to use the `FigureWidget` its on-click callback to utilize plotly for large **time series annotation**. @@ -48,7 +41,7 @@ which `plotly-resampler` is integrated | **advanced apps** | | | [dynamic sine generator](dash_apps/11_sine_generator.py) | exponential sine generator which uses [pattern matching callbacks](https://dash.plotly.com/pattern-matching-callbacks) to remove and construct plotly-resampler graphs dynamically | | [file visualization](dash_apps/12_file_selector.py) | load and visualize multiple `.parquet` files with plotly-resampler | -| [dynamic static graph](dash_apps/13_coarse_fine.py) | Visualization dashboard in which a dynamic (i.e., plotly-resampler graph) and static graph (i.e., go.Figure) are shown (made for [this issue](https://github.com/predict-idlab/plotly-resampler/issues/56)). Graph interaction events on the coarse graph update the dynamic graph. | +| [dynamic static graph](dash_apps/13_coarse_fine.py) | Visualization dashboard in which a dynamic (i.e., plotly-resampler graph) and a coarse, static graph (i.e., go.Figure) are shown (made for [this issue](https://github.com/predict-idlab/plotly-resampler/issues/56)). Graph interaction events on the coarse graph update the dynamic graph. | ## 3. Other apps diff --git a/examples/basic_example.ipynb b/examples/basic_example.ipynb index 53a6c549..8e2c86f3 100644 --- a/examples/basic_example.ipynb +++ b/examples/basic_example.ipynb @@ -8,7 +8,7 @@ "outputs": [], "source": [ "%load_ext autoreload\n", - "%autoreload 2\n" + "%autoreload 2" ] }, { @@ -23,7 +23,6 @@ "\n", "import plotly.graph_objects as go\n", "from plotly.subplots import make_subplots\n", - "from datetime import datetime\n", "\n", "from helper import groupby_consecutive\n", "\n", @@ -344,7 +343,7 @@ "
  • we add a unique _uid to each object
  • \n", "
  • we add a new endpoint to the underlying flask app that
    • \n", "
    • is only accessible via the corresponding app its _uid
    • \n", - "
    • hasCORS rights for any origin and 'Content-Type' headers
    • \n", + "
    • has CORS rights for any origin and 'Content-Type' headers
    • \n", "
    \n", "
  • Note that this is the only CORS endpoint of the JupyterDash app & is only preset when \"inline_persistent\" is used!
  • \n", "
  • we check in the JavaScript output of the notebook cell whether that endpoint is reachable and emits the expected message (i.e., \"Alive\")
    • \n", @@ -461,7 +460,7 @@ "* restart the kernel; and reopen this notebook\n", "* export this notebook to html\n", "\n", - "ou should see a static (aggregated) image of the above figure" + "you should see a static (aggregated) image of the above figure" ] }, { @@ -506,8 +505,8 @@ "id": "6bffa11a", "metadata": {}, "source": [ - "now we adjust the figure data \n", - "**note**: after running the cell below, we need to manually trigger a graph update (by for example zooming / resetting the axes) to ensure that the new data is shown." + "Now we adjust the figure data \n", + "**Note**: after running the cell below, we need to manually trigger a graph update (by for example zooming / resetting the axes) to ensure that the new data is shown." ] }, { @@ -547,8 +546,8 @@ "id": "22687274", "metadata": {}, "source": [ - "now we adjust the figure data \n", - "**note**: after running the cell below, we need to manually trigger a graph update (by for example zooming / resetting the axes) to ensure that the new data is shown." + "Now we adjust the figure data \n", + "**Note**: after running the cell below, we need to manually trigger a graph update (by for example zooming / resetting the axes) to ensure that the new data is shown." ] }, { @@ -566,7 +565,7 @@ "id": "f4130fac", "metadata": {}, "source": [ - "**pro tip**: `FigureWidgetResampler` has the `reload_data` and `reset_axes` methods to do this automatically" + "**Pro tip**: `FigureWidgetResampler` has the `reload_data` and `reset_axes` methods to do this automatically" ] }, { @@ -1137,19 +1136,14 @@ ")\n", "fig.show_dash(mode=\"inline\", port=9032)\n" ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "336f4a9f", - "metadata": {}, - "outputs": [], - "source": [] } ], "metadata": { + "interpreter": { + "hash": "7de7e435a0bf9ca45826b967e5fb3b58dc0ed05475145ede0348ba81bb2b2016" + }, "kernelspec": { - "display_name": "Python 3.10.5 ('plotly-resampler-MpVFAeoZ-py3.10')", + "display_name": "Python 3.8.10 ('venv')", "language": "python", "name": "python3" }, @@ -1163,7 +1157,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.5" + "version": "3.8.10" }, "toc-autonumbering": true, "vscode": { diff --git a/examples/dash_apps/01_minimal_global.py b/examples/dash_apps/01_minimal_global.py index dfd79451..67315b86 100644 --- a/examples/dash_apps/01_minimal_global.py +++ b/examples/dash_apps/01_minimal_global.py @@ -1,6 +1,6 @@ """Minimal dash app example. -Click on a button, and see a plotly-resampler graph of a noisy sinusoid. +Click on a button, and see a plotly-resampler graph of two noisy sinusoids. No dynamic graph construction / pattern matching callbacks are needed. This example uses a global FigureResampler object, which is considered a bad practice. @@ -30,7 +30,7 @@ app = dash.Dash(__name__) fig: FigureResampler = FigureResampler() # NOTE: in this example, this reference to a FigureResampler is essential to preserve -# throughout the whole dash app! If your dash app want to create a new go.Figure(), +# throughout the whole dash app! If your dash app wants to create a new go.Figure(), # you should not construct a new FigureResampler object, but replace the figure of this # FigureResampler object by using the FigureResampler.replace() method. diff --git a/examples/dash_apps/02_minimal_cache.py b/examples/dash_apps/02_minimal_cache.py index e0ef1630..10886717 100644 --- a/examples/dash_apps/02_minimal_cache.py +++ b/examples/dash_apps/02_minimal_cache.py @@ -1,12 +1,11 @@ """Minimal dash app example. -Click on a button, and see a plotly-resampler graph of a noisy sinusoid. +Click on a button, and see a plotly-resampler graph of two noisy sinusoids. No dynamic graph construction / pattern matching callbacks are needed. This example uses the dash-extensions its ServersideOutput functionality to cache the FigureResampler per user/session on the server side. This way, no global figure -variable is used and shows the best practice of using plotly-resampler Figures within -dash-apps. +variable is used and shows the best practice of using plotly-resampler within dash-apps. """ @@ -34,7 +33,7 @@ html.H1("plotly-resampler + dash-extensions", style={"textAlign": "center"}), html.Button("plot chart", id="plot-button", n_clicks=0), html.Hr(), - # The graph and it's needed components to serialize and update efficiently + # The graph and its needed components to serialize and update efficiently # Note: we also add a dcc.Store component, which will be used to link the # server side cached FigureResampler object dcc.Graph(id="graph-id"), diff --git a/examples/dash_apps/03_minimal_cache_dynamic.py b/examples/dash_apps/03_minimal_cache_dynamic.py index 10ebcdba..c116dabf 100644 --- a/examples/dash_apps/03_minimal_cache_dynamic.py +++ b/examples/dash_apps/03_minimal_cache_dynamic.py @@ -1,19 +1,20 @@ """Minimal dynamic dash app example. -Click on a button, and draw a new plotly-resampler graph of a noisy sinusoid. +Click on a button, and draw a new plotly-resampler graph of two noisy sinusoids. This example uses pattern-matching callbacks to update dynamically constructed graphs. The plotly-resampler graphs themselves are cached on the server side. -The main difference between this example and the dash_app_minimal_cache.py is that here, -we want to cache using a dcc.Store that is not yet available on the client side. As a -result we split up our logic into two callbacks: (1) the callback used to construct the -necessary components and send them to the client-side, and (2) the callback used to -construct the actual plotly-resampler graph and cache it on the server side. These -two callbacks are chained together using the dcc.Interval component. +The main difference between this example and 02_minimal_cache.py is that here, we want +to cache using a dcc.Store that is not yet available on the client side. As a result we +split up our logic into two callbacks: (1) the callback used to construct the necessary +components and send them to the client-side, and (2) the callback used to construct the +actual plotly-resampler graph and cache it on the server side. These two callbacks are +chained together using the dcc.Interval component. """ from uuid import uuid4 +from typing import List import numpy as np import plotly.graph_objects as go @@ -46,18 +47,18 @@ # ------------------------------------ DASH logic ------------------------------------- # This method adds the needed components to the front-end, but does not yet contain the -# figureResampler graph construction logic. +# FigureResampler graph construction logic. @app.callback( Output("container", "children"), Input("add-chart", "n_clicks"), State("container", "children"), prevent_initial_call=True, ) -def add_graph_div(n_clicks: int, div_children: list[html.Div]): +def add_graph_div(n_clicks: int, div_children: List[html.Div]): uid = str(uuid4()) new_child = html.Div( children=[ - # The graph and it's needed components to serialize and update efficiently + # The graph and its needed components to serialize and update efficiently # Note: we also add a dcc.Store component, which will be used to link the # server side cached FigureResampler object dcc.Graph(id={"type": "dynamic-graph", "index": uid}, figure=go.Figure()), @@ -75,7 +76,7 @@ def add_graph_div(n_clicks: int, div_children: list[html.Div]): return div_children -# This method constructs the figureResampler graph and caches it on the server side +# This method constructs the FigureResampler graph and caches it on the server side @app.callback( ServersideOutput({"type": "store", "index": MATCH}, "data"), Output({"type": "dynamic-graph", "index": MATCH}, "figure"), diff --git a/examples/dash_apps/11_sine_generator.py b/examples/dash_apps/11_sine_generator.py index 2584b96d..05b6268c 100644 --- a/examples/dash_apps/11_sine_generator.py +++ b/examples/dash_apps/11_sine_generator.py @@ -102,7 +102,7 @@ # ------------------------------------ DASH logic ------------------------------------- # This method adds the needed components to the front-end, but does not yet contain the -# figureResampler graph construction logic. +# FigureResampler graph construction logic. @app.callback( Output("graph-container", "children"), Input("add-graph-btn", "n_clicks"), @@ -132,7 +132,7 @@ def add_or_remove_graph(add_graph, remove_graph, n, exp, gc_children): uid = str(uuid4()) new_child = html.Div( children=[ - # The graph and it's needed components to serialize and update efficiently + # The graph and its needed components to serialize and update efficiently # Note: we also add a dcc.Store component, which will be used to link the # server side cached FigureResampler object dcc.Graph(id={"type": "dynamic-graph", "index": uid}, figure=go.Figure()), @@ -150,7 +150,7 @@ def add_or_remove_graph(add_graph, remove_graph, n, exp, gc_children): return gc_children -# This method constructs the figureResampler graph and caches it on the server side +# This method constructs the FigureResampler graph and caches it on the server side @app.callback( ServersideOutput({"type": "store", "index": MATCH}, "data"), Output({"type": "dynamic-graph", "index": MATCH}, "figure"), @@ -198,4 +198,4 @@ def update_fig(relayoutdata: dict, fig: FigureResampler): # --------------------------------- Running the app --------------------------------- if __name__ == "__main__": - app.run_server(debug=True) + app.run_server(debug=True, port=9023) diff --git a/examples/dash_apps/13_coarse_fine.py b/examples/dash_apps/13_coarse_fine.py index 239080d0..479e58d3 100644 --- a/examples/dash_apps/13_coarse_fine.py +++ b/examples/dash_apps/13_coarse_fine.py @@ -171,9 +171,12 @@ def construct_plot_graph(n_clicks, *folder_list): prevent_initial_call=True, ) def update_dynamic_fig(coarse_grained_relayout, fine_grained_relayout, fr_fig): + if fr_fig is None: # When the figure does not exist -> do nothing + return dash.no_update + ctx = dash.callback_context trigger_id = ctx.triggered[0].get("prop_id", "").split(".")[0] - + if trigger_id == "plotly-resampler-graph": return fr_fig.construct_update_data(fine_grained_relayout) elif trigger_id == "coarse-graph": diff --git a/examples/dash_apps/utils/callback_helpers.py b/examples/dash_apps/utils/callback_helpers.py index b2985a66..121e9673 100644 --- a/examples/dash_apps/utils/callback_helpers.py +++ b/examples/dash_apps/utils/callback_helpers.py @@ -6,6 +6,7 @@ from pathlib import Path from typing import Dict, List +import itertools import dash_bootstrap_components as dbc from dash import Input, Output, State, dcc, html from functional import seq @@ -119,14 +120,13 @@ def get_selector_states(n: int) -> List[State]: """ # Note: the list sum-operations flattens the list return list( - sum( + itertools.chain.from_iterable( [ ( State(f"folder-selector{i}", "value"), State(f"file-selector{i}", "value"), ) for i in range(1, n + 1) - ], - (), + ] ) ) diff --git a/examples/dash_apps/utils/graph_construction.py b/examples/dash_apps/utils/graph_construction.py index 08e637b7..0320a9ff 100644 --- a/examples/dash_apps/utils/graph_construction.py +++ b/examples/dash_apps/utils/graph_construction.py @@ -26,11 +26,10 @@ def visualize_multiple_files(file_list: List[Union[str, Path]]) -> FigureResampl fig.update_layout(height=min(900, 350 * len(file_list))) for i, f in enumerate(file_list, 1): - df = pd.read_parquet(f) # should be replaced by more generic data loading code + df = pd.read_parquet(f) # TODO: replace with more generic data loading code if "timestamp" in df.columns: df = df.set_index("timestamp") for c in df.columns[::-1]: - print(df[c].dtype) fig.add_trace(go.Scattergl(name=c), hf_x=df.index, hf_y=df[c], row=i, col=1) return fig diff --git a/plotly_resampler/aggregation/algorithms/lttb_py.py b/plotly_resampler/aggregation/algorithms/lttb_py.py index 3fb10dca..e2b160a7 100644 --- a/plotly_resampler/aggregation/algorithms/lttb_py.py +++ b/plotly_resampler/aggregation/algorithms/lttb_py.py @@ -54,7 +54,7 @@ def downsample(x: np.ndarray, y: np.ndarray, n_out) -> np.ndarray: """ # Bucket size. Leave room for start and end data points block_size = (y.shape[0] - 2) / (n_out - 2) - # Note this'astype' cast must take place after array creation (and not with the + # Note this 'astype' cast must take place after array creation (and not with the # aranage() its dtype argument) or it will cast the `block_size` step to an int # before the arange array creation offset = np.arange(start=1, stop=y.shape[0], step=block_size).astype(np.int64) @@ -64,6 +64,10 @@ def downsample(x: np.ndarray, y: np.ndarray, n_out) -> np.ndarray: sampled_x[0] = 0 sampled_x[-1] = x.shape[0] - 1 + # Convert y to int if it is boolean + if y.dtype == np.bool: + y = y.astype(np.int8) + a = 0 for i in range(n_out - 3): a = ( diff --git a/tests/test_aggregators.py b/tests/test_aggregators.py index 550d322a..799d1c7c 100644 --- a/tests/test_aggregators.py +++ b/tests/test_aggregators.py @@ -654,6 +654,7 @@ def test_lttb_bindings(): y_double = np.sin(x_int / 300) + np.random.randn(n) y_float = y_double.astype("float32") y_int = (100 * y_double).astype("int64") + y_bool = (x_int % 2).astype("bool") for n_out in np.random.randint(500, 2000, size=3): sampled_x_c = LTTB_core_c.downsample(x_int, y_double, n_out) @@ -668,6 +669,10 @@ def test_lttb_bindings(): sampled_x_py = LTTB_core_py.downsample(x_int, y_int, n_out) assert all(sampled_x_c == sampled_x_py) + sampled_x_c = LTTB_core_c.downsample(x_int, y_bool, n_out) + sampled_x_py = LTTB_core_py.downsample(x_int, y_bool, n_out) + assert all(sampled_x_c == sampled_x_py) + sampled_x_c = LTTB_core_c.downsample(x_double, y_double, n_out) sampled_x_py = LTTB_core_py.downsample(x_double, y_double, n_out) assert (sampled_x_c == sampled_x_py).sum() / n_out > 0.95 From ee2176e87888b7bc7d0d2b1b8a65b4941ee32d6e Mon Sep 17 00:00:00 2001 From: jonvdrdo Date: Thu, 11 Aug 2022 17:21:32 +0200 Subject: [PATCH 47/49] :pencil: meta-review --- examples/README.md | 2 +- examples/dash_apps/13_coarse_fine.py | 2 +- examples/dash_apps/utils/callback_helpers.py | 1 - 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/examples/README.md b/examples/README.md index bccf0ac5..bc68a990 100644 --- a/examples/README.md +++ b/examples/README.md @@ -13,7 +13,7 @@ pip install -r requirements.txt ## 1. Example notebooks ### 1.1 basic examples -The [basic example notebook](basic_example.ipynb) covers most use-cases in which plotly resampler will be employed. It servers as an ideal starting point for data-scientists who want to use plotly-resampler in their day-to-day jupyter environments. +The [basic example notebook](basic_example.ipynb) covers most use-cases in which plotly resampler will be employed. It serves as an ideal starting point for data-scientists who want to use plotly-resampler in their day-to-day jupyter environments. Additionally, this notebook also shows some more advanced functionalities, such as: * Retaining (a static) plotly-resampler figure in your notebook diff --git a/examples/dash_apps/13_coarse_fine.py b/examples/dash_apps/13_coarse_fine.py index 479e58d3..cdf8c02f 100644 --- a/examples/dash_apps/13_coarse_fine.py +++ b/examples/dash_apps/13_coarse_fine.py @@ -176,7 +176,7 @@ def update_dynamic_fig(coarse_grained_relayout, fine_grained_relayout, fr_fig): ctx = dash.callback_context trigger_id = ctx.triggered[0].get("prop_id", "").split(".")[0] - + if trigger_id == "plotly-resampler-graph": return fr_fig.construct_update_data(fine_grained_relayout) elif trigger_id == "coarse-graph": diff --git a/examples/dash_apps/utils/callback_helpers.py b/examples/dash_apps/utils/callback_helpers.py index 121e9673..dbb53bfb 100644 --- a/examples/dash_apps/utils/callback_helpers.py +++ b/examples/dash_apps/utils/callback_helpers.py @@ -118,7 +118,6 @@ def get_selector_states(n: int) -> List[State]: The number of folder selectors """ - # Note: the list sum-operations flattens the list return list( itertools.chain.from_iterable( [ From 070e6fcf9a9eda69c8470877d31c43cb524d2ace Mon Sep 17 00:00:00 2001 From: jonvdrdo Date: Thu, 11 Aug 2022 17:38:32 +0200 Subject: [PATCH 48/49] :pray: updating tests --- tests/test_aggregators.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_aggregators.py b/tests/test_aggregators.py index 799d1c7c..d682c570 100644 --- a/tests/test_aggregators.py +++ b/tests/test_aggregators.py @@ -654,7 +654,7 @@ def test_lttb_bindings(): y_double = np.sin(x_int / 300) + np.random.randn(n) y_float = y_double.astype("float32") y_int = (100 * y_double).astype("int64") - y_bool = (x_int % 2).astype("bool") + y_bool = (x_int % 250).astype("bool") for n_out in np.random.randint(500, 2000, size=3): sampled_x_c = LTTB_core_c.downsample(x_int, y_double, n_out) From 8bed8a793e8604b94f64e11768b7c8f379cb5c33 Mon Sep 17 00:00:00 2001 From: jonvdrdo Date: Thu, 11 Aug 2022 17:58:37 +0200 Subject: [PATCH 49/49] :pray: :pray: --- tests/test_aggregators.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/test_aggregators.py b/tests/test_aggregators.py index d682c570..1287cca5 100644 --- a/tests/test_aggregators.py +++ b/tests/test_aggregators.py @@ -659,20 +659,20 @@ def test_lttb_bindings(): for n_out in np.random.randint(500, 2000, size=3): sampled_x_c = LTTB_core_c.downsample(x_int, y_double, n_out) sampled_x_py = LTTB_core_py.downsample(x_int, y_double, n_out) - assert all(sampled_x_c == sampled_x_py) + assert sum(sampled_x_c == sampled_x_py) / len(sampled_x_c) > 0.995 sampled_x_c = LTTB_core_c.downsample(x_int, y_float, n_out) sampled_x_py = LTTB_core_py.downsample(x_int, y_float, n_out) - assert all(sampled_x_c == sampled_x_py) + assert sum(sampled_x_c == sampled_x_py) / len(sampled_x_c) > 0.995 sampled_x_c = LTTB_core_c.downsample(x_int, y_int, n_out) sampled_x_py = LTTB_core_py.downsample(x_int, y_int, n_out) - assert all(sampled_x_c == sampled_x_py) + assert sum(sampled_x_c == sampled_x_py) / len(sampled_x_c) > 0.995 sampled_x_c = LTTB_core_c.downsample(x_int, y_bool, n_out) sampled_x_py = LTTB_core_py.downsample(x_int, y_bool, n_out) - assert all(sampled_x_c == sampled_x_py) + assert sum(sampled_x_c == sampled_x_py) / len(sampled_x_c) > 0.995 sampled_x_c = LTTB_core_c.downsample(x_double, y_double, n_out) sampled_x_py = LTTB_core_py.downsample(x_double, y_double, n_out) - assert (sampled_x_c == sampled_x_py).sum() / n_out > 0.95 + assert sum(sampled_x_c == sampled_x_py) / len(sampled_x_c) > 0.995