From 9873e9719a042784fb37e118c14a6955659d20af Mon Sep 17 00:00:00 2001 From: FBruzzesi Date: Sat, 28 Sep 2024 19:08:19 +0200 Subject: [PATCH 001/106] non core changes --- packages/python/plotly/optional-requirements.txt | 1 + packages/python/plotly/plotly/express/__init__.py | 9 --------- packages/python/plotly/plotly/express/_imshow.py | 7 +++++-- .../plotly/express/trendline_functions/__init__.py | 12 ++++++++++-- 4 files changed, 16 insertions(+), 13 deletions(-) diff --git a/packages/python/plotly/optional-requirements.txt b/packages/python/plotly/optional-requirements.txt index ba3c054912..0f51b73713 100644 --- a/packages/python/plotly/optional-requirements.txt +++ b/packages/python/plotly/optional-requirements.txt @@ -39,6 +39,7 @@ ipython ## pandas deps for some matplotlib functionality ## pandas +narwhals>=1.8.4 ## scipy deps for some FigureFactory functions ## scipy diff --git a/packages/python/plotly/plotly/express/__init__.py b/packages/python/plotly/plotly/express/__init__.py index 935d674578..460ce1b998 100644 --- a/packages/python/plotly/plotly/express/__init__.py +++ b/packages/python/plotly/plotly/express/__init__.py @@ -2,15 +2,6 @@ `plotly.express` is a terse, consistent, high-level wrapper around `plotly.graph_objects` for rapid data exploration and figure generation. Learn more at https://plotly.com/python/plotly-express/ """ -from plotly import optional_imports - -pd = optional_imports.get_module("pandas") -if pd is None: - raise ImportError( - """\ -Plotly express requires pandas to be installed.""" - ) - from ._imshow import imshow from ._chart_types import ( # noqa: F401 scatter, diff --git a/packages/python/plotly/plotly/express/_imshow.py b/packages/python/plotly/plotly/express/_imshow.py index de0e22284b..72750d7540 100644 --- a/packages/python/plotly/plotly/express/_imshow.py +++ b/packages/python/plotly/plotly/express/_imshow.py @@ -2,7 +2,7 @@ from _plotly_utils.basevalidators import ColorscaleValidator from ._core import apply_default_cascade, init_figure, configure_animation_controls from .imshow_utils import rescale_intensity, _integer_ranges, _integer_types -import pandas as pd +import narwhals.stable.v1 as nw import numpy as np import itertools from plotly.utils import image_array_to_data_uri @@ -321,7 +321,10 @@ def imshow( aspect = "equal" # --- Set the value of binary_string (forbidden for pandas) - if isinstance(img, pd.DataFrame): + # TODO: Should this be generic for all dataframes? + if (pd := nw.dependencies.get_pandas()) is not None and isinstance( + img, pd.DataFrame + ): if binary_string: raise ValueError("Binary strings cannot be used with pandas arrays") is_dataframe = True diff --git a/packages/python/plotly/plotly/express/trendline_functions/__init__.py b/packages/python/plotly/plotly/express/trendline_functions/__init__.py index 4a1faf70b2..34b569cfb3 100644 --- a/packages/python/plotly/plotly/express/trendline_functions/__init__.py +++ b/packages/python/plotly/plotly/express/trendline_functions/__init__.py @@ -8,7 +8,7 @@ exposed as part of the public API for documentation purposes. """ -import pandas as pd +import narwhals.stable.v1 as nw import numpy as np __all__ = ["ols", "lowess", "rolling", "ewm", "expanding"] @@ -114,7 +114,15 @@ def _pandas(mode, trendline_options, x_raw, y, non_missing): trendline_options = trendline_options.copy() function_name = trendline_options.pop("function", "mean") function_args = trendline_options.pop("function_args", dict()) - series = pd.Series(y, index=x_raw) + + pd = nw.dependencies.get_pandas() + if pd is None: + msg = "Trendline requires pandas to be installed" + raise ImportError(msg) + + series = pd.Series(y, index=x_raw.to_numpy()) + + # TODO: If narwhals were to sopport rolling, ewm and expanding then we could go around these agg = getattr(series, mode) # e.g. series.rolling agg_obj = agg(**trendline_options) # e.g. series.rolling(**opts) function = getattr(agg_obj, function_name) # e.g. series.rolling(**opts).mean From 0389591c8714737e43d3f4285c7c50ee3de6a457 Mon Sep 17 00:00:00 2001 From: FBruzzesi Date: Sat, 28 Sep 2024 21:28:58 +0200 Subject: [PATCH 002/106] _core overhaul --- .../python/plotly/plotly/express/_core.py | 688 +++++++++++------- 1 file changed, 430 insertions(+), 258 deletions(-) diff --git a/packages/python/plotly/plotly/express/_core.py b/packages/python/plotly/plotly/express/_core.py index 9a79810506..708fce1242 100644 --- a/packages/python/plotly/plotly/express/_core.py +++ b/packages/python/plotly/plotly/express/_core.py @@ -1,14 +1,16 @@ import plotly.graph_objs as go import plotly.io as pio from collections import namedtuple, OrderedDict +from collections.abc import Sequence +from functools import reduce from ._special_inputs import IdentityMap, Constant, Range from .trendline_functions import ols, lowess, rolling, expanding, ewm from _plotly_utils.basevalidators import ColorscaleValidator from plotly.colors import qualitative, sequential import math -from packaging import version -import pandas as pd + +import narwhals.stable.v1 as nw import numpy as np from plotly._subplots import ( @@ -17,9 +19,9 @@ _subplot_type_for_trace_type, ) -pandas_2_2_0 = version.parse(pd.__version__) >= version.parse("2.2.0") - NO_COLOR = "px_no_color_constant" +NW_NUMERIC_DTYPES = {nw.Float32, nw.Float64, nw.Int8, nw.Int16, nw.Int32, nw.Int64} + trendline_functions = dict( lowess=lowess, rolling=rolling, ewm=ewm, expanding=expanding, ols=ols ) @@ -154,8 +156,8 @@ def invert_label(args, column): return column -def _is_continuous(df, col_name): - return df[col_name].dtype.kind in "ifc" +def _is_continuous(df: nw.DataFrame, col_name: str) -> bool: + return df.get_column(col_name).dtype in NW_NUMERIC_DTYPES def get_decorated_label(args, column, role): @@ -270,8 +272,11 @@ def make_trace_kwargs(args, trace_spec, trace_data, mapping_labels, sizeref): fit_results : dict fit information to be used for trendlines """ + trace_data: nw.DataFrame + df: nw.DataFrame = args["data_frame"] + if "line_close" in args and args["line_close"]: - trace_data = pd.concat([trace_data, trace_data.iloc[:1]]) + trace_data = nw.concat([trace_data, trace_data.head(1)], how="vertical") trace_patch = trace_spec.trace_patch.copy() or {} fit_results = None hover_header = "" @@ -280,17 +285,14 @@ def make_trace_kwargs(args, trace_spec, trace_data, mapping_labels, sizeref): attr_label = get_decorated_label(args, attr_value, attr_name) if attr_name == "dimensions": dims = [ - (name, column) - for (name, column) in trace_data.items() + (name, trace_data.get_column(name)) + for name in trace_data.columns if ((not attr_value) or (name in attr_value)) - and ( - trace_spec.constructor != go.Parcoords - or _is_continuous(args["data_frame"], name) - ) + and (trace_spec.constructor != go.Parcoords or _is_continuous(df, name)) and ( trace_spec.constructor != go.Parcats or (attr_value is not None and name in attr_value) - or len(args["data_frame"][name].unique()) + or df.get_column(name).n_unique() <= args["dimensions_max_cardinality"] ) ] @@ -308,7 +310,7 @@ def make_trace_kwargs(args, trace_spec, trace_data, mapping_labels, sizeref): if attr_name == "size": if "marker" not in trace_patch: trace_patch["marker"] = dict() - trace_patch["marker"]["size"] = trace_data[attr_value] + trace_patch["marker"]["size"] = trace_data.get_column(attr_value) trace_patch["marker"]["sizemode"] = "area" trace_patch["marker"]["sizeref"] = sizeref mapping_labels[attr_label] = "%{marker.size}" @@ -322,13 +324,16 @@ def make_trace_kwargs(args, trace_spec, trace_data, mapping_labels, sizeref): if ( args["x"] and args["y"] - and len(trace_data[[args["x"], args["y"]]].dropna()) > 1 + and len( # TODO: Handle case arg["y"] is a list + trace_data.select(nw.col(args["x"], args["y"])).drop_nulls() + ) + > 1 ): # sorting is bad but trace_specs with "trendline" have no other attrs - sorted_trace_data = trace_data.sort_values(by=args["x"]) - y = sorted_trace_data[args["y"]].values - x = sorted_trace_data[args["x"]].values - + sorted_trace_data = trace_data.sort(by=args["x"]) + y = sorted_trace_data.select(nw.col(args["y"])).to_numpy().squeeze() + x = sorted_trace_data.get_column(args["x"]).to_numpy().squeeze() + # TODO: Fix dtype if x.dtype.type == np.datetime64: # convert to unix epoch seconds x = x.astype(np.int64) / 10**9 @@ -356,17 +361,26 @@ def make_trace_kwargs(args, trace_spec, trace_data, mapping_labels, sizeref): non_missing = np.logical_not( np.logical_or(np.isnan(y), np.isnan(x)) ) - trace_patch["x"] = sorted_trace_data[args["x"]][non_missing] + # TODO: Double check slicing here or find workaround + trace_patch["x"] = sorted_trace_data[non_missing].select( + nw.col(args["x"]) + ) trendline_function = trendline_functions[attr_value] y_out, hover_header, fit_results = trendline_function( args["trendline_options"], - sorted_trace_data[args["x"]], - x, - y, + sorted_trace_data.get_column(args["x"]), # narwhals series + x, # numpy array + y, # numpy array args["x"], args["y"], - non_missing, + non_missing, # numpy array ) + # TODO: This is most likely not needed anymore + # y_out = nw.new_series( + # name="", + # values=y_out, + # native_namespace=sorted_trace_data.__native_namespace__(), + # ) assert len(y_out) == len( trace_patch["x"] ), "missing-data-handling failure in trendline code" @@ -378,19 +392,19 @@ def make_trace_kwargs(args, trace_spec, trace_data, mapping_labels, sizeref): arr = "arrayminus" if attr_name.endswith("minus") else "array" if error_xy not in trace_patch: trace_patch[error_xy] = {} - trace_patch[error_xy][arr] = trace_data[attr_value] + trace_patch[error_xy][arr] = trace_data.get_column(attr_value) elif attr_name == "custom_data": if len(attr_value) > 0: # here we store a data frame in customdata, and it's serialized # as a list of row lists, which is what we want - trace_patch["customdata"] = trace_data[attr_value] + trace_patch["customdata"] = trace_data.select(nw.col(attr_value)) elif attr_name == "hover_name": if trace_spec.constructor not in [ go.Histogram, go.Histogram2d, go.Histogram2dContour, ]: - trace_patch["hovertext"] = trace_data[attr_value] + trace_patch["hovertext"] = trace_data.get_column(attr_value) if hover_header == "": hover_header = "%{hovertext}

" elif attr_name == "hover_data": @@ -431,7 +445,7 @@ def make_trace_kwargs(args, trace_spec, trace_data, mapping_labels, sizeref): go.Choroplethmap, go.Choroplethmapbox, ]: - trace_patch["z"] = trace_data[attr_value] + trace_patch["z"] = trace_data.get_column(attr_value) trace_patch["coloraxis"] = "coloraxis1" mapping_labels[attr_label] = "%{z}" elif trace_spec.constructor in [ @@ -445,7 +459,9 @@ def make_trace_kwargs(args, trace_spec, trace_data, mapping_labels, sizeref): trace_patch["marker"] = dict() if args.get("color_is_continuous"): - trace_patch["marker"]["colors"] = trace_data[attr_value] + trace_patch["marker"]["colors"] = trace_data.get_column( + attr_value + ) trace_patch["marker"]["coloraxis"] = "coloraxis1" mapping_labels[attr_label] = "%{color}" else: @@ -454,7 +470,7 @@ def make_trace_kwargs(args, trace_spec, trace_data, mapping_labels, sizeref): mapping = args["color_discrete_map"].copy() else: mapping = {} - for cat in trace_data[attr_value]: + for cat in trace_data.get_column(attr_value): if mapping.get(cat) is None: mapping[cat] = args["color_discrete_sequence"][ len(mapping) % len(args["color_discrete_sequence"]) @@ -466,24 +482,24 @@ def make_trace_kwargs(args, trace_spec, trace_data, mapping_labels, sizeref): colorable = "line" if colorable not in trace_patch: trace_patch[colorable] = dict() - trace_patch[colorable]["color"] = trace_data[attr_value] + trace_patch[colorable]["color"] = trace_data.get_column(attr_value) trace_patch[colorable]["coloraxis"] = "coloraxis1" mapping_labels[attr_label] = "%%{%s.color}" % colorable elif attr_name == "animation_group": - trace_patch["ids"] = trace_data[attr_value] + trace_patch["ids"] = trace_data.get_column(attr_value) elif attr_name == "locations": - trace_patch[attr_name] = trace_data[attr_value] + trace_patch[attr_name] = trace_data.get_column(attr_value) mapping_labels[attr_label] = "%{location}" elif attr_name == "values": - trace_patch[attr_name] = trace_data[attr_value] + trace_patch[attr_name] = trace_data.get_column(attr_value) _label = "value" if attr_label == "values" else attr_label mapping_labels[_label] = "%{value}" elif attr_name == "parents": - trace_patch[attr_name] = trace_data[attr_value] + trace_patch[attr_name] = trace_data.get_column(attr_value) _label = "parent" if attr_label == "parents" else attr_label mapping_labels[_label] = "%{parent}" elif attr_name == "ids": - trace_patch[attr_name] = trace_data[attr_value] + trace_patch[attr_name] = trace_data.get_column(attr_value) _label = "id" if attr_label == "ids" else attr_label mapping_labels[_label] = "%{id}" elif attr_name == "names": @@ -494,13 +510,13 @@ def make_trace_kwargs(args, trace_spec, trace_data, mapping_labels, sizeref): go.Pie, go.Funnelarea, ]: - trace_patch["labels"] = trace_data[attr_value] + trace_patch["labels"] = trace_data.get_column(attr_value) _label = "label" if attr_label == "names" else attr_label mapping_labels[_label] = "%{label}" else: - trace_patch[attr_name] = trace_data[attr_value] + trace_patch[attr_name] = trace_data.get_column(attr_value) else: - trace_patch[attr_name] = trace_data[attr_value] + trace_patch[attr_name] = trace_data.get_column(attr_value) mapping_labels[attr_label] = "%%{%s}" % attr_name elif (trace_spec.constructor == go.Histogram and attr_name in ["x", "y"]) or ( trace_spec.constructor in [go.Histogram2d, go.Histogram2dContour] @@ -520,9 +536,11 @@ def make_trace_kwargs(args, trace_spec, trace_data, mapping_labels, sizeref): formatter = args["hover_data"][k_args][0] if formatter: if isinstance(formatter, str): + # TODO: Check if is it possible that `v` is a Series mapping_labels_copy[k] = v.replace("}", "%s}" % formatter) else: _ = mapping_labels_copy.pop(k) + # TODO: Check if is it possible that `v` is a Series hover_lines = [k + "=" + v for k, v in mapping_labels_copy.items()] trace_patch["hovertemplate"] = hover_header + "
".join(hover_lines) trace_patch["hovertemplate"] += "" @@ -1015,7 +1033,7 @@ def _get_reserved_col_names(args): as arguments, either as str/int arguments or given as columns (pandas series type). """ - df = args["data_frame"] + df: nw.DataFrame = args["data_frame"] reserved_names = set() for field in args: if field not in all_attrables: @@ -1028,26 +1046,26 @@ def _get_reserved_col_names(args): continue elif isinstance(arg, str): # no need to add ints since kw arg are not ints reserved_names.add(arg) - elif isinstance(arg, pd.Series): - arg_name = arg.name - if arg_name and hasattr(df, arg_name): - in_df = arg is df[arg_name] + elif is_into_series(arg): + arg_name = nw.from_native(arg, series_only=True).name + if arg_name and arg_name in df.columns: + in_df = arg == df.get_column(arg_name) if in_df: reserved_names.add(arg_name) - elif arg is df.index and arg.name is not None: + elif arg == nw.maybe_get_index(df) and arg.name is not None: reserved_names.add(arg.name) return reserved_names -def _is_col_list(columns, arg): +def _is_col_list(columns, arg, is_pd_like, native_namespace): """Returns True if arg looks like it's a list of columns or references to columns in df_input, and False otherwise (in which case it's assumed to be a single column or reference to a column). """ if arg is None or isinstance(arg, str) or isinstance(arg, int): return False - if isinstance(arg, pd.MultiIndex): + if is_pd_like and isinstance(arg, native_namespace.MultiIndex): return False # just to keep existing behaviour for now try: iter(arg) @@ -1087,17 +1105,30 @@ def _escape_col_name(columns, col_name, extra): return col_name -def to_unindexed_series(x, name=None): +def to_unindexed_series(x, name=None, native_namespace=None): """ assuming x is list-like or even an existing pd.Series, return a new pd.Series with no index, without extracting the data from an existing Series via numpy, which seems to mangle datetime columns. Stripping the index from existing pd.Series is required to get things to match up right in the new DataFrame we're building """ - return pd.Series(x, name=name).reset_index(drop=True) + if nw.dependencies.is_pandas_like_series(x): + return nw.from_native(x.rename(name).reset_index(drop=True), series_only=True) + elif isinstance(x, nw.Series): + return x + else: + if native_namespace is not None: + return nw.new_series(name=name, values=x, native_namespace=native_namespace) + elif (pd := nw.dependencies.get_pandas()) is not None: + return nw.new_series(name=name, values=x, native_namespace=pd) + else: + msg = "Pandas is required" + raise NotImplementedError(msg) -def process_args_into_dataframe(args, wide_mode, var_name, value_name): +def process_args_into_dataframe( + args, wide_mode, var_name, value_name, is_pd_like, native_namespace +): """ After this function runs, the `all_attrables` keys of `args` all contain only references to columns of `df_output`. This function handles the extraction of data @@ -1106,7 +1137,7 @@ def process_args_into_dataframe(args, wide_mode, var_name, value_name): reference. """ - df_input = args["data_frame"] + df_input: nw.DataFrame | None = args["data_frame"] df_provided = df_input is not None # we use a dict instead of a dataframe directly so that it doesn't cause @@ -1125,7 +1156,7 @@ def process_args_into_dataframe(args, wide_mode, var_name, value_name): "No data were provided. Please provide data either with the `data_frame` or with the `dimensions` argument." ) else: - df_output = {col: series for col, series in df_input.items()} + df_output = {col: df_input.get_column(col) for col in df_input.columns} # hover_data is a dict hover_data_is_dict = ( @@ -1171,14 +1202,14 @@ def process_args_into_dataframe(args, wide_mode, var_name, value_name): continue col_name = None # Case of multiindex - if isinstance(argument, pd.MultiIndex): + if is_pd_like and isinstance(argument, native_namespace.MultiIndex): raise TypeError( - "Argument '%s' is a pandas MultiIndex. " - "pandas MultiIndex is not supported by plotly express " + "Argument '%s' is a MultiIndex. " + " MultiIndex is not supported by plotly express " "at the moment." % field ) # ----------------- argument is a special value ---------------------- - if isinstance(argument, Constant) or isinstance(argument, Range): + if isinstance(argument, (Constant, Range)): col_name = _check_name_not_reserved( str(argument.label) if argument.label is not None else field, reserved_names, @@ -1211,7 +1242,9 @@ def process_args_into_dataframe(args, wide_mode, var_name, value_name): length, ) ) - df_output[col_name] = to_unindexed_series(real_argument, col_name) + df_output[col_name] = to_unindexed_series( + real_argument, col_name, native_namespace + ) elif not df_provided: raise ValueError( "String or int arguments are only possible when a " @@ -1232,14 +1265,17 @@ def process_args_into_dataframe(args, wide_mode, var_name, value_name): if argument == "index": err_msg += "\n To use the index, pass it in directly as `df.index`." raise ValueError(err_msg) - elif length and len(df_input[argument]) != length: + elif ( + length + and (actual_len := len(df_input.select(nw.col(argument)))) != length + ): raise ValueError( "All arguments should have the same length. " "The length of column argument `df[%s]` is %d, whereas the " "length of previously-processed arguments %s is %d" % ( field, - len(df_input[argument]), + actual_len, str(list(df_output.keys())), length, ) @@ -1252,19 +1288,25 @@ def process_args_into_dataframe(args, wide_mode, var_name, value_name): # ----------------- argument is likely a column / array / list.... ------- else: if df_provided and hasattr(argument, "name"): - if argument is df_input.index: - if argument.name is None or argument.name in df_input: + if ( + is_pd_like + and argument is df_input._compliant_frame._native_frame.index + ): + if argument.name is None or argument.name in df_input.columns: col_name = "index" else: col_name = argument.name col_name = _escape_col_name( - df_input, col_name, [var_name, value_name] + df_input.columns, col_name, [var_name, value_name] ) else: if ( argument.name is not None - and argument.name in df_input - and argument is df_input[argument.name] + and argument.name in df_input.columns + and argument + is df_input[ + argument.name + ] # TODO: Check what argument is at this point and the check translate in pandas case ): col_name = argument.name if col_name is None: # numpy array, list... @@ -1277,7 +1319,9 @@ def process_args_into_dataframe(args, wide_mode, var_name, value_name): "length of previously-processed arguments %s is %d" % (field, len(argument), str(list(df_output.keys())), length) ) - df_output[str(col_name)] = to_unindexed_series(argument, str(col_name)) + df_output[str(col_name)] = to_unindexed_series( + argument, str(col_name), native_namespace=native_namespace + ) # Finally, update argument with column name now that column exists assert col_name is not None, ( @@ -1297,17 +1341,26 @@ def process_args_into_dataframe(args, wide_mode, var_name, value_name): length = len(df_output[next(iter(df_output))]) if len(df_output) else 0 df_output.update( - {col_name: to_unindexed_series(range(length), col_name) for col_name in ranges} + { + col_name: to_unindexed_series(range(length), col_name, native_namespace) + for col_name in ranges + } ) df_output.update( { # constant is single value. repeat by len to avoid creating NaN on concating - col_name: to_unindexed_series([constants[col_name]] * length, col_name) + col_name: to_unindexed_series( + [constants[col_name]] * length, col_name, native_namespace + ) for col_name in constants } ) - df_output = pd.DataFrame(df_output) + if df_output: + df_output = nw.from_dict(df_output) + else: + pd = nw.dependencies.get_pandas() + df_output = nw.from_native(pd.DataFrame({}), eager_only=True) return df_output, wide_id_vars @@ -1341,46 +1394,70 @@ def build_dataframe(args, constructor): # Cast data_frame argument to DataFrame (it could be a numpy array, dict etc.) df_provided = args["data_frame"] is not None - needs_interchanging = False - if df_provided and not isinstance(args["data_frame"], pd.DataFrame): - if hasattr(args["data_frame"], "__dataframe__") and version.parse( - pd.__version__ - ) >= version.parse("2.0.2"): - import pandas.api.interchange - - df_not_pandas = args["data_frame"] - args["data_frame"] = df_not_pandas.__dataframe__() - # According interchange protocol: `def column_names(self) -> Iterable[str]:` - # so this function can return for example a generator. - # The easiest way is to convert `columns` to `pandas.Index` so that the - # type is similar to the types in other code branches. - columns = pd.Index(args["data_frame"].column_names()) - needs_interchanging = True - elif hasattr(args["data_frame"], "to_pandas"): - args["data_frame"] = args["data_frame"].to_pandas() + is_pd_like = False + if df_provided: + + if nw.dependencies.is_polars_dataframe( + args["data_frame"] + ) or nw.dependencies.is_pyarrow_table(args["data_frame"]): + args["data_frame"] = nw.from_native(args["data_frame"]) columns = args["data_frame"].columns - elif hasattr(args["data_frame"], "toPandas"): - args["data_frame"] = args["data_frame"].toPandas() + + elif nw.dependencies.is_polars_series( + args["data_frame"] + ) or nw.dependencies.is_pyarrow_chunked_array(args["data_frame"]): + args["data_frame"] = nw.from_native( + args["data_frame"], series_only=True + ).to_frame() columns = args["data_frame"].columns - elif hasattr(args["data_frame"], "to_pandas_df"): - args["data_frame"] = args["data_frame"].to_pandas_df() + + elif nw.dependencies.is_pandas_like_dataframe(args["data_frame"]): + + columns = args["data_frame"].columns # This can be multi index + args["data_frame"] = nw.from_native(args["data_frame"]) + is_pd_like = True + + elif nw.dependencies.is_pandas_like_series(args["data_frame"]): columns = args["data_frame"].columns - else: - args["data_frame"] = pd.DataFrame(args["data_frame"]) + args["data_frame"] = nw.from_native( + args["data_frame"], series_only=True + ).to_frame() + is_pd_like = True + + elif hasattr(args["data_frame"], "__dataframe__"): + args["data_frame"] = nw.from_native( + args["data_frame"], eager_or_interchange_only=True + ) columns = args["data_frame"].columns - elif df_provided: - columns = args["data_frame"].columns - else: - columns = None - df_input = args["data_frame"] + else: + msg = f"Unsupported type: {type(args['data_frame'])}" + raise NotImplementedError(msg) + else: + columns = None # no data_frame + + df_input: nw.DataFrame | None = args["data_frame"] + # Narwhals does not support native namespace for interchange level + native_namespace = ( + getattr(df_input._compliant_frame, "__native_namespace__", lambda: None)() + if df_provided + else None + ) # now we handle special cases like wide-mode or x-xor-y specification # by rearranging args to tee things up for process_args_into_dataframe to work no_x = args.get("x") is None no_y = args.get("y") is None - wide_x = False if no_x else _is_col_list(columns, args["x"]) - wide_y = False if no_y else _is_col_list(columns, args["y"]) + wide_x = ( + False + if no_x + else _is_col_list(columns, args["x"], is_pd_like, native_namespace) + ) + wide_y = ( + False + if no_y + else _is_col_list(columns, args["y"], is_pd_like, native_namespace) + ) wide_mode = False var_name = None # will likely be "variable" in wide_mode @@ -1395,14 +1472,14 @@ def build_dataframe(args, constructor): ) if df_provided and no_x and no_y: wide_mode = True - if isinstance(columns, pd.MultiIndex): + if is_pd_like and isinstance(columns, native_namespace.MultiIndex): raise TypeError( - "Data frame columns is a pandas MultiIndex. " - "pandas MultiIndex is not supported by plotly express " + "Data frame columns is a MultiIndex. " + "MultiIndex is not supported by plotly express " "at the moment." ) - args["wide_variable"] = list(columns) - if isinstance(columns, pd.Index): + args["wide_variable"] = columns + if is_pd_like and isinstance(columns, native_namespace.Index): var_name = columns.name else: var_name = None @@ -1419,7 +1496,7 @@ def build_dataframe(args, constructor): args["wide_variable"] = args["y"] if wide_y else args["x"] if df_provided and args["wide_variable"] is columns: var_name = columns.name - if isinstance(args["wide_variable"], pd.Index): + if is_pd_like and isinstance(args["wide_variable"], native_namespace.Index): args["wide_variable"] = list(args["wide_variable"]) if var_name in [None, "value", "index"] or ( df_provided and var_name in columns @@ -1438,42 +1515,6 @@ def build_dataframe(args, constructor): value_name = _escape_col_name(columns, "value", []) var_name = _escape_col_name(columns, var_name, []) - if needs_interchanging: - try: - if wide_mode or not hasattr(args["data_frame"], "select_columns_by_name"): - args["data_frame"] = pd.api.interchange.from_dataframe( - args["data_frame"] - ) - else: - # Save precious resources by only interchanging columns that are - # actually going to be plotted. - necessary_columns = { - i for i in args.values() if isinstance(i, str) and i in columns - } - for field in args: - if args[field] is not None and field in array_attrables: - necessary_columns.update(i for i in args[field] if i in columns) - columns = list(necessary_columns) - args["data_frame"] = pd.api.interchange.from_dataframe( - args["data_frame"].select_columns_by_name(columns) - ) - except (ImportError, NotImplementedError) as exc: - # temporary workaround; developers of third-party libraries themselves - # should try a different implementation, if available. For example: - # def __dataframe__(self, ...): - # if not some_condition: - # self.to_pandas(...) - if hasattr(df_not_pandas, "toPandas"): - args["data_frame"] = df_not_pandas.toPandas() - elif hasattr(df_not_pandas, "to_pandas_df"): - args["data_frame"] = df_not_pandas.to_pandas_df() - elif hasattr(df_not_pandas, "to_pandas"): - args["data_frame"] = df_not_pandas.to_pandas() - else: - raise exc - - df_input = args["data_frame"] - missing_bar_dim = None if ( constructor in [go.Scatter, go.Bar, go.Funnel] + hist2d_types @@ -1491,34 +1532,42 @@ def build_dataframe(args, constructor): if wide_mode and wide_cross_name is None: if no_x != no_y and args["orientation"] is None: args["orientation"] = "v" if no_x else "h" - if df_provided: - if isinstance(df_input.index, pd.MultiIndex): + if df_provided and is_pd_like: + if isinstance(columns, native_namespace.MultiIndex): raise TypeError( "Data frame index is a pandas MultiIndex. " "pandas MultiIndex is not supported by plotly express " "at the moment." ) - args["wide_cross"] = df_input.index + args["wide_cross"] = df_input._compliant_frame._native_frame.index else: args["wide_cross"] = Range( - label=_escape_col_name(df_input, "index", [var_name, value_name]) + label=_escape_col_name( + df_input.columns, "index", [var_name, value_name] + ) ) no_color = False - if type(args.get("color")) == str and args["color"] == NO_COLOR: + if isinstance(args.get("color"), str) and args["color"] == NO_COLOR: no_color = True args["color"] = None # now that things have been prepped, we do the systematic rewriting of `args` df_output, wide_id_vars = process_args_into_dataframe( - args, wide_mode, var_name, value_name + args, + wide_mode, + var_name, + value_name, + is_pd_like, + native_namespace, ) + df_output: nw.DataFrame # now that `df_output` exists and `args` contains only references, we complete # the special-case and wide-mode handling by further rewriting args and/or mutating # df_output - count_name = _escape_col_name(df_output, "count", [var_name, value_name]) + count_name = _escape_col_name(df_output.columns, "count", [var_name, value_name]) if not wide_mode and missing_bar_dim and constructor == go.Bar: # now that we've populated df_output, we check to see if the non-missing # dimension is categorical: if so, then setting the missing dimension to a @@ -1527,7 +1576,7 @@ def build_dataframe(args, constructor): other_dim = "x" if missing_bar_dim == "y" else "y" if not _is_continuous(df_output, args[other_dim]): args[missing_bar_dim] = count_name - df_output[count_name] = 1 + df_output = df_output.with_columns(**{count_name: nw.lit(1)}) else: # on the other hand, if the non-missing dimension is continuous, then we # can use this information to override the normal auto-orientation code @@ -1552,18 +1601,18 @@ def build_dataframe(args, constructor): del args["wide_cross"] dtype = None for v in wide_value_vars: - v_dtype = df_output[v].dtype.kind - v_dtype = "number" if v_dtype in ["i", "f", "u"] else v_dtype + v_dtype = df_output.get_column(v).dtype + v_dtype = "number" if v_dtype in NW_NUMERIC_DTYPES else v_dtype if dtype is None: dtype = v_dtype elif dtype != v_dtype: raise ValueError( "Plotly Express cannot process wide-form data with columns of different type." ) - df_output = df_output.melt( - id_vars=wide_id_vars, - value_vars=wide_value_vars, - var_name=var_name, + df_output = df_output.unpivot( + index=wide_id_vars, + on=wide_value_vars, + variable_name=var_name, value_name=value_name, ) assert len(df_output.columns) == len(set(df_output.columns)), ( @@ -1572,7 +1621,7 @@ def build_dataframe(args, constructor): "https://github.com/plotly/plotly.py/issues/new and we will try to " "replicate and fix it." ) - df_output[var_name] = df_output[var_name].astype(str) + df_output = df_output.with_columns(nw.col(var_name).cast(nw.String)) orient_v = wide_orientation == "v" if hist1d_orientation: @@ -1594,7 +1643,7 @@ def build_dataframe(args, constructor): else: args["x" if orient_v else "y"] = value_name args["y" if orient_v else "x"] = count_name - df_output[count_name] = 1 + df_output = df_output.with_columns(**{count_name: nw.lit(1)}) args["color"] = args["color"] or var_name elif constructor in [go.Violin, go.Box]: args["x" if orient_v else "y"] = wide_cross_name or var_name @@ -1607,12 +1656,12 @@ def build_dataframe(args, constructor): args["histfunc"] = None args["orientation"] = "h" args["x"] = count_name - df_output[count_name] = 1 + df_output = df_output.with_columns(**{count_name: nw.lit(1)}) else: args["histfunc"] = None args["orientation"] = "v" args["y"] = count_name - df_output[count_name] = 1 + df_output = df_output.with_columns(**{count_name: nw.lit(1)}) if no_color: args["color"] = None @@ -1620,21 +1669,36 @@ def build_dataframe(args, constructor): return args -def _check_dataframe_all_leaves(df): - df_sorted = df.sort_values(by=list(df.columns)) - null_mask = df_sorted.isnull() - df_sorted = df_sorted.astype(str) - null_indices = np.nonzero(null_mask.any(axis=1).values)[0] +def _check_dataframe_all_leaves(df: nw.DataFrame) -> None: + cols = df.columns + df_sorted = df.sort(by=cols) + null_mask = df_sorted.select(*[nw.col(c).is_null() for c in cols]) + df_sorted = df_sorted.with_columns(*[nw.col(c).cast(nw.String) for c in cols]) + null_indices = ( + null_mask.select(null_mask=nw.any_horizontal(nw.col(cols))) + .get_column("null_mask") + .arg_true() + ) for null_row_index in null_indices: - row = null_mask.iloc[null_row_index] - i = np.nonzero(row.values)[0][0] + row = np.array(null_mask.row(null_row_index)) + i = np.nonzero(row)[0][0] + if not row[i:].all(): raise ValueError( "None entries cannot have not-None children", - df_sorted.iloc[null_row_index], + df_sorted.row(null_row_index), ) - df_sorted[null_mask] = "" - row_strings = list(df_sorted.apply(lambda x: "".join(x), axis=1)) + df_sorted = df_sorted.with_columns( + **{ + c: df_sorted.get_column(c).zip_with(null_mask.get_column(c), nw.lit("")) + for c in cols + } + ) + row_strings = reduce( + lambda x, y: x + y, + [df_sorted.get_column(c).cast(nw.String()) for c in cols], + "", + ) for i, row in enumerate(row_strings[:-1]): if row_strings[i + 1] in row and (i + 1) in null_indices: raise ValueError( @@ -1648,42 +1712,45 @@ def process_dataframe_hierarchy(args): """ Build dataframe for sunburst, treemap, or icicle when the path argument is provided. """ - df = args["data_frame"] + df: nw.DataFrame = args["data_frame"] path = args["path"][::-1] _check_dataframe_all_leaves(df[path[::-1]]) discrete_color = False - new_path = [] - for col_name in path: - new_col_name = col_name + "_path_copy" - new_path.append(new_col_name) - df[new_col_name] = df[col_name] + new_path = [col_name + "_path_copy" for col_name in path] + df = df.with_columns( + **{ + new_col_name: nw.col(col_name) + for new_col_name, col_name in zip(new_path, path) + } + ) path = new_path # ------------ Define aggregation functions -------------------------------- - - def aggfunc_discrete(x): - uniques = x.unique() - if len(uniques) == 1: - return uniques[0] - else: - return "(?)" - agg_f = {} aggfunc_color = None if args["values"]: try: - df[args["values"]] = pd.to_numeric(df[args["values"]]) + if isinstance(args["values"], Sequence) and not isinstance( + args["values"], str + ): + df = df.with_columns( + **{c: nw.col(c).cast(nw.Float64()) for c in args["values"]} + ) + else: + df = df.with_columns( + **{args["values"]: nw.col(args["values"]).cast(nw.Float64())} + ) + except ValueError: raise ValueError( "Column `%s` of `df` could not be converted to a numerical data type." % args["values"] ) - if args["color"]: - if args["color"] == args["values"]: - new_value_col_name = args["values"] + "_sum" - df[new_value_col_name] = df[args["values"]] - args["values"] = new_value_col_name + if args["color"] and args["color"] == args["values"]: + new_value_col_name = args["values"] + "_sum" + df = df.with_columns(**{new_value_col_name: nw.col(args["values"])}) + args["values"] = new_value_col_name count_colname = args["values"] else: # we need a count column for the first groupby and the weighted mean of color @@ -1691,66 +1758,149 @@ def aggfunc_discrete(x): count_colname = ( "count" if "count" not in df.columns - else "".join([str(el) for el in list(df.columns)]) + else "".join([str(el) for el in df.columns]) ) # we can modify df because it's a copy of the px argument - df[count_colname] = 1 + df = df.with_columns(**{count_colname: nw.lit(1)}) args["values"] = count_colname - agg_f[count_colname] = "sum" + + # Since count_colname is always in agg_f, it can be used later to normalize color + # in the continuous case after some gymnastic + agg_f[count_colname] = nw.sum(count_colname) + + discrete_aggs = [] + continuous_aggs = [] if args["color"]: if not _is_continuous(df, args["color"]): - aggfunc_color = aggfunc_discrete + + discrete_aggs.append(args["color"]) discrete_color = True + # TODO: In theory, we should have a way to do nw.col(x).unique() and + # successively do: + # nw.when(nw.col(x).list.len()==1).then(nw.col(x).list.first()).otherwise(nw.lit("(?)")) + # which replicates: + # ``` + # uniques = x.unique() + # if len(uniques) == 1: + # return uniques[0] + # else: + # return "(?)" + # ``` + # However we cannot do that just yet, therefore a workaround is provided + agg_f[args["color"]] = nw.col(args["color"]).max() + agg_f[f'{args["color"]}__n_unique__'] = ( + nw.col(args["color"]).n_unique().alias(f'{args["color"]}__n_unique__') + ) else: + # This first needs to be multiplied by `count_colname` + continuous_aggs.append(args["color"]) + discrete_color = False - def aggfunc_continuous(x): - return np.average(x, weights=df.loc[x.index, count_colname]) - - aggfunc_color = aggfunc_continuous - agg_f[args["color"]] = aggfunc_color + agg_f[args["color"]] = nw.sum(args["color"]) # Other columns (for color, hover_data, custom_data etc.) cols = list(set(df.columns).difference(path)) + df = df.with_columns( + **{c: nw.col(c).cast(nw.String()) for c in cols if c not in agg_f} + ) for col in cols: # for hover_data, custom_data etc. if col not in agg_f: - agg_f[col] = aggfunc_discrete + # Similar trick as above + discrete_aggs.append(col) + agg_f[col] = nw.col(col).max() + agg_f[f"{col}__n_unique__"] = ( + nw.col(col).n_unique().alias(f"{col}__n_unique__") + ) # Avoid collisions with reserved names - columns in the path have been copied already cols = list(set(cols) - set(["labels", "parent", "id"])) # ---------------------------------------------------------------------------- - df_all_trees = pd.DataFrame(columns=["labels", "parent", "id"] + cols) - # Set column type here (useful for continuous vs discrete colorscale) - for col in cols: - df_all_trees[col] = df_all_trees[col].astype(df[col].dtype) + all_trees = [] + + if args["color"] and not discrete_color: + df = df.with_columns( + **{args["color"]: nw.col(args["color"]) * nw.col(count_colname)} + ) + + def post_agg( + dframe: nw.DataFrame, continuous_aggs: list[str], discrete_aggs: list[str] + ) -> nw.DataFrame: + """ + - continuous_aggs is either [] or [args["color"]] + - discrete_aggs is either [args["color"], ] or [] + """ + return dframe.with_columns( + **{c: nw.col(c) / nw.col(count_colname) for c in continuous_aggs}, + **{ + c: nw.when(nw.col(f"{c}__n_unique__") == nw.lit(1)) + .then(nw.col(c)) + .otherwise(nw.lit("(?)")) + for c in discrete_aggs + }, + ).drop([f"{c}__n_unique__" for c in discrete_aggs]) + for i, level in enumerate(path): - df_tree = pd.DataFrame(columns=df_all_trees.columns) - dfg = df.groupby(path[i:]).agg(agg_f) - dfg = dfg.reset_index() + + dfg = ( + df.group_by(path[i:]) + .agg(**agg_f) + .pipe(post_agg, continuous_aggs, discrete_aggs) + ) + # Path label massaging - df_tree["labels"] = dfg[level].copy().astype(str) - df_tree["parent"] = "" - df_tree["id"] = dfg[level].copy().astype(str) + df_tree = dfg.clone().with_columns( + *cols, + labels=nw.col(level).cast(nw.String()), + parent=nw.lit(""), + id=nw.col(level).cast(nw.String()), + ) if i < len(path) - 1: j = i + 1 + + # TODO: There should be a fast path along the following lines.. come back to this later. + # path_j_col = reduce(lambda c1, c2: c1 + "/" + c2, (dfg.get_column(path[j]).cast(nw.String()) for j in range(len(path)-1, j, -1)), "") + # parent_col = df_tree.get_column("parent").cast(nw.String()) + # id_col = df_tree.get_column("id").cast(nw.String()) + # df_tree = df_tree.with_columns( + # **{ + # "parent": path_j_col + "/" + parent_col, + # "id": path_j_col + "/" + id_col, + # } + # ) + while j < len(path): - df_tree["parent"] = ( - dfg[path[j]].copy().astype(str) + "/" + df_tree["parent"] + path_j_col = dfg.get_column(path[j]).cast(nw.String()) + parent_col = df_tree.get_column("parent").cast(nw.String()) + id_col = df_tree.get_column("id").cast(nw.String()) + + df_tree = df_tree.with_columns( + **{ + "parent": path_j_col + "/" + parent_col, + "id": path_j_col + "/" + id_col, + } ) - df_tree["id"] = dfg[path[j]].copy().astype(str) + "/" + df_tree["id"] j += 1 - df_tree["parent"] = df_tree["parent"].str.rstrip("/") - if cols: - df_tree[cols] = dfg[cols] - df_all_trees = pd.concat([df_all_trees, df_tree], ignore_index=True) + # TODO: breakpoint + df_tree = df_tree.with_columns( + parent=nw.col("parent").str.replace( + "/?$", "" + ) # strip "/" if at the end of the string, equivalent to `.str.rstrip` + ) + + all_trees.append(df_tree.select(*["labels", "parent", "id", *cols])) + + # TODO: Why does this fail in tests? Is is because of vertical concat without `ignore_index=True` of narwhals? + df_all_trees = nw.concat(all_trees, how="vertical") # we want to make sure than (?) is the first color of the sequence if args["color"] and discrete_color: sort_col_name = "sort_color_if_discrete_color" while sort_col_name in df_all_trees.columns: sort_col_name += "0" - df_all_trees[sort_col_name] = df[args["color"]].astype(str) - df_all_trees = df_all_trees.sort_values(by=sort_col_name) + df_all_trees = df_all_trees.with_columns( + **{sort_col_name: df[args["color"]].cast(nw.String())} + ).sort(by=sort_col_name) # Now modify arguments args["data_frame"] = df_all_trees @@ -1778,17 +1928,18 @@ def process_dataframe_timeline(args): raise ValueError("Both x_start and x_end are required") try: - x_start = pd.to_datetime(args["data_frame"][args["x_start"]]) - x_end = pd.to_datetime(args["data_frame"][args["x_end"]]) + df: nw.DataFrame = args["data_frame"] + x_start = df.get_column([args["x_start"]]).cast(nw.Datetime()) + x_end = df.get_column(args["x_end"]).cast(nw.Datetime()) except (ValueError, TypeError): raise TypeError( "Both x_start and x_end must refer to data convertible to datetimes." ) # note that we are not adding any columns to the data frame here, so no risk of overwrite - args["data_frame"][args["x_end"]] = (x_end - x_start).astype( - "timedelta64[ns]" - ) / np.timedelta64(1, "ms") + args["data_frame"] = df.with_columns( + **{args["x_end"]: (x_end - x_start).cast(nw.Duration()).dt.total_milliseconds()} + ) args["x"] = args["x_end"] del args["x_end"] args["base"] = args["x_start"] @@ -1803,23 +1954,27 @@ def process_dataframe_pie(args, trace_patch): order_in = args["category_orders"].get(names, {}).copy() if not order_in: return args, trace_patch - df = args["data_frame"] + df: nw.DataFrame = args["data_frame"] trace_patch["sort"] = False trace_patch["direction"] = "clockwise" - uniques = list(df[names].unique()) + uniques = df.get_column(names).unique().to_list() order = [x for x in OrderedDict.fromkeys(list(order_in) + uniques) if x in uniques] - args["data_frame"] = df.set_index(names).loc[order].reset_index() + + breakpoint() + # TODO: Replicate: args["data_frame"] = df.set_index(names).loc[order].reset_index() + # args["data_frame"] = df.select(*[names, *order]) return args, trace_patch def infer_config(args, constructor, trace_patch, layout_patch): attrs = [k for k in direct_attrables + array_attrables if k in args] grouped_attrs = [] + df: nw.DataFrame = args["data_frame"] # Compute sizeref sizeref = 0 if "size" in args and args["size"]: - sizeref = args["data_frame"][args["size"]].max() / args["size_max"] ** 2 + sizeref = df.get_column(args["size"]).max() / args["size_max"] ** 2 # Compute color attributes and grouping attributes if "color" in args: @@ -1827,7 +1982,7 @@ def infer_config(args, constructor, trace_patch, layout_patch): if "color_discrete_sequence" not in args: attrs.append("color") else: - if args["color"] and _is_continuous(args["data_frame"], args["color"]): + if args["color"] and _is_continuous(df, args["color"]): attrs.append("color") args["color_is_continuous"] = True elif constructor in [go.Sunburst, go.Treemap, go.Icicle]: @@ -1882,8 +2037,8 @@ def infer_config(args, constructor, trace_patch, layout_patch): args["orientation"] = "h" if args["orientation"] is None and has_x and has_y: - x_is_continuous = _is_continuous(args["data_frame"], args["x"]) - y_is_continuous = _is_continuous(args["data_frame"], args["y"]) + x_is_continuous = _is_continuous(df, args["x"]) + y_is_continuous = _is_continuous(df, args["y"]) if x_is_continuous and not y_is_continuous: args["orientation"] = "h" if y_is_continuous and not x_is_continuous: @@ -1991,7 +2146,7 @@ def infer_config(args, constructor, trace_patch, layout_patch): args[other_position] = None # Ignore facet rows and columns when data frame is empty so as to prevent nrows/ncols equaling 0 - if len(args["data_frame"]) == 0: + if df.is_empty(): args["facet_row"] = args["facet_col"] = None # If both marginals and faceting are specified, faceting wins @@ -2052,7 +2207,7 @@ def get_groups_and_orders(args, grouper): of a single dimension-group """ orders = {} if "category_orders" not in args else args["category_orders"].copy() - + df: nw.DataFrame = args["data_frame"] # figure out orders and what the single group name would be if there were one single_group_name = [] unique_cache = dict() @@ -2061,7 +2216,7 @@ def get_groups_and_orders(args, grouper): single_group_name.append("") else: if col not in unique_cache: - unique_cache[col] = list(args["data_frame"][col].unique()) + unique_cache[col] = df.get_column(col).unique().to_list() uniques = unique_cache[col] if len(uniques) == 1: single_group_name.append(uniques[0]) @@ -2069,19 +2224,14 @@ def get_groups_and_orders(args, grouper): orders[col] = uniques else: orders[col] = list(OrderedDict.fromkeys(list(orders[col]) + uniques)) - df = args["data_frame"] + if len(single_group_name) == len(grouper): # we have a single group, so we can skip all group-by operations! groups = {tuple(single_group_name): df} else: required_grouper = [g for g in grouper if g != one_group] - grouped = df.groupby( - required_grouper, sort=False, observed=True - ) # skip one_group groupers - group_indices = grouped.indices - sorted_group_names = [ - g if len(required_grouper) != 1 else (g,) for g in group_indices - ] + grouped = dict(df.group_by(required_grouper).__iter__()) + sorted_group_names = list(grouped.keys()) for i, col in reversed(list(enumerate(required_grouper))): sorted_group_names = sorted( @@ -2097,15 +2247,9 @@ def get_groups_and_orders(args, grouper): g.insert(i, "") full_sorted_group_names = [tuple(g) for g in full_sorted_group_names] - groups = {} - for sf, s in zip(full_sorted_group_names, sorted_group_names): - if len(s) > 1: - groups[sf] = grouped.get_group(s) - else: - if pandas_2_2_0: - groups[sf] = grouped.get_group((s[0],)) - else: - groups[sf] = grouped.get_group(s[0]) + groups = { + sf: grouped[s] for sf, s in zip(full_sorted_group_names, sorted_group_names) + } return groups, orders @@ -2280,19 +2424,25 @@ def make_figure(args, constructor, trace_patch=None, layout_patch=None): base = args["x"] if args["orientation"] == "v" else args["y"] var = args["x"] if args["orientation"] == "h" else args["y"] ascending = args.get("ecdfmode", "standard") != "reversed" - group = group.sort_values(by=base, ascending=ascending) - group_sum = group[var].sum() # compute here before next line mutates - group[var] = group[var].cumsum() + group = group.sort(by=base, descending=not ascending) + group_sum = group.get_column( + var + ).sum() # compute here before next line mutates + group = group.with_columns(**{var: nw.col(var).cum_sum()}) if not ascending: - group = group.sort_values(by=base, ascending=True) + group = group.sort(by=base, descending=False) if args.get("ecdfmode", "standard") == "complementary": - group[var] = group_sum - group[var] + group = group.with_columns( + **{var: -nw.col(var) + nw.lit(group_sum)} + ) if args["ecdfnorm"] == "probability": - group[var] = group[var] / group_sum + group = group.with_columns(**{var: nw.col(var) / nw.lit(group_sum)}) elif args["ecdfnorm"] == "percent": - group[var] = 100.0 * group[var] / group_sum + group = group.with_columns( + **{var: nw.col(var) / nw.lit(group_sum) * nw.lit(100.0)} + ) patch, fit_results = make_trace_kwargs( args, trace_spec, group, mapping_labels.copy(), sizeref @@ -2406,7 +2556,11 @@ def make_figure(args, constructor, trace_patch=None, layout_patch=None): if fit_results is not None: trendline_rows.append(dict(px_fit_results=fit_results)) - fig._px_trendlines = pd.DataFrame(trendline_rows) + if (pd := nw.dependencies.get_pandas()) is not None: + fig._px_trendlines = pd.DataFrame(trendline_rows) + else: + msg = "Trendlines require pandas to be installed" + raise NotImplementedError(msg) configure_axes(args, constructor, fig, orders) configure_animation_controls(args, constructor, fig) @@ -2508,3 +2662,21 @@ def _spacing_error_translator(e, direction, facet_arg): annot.update(font=None) return fig + + +def is_into_eager_dataframe(df) -> bool: + """Check if `df` is a supported narwhals eager dataframe.""" + return ( + nw.dependencies.is_polars_dataframe(df) + or nw.dependencies.is_pyarrow_table(df) + or nw.dependencies.is_pandas_like_dataframe(df) + ) + + +def is_into_series(df) -> bool: + """Check if `df` is a supported narwhals eager dataframe.""" + return ( + nw.dependencies.is_polars_series(df) + or nw.dependencies.is_pyarrow_chunked_array(df) + or nw.dependencies.is_pandas_like_series(df) + ) From ba932366a2e4cddc95b0ee2567d6ef40c24325e6 Mon Sep 17 00:00:00 2001 From: FBruzzesi Date: Sat, 28 Sep 2024 22:06:22 +0200 Subject: [PATCH 003/106] some _core fixes --- .../python/plotly/plotly/express/_core.py | 68 +++++++++++++------ 1 file changed, 48 insertions(+), 20 deletions(-) diff --git a/packages/python/plotly/plotly/express/_core.py b/packages/python/plotly/plotly/express/_core.py index 708fce1242..194a4eb7eb 100644 --- a/packages/python/plotly/plotly/express/_core.py +++ b/packages/python/plotly/plotly/express/_core.py @@ -1049,10 +1049,12 @@ def _get_reserved_col_names(args): elif is_into_series(arg): arg_name = nw.from_native(arg, series_only=True).name if arg_name and arg_name in df.columns: - in_df = arg == df.get_column(arg_name) + in_df = ( + nw.from_native(arg, series_only=True) == df.get_column(arg_name) + ).all() if in_df: reserved_names.add(arg_name) - elif arg == nw.maybe_get_index(df) and arg.name is not None: + elif (arg == nw.maybe_get_index(df)).all() and arg.name is not None: reserved_names.add(arg.name) return reserved_names @@ -1288,10 +1290,7 @@ def process_args_into_dataframe( # ----------------- argument is likely a column / array / list.... ------- else: if df_provided and hasattr(argument, "name"): - if ( - is_pd_like - and argument is df_input._compliant_frame._native_frame.index - ): + if is_pd_like and argument is nw.maybe_get_index(df_input): if argument.name is None or argument.name in df_input.columns: col_name = "index" else: @@ -1303,10 +1302,10 @@ def process_args_into_dataframe( if ( argument.name is not None and argument.name in df_input.columns - and argument - is df_input[ - argument.name - ] # TODO: Check what argument is at this point and the check translate in pandas case + and ( + nw.from_native(argument, allow_series=True) + == df_input.get_column(argument.name) + ).all() ): col_name = argument.name if col_name is None: # numpy array, list... @@ -1340,17 +1339,29 @@ def process_args_into_dataframe( wide_id_vars.add(str(col_name)) length = len(df_output[next(iter(df_output))]) if len(df_output) else 0 + df_output.update( { - col_name: to_unindexed_series(range(length), col_name, native_namespace) + col_name: nw.new_series( + name=col_name, + values=range(length), + native_namespace=( + native_namespace if df_provided else nw.dependencies.get_pandas() + ), + ) for col_name in ranges } ) + df_output.update( { # constant is single value. repeat by len to avoid creating NaN on concating - col_name: to_unindexed_series( - [constants[col_name]] * length, col_name, native_namespace + col_name: nw.new_series( + name=col_name, + values=[constants[col_name]] * length, + native_namespace=( + native_namespace if df_provided else nw.dependencies.get_pandas() + ), ) for col_name in constants } @@ -1426,10 +1437,26 @@ def build_dataframe(args, constructor): elif hasattr(args["data_frame"], "__dataframe__"): args["data_frame"] = nw.from_native( - args["data_frame"], eager_or_interchange_only=True + nw.from_native( + args["data_frame"], eager_or_interchange_only=True + ).to_pandas(), + eager_only=True, ) columns = args["data_frame"].columns + is_pd_like = True + + elif isinstance(args["data_frame"], dict): + pd = nw.dependencies.get_pandas() + if pd is None: + msg = ( + "data_frame of type dict requires Pandas to be installed. " + "Convert it to supported dataframe type of install Pandas." + ) + raise ValueError(msg) + args["data_frame"] = nw.from_native(pd.DataFrame(args["data_frame"])) + columns = args["data_frame"].columns + is_pd_like = True else: msg = f"Unsupported type: {type(args['data_frame'])}" raise NotImplementedError(msg) @@ -1437,6 +1464,7 @@ def build_dataframe(args, constructor): columns = None # no data_frame df_input: nw.DataFrame | None = args["data_frame"] + index = nw.maybe_get_index(df_input) if df_provided else None # Narwhals does not support native namespace for interchange level native_namespace = ( getattr(df_input._compliant_frame, "__native_namespace__", lambda: None)() @@ -1494,7 +1522,7 @@ def build_dataframe(args, constructor): elif wide_x != wide_y: wide_mode = True args["wide_variable"] = args["y"] if wide_y else args["x"] - if df_provided and args["wide_variable"] is columns: + if df_provided and is_pd_like and args["wide_variable"] is columns: var_name = columns.name if is_pd_like and isinstance(args["wide_variable"], native_namespace.Index): args["wide_variable"] = list(args["wide_variable"]) @@ -1523,7 +1551,7 @@ def build_dataframe(args, constructor): if not wide_mode and (no_x != no_y): for ax in ["x", "y"]: if args.get(ax) is None: - args[ax] = df_input.index if df_provided else Range() + args[ax] = index if index is not None else Range() if constructor == go.Bar: missing_bar_dim = ax else: @@ -1532,14 +1560,14 @@ def build_dataframe(args, constructor): if wide_mode and wide_cross_name is None: if no_x != no_y and args["orientation"] is None: args["orientation"] = "v" if no_x else "h" - if df_provided and is_pd_like: - if isinstance(columns, native_namespace.MultiIndex): + if df_provided and is_pd_like and index is not None: + if isinstance(index, native_namespace.MultiIndex): raise TypeError( "Data frame index is a pandas MultiIndex. " "pandas MultiIndex is not supported by plotly express " "at the moment." ) - args["wide_cross"] = df_input._compliant_frame._native_frame.index + args["wide_cross"] = index else: args["wide_cross"] = Range( label=_escape_col_name( @@ -1929,7 +1957,7 @@ def process_dataframe_timeline(args): try: df: nw.DataFrame = args["data_frame"] - x_start = df.get_column([args["x_start"]]).cast(nw.Datetime()) + x_start = df.get_column(args["x_start"]).cast(nw.Datetime()) x_end = df.get_column(args["x_end"]).cast(nw.Datetime()) except (ValueError, TypeError): raise TypeError( From 421fc1d69dfaded9e940d095962501e890f88275 Mon Sep 17 00:00:00 2001 From: FBruzzesi Date: Sat, 28 Sep 2024 22:15:41 +0200 Subject: [PATCH 004/106] tests replace sort_index(axis=1) --- .../tests/test_optional/test_px/test_px_input.py | 9 +++++---- .../tests/test_optional/test_px/test_px_wide.py | 16 ++++++++-------- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/packages/python/plotly/plotly/tests/test_optional/test_px/test_px_input.py b/packages/python/plotly/plotly/tests/test_optional/test_px/test_px_input.py index a6bbf9b4e4..1cf4574cc6 100644 --- a/packages/python/plotly/plotly/tests/test_optional/test_px/test_px_input.py +++ b/packages/python/plotly/plotly/tests/test_optional/test_px/test_px_input.py @@ -230,8 +230,8 @@ def test_build_df_from_lists(): df = pd.DataFrame(args) args["data_frame"] = None out = build_dataframe(args, go.Scatter) - assert_frame_equal(df.sort_index(axis=1), out["data_frame"].sort_index(axis=1)) - out.pop("data_frame") + df_out = out.pop("data_frame") + assert_frame_equal(df_out, df[df_out.columns]) assert out == output # Arrays @@ -240,8 +240,9 @@ def test_build_df_from_lists(): df = pd.DataFrame(args) args["data_frame"] = None out = build_dataframe(args, go.Scatter) - assert_frame_equal(df.sort_index(axis=1), out["data_frame"].sort_index(axis=1)) - out.pop("data_frame") + df_out = out.pop("data_frame") + assert_frame_equal(df_out, df[df_out.columns]) + assert out == output diff --git a/packages/python/plotly/plotly/tests/test_optional/test_px/test_px_wide.py b/packages/python/plotly/plotly/tests/test_optional/test_px/test_px_wide.py index 1aac7b70ea..ed5a7ecc77 100644 --- a/packages/python/plotly/plotly/tests/test_optional/test_px/test_px_wide.py +++ b/packages/python/plotly/plotly/tests/test_optional/test_px/test_px_wide.py @@ -157,8 +157,8 @@ def test_wide_mode_internal(trace_type, x, y, color, orientation): if x == "index": expected["index"] = [11, 12, 13, 11, 12, 13] assert_frame_equal( - df_out.sort_index(axis=1), - pd.DataFrame(expected).sort_index(axis=1), + df_out.to_native(), + pd.DataFrame(expected)[df_out.columns], ) if trace_type in [go.Histogram2dContour, go.Histogram2d]: if orientation is None or orientation == "v": @@ -285,8 +285,8 @@ def test_wide_x_or_y(tt, df_in, args_in, x, y, color, df_out_exp, transpose): args_in["y"], args_in["x"] = args_in["x"], args_in["y"] args_in["data_frame"] = df_in args_out = build_dataframe(args_in, tt) - df_out = args_out.pop("data_frame").sort_index(axis=1) - assert_frame_equal(df_out, pd.DataFrame(df_out_exp).sort_index(axis=1)) + df_out = args_out.pop("data_frame") + assert_frame_equal(df_out, pd.DataFrame(df_out_exp)[df_out.columns]) if transpose: args_exp = dict(x=y, y=x, color=color) else: @@ -306,7 +306,7 @@ def test_wide_mode_internal_bar_exception(orientation): args_out = build_dataframe(args_in, go.Bar) df_out = args_out.pop("data_frame") assert_frame_equal( - df_out.sort_index(axis=1), + df_out.to_native(), pd.DataFrame( dict( index=[11, 12, 13, 11, 12, 13], @@ -314,7 +314,7 @@ def test_wide_mode_internal_bar_exception(orientation): value=["q", "r", "s", "t", "u", "v"], count=[1, 1, 1, 1, 1, 1], ) - ).sort_index(axis=1), + )[df_out.columns], ) if orientation is None or orientation == "v": assert args_out == dict(x="value", y="count", color="variable", orientation="v") @@ -799,8 +799,8 @@ def test_wide_mode_internal_special_cases(df_in, args_in, args_expect, df_expect df_out = args_out.pop("data_frame") assert args_out == args_expect assert_frame_equal( - df_out.sort_index(axis=1), - df_expect.sort_index(axis=1), + df_out.to_native(), + df_expect[df_out.columns], ) From ca5c82012048ad3505ee5a5b21877c543746bdb7 Mon Sep 17 00:00:00 2001 From: FBruzzesi Date: Sat, 28 Sep 2024 23:17:21 +0200 Subject: [PATCH 005/106] reset_index in concat and allow any object to pandas --- .../python/plotly/plotly/express/_core.py | 38 +++++++++++-------- 1 file changed, 23 insertions(+), 15 deletions(-) diff --git a/packages/python/plotly/plotly/express/_core.py b/packages/python/plotly/plotly/express/_core.py index 194a4eb7eb..57f80b3039 100644 --- a/packages/python/plotly/plotly/express/_core.py +++ b/packages/python/plotly/plotly/express/_core.py @@ -277,6 +277,10 @@ def make_trace_kwargs(args, trace_spec, trace_data, mapping_labels, sizeref): if "line_close" in args and args["line_close"]: trace_data = nw.concat([trace_data, trace_data.head(1)], how="vertical") + native_trace = trace_data.to_native() + if nw.dependencies.is_pandas_like_dataframe(native_trace): + trace_data = nw.from_native(native_trace.reset_index(drop=True)) + trace_patch = trace_spec.trace_patch.copy() or {} fit_results = None hover_header = "" @@ -1445,21 +1449,22 @@ def build_dataframe(args, constructor): columns = args["data_frame"].columns is_pd_like = True - elif isinstance(args["data_frame"], dict): - pd = nw.dependencies.get_pandas() - if pd is None: - msg = ( - "data_frame of type dict requires Pandas to be installed. " - "Convert it to supported dataframe type of install Pandas." - ) - raise ValueError(msg) - - args["data_frame"] = nw.from_native(pd.DataFrame(args["data_frame"])) - columns = args["data_frame"].columns - is_pd_like = True else: - msg = f"Unsupported type: {type(args['data_frame'])}" - raise NotImplementedError(msg) + try: + pd = nw.dependencies.get_pandas() + if pd is None: + msg = ( + "data_frame of type dict requires Pandas to be installed. " + "Convert it to supported dataframe type of install Pandas." + ) + raise ValueError(msg) + + args["data_frame"] = nw.from_native(pd.DataFrame(args["data_frame"])) + columns = args["data_frame"].columns + is_pd_like = True + except Exception: + msg = f"Unsupported type: {type(args['data_frame'])}" + raise NotImplementedError(msg) else: columns = None # no data_frame @@ -1918,8 +1923,11 @@ def post_agg( all_trees.append(df_tree.select(*["labels", "parent", "id", *cols])) - # TODO: Why does this fail in tests? Is is because of vertical concat without `ignore_index=True` of narwhals? + # TODO: Why does this fail in tests? df_all_trees = nw.concat(all_trees, how="vertical") + native_trees = df_all_trees.to_native() + if nw.dependencies.is_pandas_like_dataframe(native_trees): + df_all_trees = nw.from_native(native_trees.reset_index(drop=True)) # we want to make sure than (?) is the first color of the sequence if args["color"] and discrete_color: From a6aab24b873c94edf10de471af851240ea8e2911 Mon Sep 17 00:00:00 2001 From: FBruzzesi Date: Sun, 29 Sep 2024 17:32:30 +0200 Subject: [PATCH 006/106] trendline prep --- .../python/plotly/plotly/express/_core.py | 57 +++++++++++-------- .../express/trendline_functions/__init__.py | 2 +- 2 files changed, 35 insertions(+), 24 deletions(-) diff --git a/packages/python/plotly/plotly/express/_core.py b/packages/python/plotly/plotly/express/_core.py index 57f80b3039..9c6a7b05fe 100644 --- a/packages/python/plotly/plotly/express/_core.py +++ b/packages/python/plotly/plotly/express/_core.py @@ -335,24 +335,43 @@ def make_trace_kwargs(args, trace_spec, trace_data, mapping_labels, sizeref): ): # sorting is bad but trace_specs with "trendline" have no other attrs sorted_trace_data = trace_data.sort(by=args["x"]) - y = sorted_trace_data.select(nw.col(args["y"])).to_numpy().squeeze() - x = sorted_trace_data.get_column(args["x"]).to_numpy().squeeze() + y = sorted_trace_data.get_column(args["y"]) + x = sorted_trace_data.get_column(args["x"]) + # TODO: Fix dtype - if x.dtype.type == np.datetime64: + if isinstance(x.dtype, nw.Datetime): + x = ( + x.to_frame() + .select( + **{ + args["x"]: nw.when(~x.is_null()) + .then(x.cast(nw.Int64)) + .otherwise(nw.lit(None, nw.Int64)) + } + ) + .get_column(args["x"]) + ) # convert to unix epoch seconds - x = x.astype(np.int64) / 10**9 - elif x.dtype.type == np.object_: + # x = x.astype(np.int64) / 10**9 + elif x.dtype in { + nw.Object(), + nw.String(), + }: # TODO: Should this just be: x non numeric? try: - x = x.astype(np.float64) + x = x.cast(nw.Float64()) except ValueError: raise ValueError( "Could not convert value of 'x' ('%s') into a numeric type. " "If 'x' contains stringified dates, please convert to a datetime column." % args["x"] ) - if y.dtype.type == np.object_: + + if y.dtype in { + nw.Object(), + nw.String(), + }: # TODO: Should this just be: x_series non numeric? try: - y = y.astype(np.float64) + y = y.cast(nw.Float64()).to_numpy() except ValueError: raise ValueError( "Could not convert value of 'y' into a numeric type." @@ -362,29 +381,21 @@ def make_trace_kwargs(args, trace_spec, trace_data, mapping_labels, sizeref): # otherwise numpy/pandas can mess with the timezones # NB this means trendline functions must output one-to-one with the input series # i.e. we can't do resampling, because then the X values might not line up! - non_missing = np.logical_not( - np.logical_or(np.isnan(y), np.isnan(x)) - ) - # TODO: Double check slicing here or find workaround - trace_patch["x"] = sorted_trace_data[non_missing].select( - nw.col(args["x"]) + non_missing = ~(x.is_null() | y.is_null()) + trace_patch["x"] = sorted_trace_data.filter(non_missing).get_column( + args["x"] ) + trendline_function = trendline_functions[attr_value] y_out, hover_header, fit_results = trendline_function( args["trendline_options"], sorted_trace_data.get_column(args["x"]), # narwhals series - x, # numpy array - y, # numpy array + x.to_numpy(), # numpy array + y.to_numpy(), # numpy array args["x"], args["y"], - non_missing, # numpy array + non_missing.to_numpy(), # numpy array ) - # TODO: This is most likely not needed anymore - # y_out = nw.new_series( - # name="", - # values=y_out, - # native_namespace=sorted_trace_data.__native_namespace__(), - # ) assert len(y_out) == len( trace_patch["x"] ), "missing-data-handling failure in trendline code" diff --git a/packages/python/plotly/plotly/express/trendline_functions/__init__.py b/packages/python/plotly/plotly/express/trendline_functions/__init__.py index 34b569cfb3..1133462468 100644 --- a/packages/python/plotly/plotly/express/trendline_functions/__init__.py +++ b/packages/python/plotly/plotly/express/trendline_functions/__init__.py @@ -120,7 +120,7 @@ def _pandas(mode, trendline_options, x_raw, y, non_missing): msg = "Trendline requires pandas to be installed" raise ImportError(msg) - series = pd.Series(y, index=x_raw.to_numpy()) + series = pd.Series(y, index=x_raw.to_pandas()) # TODO: If narwhals were to sopport rolling, ewm and expanding then we could go around these agg = getattr(series, mode) # e.g. series.rolling From 7665f10220c6ace3df7f508325bf2df29fc55d35 Mon Sep 17 00:00:00 2001 From: FBruzzesi Date: Sun, 29 Sep 2024 19:12:16 +0200 Subject: [PATCH 007/106] WIP Index --- notes.md | 30 ++++++++ .../python/plotly/plotly/express/_core.py | 56 ++++++++------ .../test_optional/test_px/test_px_wide.py | 74 ++++++++++--------- 3 files changed, 105 insertions(+), 55 deletions(-) create mode 100644 notes.md diff --git a/notes.md b/notes.md new file mode 100644 index 0000000000..0fa4d4723d --- /dev/null +++ b/notes.md @@ -0,0 +1,30 @@ +# Plotly express narwhalificantion notes + +Everything happens in `make_figure` + +- `apply_default_cascade` does not affect dataframe ✅ +- `build_dataframe` ✅ + - `process_args_into_dataframe` ✅ +- `process_dataframe_hierarchy` ✅ + - `_check_dataframe_all_leaves` ✅ +- `process_dataframe_pie` +- `process_dataframe_timeline`: ✅ +- `infer_config` ✅ + - `make_mapping` does not affect dataframe ✅ + - `make_trace_spec`: does not affect dataframe ✅ +- `get_groups_and_orders`: ✅ +- `make_trace_kwargs` ✅ + +## Tests + +- packages/python/plotly/plotly/tests/test_optional/test_px/test_colors.py ✅ +packages/python/plotly/plotly/tests/test_optional/test_px/test_facets ✅ +packages/python/plotly/plotly/tests/test_optional/test_px/test_imshow ✅ +packages/python/plotly/plotly/tests/test_optional/test_px/test_marginals ✅ +packages/python/plotly/plotly/tests/test_optional/test_px/test_pandas_backend.py 🚧 +packages/python/plotly/plotly/tests/test_optional/test_px/test_px ✅ +packages/python/plotly/plotly/tests/test_optional/test_px/test_px_functions.py 🚧 +packages/python/plotly/plotly/tests/test_optional/test_px/test_px_hover.py ✅ +packages/python/plotly/plotly/tests/test_optional/test_px/test_px_input.py 🚧 +packages/python/plotly/plotly/tests/test_optional/test_px/test_px_wide.py 🚧 +packages/python/plotly/plotly/tests/test_optional/test_px/test_trendline.py 🚧 (Timeseries only is failing) \ No newline at end of file diff --git a/packages/python/plotly/plotly/express/_core.py b/packages/python/plotly/plotly/express/_core.py index 9c6a7b05fe..a150aebc49 100644 --- a/packages/python/plotly/plotly/express/_core.py +++ b/packages/python/plotly/plotly/express/_core.py @@ -338,7 +338,6 @@ def make_trace_kwargs(args, trace_spec, trace_data, mapping_labels, sizeref): y = sorted_trace_data.get_column(args["y"]) x = sorted_trace_data.get_column(args["x"]) - # TODO: Fix dtype if isinstance(x.dtype, nw.Datetime): x = ( x.to_frame() @@ -1117,7 +1116,9 @@ def _isinstance_listlike(x): def _escape_col_name(columns, col_name, extra): - while columns is not None and (col_name in columns or col_name in extra): + if columns is None: + return col_name + while col_name in columns or col_name in extra: col_name = "_" + col_name return col_name @@ -1202,10 +1203,15 @@ def process_args_into_dataframe( else args.get(field_name) ) # argument not specified, continue - if argument_list is None or argument_list is [None]: + if argument_list is None or ( + hasattr(argument_list, "__len__") + and len(argument_list) == 1 + and argument_list[0] is None + ): continue # Argument name: field_name if the argument is not a list # Else we give names like ["hover_data_0, hover_data_1"] etc. + # breakpoint() field_list = ( [field_name] if field_name not in array_attrables @@ -1247,14 +1253,14 @@ def process_args_into_dataframe( col_name = str(argument) real_argument = args["hover_data"][col_name][1] - if length and len(real_argument) != length: + if length and (real_length := len(real_argument)) != length: raise ValueError( "All arguments should have the same length. " "The length of hover_data key `%s` is %d, whereas the " "length of previously-processed arguments %s is %d" % ( argument, - len(real_argument), + real_length, str(list(df_output.keys())), length, ) @@ -1282,10 +1288,7 @@ def process_args_into_dataframe( if argument == "index": err_msg += "\n To use the index, pass it in directly as `df.index`." raise ValueError(err_msg) - elif ( - length - and (actual_len := len(df_input.select(nw.col(argument)))) != length - ): + elif length and (actual_len := len(df_input)) != length: raise ValueError( "All arguments should have the same length. " "The length of column argument `df[%s]` is %d, whereas the " @@ -1318,7 +1321,9 @@ def process_args_into_dataframe( argument.name is not None and argument.name in df_input.columns and ( - nw.from_native(argument, allow_series=True) + to_unindexed_series( + argument, argument.name, native_namespace + ) == df_input.get_column(argument.name) ).all() ): @@ -1345,10 +1350,12 @@ def process_args_into_dataframe( "replicate and fix it." ) if field_name not in array_attrables: + del args[field_name] args[field_name] = str(col_name) elif isinstance(args[field_name], dict): pass else: + breakpoint() args[field_name][i] = str(col_name) if field_name != "wide_variable": wide_id_vars.add(str(col_name)) @@ -1426,14 +1433,15 @@ def build_dataframe(args, constructor): if nw.dependencies.is_polars_dataframe( args["data_frame"] ) or nw.dependencies.is_pyarrow_table(args["data_frame"]): - args["data_frame"] = nw.from_native(args["data_frame"]) + args["data_frame"] = nw.from_native(args["data_frame"], eager_only=True) columns = args["data_frame"].columns elif nw.dependencies.is_polars_series( args["data_frame"] ) or nw.dependencies.is_pyarrow_chunked_array(args["data_frame"]): args["data_frame"] = nw.from_native( - args["data_frame"], series_only=True + args["data_frame"], + series_only=True, ).to_frame() columns = args["data_frame"].columns @@ -1444,10 +1452,12 @@ def build_dataframe(args, constructor): is_pd_like = True elif nw.dependencies.is_pandas_like_series(args["data_frame"]): - columns = args["data_frame"].columns + args["data_frame"] = nw.from_native( - args["data_frame"], series_only=True + args["data_frame"], + series_only=True, ).to_frame() + columns = args["data_frame"].columns is_pd_like = True elif hasattr(args["data_frame"], "__dataframe__"): @@ -1465,8 +1475,9 @@ def build_dataframe(args, constructor): pd = nw.dependencies.get_pandas() if pd is None: msg = ( - "data_frame of type dict requires Pandas to be installed. " - "Convert it to supported dataframe type of install Pandas." + f"data_frame of type {type(args['data_frame'])} requires Pandas " + "to be installed. Convert it to supported dataframe type or " + "install Pandas." ) raise ValueError(msg) @@ -1538,8 +1549,12 @@ def build_dataframe(args, constructor): elif wide_x != wide_y: wide_mode = True args["wide_variable"] = args["y"] if wide_y else args["x"] - if df_provided and is_pd_like and args["wide_variable"] is columns: - var_name = columns.name + if ( + df_provided + and is_pd_like + and all(c1 == c2 for c1, c2 in zip(args["wide_variable"], columns)) + ): + var_name = df_input.to_native().columns.name if is_pd_like and isinstance(args["wide_variable"], native_namespace.Index): args["wide_variable"] = list(args["wide_variable"]) if var_name in [None, "value", "index"] or ( @@ -1586,9 +1601,7 @@ def build_dataframe(args, constructor): args["wide_cross"] = index else: args["wide_cross"] = Range( - label=_escape_col_name( - df_input.columns, "index", [var_name, value_name] - ) + label=_escape_col_name(columns, "index", [var_name, value_name]) ) no_color = False @@ -1606,7 +1619,6 @@ def build_dataframe(args, constructor): native_namespace, ) df_output: nw.DataFrame - # now that `df_output` exists and `args` contains only references, we complete # the special-case and wide-mode handling by further rewriting args and/or mutating # df_output diff --git a/packages/python/plotly/plotly/tests/test_optional/test_px/test_px_wide.py b/packages/python/plotly/plotly/tests/test_optional/test_px/test_px_wide.py index ed5a7ecc77..f993d6ad5d 100644 --- a/packages/python/plotly/plotly/tests/test_optional/test_px/test_px_wide.py +++ b/packages/python/plotly/plotly/tests/test_optional/test_px/test_px_wide.py @@ -10,41 +10,49 @@ def test_is_col_list(): df_input = pd.DataFrame(dict(a=[1, 2], b=[1, 2])) - assert _is_col_list(df_input, ["a"]) - assert _is_col_list(df_input, ["a", "b"]) - assert _is_col_list(df_input, [[3, 4]]) - assert _is_col_list(df_input, [[3, 4], [3, 4]]) - assert not _is_col_list(df_input, pytest) - assert not _is_col_list(df_input, False) - assert not _is_col_list(df_input, ["a", 1]) - assert not _is_col_list(df_input, "a") - assert not _is_col_list(df_input, 1) - assert not _is_col_list(df_input, ["a", "b", "c"]) - assert not _is_col_list(df_input, [1, 2]) + is_pd_like = True + native_namespace = pd + assert _is_col_list(df_input, ["a"], is_pd_like, native_namespace) + assert _is_col_list(df_input, ["a", "b"], is_pd_like, native_namespace) + assert _is_col_list(df_input, [[3, 4]], is_pd_like, native_namespace) + assert _is_col_list(df_input, [[3, 4], [3, 4]], is_pd_like, native_namespace) + assert not _is_col_list(df_input, pytest, is_pd_like, native_namespace) + assert not _is_col_list(df_input, False, is_pd_like, native_namespace) + assert not _is_col_list(df_input, ["a", 1], is_pd_like, native_namespace) + assert not _is_col_list(df_input, "a", is_pd_like, native_namespace) + assert not _is_col_list(df_input, 1, is_pd_like, native_namespace) + assert not _is_col_list(df_input, ["a", "b", "c"], is_pd_like, native_namespace) + assert not _is_col_list(df_input, [1, 2], is_pd_like, native_namespace) + df_input = pd.DataFrame([[1, 2], [1, 2]]) - assert _is_col_list(df_input, [0]) - assert _is_col_list(df_input, [0, 1]) - assert _is_col_list(df_input, [[3, 4]]) - assert _is_col_list(df_input, [[3, 4], [3, 4]]) - assert not _is_col_list(df_input, pytest) - assert not _is_col_list(df_input, False) - assert not _is_col_list(df_input, ["a", 1]) - assert not _is_col_list(df_input, "a") - assert not _is_col_list(df_input, 1) - assert not _is_col_list(df_input, [0, 1, 2]) - assert not _is_col_list(df_input, ["a", "b"]) + is_pd_like = True + native_namespace = pd + assert _is_col_list(df_input, [0], is_pd_like, native_namespace) + assert _is_col_list(df_input, [0, 1], is_pd_like, native_namespace) + assert _is_col_list(df_input, [[3, 4]], is_pd_like, native_namespace) + assert _is_col_list(df_input, [[3, 4], [3, 4]], is_pd_like, native_namespace) + assert not _is_col_list(df_input, pytest, is_pd_like, native_namespace) + assert not _is_col_list(df_input, False, is_pd_like, native_namespace) + assert not _is_col_list(df_input, ["a", 1], is_pd_like, native_namespace) + assert not _is_col_list(df_input, "a", is_pd_like, native_namespace) + assert not _is_col_list(df_input, 1, is_pd_like, native_namespace) + assert not _is_col_list(df_input, [0, 1, 2], is_pd_like, native_namespace) + assert not _is_col_list(df_input, ["a", "b"], is_pd_like, native_namespace) + df_input = None - assert _is_col_list(df_input, [[3, 4]]) - assert _is_col_list(df_input, [[3, 4], [3, 4]]) - assert not _is_col_list(df_input, [0]) - assert not _is_col_list(df_input, [0, 1]) - assert not _is_col_list(df_input, pytest) - assert not _is_col_list(df_input, False) - assert not _is_col_list(df_input, ["a", 1]) - assert not _is_col_list(df_input, "a") - assert not _is_col_list(df_input, 1) - assert not _is_col_list(df_input, [0, 1, 2]) - assert not _is_col_list(df_input, ["a", "b"]) + is_pd_like = False + native_namespace = None + assert _is_col_list(df_input, [[3, 4]], is_pd_like, native_namespace) + assert _is_col_list(df_input, [[3, 4], [3, 4]], is_pd_like, native_namespace) + assert not _is_col_list(df_input, [0], is_pd_like, native_namespace) + assert not _is_col_list(df_input, [0, 1], is_pd_like, native_namespace) + assert not _is_col_list(df_input, pytest, is_pd_like, native_namespace) + assert not _is_col_list(df_input, False, is_pd_like, native_namespace) + assert not _is_col_list(df_input, ["a", 1], is_pd_like, native_namespace) + assert not _is_col_list(df_input, "a", is_pd_like, native_namespace) + assert not _is_col_list(df_input, 1, is_pd_like, native_namespace) + assert not _is_col_list(df_input, [0, 1, 2], is_pd_like, native_namespace) + assert not _is_col_list(df_input, ["a", "b"], is_pd_like, native_namespace) @pytest.mark.parametrize( From ec4f250f68a7fdbfba492ac262947263314d26e7 Mon Sep 17 00:00:00 2001 From: FBruzzesi Date: Sun, 29 Sep 2024 19:28:27 +0200 Subject: [PATCH 008/106] clean from breakpoints --- notes.md | 30 ------------------- .../python/plotly/plotly/express/_core.py | 13 ++++---- 2 files changed, 6 insertions(+), 37 deletions(-) delete mode 100644 notes.md diff --git a/notes.md b/notes.md deleted file mode 100644 index 0fa4d4723d..0000000000 --- a/notes.md +++ /dev/null @@ -1,30 +0,0 @@ -# Plotly express narwhalificantion notes - -Everything happens in `make_figure` - -- `apply_default_cascade` does not affect dataframe ✅ -- `build_dataframe` ✅ - - `process_args_into_dataframe` ✅ -- `process_dataframe_hierarchy` ✅ - - `_check_dataframe_all_leaves` ✅ -- `process_dataframe_pie` -- `process_dataframe_timeline`: ✅ -- `infer_config` ✅ - - `make_mapping` does not affect dataframe ✅ - - `make_trace_spec`: does not affect dataframe ✅ -- `get_groups_and_orders`: ✅ -- `make_trace_kwargs` ✅ - -## Tests - -- packages/python/plotly/plotly/tests/test_optional/test_px/test_colors.py ✅ -packages/python/plotly/plotly/tests/test_optional/test_px/test_facets ✅ -packages/python/plotly/plotly/tests/test_optional/test_px/test_imshow ✅ -packages/python/plotly/plotly/tests/test_optional/test_px/test_marginals ✅ -packages/python/plotly/plotly/tests/test_optional/test_px/test_pandas_backend.py 🚧 -packages/python/plotly/plotly/tests/test_optional/test_px/test_px ✅ -packages/python/plotly/plotly/tests/test_optional/test_px/test_px_functions.py 🚧 -packages/python/plotly/plotly/tests/test_optional/test_px/test_px_hover.py ✅ -packages/python/plotly/plotly/tests/test_optional/test_px/test_px_input.py 🚧 -packages/python/plotly/plotly/tests/test_optional/test_px/test_px_wide.py 🚧 -packages/python/plotly/plotly/tests/test_optional/test_px/test_trendline.py 🚧 (Timeseries only is failing) \ No newline at end of file diff --git a/packages/python/plotly/plotly/express/_core.py b/packages/python/plotly/plotly/express/_core.py index a150aebc49..43e07b6ef9 100644 --- a/packages/python/plotly/plotly/express/_core.py +++ b/packages/python/plotly/plotly/express/_core.py @@ -328,7 +328,7 @@ def make_trace_kwargs(args, trace_spec, trace_data, mapping_labels, sizeref): if ( args["x"] and args["y"] - and len( # TODO: Handle case arg["y"] is a list + and len( # TODO: Handle case arg["y"] is a list? trace_data.select(nw.col(args["x"], args["y"])).drop_nulls() ) > 1 @@ -1211,7 +1211,6 @@ def process_args_into_dataframe( continue # Argument name: field_name if the argument is not a list # Else we give names like ["hover_data_0, hover_data_1"] etc. - # breakpoint() field_list = ( [field_name] if field_name not in array_attrables @@ -1355,7 +1354,7 @@ def process_args_into_dataframe( elif isinstance(args[field_name], dict): pass else: - breakpoint() + # FIXME: Here field name can be an index, but this should not be the case args[field_name][i] = str(col_name) if field_name != "wide_variable": wide_id_vars.add(str(col_name)) @@ -1937,7 +1936,6 @@ def post_agg( ) j += 1 - # TODO: breakpoint df_tree = df_tree.with_columns( parent=nw.col("parent").str.replace( "/?$", "" @@ -2019,9 +2017,10 @@ def process_dataframe_pie(args, trace_patch): uniques = df.get_column(names).unique().to_list() order = [x for x in OrderedDict.fromkeys(list(order_in) + uniques) if x in uniques] - breakpoint() - # TODO: Replicate: args["data_frame"] = df.set_index(names).loc[order].reset_index() - # args["data_frame"] = df.select(*[names, *order]) + # TODO + # Original implementation: args["data_frame"] = df.set_index(names).loc[order].reset_index() + # However this is untested, therefore will need some special attention to verify + args["data_frame"] = df[order].select(names) return args, trace_patch From 7e0d4c26cc272e7ba39c7256ad7484b54fd488f0 Mon Sep 17 00:00:00 2001 From: FBruzzesi Date: Sun, 29 Sep 2024 19:44:01 +0200 Subject: [PATCH 009/106] some tests fix --- .../test_optional/test_px/test_px_input.py | 153 +++++++++--------- 1 file changed, 79 insertions(+), 74 deletions(-) diff --git a/packages/python/plotly/plotly/tests/test_optional/test_px/test_px_input.py b/packages/python/plotly/plotly/tests/test_optional/test_px/test_px_input.py index 1cf4574cc6..88ff6df7bb 100644 --- a/packages/python/plotly/plotly/tests/test_optional/test_px/test_px_input.py +++ b/packages/python/plotly/plotly/tests/test_optional/test_px/test_px_input.py @@ -231,7 +231,7 @@ def test_build_df_from_lists(): args["data_frame"] = None out = build_dataframe(args, go.Scatter) df_out = out.pop("data_frame") - assert_frame_equal(df_out, df[df_out.columns]) + assert_frame_equal(df_out.to_pandas(), df[df_out.columns]) assert out == output # Arrays @@ -241,7 +241,7 @@ def test_build_df_from_lists(): args["data_frame"] = None out = build_dataframe(args, go.Scatter) df_out = out.pop("data_frame") - assert_frame_equal(df_out, df[df_out.columns]) + assert_frame_equal(df_out.to_pandas(), df[df_out.columns]) assert out == output @@ -250,74 +250,77 @@ def test_build_df_with_index(): tips = px.data.tips() args = dict(data_frame=tips, x=tips.index, y="total_bill") out = build_dataframe(args, go.Scatter) - assert_frame_equal(tips.reset_index()[out["data_frame"].columns], out["data_frame"]) - - -@pytest.mark.parametrize("column_names_as_generator", [False, True]) -def test_build_df_using_interchange_protocol_mock( - add_interchange_module_for_old_pandas, column_names_as_generator -): - class InterchangeDataFrame: - def __init__(self, columns): - self._columns = columns - - if column_names_as_generator: - - def column_names(self): - for col in self._columns: - yield col - - else: - - def column_names(self): - return self._columns - - interchange_dataframe = InterchangeDataFrame( - ["petal_width", "sepal_length", "sepal_width"] - ) - interchange_dataframe_reduced = InterchangeDataFrame( - ["petal_width", "sepal_length"] - ) - interchange_dataframe.select_columns_by_name = mock.MagicMock( - return_value=interchange_dataframe_reduced - ) - interchange_dataframe_reduced.select_columns_by_name = mock.MagicMock( - return_value=interchange_dataframe_reduced + assert_frame_equal( + tips.reset_index()[out["data_frame"].columns], out["data_frame"].to_pandas() ) - class CustomDataFrame: - def __dataframe__(self): - return interchange_dataframe - - class CustomDataFrameReduced: - def __dataframe__(self): - return interchange_dataframe_reduced - - input_dataframe = CustomDataFrame() - input_dataframe_reduced = CustomDataFrameReduced() - - iris_pandas = px.data.iris() - - with mock.patch("pandas.__version__", "2.0.2"): - args = dict(data_frame=input_dataframe, x="petal_width", y="sepal_length") - with mock.patch( - "pandas.api.interchange.from_dataframe", return_value=iris_pandas - ) as mock_from_dataframe: - build_dataframe(args, go.Scatter) - mock_from_dataframe.assert_called_once_with(interchange_dataframe_reduced) - assert set(interchange_dataframe.select_columns_by_name.call_args[0][0]) == { - "petal_width", - "sepal_length", - } - - args = dict(data_frame=input_dataframe_reduced, color=None) - with mock.patch( - "pandas.api.interchange.from_dataframe", - return_value=iris_pandas[["petal_width", "sepal_length"]], - ) as mock_from_dataframe: - build_dataframe(args, go.Scatter) - mock_from_dataframe.assert_called_once_with(interchange_dataframe_reduced) - interchange_dataframe_reduced.select_columns_by_name.assert_not_called() + +# TODO: Changed how this is accomplished in the first place +# @pytest.mark.parametrize("column_names_as_generator", [False, True]) +# def test_build_df_using_interchange_protocol_mock( +# add_interchange_module_for_old_pandas, column_names_as_generator +# ): +# class InterchangeDataFrame: +# def __init__(self, columns): +# self._columns = columns + +# if column_names_as_generator: + +# def column_names(self): +# for col in self._columns: +# yield col + +# else: + +# def column_names(self): +# return self._columns + +# interchange_dataframe = InterchangeDataFrame( +# ["petal_width", "sepal_length", "sepal_width"] +# ) +# interchange_dataframe_reduced = InterchangeDataFrame( +# ["petal_width", "sepal_length"] +# ) +# interchange_dataframe.select_columns_by_name = mock.MagicMock( +# return_value=interchange_dataframe_reduced +# ) +# interchange_dataframe_reduced.select_columns_by_name = mock.MagicMock( +# return_value=interchange_dataframe_reduced +# ) + +# class CustomDataFrame: +# def __dataframe__(self): +# return interchange_dataframe + +# class CustomDataFrameReduced: +# def __dataframe__(self): +# return interchange_dataframe_reduced + +# input_dataframe = CustomDataFrame() +# input_dataframe_reduced = CustomDataFrameReduced() + +# iris_pandas = px.data.iris() + +# with mock.patch("pandas.__version__", "2.0.2"): +# args = dict(data_frame=input_dataframe, x="petal_width", y="sepal_length") +# with mock.patch( +# "pandas.api.interchange.from_dataframe", return_value=iris_pandas +# ) as mock_from_dataframe: +# build_dataframe(args, go.Scatter) +# mock_from_dataframe.assert_called_once_with(interchange_dataframe_reduced) +# assert set(interchange_dataframe.select_columns_by_name.call_args[0][0]) == { +# "petal_width", +# "sepal_length", +# } + +# args = dict(data_frame=input_dataframe_reduced, color=None) +# with mock.patch( +# "pandas.api.interchange.from_dataframe", +# return_value=iris_pandas[["petal_width", "sepal_length"]], +# ) as mock_from_dataframe: +# build_dataframe(args, go.Scatter) +# mock_from_dataframe.assert_called_once_with(interchange_dataframe_reduced) +# interchange_dataframe_reduced.select_columns_by_name.assert_not_called() @pytest.mark.skipif( @@ -338,7 +341,8 @@ def test_build_df_from_vaex_and_polars(test_lib): args = dict(data_frame=iris_vaex, x="petal_width", y="sepal_length") out = build_dataframe(args, go.Scatter) assert_frame_equal( - iris_pandas.reset_index()[out["data_frame"].columns], out["data_frame"] + iris_pandas.reset_index()[out["data_frame"].columns], + out["data_frame"].to_pandas(), ) @@ -368,7 +372,8 @@ def test_build_df_with_hover_data_from_vaex_and_polars(test_lib, hover_data): ) out = build_dataframe(args, go.Scatter) assert_frame_equal( - iris_pandas.reset_index()[out["data_frame"].columns], out["data_frame"] + iris_pandas.reset_index()[out["data_frame"].columns], + out["data_frame"].to_pandas(), ) @@ -377,7 +382,7 @@ def test_timezones(): df["date"] = pd.to_datetime(df["date"]) args = dict(data_frame=df, x="date", y="value") out = build_dataframe(args, go.Scatter) - assert str(out["data_frame"]["date"][0]) == str(df["date"][0]) + assert str(out["data_frame"].to_pandas()["date"][0]) == str(df["date"][0]) def test_non_matching_index(): @@ -387,17 +392,17 @@ def test_non_matching_index(): args = dict(data_frame=df, x=df.index, y="y") out = build_dataframe(args, go.Scatter) - assert_frame_equal(expected, out["data_frame"]) + assert_frame_equal(expected, out["data_frame"].to_pandas()) expected = pd.DataFrame(dict(x=["a", "b", "c"], y=[1, 2, 3])) args = dict(data_frame=None, x=df.index, y=df.y) out = build_dataframe(args, go.Scatter) - assert_frame_equal(expected, out["data_frame"]) + assert_frame_equal(expected, out["data_frame"].to_pandas()) args = dict(data_frame=None, x=["a", "b", "c"], y=df.y) out = build_dataframe(args, go.Scatter) - assert_frame_equal(expected, out["data_frame"]) + assert_frame_equal(expected, out["data_frame"].to_pandas()) def test_splom_case(): From 5543638686fb82cf425b1ed077555569e003ed44 Mon Sep 17 00:00:00 2001 From: FBruzzesi Date: Sun, 29 Sep 2024 19:52:05 +0200 Subject: [PATCH 010/106] hotfix and tests output to pandas --- packages/python/plotly/plotly/express/_core.py | 2 +- .../plotly/tests/test_optional/test_px/test_px_wide.py | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/python/plotly/plotly/express/_core.py b/packages/python/plotly/plotly/express/_core.py index 43e07b6ef9..0c92fbd08a 100644 --- a/packages/python/plotly/plotly/express/_core.py +++ b/packages/python/plotly/plotly/express/_core.py @@ -1302,7 +1302,7 @@ def process_args_into_dataframe( else: col_name = str(argument) df_output[col_name] = to_unindexed_series( - df_input[argument], col_name + df_input.get_column(argument), col_name ) # ----------------- argument is likely a column / array / list.... ------- else: diff --git a/packages/python/plotly/plotly/tests/test_optional/test_px/test_px_wide.py b/packages/python/plotly/plotly/tests/test_optional/test_px/test_px_wide.py index f993d6ad5d..e9bb97f592 100644 --- a/packages/python/plotly/plotly/tests/test_optional/test_px/test_px_wide.py +++ b/packages/python/plotly/plotly/tests/test_optional/test_px/test_px_wide.py @@ -165,7 +165,7 @@ def test_wide_mode_internal(trace_type, x, y, color, orientation): if x == "index": expected["index"] = [11, 12, 13, 11, 12, 13] assert_frame_equal( - df_out.to_native(), + df_out.to_pandas(), pd.DataFrame(expected)[df_out.columns], ) if trace_type in [go.Histogram2dContour, go.Histogram2d]: @@ -294,7 +294,7 @@ def test_wide_x_or_y(tt, df_in, args_in, x, y, color, df_out_exp, transpose): args_in["data_frame"] = df_in args_out = build_dataframe(args_in, tt) df_out = args_out.pop("data_frame") - assert_frame_equal(df_out, pd.DataFrame(df_out_exp)[df_out.columns]) + assert_frame_equal(df_out.to_pandas(), pd.DataFrame(df_out_exp)[df_out.columns]) if transpose: args_exp = dict(x=y, y=x, color=color) else: @@ -314,7 +314,7 @@ def test_wide_mode_internal_bar_exception(orientation): args_out = build_dataframe(args_in, go.Bar) df_out = args_out.pop("data_frame") assert_frame_equal( - df_out.to_native(), + df_out.to_pandas(), pd.DataFrame( dict( index=[11, 12, 13, 11, 12, 13], @@ -807,7 +807,7 @@ def test_wide_mode_internal_special_cases(df_in, args_in, args_expect, df_expect df_out = args_out.pop("data_frame") assert args_out == args_expect assert_frame_equal( - df_out.to_native(), + df_out.to_pandas(), df_expect[df_out.columns], ) From cd0dab7a67d9c870a97b15ec1ec850c14c2b8b5f Mon Sep 17 00:00:00 2001 From: FBruzzesi Date: Sun, 29 Sep 2024 22:38:39 +0200 Subject: [PATCH 011/106] FIX: columns never as index --- packages/python/plotly/plotly/express/_core.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/packages/python/plotly/plotly/express/_core.py b/packages/python/plotly/plotly/express/_core.py index 0c92fbd08a..55e9593ec6 100644 --- a/packages/python/plotly/plotly/express/_core.py +++ b/packages/python/plotly/plotly/express/_core.py @@ -1307,7 +1307,7 @@ def process_args_into_dataframe( # ----------------- argument is likely a column / array / list.... ------- else: if df_provided and hasattr(argument, "name"): - if is_pd_like and argument is nw.maybe_get_index(df_input): + if is_pd_like and (argument == nw.maybe_get_index(df_input)).all(): if argument.name is None or argument.name in df_input.columns: col_name = "index" else: @@ -1330,12 +1330,12 @@ def process_args_into_dataframe( if col_name is None: # numpy array, list... col_name = _check_name_not_reserved(field, reserved_names) - if length and len(argument) != length: + if length and (len_arg := len(argument)) != length: raise ValueError( "All arguments should have the same length. " "The length of argument `%s` is %d, whereas the " "length of previously-processed arguments %s is %d" - % (field, len(argument), str(list(df_output.keys())), length) + % (field, len_arg, str(list(df_output.keys())), length) ) df_output[str(col_name)] = to_unindexed_series( argument, str(col_name), native_namespace=native_namespace @@ -1349,12 +1349,10 @@ def process_args_into_dataframe( "replicate and fix it." ) if field_name not in array_attrables: - del args[field_name] args[field_name] = str(col_name) elif isinstance(args[field_name], dict): pass else: - # FIXME: Here field name can be an index, but this should not be the case args[field_name][i] = str(col_name) if field_name != "wide_variable": wide_id_vars.add(str(col_name)) @@ -1532,7 +1530,7 @@ def build_dataframe(args, constructor): "MultiIndex is not supported by plotly express " "at the moment." ) - args["wide_variable"] = columns + args["wide_variable"] = list(columns) if is_pd_like and isinstance(columns, native_namespace.Index): var_name = columns.name else: From f334b32640c332a435785df4846060065fb862b6 Mon Sep 17 00:00:00 2001 From: FBruzzesi Date: Sun, 29 Sep 2024 23:24:16 +0200 Subject: [PATCH 012/106] getting there with the tests --- packages/python/plotly/plotly/express/_core.py | 14 +++----------- .../tests/test_optional/test_px/test_px_wide.py | 1 + 2 files changed, 4 insertions(+), 11 deletions(-) diff --git a/packages/python/plotly/plotly/express/_core.py b/packages/python/plotly/plotly/express/_core.py index 55e9593ec6..16ba732613 100644 --- a/packages/python/plotly/plotly/express/_core.py +++ b/packages/python/plotly/plotly/express/_core.py @@ -1203,11 +1203,7 @@ def process_args_into_dataframe( else args.get(field_name) ) # argument not specified, continue - if argument_list is None or ( - hasattr(argument_list, "__len__") - and len(argument_list) == 1 - and argument_list[0] is None - ): + if argument_list is None or argument_list is [None]: continue # Argument name: field_name if the argument is not a list # Else we give names like ["hover_data_0, hover_data_1"] etc. @@ -1546,12 +1542,8 @@ def build_dataframe(args, constructor): elif wide_x != wide_y: wide_mode = True args["wide_variable"] = args["y"] if wide_y else args["x"] - if ( - df_provided - and is_pd_like - and all(c1 == c2 for c1, c2 in zip(args["wide_variable"], columns)) - ): - var_name = df_input.to_native().columns.name + if df_provided and is_pd_like and args["wide_variable"] is columns: + var_name = columns.name if is_pd_like and isinstance(args["wide_variable"], native_namespace.Index): args["wide_variable"] = list(args["wide_variable"]) if var_name in [None, "value", "index"] or ( diff --git a/packages/python/plotly/plotly/tests/test_optional/test_px/test_px_wide.py b/packages/python/plotly/plotly/tests/test_optional/test_px/test_px_wide.py index e9bb97f592..7d8ce144ae 100644 --- a/packages/python/plotly/plotly/tests/test_optional/test_px/test_px_wide.py +++ b/packages/python/plotly/plotly/tests/test_optional/test_px/test_px_wide.py @@ -805,6 +805,7 @@ def test_wide_mode_internal_special_cases(df_in, args_in, args_expect, df_expect args_in["data_frame"] = df_in args_out = build_dataframe(args_in, go.Scatter) df_out = args_out.pop("data_frame") + assert args_out == args_expect assert_frame_equal( df_out.to_pandas(), From e5eb949ccad08553d2384de3715fbe45af061fb9 Mon Sep 17 00:00:00 2001 From: FBruzzesi Date: Mon, 30 Sep 2024 10:45:50 +0200 Subject: [PATCH 013/106] get_column instead of pandas slicing, unix to seconds --- .../python/plotly/plotly/express/_core.py | 25 ++++++++++--------- 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/packages/python/plotly/plotly/express/_core.py b/packages/python/plotly/plotly/express/_core.py index 16ba732613..b40b32bb0f 100644 --- a/packages/python/plotly/plotly/express/_core.py +++ b/packages/python/plotly/plotly/express/_core.py @@ -339,6 +339,7 @@ def make_trace_kwargs(args, trace_spec, trace_data, mapping_labels, sizeref): x = sorted_trace_data.get_column(args["x"]) if isinstance(x.dtype, nw.Datetime): + # convert to unix epoch seconds x = ( x.to_frame() .select( @@ -346,16 +347,15 @@ def make_trace_kwargs(args, trace_spec, trace_data, mapping_labels, sizeref): args["x"]: nw.when(~x.is_null()) .then(x.cast(nw.Int64)) .otherwise(nw.lit(None, nw.Int64)) + / 10**9 } ) .get_column(args["x"]) ) - # convert to unix epoch seconds - # x = x.astype(np.int64) / 10**9 elif x.dtype in { nw.Object(), nw.String(), - }: # TODO: Should this just be: x non numeric? + }: # TODO: Should this case just be: x non numeric? try: x = x.cast(nw.Float64()) except ValueError: @@ -368,7 +368,7 @@ def make_trace_kwargs(args, trace_spec, trace_data, mapping_labels, sizeref): if y.dtype in { nw.Object(), nw.String(), - }: # TODO: Should this just be: x_series non numeric? + }: # TODO: Should this case just be: y non numeric? try: y = y.cast(nw.Float64()).to_numpy() except ValueError: @@ -1821,16 +1821,17 @@ def process_dataframe_hierarchy(args): discrete_aggs.append(args["color"]) discrete_color = True - # TODO: In theory, we should have a way to do nw.col(x).unique() and + # Hack: In theory, we should have a way to do nw.col(x).unique() and # successively do: - # nw.when(nw.col(x).list.len()==1).then(nw.col(x).list.first()).otherwise(nw.lit("(?)")) + # (nw.when(nw.col(x).list.len()==1) + # .then(nw.col(x).list.first()) + # .otherwise(nw.lit("(?)")) + # ) # which replicates: # ``` - # uniques = x.unique() - # if len(uniques) == 1: - # return uniques[0] - # else: - # return "(?)" + # def discrete_agg(x): + # uniques = x.unique() + # return uniques[0] if len(uniques) == 1 else "(?)" # ``` # However we cannot do that just yet, therefore a workaround is provided agg_f[args["color"]] = nw.col(args["color"]).max() @@ -1946,7 +1947,7 @@ def post_agg( while sort_col_name in df_all_trees.columns: sort_col_name += "0" df_all_trees = df_all_trees.with_columns( - **{sort_col_name: df[args["color"]].cast(nw.String())} + **{sort_col_name: df.get_column(args["color"]).cast(nw.String())} ).sort(by=sort_col_name) # Now modify arguments From 7747e300de4cd29177925a03dc773e5633eed112 Mon Sep 17 00:00:00 2001 From: FBruzzesi Date: Tue, 1 Oct 2024 13:27:09 +0200 Subject: [PATCH 014/106] bump narhwals, hierarchy fastpath --- .../python/plotly/optional-requirements.txt | 2 +- .../python/plotly/plotly/express/_core.py | 116 +++++++++--------- .../test_px/test_px_functions.py | 4 - 3 files changed, 60 insertions(+), 62 deletions(-) diff --git a/packages/python/plotly/optional-requirements.txt b/packages/python/plotly/optional-requirements.txt index 0f51b73713..dc29447175 100644 --- a/packages/python/plotly/optional-requirements.txt +++ b/packages/python/plotly/optional-requirements.txt @@ -39,7 +39,7 @@ ipython ## pandas deps for some matplotlib functionality ## pandas -narwhals>=1.8.4 +narwhals>=1.9.0 ## scipy deps for some FigureFactory functions ## scipy diff --git a/packages/python/plotly/plotly/express/_core.py b/packages/python/plotly/plotly/express/_core.py index b40b32bb0f..b6b2771fa5 100644 --- a/packages/python/plotly/plotly/express/_core.py +++ b/packages/python/plotly/plotly/express/_core.py @@ -11,6 +11,17 @@ import math import narwhals.stable.v1 as nw +from narwhals.dtypes import Datetime +from narwhals.dtypes import Float32 +from narwhals.dtypes import Float64 +from narwhals.dtypes import Int8 +from narwhals.dtypes import Int16 +from narwhals.dtypes import Int32 +from narwhals.dtypes import Int64 +from narwhals.dtypes import UInt8 +from narwhals.dtypes import UInt16 +from narwhals.dtypes import UInt32 +from narwhals.dtypes import UInt64 import numpy as np from plotly._subplots import ( @@ -20,7 +31,18 @@ ) NO_COLOR = "px_no_color_constant" -NW_NUMERIC_DTYPES = {nw.Float32, nw.Float64, nw.Int8, nw.Int16, nw.Int32, nw.Int64} +NW_NUMERIC_DTYPES = { + Float32, + Float64, + Int8, + Int16, + Int32, + Int64, + UInt8, + UInt16, + UInt32, + UInt64, +} trendline_functions = dict( lowess=lowess, rolling=rolling, ewm=ewm, expanding=expanding, ols=ols @@ -276,10 +298,9 @@ def make_trace_kwargs(args, trace_spec, trace_data, mapping_labels, sizeref): df: nw.DataFrame = args["data_frame"] if "line_close" in args and args["line_close"]: - trace_data = nw.concat([trace_data, trace_data.head(1)], how="vertical") - native_trace = trace_data.to_native() - if nw.dependencies.is_pandas_like_dataframe(native_trace): - trace_data = nw.from_native(native_trace.reset_index(drop=True)) + trace_data = nw.maybe_reset_index( + nw.concat([trace_data, trace_data.head(1)], how="vertical") + ) trace_patch = trace_spec.trace_patch.copy() or {} fit_results = None @@ -338,7 +359,7 @@ def make_trace_kwargs(args, trace_spec, trace_data, mapping_labels, sizeref): y = sorted_trace_data.get_column(args["y"]) x = sorted_trace_data.get_column(args["x"]) - if isinstance(x.dtype, nw.Datetime): + if isinstance(x.dtype, Datetime): # convert to unix epoch seconds x = ( x.to_frame() @@ -347,17 +368,14 @@ def make_trace_kwargs(args, trace_spec, trace_data, mapping_labels, sizeref): args["x"]: nw.when(~x.is_null()) .then(x.cast(nw.Int64)) .otherwise(nw.lit(None, nw.Int64)) - / 10**9 } ) .get_column(args["x"]) + / 10**9 ) - elif x.dtype in { - nw.Object(), - nw.String(), - }: # TODO: Should this case just be: x non numeric? + elif x.dtype not in NW_NUMERIC_DTYPES: try: - x = x.cast(nw.Float64()) + x = x.cast(Float64()) except ValueError: raise ValueError( "Could not convert value of 'x' ('%s') into a numeric type. " @@ -365,12 +383,9 @@ def make_trace_kwargs(args, trace_spec, trace_data, mapping_labels, sizeref): % args["x"] ) - if y.dtype in { - nw.Object(), - nw.String(), - }: # TODO: Should this case just be: y non numeric? + if y.dtype not in NW_NUMERIC_DTYPES: try: - y = y.cast(nw.Float64()).to_numpy() + y = y.cast(Float64()).to_numpy() except ValueError: raise ValueError( "Could not convert value of 'y' into a numeric type." @@ -1772,18 +1787,17 @@ def process_dataframe_hierarchy(args): path = new_path # ------------ Define aggregation functions -------------------------------- agg_f = {} - aggfunc_color = None if args["values"]: try: if isinstance(args["values"], Sequence) and not isinstance( args["values"], str ): df = df.with_columns( - **{c: nw.col(c).cast(nw.Float64()) for c in args["values"]} + **{c: nw.col(c).cast(Float64()) for c in args["values"]} ) else: df = df.with_columns( - **{args["values"]: nw.col(args["values"]).cast(nw.Float64())} + **{args["values"]: nw.col(args["values"]).cast(Float64())} ) except ValueError: @@ -1903,43 +1917,29 @@ def post_agg( if i < len(path) - 1: j = i + 1 - # TODO: There should be a fast path along the following lines.. come back to this later. - # path_j_col = reduce(lambda c1, c2: c1 + "/" + c2, (dfg.get_column(path[j]).cast(nw.String()) for j in range(len(path)-1, j, -1)), "") - # parent_col = df_tree.get_column("parent").cast(nw.String()) - # id_col = df_tree.get_column("id").cast(nw.String()) - # df_tree = df_tree.with_columns( - # **{ - # "parent": path_j_col + "/" + parent_col, - # "id": path_j_col + "/" + id_col, - # } - # ) - - while j < len(path): - path_j_col = dfg.get_column(path[j]).cast(nw.String()) - parent_col = df_tree.get_column("parent").cast(nw.String()) - id_col = df_tree.get_column("id").cast(nw.String()) - - df_tree = df_tree.with_columns( - **{ - "parent": path_j_col + "/" + parent_col, - "id": path_j_col + "/" + id_col, - } - ) - j += 1 + path_j_col = reduce( + lambda c1, c2: c1 + "/" + c2, + ( + dfg.get_column(path[j]).cast(nw.String()) + for j in range(len(path) - 1, j, -1) + ), + "", + ) + parent_col = df_tree.get_column("parent").cast(nw.String()) + id_col = df_tree.get_column("id").cast(nw.String()) + df_tree = df_tree.with_columns( + **{ + "parent": path_j_col + "/" + parent_col, + "id": path_j_col + "/" + id_col, + } + ) - df_tree = df_tree.with_columns( - parent=nw.col("parent").str.replace( - "/?$", "" - ) # strip "/" if at the end of the string, equivalent to `.str.rstrip` - ) + # strip "/" if at the end of the string, equivalent to `.str.rstrip` + df_tree = df_tree.with_columns(parent=nw.col("parent").str.replace("/?$", "")) all_trees.append(df_tree.select(*["labels", "parent", "id", *cols])) - # TODO: Why does this fail in tests? - df_all_trees = nw.concat(all_trees, how="vertical") - native_trees = df_all_trees.to_native() - if nw.dependencies.is_pandas_like_dataframe(native_trees): - df_all_trees = nw.from_native(native_trees.reset_index(drop=True)) + df_all_trees = nw.maybe_reset_index(nw.concat(all_trees, how="vertical")) # we want to make sure than (?) is the first color of the sequence if args["color"] and discrete_color: @@ -1947,7 +1947,7 @@ def post_agg( while sort_col_name in df_all_trees.columns: sort_col_name += "0" df_all_trees = df_all_trees.with_columns( - **{sort_col_name: df.get_column(args["color"]).cast(nw.String())} + **{sort_col_name: df_all_trees.get_column(args["color"]).cast(nw.String())} ).sort(by=sort_col_name) # Now modify arguments @@ -1963,6 +1963,8 @@ def post_agg( if not args["hover_data"].get(args["color"]): args["hover_data"][args["color"]] = (True, None) else: + # FIXME: Hover data can become a list with duplicate values, and then we select that! + # In narwhals this raises `ValueError: Expected unique column names, got: Index(['smoker', 'smoker'], dtype='object')` args["hover_data"].append(args["color"]) return args @@ -1977,8 +1979,8 @@ def process_dataframe_timeline(args): try: df: nw.DataFrame = args["data_frame"] - x_start = df.get_column(args["x_start"]).cast(nw.Datetime()) - x_end = df.get_column(args["x_end"]).cast(nw.Datetime()) + x_start = df.get_column(args["x_start"]).cast(Datetime) + x_end = df.get_column(args["x_end"]).cast(Datetime) except (ValueError, TypeError): raise TypeError( "Both x_start and x_end must refer to data convertible to datetimes." @@ -1986,7 +1988,7 @@ def process_dataframe_timeline(args): # note that we are not adding any columns to the data frame here, so no risk of overwrite args["data_frame"] = df.with_columns( - **{args["x_end"]: (x_end - x_start).cast(nw.Duration()).dt.total_milliseconds()} + **{args["x_end"]: (x_end - x_start).dt.total_milliseconds()} ) args["x"] = args["x_end"] del args["x_end"] diff --git a/packages/python/plotly/plotly/tests/test_optional/test_px/test_px_functions.py b/packages/python/plotly/plotly/tests/test_optional/test_px/test_px_functions.py index ec27441d6c..3fbf7170e1 100644 --- a/packages/python/plotly/plotly/tests/test_optional/test_px/test_px_functions.py +++ b/packages/python/plotly/plotly/tests/test_optional/test_px/test_px_functions.py @@ -150,10 +150,6 @@ def test_sunburst_treemap_with_path(): fig = px.sunburst(df, path=path, values="values") assert fig.data[0].branchvalues == "total" assert fig.data[0].values[-1] == np.sum(values) - # Values passed - fig = px.sunburst(df, path=path, values="values") - assert fig.data[0].branchvalues == "total" - assert fig.data[0].values[-1] == np.sum(values) # Error when values cannot be converted to numerical data type df["values"] = ["1 000", "3 000", "2", "4", "2", "2", "1 000", "4 000"] msg = "Column `values` of `df` could not be converted to a numerical data type." From ac00b36308ae2458e586937045be9dc9e5ccc319 Mon Sep 17 00:00:00 2001 From: FBruzzesi Date: Tue, 1 Oct 2024 14:20:36 +0200 Subject: [PATCH 015/106] fix to_unindexed_series --- packages/python/plotly/plotly/express/_core.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/python/plotly/plotly/express/_core.py b/packages/python/plotly/plotly/express/_core.py index b6b2771fa5..aee9307551 100644 --- a/packages/python/plotly/plotly/express/_core.py +++ b/packages/python/plotly/plotly/express/_core.py @@ -1145,10 +1145,8 @@ def to_unindexed_series(x, name=None, native_namespace=None): seems to mangle datetime columns. Stripping the index from existing pd.Series is required to get things to match up right in the new DataFrame we're building """ - if nw.dependencies.is_pandas_like_series(x): - return nw.from_native(x.rename(name).reset_index(drop=True), series_only=True) - elif isinstance(x, nw.Series): - return x + if isinstance(x, nw.Series): + return nw.maybe_reset_index(x).rename(name) else: if native_namespace is not None: return nw.new_series(name=name, values=x, native_namespace=native_namespace) From da80c5b64181a9e4b1a9437012652a0f770c0d75 Mon Sep 17 00:00:00 2001 From: FBruzzesi Date: Tue, 1 Oct 2024 14:54:48 +0200 Subject: [PATCH 016/106] fix trendline --- packages/python/plotly/plotly/express/_core.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/python/plotly/plotly/express/_core.py b/packages/python/plotly/plotly/express/_core.py index aee9307551..6a40ddd1ac 100644 --- a/packages/python/plotly/plotly/express/_core.py +++ b/packages/python/plotly/plotly/express/_core.py @@ -1083,7 +1083,7 @@ def _get_reserved_col_names(args): ).all() if in_df: reserved_names.add(arg_name) - elif (arg == nw.maybe_get_index(df)).all() and arg.name is not None: + elif arg is nw.maybe_get_index(df) and arg.name is not None: reserved_names.add(arg.name) return reserved_names @@ -2605,11 +2605,11 @@ def make_figure(args, constructor, trace_patch=None, layout_patch=None): if fit_results is not None: trendline_rows.append(dict(px_fit_results=fit_results)) - if (pd := nw.dependencies.get_pandas()) is not None: + if trendline_rows: + if (pd := nw.dependencies.get_pandas()) is None: + msg = "Trendlines require pandas to be installed" + raise NotImplementedError(msg) fig._px_trendlines = pd.DataFrame(trendline_rows) - else: - msg = "Trendlines require pandas to be installed" - raise NotImplementedError(msg) configure_axes(args, constructor, fig, orders) configure_animation_controls(args, constructor, fig) From 8a72ba1dfa799a6335812595560656e80461e42a Mon Sep 17 00:00:00 2001 From: FBruzzesi Date: Wed, 2 Oct 2024 06:54:57 +0200 Subject: [PATCH 017/106] rm numpy dep in _core --- .../python/plotly/plotly/express/_core.py | 40 ++++++++++--------- 1 file changed, 21 insertions(+), 19 deletions(-) diff --git a/packages/python/plotly/plotly/express/_core.py b/packages/python/plotly/plotly/express/_core.py index 6a40ddd1ac..6fcca6f21e 100644 --- a/packages/python/plotly/plotly/express/_core.py +++ b/packages/python/plotly/plotly/express/_core.py @@ -22,7 +22,6 @@ from narwhals.dtypes import UInt16 from narwhals.dtypes import UInt32 from narwhals.dtypes import UInt64 -import numpy as np from plotly._subplots import ( make_subplots, @@ -366,8 +365,8 @@ def make_trace_kwargs(args, trace_spec, trace_data, mapping_labels, sizeref): .select( **{ args["x"]: nw.when(~x.is_null()) - .then(x.cast(nw.Int64)) - .otherwise(nw.lit(None, nw.Int64)) + .then(x.cast(Int64())) + .otherwise(nw.lit(None, Int64())) } ) .get_column(args["x"]) @@ -385,7 +384,7 @@ def make_trace_kwargs(args, trace_spec, trace_data, mapping_labels, sizeref): if y.dtype not in NW_NUMERIC_DTYPES: try: - y = y.cast(Float64()).to_numpy() + y = y.cast(Float64()) except ValueError: raise ValueError( "Could not convert value of 'y' into a numeric type." @@ -1233,11 +1232,12 @@ def process_args_into_dataframe( continue col_name = None # Case of multiindex + native_namespace if is_pd_like and isinstance(argument, native_namespace.MultiIndex): raise TypeError( - "Argument '%s' is a MultiIndex. " - " MultiIndex is not supported by plotly express " - "at the moment." % field + f"Argument '{field}' is a {native_namespace.__name__} MultiIndex. " + f"{native_namespace.__name__} MultiIndex is not supported by plotly " + "express at the moment." ) # ----------------- argument is a special value ---------------------- if isinstance(argument, (Constant, Range)): @@ -1316,7 +1316,7 @@ def process_args_into_dataframe( # ----------------- argument is likely a column / array / list.... ------- else: if df_provided and hasattr(argument, "name"): - if is_pd_like and (argument == nw.maybe_get_index(df_input)).all(): + if is_pd_like and argument is nw.maybe_get_index(df_input): if argument.name is None or argument.name in df_input.columns: col_name = "index" else: @@ -1535,9 +1535,9 @@ def build_dataframe(args, constructor): wide_mode = True if is_pd_like and isinstance(columns, native_namespace.MultiIndex): raise TypeError( - "Data frame columns is a MultiIndex. " - "MultiIndex is not supported by plotly express " - "at the moment." + f"Data frame columns is a {native_namespace.__name__} MultiIndex. " + f"{native_namespace.__name__} MultiIndex is not supported by plotly " + "express at the moment." ) args["wide_variable"] = list(columns) if is_pd_like and isinstance(columns, native_namespace.Index): @@ -1596,9 +1596,9 @@ def build_dataframe(args, constructor): if df_provided and is_pd_like and index is not None: if isinstance(index, native_namespace.MultiIndex): raise TypeError( - "Data frame index is a pandas MultiIndex. " - "pandas MultiIndex is not supported by plotly express " - "at the moment." + f"Data frame index is a {native_namespace.__name__} MultiIndex. " + f"{native_namespace.__name__} MultiIndex is not supported by " + "plotly express at the moment." ) args["wide_cross"] = index else: @@ -1660,7 +1660,7 @@ def build_dataframe(args, constructor): dtype = None for v in wide_value_vars: v_dtype = df_output.get_column(v).dtype - v_dtype = "number" if v_dtype in NW_NUMERIC_DTYPES else v_dtype + v_dtype = "number" if v_dtype in NW_NUMERIC_DTYPES else str(v_dtype) if dtype is None: dtype = v_dtype elif dtype != v_dtype: @@ -1731,17 +1731,17 @@ def _check_dataframe_all_leaves(df: nw.DataFrame) -> None: cols = df.columns df_sorted = df.sort(by=cols) null_mask = df_sorted.select(*[nw.col(c).is_null() for c in cols]) - df_sorted = df_sorted.with_columns(*[nw.col(c).cast(nw.String) for c in cols]) + df_sorted = df_sorted.with_columns(*[nw.col(c).cast(nw.String()) for c in cols]) null_indices = ( null_mask.select(null_mask=nw.any_horizontal(nw.col(cols))) .get_column("null_mask") .arg_true() ) for null_row_index in null_indices: - row = np.array(null_mask.row(null_row_index)) - i = np.nonzero(row)[0][0] + row = null_mask.row(null_row_index) + i = row.index(True) - if not row[i:].all(): + if not all(row[i:]): raise ValueError( "None entries cannot have not-None children", df_sorted.row(null_row_index), @@ -2610,6 +2610,8 @@ def make_figure(args, constructor, trace_patch=None, layout_patch=None): msg = "Trendlines require pandas to be installed" raise NotImplementedError(msg) fig._px_trendlines = pd.DataFrame(trendline_rows) + else: + fig._px_trendlines = [] configure_axes(args, constructor, fig, orders) configure_animation_controls(args, constructor, fig) From aeff20319caae9f20726a28584f54f666d6b1c0a Mon Sep 17 00:00:00 2001 From: FBruzzesi Date: Wed, 2 Oct 2024 09:26:40 +0200 Subject: [PATCH 018/106] fix: _check_dataframe_all_leaves --- .../python/plotly/plotly/express/_core.py | 30 ++++++++++++------- .../express/trendline_functions/__init__.py | 3 +- .../test_optional/test_px/test_trendline.py | 4 +-- 3 files changed, 24 insertions(+), 13 deletions(-) diff --git a/packages/python/plotly/plotly/express/_core.py b/packages/python/plotly/plotly/express/_core.py index 6fcca6f21e..44968f0c54 100644 --- a/packages/python/plotly/plotly/express/_core.py +++ b/packages/python/plotly/plotly/express/_core.py @@ -348,7 +348,7 @@ def make_trace_kwargs(args, trace_spec, trace_data, mapping_labels, sizeref): if ( args["x"] and args["y"] - and len( # TODO: Handle case arg["y"] is a list? + and len( trace_data.select(nw.col(args["x"], args["y"])).drop_nulls() ) > 1 @@ -395,8 +395,12 @@ def make_trace_kwargs(args, trace_spec, trace_data, mapping_labels, sizeref): # NB this means trendline functions must output one-to-one with the input series # i.e. we can't do resampling, because then the X values might not line up! non_missing = ~(x.is_null() | y.is_null()) - trace_patch["x"] = sorted_trace_data.filter(non_missing).get_column( - args["x"] + trace_patch["x"] = ( + sorted_trace_data.filter(non_missing) + .get_column(args["x"]) + .to_numpy() + # FIXME: Converting to numpy is needed to pass `test_trendline_on_timeseries` + # test, but I wonder if it is the right way to do it in the first place. ) trendline_function = trendline_functions[attr_value] @@ -564,11 +568,9 @@ def make_trace_kwargs(args, trace_spec, trace_data, mapping_labels, sizeref): formatter = args["hover_data"][k_args][0] if formatter: if isinstance(formatter, str): - # TODO: Check if is it possible that `v` is a Series mapping_labels_copy[k] = v.replace("}", "%s}" % formatter) else: _ = mapping_labels_copy.pop(k) - # TODO: Check if is it possible that `v` is a Series hover_lines = [k + "=" + v for k, v in mapping_labels_copy.items()] trace_patch["hovertemplate"] = hover_header + "
".join(hover_lines) trace_patch["hovertemplate"] += "" @@ -1731,7 +1733,7 @@ def _check_dataframe_all_leaves(df: nw.DataFrame) -> None: cols = df.columns df_sorted = df.sort(by=cols) null_mask = df_sorted.select(*[nw.col(c).is_null() for c in cols]) - df_sorted = df_sorted.with_columns(*[nw.col(c).cast(nw.String()) for c in cols]) + df_sorted = df_sorted.with_columns(nw.col(*cols).cast(nw.String())) null_indices = ( null_mask.select(null_mask=nw.any_horizontal(nw.col(cols))) .get_column("null_mask") @@ -1746,9 +1748,15 @@ def _check_dataframe_all_leaves(df: nw.DataFrame) -> None: "None entries cannot have not-None children", df_sorted.row(null_row_index), ) + fill_series = nw.new_series( + name="fill_value", + values=[""] * len(df_sorted), + dtype=nw.String(), + native_namespace=df_sorted.__native_namespace__(), + ) df_sorted = df_sorted.with_columns( **{ - c: df_sorted.get_column(c).zip_with(null_mask.get_column(c), nw.lit("")) + c: df_sorted.get_column(c).zip_with(~null_mask.get_column(c), fill_series) for c in cols } ) @@ -1757,11 +1765,13 @@ def _check_dataframe_all_leaves(df: nw.DataFrame) -> None: [df_sorted.get_column(c).cast(nw.String()) for c in cols], "", ) - for i, row in enumerate(row_strings[:-1]): - if row_strings[i + 1] in row and (i + 1) in null_indices: + for i, (current_row, next_row) in enumerate( + zip(row_strings[:-1], row_strings[1:]), start=1 + ): + if next_row in current_row and (i) in null_indices: raise ValueError( "Non-leaves rows are not permitted in the dataframe \n", - df_sorted.iloc[i + 1], + df_sorted.row(i), "is not a leaf.", ) diff --git a/packages/python/plotly/plotly/express/trendline_functions/__init__.py b/packages/python/plotly/plotly/express/trendline_functions/__init__.py index 1133462468..c7dd2ca8fc 100644 --- a/packages/python/plotly/plotly/express/trendline_functions/__init__.py +++ b/packages/python/plotly/plotly/express/trendline_functions/__init__.py @@ -9,7 +9,6 @@ """ import narwhals.stable.v1 as nw -import numpy as np __all__ = ["ols", "lowess", "rolling", "ewm", "expanding"] @@ -32,6 +31,8 @@ def ols(trendline_options, x_raw, x, y, x_label, y_label, non_missing): respect to the base 10 logarithm of the input. Note that this means no zeros can be present in the input. """ + import numpy as np + valid_options = ["add_constant", "log_x", "log_y"] for k in trendline_options.keys(): if k not in valid_options: diff --git a/packages/python/plotly/plotly/tests/test_optional/test_px/test_trendline.py b/packages/python/plotly/plotly/tests/test_optional/test_px/test_trendline.py index 66046981ef..b351b410ca 100644 --- a/packages/python/plotly/plotly/tests/test_optional/test_px/test_trendline.py +++ b/packages/python/plotly/plotly/tests/test_optional/test_px/test_trendline.py @@ -196,8 +196,8 @@ def test_trendline_on_timeseries(mode, options): fig = px.scatter(df, x="date", y="GOOG", trendline=mode, trendline_options=options) assert len(fig.data) == 2 assert len(fig.data[0].x) == len(fig.data[1].x) - assert type(fig.data[0].x[0]) == datetime - assert type(fig.data[1].x[0]) == datetime + assert isinstance(fig.data[0].x[0], datetime) + assert isinstance(fig.data[1].x[0], datetime) assert np.all(fig.data[0].x == fig.data[1].x) assert str(fig.data[0].x[0]) == str(fig.data[1].x[0]) From 2041bef2af3263367f5b4c02f6ac7b0ec13ce86a Mon Sep 17 00:00:00 2001 From: FBruzzesi Date: Wed, 2 Oct 2024 09:58:26 +0200 Subject: [PATCH 019/106] (maybe) fix to_unindexed_series --- packages/python/plotly/plotly/express/_core.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/packages/python/plotly/plotly/express/_core.py b/packages/python/plotly/plotly/express/_core.py index 44968f0c54..9d92864b33 100644 --- a/packages/python/plotly/plotly/express/_core.py +++ b/packages/python/plotly/plotly/express/_core.py @@ -1146,6 +1146,7 @@ def to_unindexed_series(x, name=None, native_namespace=None): seems to mangle datetime columns. Stripping the index from existing pd.Series is required to get things to match up right in the new DataFrame we're building """ + x = nw.from_native(x, series_only=True, strict=False) if isinstance(x, nw.Series): return nw.maybe_reset_index(x).rename(name) else: @@ -1234,7 +1235,6 @@ def process_args_into_dataframe( continue col_name = None # Case of multiindex - native_namespace if is_pd_like and isinstance(argument, native_namespace.MultiIndex): raise TypeError( f"Argument '{field}' is a {native_namespace.__name__} MultiIndex. " @@ -1348,8 +1348,11 @@ def process_args_into_dataframe( "length of previously-processed arguments %s is %d" % (field, len_arg, str(list(df_output.keys())), length) ) + df_output[str(col_name)] = to_unindexed_series( - argument, str(col_name), native_namespace=native_namespace + x=nw.from_native(argument, allow_series=True, strict=False), + name=str(col_name), + native_namespace=native_namespace, ) # Finally, update argument with column name now that column exists @@ -1500,11 +1503,11 @@ def build_dataframe(args, constructor): df_input: nw.DataFrame | None = args["data_frame"] index = nw.maybe_get_index(df_input) if df_provided else None - # Narwhals does not support native namespace for interchange level + + # This is safe since at this point `_compliant_frame` is one of the "full" level + # support dataframe(s) native_namespace = ( - getattr(df_input._compliant_frame, "__native_namespace__", lambda: None)() - if df_provided - else None + df_input._compliant_frame.__native_namespace__() if df_provided else None ) # now we handle special cases like wide-mode or x-xor-y specification From 71473f1af45be3c20e5a236e0436ee434780a0a6 Mon Sep 17 00:00:00 2001 From: FBruzzesi Date: Wed, 2 Oct 2024 09:58:47 +0200 Subject: [PATCH 020/106] (maybe) fix to_unindexed_series --- packages/python/plotly/plotly/express/_core.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/packages/python/plotly/plotly/express/_core.py b/packages/python/plotly/plotly/express/_core.py index 9d92864b33..08eba4779e 100644 --- a/packages/python/plotly/plotly/express/_core.py +++ b/packages/python/plotly/plotly/express/_core.py @@ -1140,11 +1140,9 @@ def _escape_col_name(columns, col_name, extra): def to_unindexed_series(x, name=None, native_namespace=None): - """ - assuming x is list-like or even an existing pd.Series, return a new pd.Series with - no index, without extracting the data from an existing Series via numpy, which - seems to mangle datetime columns. Stripping the index from existing pd.Series is - required to get things to match up right in the new DataFrame we're building + """Assuming x is list-like or even an existing Series, returns a new Series with + no index (if pandas-like). Stripping the index from existing pd.Series is + required to get things to match up right in the new DataFrame we're building. """ x = nw.from_native(x, series_only=True, strict=False) if isinstance(x, nw.Series): From 9f74c383910357a478f4447c43faed3bdb797926 Mon Sep 17 00:00:00 2001 From: FBruzzesi Date: Wed, 2 Oct 2024 17:41:50 +0200 Subject: [PATCH 021/106] started tests with constructor --- .../python/plotly/plotly/express/_core.py | 2 +- .../test_optional/test_px/test_facets.py | 23 ++- .../test_optional/test_px/test_marginals.py | 17 ++- .../test_px/test_px_functions.py | 127 +++++++++++----- .../test_optional/test_px/test_px_hover.py | 44 ++++-- .../test_optional/test_px/test_px_input.py | 135 +++++++++++------- 6 files changed, 230 insertions(+), 118 deletions(-) diff --git a/packages/python/plotly/plotly/express/_core.py b/packages/python/plotly/plotly/express/_core.py index 08eba4779e..e6cca7252c 100644 --- a/packages/python/plotly/plotly/express/_core.py +++ b/packages/python/plotly/plotly/express/_core.py @@ -1740,7 +1740,7 @@ def _check_dataframe_all_leaves(df: nw.DataFrame) -> None: .get_column("null_mask") .arg_true() ) - for null_row_index in null_indices: + for null_row_index in null_indices.to_list(): row = null_mask.row(null_row_index) i = row.index(True) diff --git a/packages/python/plotly/plotly/tests/test_optional/test_px/test_facets.py b/packages/python/plotly/plotly/tests/test_optional/test_px/test_facets.py index c1db2afe77..aeddef1237 100644 --- a/packages/python/plotly/plotly/tests/test_optional/test_px/test_facets.py +++ b/packages/python/plotly/plotly/tests/test_optional/test_px/test_facets.py @@ -1,12 +1,19 @@ import pandas as pd +import polars as pl +import pyarrow as pa import plotly.express as px from pytest import approx import pytest import random +constructors = (pd.DataFrame, pl.DataFrame, pa.table) + + +@pytest.mark.parametrize("constructor", constructors) +def test_facets(constructor): + data = px.data.tips().to_dict(orient="list") + df = constructor(data) -def test_facets(): - df = px.data.tips() fig = px.scatter(df, x="total_bill", y="tip") assert "xaxis2" not in fig.layout assert "yaxis2" not in fig.layout @@ -46,8 +53,10 @@ def test_facets(): assert fig.layout.yaxis4.domain[0] - fig.layout.yaxis.domain[1] == approx(0.08) -def test_facets_with_marginals(): - df = px.data.tips() +@pytest.mark.parametrize("constructor", constructors) +def test_facets_with_marginals(constructor): + data = px.data.tips().to_dict(orient="list") + df = constructor(data) fig = px.histogram(df, x="total_bill", facet_col="sex", marginal="rug") assert len(fig.data) == 4 @@ -93,12 +102,12 @@ def test_facets_with_marginals(): assert len(fig.data) == 2 # ignore all marginals -@pytest.fixture -def bad_facet_spacing_df(): +@pytest.fixture(params=constructors) +def bad_facet_spacing_df(request): NROWS = 101 NDATA = 1000 categories = [n % NROWS for n in range(NDATA)] - df = pd.DataFrame( + df = request.param( { "x": [random.random() for _ in range(NDATA)], "y": [random.random() for _ in range(NDATA)], diff --git a/packages/python/plotly/plotly/tests/test_optional/test_px/test_marginals.py b/packages/python/plotly/plotly/tests/test_optional/test_px/test_marginals.py index 0274068d27..9be3bbb2c0 100644 --- a/packages/python/plotly/plotly/tests/test_optional/test_px/test_marginals.py +++ b/packages/python/plotly/plotly/tests/test_optional/test_px/test_marginals.py @@ -1,12 +1,19 @@ +import pandas as pd +import polars as pl +import pyarrow as pa import plotly.express as px import pytest +constructors = (pd.DataFrame, pl.DataFrame, pa.table) + +@pytest.mark.parametrize("constructor", constructors) @pytest.mark.parametrize("px_fn", [px.scatter, px.density_heatmap, px.density_contour]) @pytest.mark.parametrize("marginal_x", [None, "histogram", "box", "violin"]) @pytest.mark.parametrize("marginal_y", [None, "rug"]) -def test_xy_marginals(px_fn, marginal_x, marginal_y): - df = px.data.tips() +def test_xy_marginals(constructor, px_fn, marginal_x, marginal_y): + data = px.data.tips().to_dict(orient="list") + df = constructor(data) fig = px_fn( df, x="total_bill", y="tip", marginal_x=marginal_x, marginal_y=marginal_y @@ -14,11 +21,13 @@ def test_xy_marginals(px_fn, marginal_x, marginal_y): assert len(fig.data) == 1 + (marginal_x is not None) + (marginal_y is not None) +@pytest.mark.parametrize("constructor", constructors) @pytest.mark.parametrize("px_fn", [px.histogram, px.ecdf]) @pytest.mark.parametrize("marginal", [None, "rug", "histogram", "box", "violin"]) @pytest.mark.parametrize("orientation", ["h", "v"]) -def test_single_marginals(px_fn, marginal, orientation): - df = px.data.tips() +def test_single_marginals(constructor, px_fn, marginal, orientation): + data = px.data.tips().to_dict(orient="list") + df = constructor(data) fig = px_fn( df, x="total_bill", y="total_bill", marginal=marginal, orientation=orientation diff --git a/packages/python/plotly/plotly/tests/test_optional/test_px/test_px_functions.py b/packages/python/plotly/plotly/tests/test_optional/test_px/test_px_functions.py index 3fbf7170e1..cff27c73be 100644 --- a/packages/python/plotly/plotly/tests/test_optional/test_px/test_px_functions.py +++ b/packages/python/plotly/plotly/tests/test_optional/test_px/test_px_functions.py @@ -1,11 +1,17 @@ import plotly.express as px import plotly.graph_objects as go from numpy.testing import assert_array_equal +import narwhals.stable.v1 as nw import numpy as np import pandas as pd +import polars as pl +import pyarrow as pa import pytest +constructors = (pd.DataFrame, pl.DataFrame, pa.table) + + def _compare_figures(go_trace, px_fig): """Compare a figure created with a go trace and a figure created with a px function call. Check that all values inside the go Figure are the @@ -118,7 +124,8 @@ def test_sunburst_treemap_colorscales(): assert list(fig.layout[colorway]) == color_seq -def test_sunburst_treemap_with_path(): +@pytest.mark.parametrize("constructor", constructors) +def test_sunburst_treemap_with_path(constructor): vendors = ["A", "B", "C", "D", "E", "F", "G", "H"] sectors = [ "Tech", @@ -133,7 +140,7 @@ def test_sunburst_treemap_with_path(): regions = ["North", "North", "North", "North", "South", "South", "South", "South"] values = [1, 3, 2, 4, 2, 2, 1, 4] total = ["total"] * 8 - df = pd.DataFrame( + df = constructor( dict( vendors=vendors, sectors=sectors, @@ -150,6 +157,8 @@ def test_sunburst_treemap_with_path(): fig = px.sunburst(df, path=path, values="values") assert fig.data[0].branchvalues == "total" assert fig.data[0].values[-1] == np.sum(values) + + # TODO: Fix from here # Error when values cannot be converted to numerical data type df["values"] = ["1 000", "3 000", "2", "4", "2", "2", "1 000", "4 000"] msg = "Column `values` of `df` could not be converted to a numerical data type." @@ -167,32 +176,41 @@ def test_sunburst_treemap_with_path(): assert fig.data[0].values[-1] == 8 -def test_sunburst_treemap_with_path_and_hover(): - df = px.data.tips() +# TODO: Fix duplicate column, pyarrow cannot sum strings to concatenate them +@pytest.mark.parametrize("constructor", constructors) +def test_sunburst_treemap_with_path_and_hover(constructor): + data = px.data.tips().to_dict(orient="list") + df = constructor(data) fig = px.sunburst( df, path=["sex", "day", "time", "smoker"], color="smoker", hover_data=["smoker"] ) assert "smoker" in fig.data[0].hovertemplate - df = px.data.gapminder().query("year == 2007") + data = px.data.gapminder().query("year == 2007").to_dict(orient="list") + df = constructor(data) + fig = px.sunburst( df, path=["continent", "country"], color="lifeExp", hover_data=df.columns ) assert fig.layout.coloraxis.colorbar.title.text == "lifeExp" - df = px.data.tips() + data = px.data.tips().to_dict(orient="list") + df = constructor(data) + fig = px.sunburst(df, path=["sex", "day", "time", "smoker"], hover_name="smoker") assert "smoker" not in fig.data[0].hovertemplate # represented as '%{hovertext}' assert "%{hovertext}" in fig.data[0].hovertemplate # represented as '%{hovertext}' - df = px.data.tips() + data = px.data.tips().to_dict(orient="list") + df = constructor(data) fig = px.sunburst(df, path=["sex", "day", "time", "smoker"], custom_data=["smoker"]) assert fig.data[0].customdata[0][0] in ["Yes", "No"] assert "smoker" not in fig.data[0].hovertemplate assert "%{hovertext}" not in fig.data[0].hovertemplate -def test_sunburst_treemap_with_path_color(): +@pytest.mark.parametrize("constructor", constructors) +def test_sunburst_treemap_with_path_color(constructor): vendors = ["A", "B", "C", "D", "E", "F", "G", "H"] sectors = [ "Tech", @@ -208,16 +226,17 @@ def test_sunburst_treemap_with_path_color(): values = [1, 3, 2, 4, 2, 2, 1, 4] calls = [8, 2, 1, 3, 2, 2, 4, 1] total = ["total"] * 8 - df = pd.DataFrame( - dict( - vendors=vendors, - sectors=sectors, - regions=regions, - values=values, - total=total, - calls=calls, - ) + hover = [el.lower() for el in vendors] + + data = dict( + vendors=vendors, + sectors=sectors, + regions=regions, + values=values, + total=total, + calls=calls, ) + df = constructor(data) path = ["total", "regions", "sectors", "vendors"] fig = px.sunburst(df, path=path, values="values", color="calls") colors = fig.data[0].marker.colors @@ -227,12 +246,12 @@ def test_sunburst_treemap_with_path_color(): assert np.all(np.array(colors[:8]) == np.array(calls)) # Hover info - df["hover"] = [el.lower() for el in vendors] + df = nw.from_native(df).with_columns(hover=hover).to_native() fig = px.sunburst(df, path=path, color="calls", hover_data=["hover"]) custom = fig.data[0].customdata - assert np.all(custom[:8, 0] == df["hover"]) + assert np.all(custom[:8, 0] == hover) assert np.all(custom[8:, 0] == "(?)") - assert np.all(custom[:8, 1] == df["calls"]) + assert np.all(custom[:8, 1] == calls) # Discrete color fig = px.sunburst(df, path=path, color="vendors") @@ -244,14 +263,24 @@ def test_sunburst_treemap_with_path_color(): assert np.all(np.in1d(fig.data[0].marker.colors, list(cmap.values()))) # Numerical column in path - df["regions"] = df["regions"].map({"North": 1, "South": 2}) + df = ( + nw.from_native(df) + .with_columns( + regions=nw.when(nw.col("region") == "North") + .then(1) + .otherwise(2) + .cast(nw.Int64()) + ) + .to_native() + ) path = ["total", "regions", "sectors", "vendors"] fig = px.sunburst(df, path=path, values="values", color="calls") colors = fig.data[0].marker.colors assert np.all(np.array(colors[:8]) == np.array(calls)) -def test_sunburst_treemap_column_parent(): +@pytest.mark.parametrize("constructor", constructors) +def test_sunburst_treemap_column_parent(constructor): vendors = ["A", "B", "C", "D", "E", "F", "G", "H"] sectors = [ "Tech", @@ -265,7 +294,7 @@ def test_sunburst_treemap_column_parent(): ] regions = ["North", "North", "North", "North", "South", "South", "South", "South"] values = [1, 3, 2, 4, 2, 2, 1, 4] - df = pd.DataFrame( + df = constructor( dict( id=vendors, sectors=sectors, @@ -278,7 +307,9 @@ def test_sunburst_treemap_column_parent(): px.sunburst(df, path=path, values="values") -def test_sunburst_treemap_with_path_non_rectangular(): +@pytest.mark.parametrize("constructor", constructors) +def test_sunburst_treemap_with_path_non_rectangular(constructor): + print(str(constructor)) vendors = ["A", "B", "C", "D", None, "E", "F", "G", "H", None] sectors = [ "Tech", @@ -306,7 +337,7 @@ def test_sunburst_treemap_with_path_non_rectangular(): ] values = [1, 3, 2, 4, 1, 2, 2, 1, 4, 1] total = ["total"] * 10 - df = pd.DataFrame( + df = constructor( dict( vendors=vendors, sectors=sectors, @@ -319,7 +350,18 @@ def test_sunburst_treemap_with_path_non_rectangular(): msg = "Non-leaves rows are not permitted in the dataframe" with pytest.raises(ValueError, match=msg): fig = px.sunburst(df, path=path, values="values") - df.loc[df["vendors"].isnull(), "sectors"] = "Other" + + df = ( + nw.from_native(df) + .with_columns( + sectors=( + nw.when(~nw.col("vendors").is_null()) + .then(nw.col("sectors")) + .otherwise(nw.lit("Other")) + ) + ) + .to_native() + ) fig = px.sunburst(df, path=path, values="values") assert fig.data[0].values[-1] == np.sum(values) @@ -358,8 +400,10 @@ def test_funnel(): assert len(fig.data) == 2 -def test_parcats_dimensions_max(): - df = px.data.tips() +@pytest.mark.parametrize("constructor", constructors) +def test_parcats_dimensions_max(constructor): + data = px.data.tips().to_dict(orient="list") + df = constructor(data) # default behaviour fig = px.parallel_categories(df) @@ -391,13 +435,15 @@ def test_parcats_dimensions_max(): assert [d.label for d in fig.data[0].dimensions] == ["sex", "smoker", "day", "size"] +@pytest.mark.parametrize("constructor", constructors) @pytest.mark.parametrize("histfunc,y", [(None, None), ("count", "tip")]) -def test_histfunc_hoverlabels_univariate(histfunc, y): +def test_histfunc_hoverlabels_univariate(constructor, histfunc, y): def check_label(label, fig): assert fig.layout.yaxis.title.text == label assert label + "=" in fig.data[0].hovertemplate - df = px.data.tips() + data = px.data.tips().to_dict(orient="list") + df = constructor(data) # base case, just "count" (note count(tip) is same as count()) fig = px.histogram(df, x="total_bill", y=y, histfunc=histfunc) @@ -423,12 +469,14 @@ def check_label(label, fig): check_label("%s (normalized as %s)" % (histnorm, barnorm), fig) -def test_histfunc_hoverlabels_bivariate(): +@pytest.mark.parametrize("constructor", constructors) +def test_histfunc_hoverlabels_bivariate(constructor): def check_label(label, fig): assert fig.layout.yaxis.title.text == label assert label + "=" in fig.data[0].hovertemplate - df = px.data.tips() + data = px.data.tips().to_dict(orient="list") + df = constructor(data) # with y, should be same as forcing histfunc to sum fig = px.histogram(df, x="total_bill", y="tip") @@ -483,13 +531,14 @@ def check_label(label, fig): check_label("density of max of tip", fig) -def test_timeline(): - df = pd.DataFrame( - [ - dict(Task="Job A", Start="2009-01-01", Finish="2009-02-28"), - dict(Task="Job B", Start="2009-03-05", Finish="2009-04-15"), - dict(Task="Job C", Start="2009-02-20", Finish="2009-05-30"), - ] +@pytest.mark.parametrize("constructor", constructors) +def test_timeline(constructor): + df = constructor( + { + "Task": ["Job A", "Job B", "Job C"], + "Start": ["2009-01-01", "2009-03-05", "2009-02-20"], + "Finish": ["2009-02-28", "2009-04-15", "2009-05-30"], + } ) fig = px.timeline(df, x_start="Start", x_end="Finish", y="Task", color="Task") assert len(fig.data) == 3 diff --git a/packages/python/plotly/plotly/tests/test_optional/test_px/test_px_hover.py b/packages/python/plotly/plotly/tests/test_optional/test_px/test_px_hover.py index 26ee1a2619..463c250361 100644 --- a/packages/python/plotly/plotly/tests/test_optional/test_px/test_px_hover.py +++ b/packages/python/plotly/plotly/tests/test_optional/test_px/test_px_hover.py @@ -1,12 +1,20 @@ import plotly.express as px +import narwhals.stable.v1 as nw import numpy as np import pandas as pd +import polars as pl +import pyarrow as pa import pytest from collections import OrderedDict # an OrderedDict is needed for Python 2 -def test_skip_hover(): - df = px.data.iris() +constructors = (pd.DataFrame, pl.DataFrame, pa.table) + + +@pytest.mark.parametrize("constructor", constructors) +def test_skip_hover(constructor): + data = px.data.iris().to_dict(orient="list") + df = constructor(data) fig = px.scatter( df, x="petal_length", @@ -17,8 +25,10 @@ def test_skip_hover(): assert fig.data[0].hovertemplate == "species_id=%{marker.size}" -def test_hover_data_string_column(): - df = px.data.tips() +@pytest.mark.parametrize("constructor", constructors) +def test_hover_data_string_column(constructor): + data = px.data.tips().to_dict(orient="list") + df = constructor(data) fig = px.scatter( df, x="tip", @@ -28,8 +38,10 @@ def test_hover_data_string_column(): assert "sex" in fig.data[0].hovertemplate -def test_composite_hover(): - df = px.data.tips() +@pytest.mark.parametrize("constructor", constructors) +def test_composite_hover(constructor): + data = px.data.tips().to_dict(orient="list") + df = constructor(data) hover_dict = OrderedDict( {"day": False, "time": False, "sex": True, "total_bill": ":.1f"} ) @@ -87,8 +99,10 @@ def test_newdatain_hover_data(): ) -def test_formatted_hover_and_labels(): - df = px.data.tips() +@pytest.mark.parametrize("constructor", constructors) +def test_formatted_hover_and_labels(constructor): + data = px.data.tips().to_dict(orient="list") + df = constructor(data) fig = px.scatter( df, x="tip", @@ -171,8 +185,10 @@ def test_fail_wrong_column(): ) -def test_sunburst_hoverdict_color(): - df = px.data.gapminder().query("year == 2007") +@pytest.mark.parametrize("constructor", constructors) +def test_sunburst_hoverdict_color(constructor): + data = px.data.gapminder().query("year == 2007").to_dict(orient="list") + df = constructor(data) fig = px.sunburst( df, path=["continent", "country"], @@ -183,8 +199,10 @@ def test_sunburst_hoverdict_color(): assert "color" in fig.data[0].hovertemplate -def test_date_in_hover(): - df = pd.DataFrame({"date": ["2015-04-04 19:31:30+1:00"], "value": [3]}) - df["date"] = pd.to_datetime(df["date"]) +@pytest.mark.parametrize("constructor", constructors) +def test_date_in_hover(constructor): + df = nw.from_native( + constructor({"date": ["2015-04-04 19:31:30+1:00"], "value": [3]}) + ).with_columns(date=nw.col("date").cast(nw.Datetime())) fig = px.scatter(df, x="value", y="value", hover_data=["date"]) assert str(fig.data[0].customdata[0][0]) == str(df["date"][0]) diff --git a/packages/python/plotly/plotly/tests/test_optional/test_px/test_px_input.py b/packages/python/plotly/plotly/tests/test_optional/test_px/test_px_input.py index 88ff6df7bb..946b5984da 100644 --- a/packages/python/plotly/plotly/tests/test_optional/test_px/test_px_input.py +++ b/packages/python/plotly/plotly/tests/test_optional/test_px/test_px_input.py @@ -1,7 +1,10 @@ import plotly.express as px import plotly.graph_objects as go +import narwhals.stable.v1 as nw import numpy as np import pandas as pd +import polars as pl +import pyarrow as pa import pytest from packaging import version import unittest.mock as mock @@ -10,6 +13,7 @@ import sys import warnings +constructors = (pd.DataFrame, pl.DataFrame, pa.table) # Fixtures # -------- @@ -64,50 +68,56 @@ def test_with_index(): assert fig.data[0]["hovertemplate"] == "item=%{x}
total_bill=%{y}" -def test_pandas_series(): - tips = px.data.tips() - before_tip = tips.total_bill - tips.tip +@pytest.mark.parametrize("constructor", constructors) +def test_series(constructor): + data = px.data.tips().to_dict(orient="list") + tips = nw.from_native(constructor(data)) + before_tip = (tips.get_column("total_bill") - tips.get_column("tip")).to_native() + day = tips.get_column("day").to_native() + tips = tips.to_native() + fig = px.bar(tips, x="day", y=before_tip) assert fig.data[0].hovertemplate == "day=%{x}
y=%{y}" fig = px.bar(tips, x="day", y=before_tip, labels={"y": "bill"}) assert fig.data[0].hovertemplate == "day=%{x}
bill=%{y}" # lock down that we can pass df.col to facet_* - fig = px.bar(tips, x="day", y="tip", facet_row=tips.day, facet_col=tips.day) + fig = px.bar(tips, x="day", y="tip", facet_row=day, facet_col=day) assert fig.data[0].hovertemplate == "day=%{x}
tip=%{y}" -def test_several_dataframes(): - df = pd.DataFrame(dict(x=[0, 1], y=[1, 10], z=[0.1, 0.8])) - df2 = pd.DataFrame(dict(time=[23, 26], money=[100, 200])) - fig = px.scatter(df, x="z", y=df2.money, size="x") +@pytest.mark.parametrize("constructor", constructors) +def test_several_dataframes(constructor): + df = constructor(dict(x=[0, 1], y=[1, 10], z=[0.1, 0.8])) + df2 = constructor(dict(time=[23, 26], money=[100, 200])) + fig = px.scatter(df, x="z", y=df2["money"], size="x") assert ( fig.data[0].hovertemplate == "z=%{x}
y=%{y}
x=%{marker.size}" ) - fig = px.scatter(df2, x=df.z, y=df2.money, size=df.z) + fig = px.scatter(df2, x=df["z"], y=df2["money"], size=df.z) assert ( fig.data[0].hovertemplate == "x=%{x}
money=%{y}
size=%{marker.size}" ) # Name conflict with pytest.raises(NameError) as err_msg: - fig = px.scatter(df, x="z", y=df2.money, size="y") + fig = px.scatter(df, x="z", y=df2["money"], size="y") assert "A name conflict was encountered for argument 'y'" in str(err_msg.value) with pytest.raises(NameError) as err_msg: - fig = px.scatter(df, x="z", y=df2.money, size=df.y) + fig = px.scatter(df, x="z", y=df2["money"], size=df.y) assert "A name conflict was encountered for argument 'y'" in str(err_msg.value) # No conflict when the dataframe is not given, fields are used - df = pd.DataFrame(dict(x=[0, 1], y=[3, 4])) - df2 = pd.DataFrame(dict(x=[3, 5], y=[23, 24])) - fig = px.scatter(x=df.y, y=df2.y) + df = constructor(dict(x=[0, 1], y=[3, 4])) + df2 = constructor(dict(x=[3, 5], y=[23, 24])) + fig = px.scatter(x=df["y"], y=df2["y"]) assert np.all(fig.data[0].x == np.array([3, 4])) assert np.all(fig.data[0].y == np.array([23, 24])) assert fig.data[0].hovertemplate == "x=%{x}
y=%{y}" - df = pd.DataFrame(dict(x=[0, 1], y=[3, 4])) - df2 = pd.DataFrame(dict(x=[3, 5], y=[23, 24])) - df3 = pd.DataFrame(dict(y=[0.1, 0.2])) + df = constructor(dict(x=[0, 1], y=[3, 4])) + df2 = constructor(dict(x=[3, 5], y=[23, 24])) + df3 = constructor(dict(y=[0.1, 0.2])) fig = px.scatter(x=df.y, y=df2.y, size=df3.y) assert np.all(fig.data[0].x == np.array([3, 4])) assert np.all(fig.data[0].y == np.array([23, 24])) @@ -116,9 +126,9 @@ def test_several_dataframes(): == "x=%{x}
y=%{y}
size=%{marker.size}" ) - df = pd.DataFrame(dict(x=[0, 1], y=[3, 4])) - df2 = pd.DataFrame(dict(x=[3, 5], y=[23, 24])) - df3 = pd.DataFrame(dict(y=[0.1, 0.2])) + df = constructor(dict(x=[0, 1], y=[3, 4])) + df2 = constructor(dict(x=[3, 5], y=[23, 24])) + df3 = constructor(dict(y=[0.1, 0.2])) fig = px.scatter(x=df.y, y=df2.y, hover_data=[df3.y]) assert np.all(fig.data[0].x == np.array([3, 4])) assert np.all(fig.data[0].y == np.array([23, 24])) @@ -128,16 +138,19 @@ def test_several_dataframes(): ) -def test_name_heuristics(): - df = pd.DataFrame(dict(x=[0, 1], y=[3, 4], z=[0.1, 0.2])) +@pytest.mark.parametrize("constructor", constructors) +def test_name_heuristics(constructor): + df = constructor(dict(x=[0, 1], y=[3, 4], z=[0.1, 0.2])) fig = px.scatter(df, x=df.y, y=df.x, size=df.y) assert np.all(fig.data[0].x == np.array([3, 4])) assert np.all(fig.data[0].y == np.array([0, 1])) assert fig.data[0].hovertemplate == "y=%{marker.size}
x=%{y}" -def test_repeated_name(): - iris = px.data.iris() +@pytest.mark.parametrize("constructor", constructors) +def test_repeated_name(constructor): + data = px.data.iris().to_dict(orient="list") + iris = constructor(data) fig = px.scatter( iris, x="sepal_width", @@ -148,8 +161,10 @@ def test_repeated_name(): assert fig.data[0].customdata.shape[1] == 4 -def test_arrayattrable_numpy(): - tips = px.data.tips() +@pytest.mark.parametrize("constructor", constructors) +def test_arrayattrable_numpy(constructor): + data = px.data.tips().to_dict(orient="list") + tips = constructor(data) fig = px.scatter( tips, x="total_bill", y="tip", hover_data=[np.random.random(tips.shape[0])] ) @@ -157,7 +172,6 @@ def test_arrayattrable_numpy(): fig.data[0]["hovertemplate"] == "total_bill=%{x}
tip=%{y}
hover_data_0=%{customdata[0]}" ) - tips = px.data.tips() fig = px.scatter( tips, x="total_bill", @@ -191,20 +205,24 @@ def test_wrong_dimensions_of_array(): assert "All arguments should have the same length." in str(err_msg.value) -def test_wrong_dimensions_mixed_case(): +@pytest.mark.parametrize("constructor", constructors) +def test_wrong_dimensions_mixed_case(constructor): with pytest.raises(ValueError) as err_msg: - df = pd.DataFrame(dict(time=[1, 2, 3], temperature=[20, 30, 25])) + df = constructor(dict(time=[1, 2, 3], temperature=[20, 30, 25])) px.scatter(df, x="time", y="temperature", color=[1, 3, 9, 5]) assert "All arguments should have the same length." in str(err_msg.value) -def test_wrong_dimensions(): +@pytest.mark.parametrize("constructor", constructors) +def test_wrong_dimensions(constructor): + data = px.data.tips().to_dict(orient="list") + df = constructor(data) with pytest.raises(ValueError) as err_msg: - px.scatter(px.data.tips(), x="tip", y=[1, 2, 3]) + px.scatter(df, x="tip", y=[1, 2, 3]) assert "All arguments should have the same length." in str(err_msg.value) # the order matters with pytest.raises(ValueError) as err_msg: - px.scatter(px.data.tips(), x=[1, 2, 3], y="tip") + px.scatter(df, x=[1, 2, 3], y="tip") assert "All arguments should have the same length." in str(err_msg.value) with pytest.raises(ValueError): px.scatter(px.data.tips(), x=px.data.iris().index, y="tip") @@ -223,26 +241,27 @@ def test_multiindex_raise_error(): assert "pandas MultiIndex is not supported by plotly express" in str(err_msg.value) -def test_build_df_from_lists(): +@pytest.mark.parametrize("constructor", constructors) +def test_build_df_from_lists(constructor): # Just lists args = dict(x=[1, 2, 3], y=[2, 3, 4], color=[1, 3, 9]) output = {key: key for key in args} - df = pd.DataFrame(args) + df = constructor(args) args["data_frame"] = None out = build_dataframe(args, go.Scatter) - df_out = out.pop("data_frame") - assert_frame_equal(df_out.to_pandas(), df[df_out.columns]) + df_out = out.pop("data_frame").to_native() + + assert df_out.equals(df) assert out == output # Arrays args = dict(x=np.array([1, 2, 3]), y=np.array([2, 3, 4]), color=[1, 3, 9]) output = {key: key for key in args} - df = pd.DataFrame(args) + df = constructor(args) args["data_frame"] = None out = build_dataframe(args, go.Scatter) - df_out = out.pop("data_frame") - assert_frame_equal(df_out.to_pandas(), df[df_out.columns]) - + df_out = out.pop("data_frame").to_native() + assert df_out.equals(df) assert out == output @@ -377,12 +396,16 @@ def test_build_df_with_hover_data_from_vaex_and_polars(test_lib, hover_data): ) -def test_timezones(): - df = pd.DataFrame({"date": ["2015-04-04 19:31:30+1:00"], "value": [3]}) +@pytest.mark.parametrize("constructor", constructors) +def test_timezones(constructor): + df = constructor({"date": ["2015-04-04 19:31:30+1:00"], "value": [3]}) df["date"] = pd.to_datetime(df["date"]) args = dict(data_frame=df, x="date", y="value") out = build_dataframe(args, go.Scatter) - assert str(out["data_frame"].to_pandas()["date"][0]) == str(df["date"][0]) + + assert str(out["data_frame"].item(row=0, column="date")) == str( + nw.from_native(df).item(row=0, column="date") + ) def test_non_matching_index(): @@ -405,8 +428,10 @@ def test_non_matching_index(): assert_frame_equal(expected, out["data_frame"].to_pandas()) -def test_splom_case(): - iris = px.data.iris() +@pytest.mark.parametrize("constructor", constructors) +def test_splom_case(constructor): + data = px.data.iris().to_dict(orient="list") + iris = constructor(data) fig = px.scatter_matrix(iris) assert len(fig.data[0].dimensions) == len(iris.columns) dic = {"a": [1, 2, 3], "b": [4, 5, 6], "c": [7, 8, 9]} @@ -417,11 +442,12 @@ def test_splom_case(): assert np.all(fig.data[0].dimensions[0].values == ar[:, 0]) -def test_int_col_names(): +@pytest.mark.parametrize("constructor", constructors) +def test_int_col_names(constructor): # DataFrame with int column names - lengths = pd.DataFrame(np.random.random(100)) - fig = px.histogram(lengths, x=0) - assert np.all(np.array(lengths).flatten() == fig.data[0].x) + lengths = constructor({"0": np.random.random(100)}) + fig = px.histogram(lengths, x="0") + assert np.all(nw.from_native(lengths).to_numpy().flatten() == fig.data[0].x) # Numpy array ar = np.arange(100).reshape((10, 10)) fig = px.scatter(ar, x=2, y=8) @@ -435,7 +461,7 @@ def test_data_frame_from_dict(): def test_arguments_not_modified(): - iris = px.data.iris() + iris = px.data.iris() # TODO petal_length = iris.petal_length hover_data = [iris.sepal_length] px.scatter(iris, x=petal_length, y="petal_width", hover_data=hover_data) @@ -444,7 +470,7 @@ def test_arguments_not_modified(): def test_pass_df_columns(): - tips = px.data.tips() + tips = px.data.tips() # TODO fig = px.histogram( tips, x="total_bill", @@ -460,7 +486,7 @@ def test_pass_df_columns(): def test_size_column(): - df = px.data.tips() + df = px.data.tips() # TODO fig = px.scatter(df, x=df["size"], y=df.tip) assert fig.data[0].hovertemplate == "size=%{x}
tip=%{y}" @@ -587,6 +613,7 @@ def test_auto_histfunc(): assert px.density_heatmap(x=a, y=a, z=a, histfunc="avg").data[0].histfunc == "avg" +@pytest.mark.parametrize("constructor", constructors) @pytest.mark.parametrize( "fn,mode", [(px.violin, "violinmode"), (px.box, "boxmode"), (px.strip, "boxmode")] ) @@ -601,8 +628,8 @@ def test_auto_histfunc(): ("numerical", "categorical1", "categorical1", "overlay"), ], ) -def test_auto_boxlike_overlay(fn, mode, x, y, color, result): - df = pd.DataFrame( +def test_auto_boxlike_overlay(constructor, fn, mode, x, y, color, result): + df = constructor( dict( categorical1=["a", "a", "b", "b"], categorical2=["a", "a", "b", "b"], From 28587c9c6ca08bd1582ae822c46d70faa194325c Mon Sep 17 00:00:00 2001 From: FBruzzesi Date: Wed, 2 Oct 2024 23:06:54 +0200 Subject: [PATCH 022/106] added constructor to all tests --- .../python/plotly/plotly/express/_core.py | 15 +- .../tests/test_optional/test_px/test_px.py | 136 ++++++++++++------ .../test_px/test_px_functions.py | 27 +++- .../test_optional/test_px/test_px_wide.py | 35 +++-- .../test_optional/test_px/test_trendline.py | 60 +++++--- 5 files changed, 183 insertions(+), 90 deletions(-) diff --git a/packages/python/plotly/plotly/express/_core.py b/packages/python/plotly/plotly/express/_core.py index e6cca7252c..65fcb1c151 100644 --- a/packages/python/plotly/plotly/express/_core.py +++ b/packages/python/plotly/plotly/express/_core.py @@ -1735,20 +1735,19 @@ def _check_dataframe_all_leaves(df: nw.DataFrame) -> None: df_sorted = df.sort(by=cols) null_mask = df_sorted.select(*[nw.col(c).is_null() for c in cols]) df_sorted = df_sorted.with_columns(nw.col(*cols).cast(nw.String())) - null_indices = ( - null_mask.select(null_mask=nw.any_horizontal(nw.col(cols))) - .get_column("null_mask") - .arg_true() - ) - for null_row_index in null_indices.to_list(): - row = null_mask.row(null_row_index) + null_indices = null_mask.select( + null_mask=nw.any_horizontal(nw.col(cols)) + ).get_column("null_mask") + for row_idx, row in zip(null_indices, null_mask.filter(null_indices).iter_rows()): + i = row.index(True) if not all(row[i:]): raise ValueError( "None entries cannot have not-None children", - df_sorted.row(null_row_index), + df_sorted.row(row_idx), ) + fill_series = nw.new_series( name="fill_value", values=[""] * len(df_sorted), diff --git a/packages/python/plotly/plotly/tests/test_optional/test_px/test_px.py b/packages/python/plotly/plotly/tests/test_optional/test_px/test_px.py index 8bcff763ab..84d9ecb7c0 100644 --- a/packages/python/plotly/plotly/tests/test_optional/test_px/test_px.py +++ b/packages/python/plotly/plotly/tests/test_optional/test_px/test_px.py @@ -1,54 +1,72 @@ import plotly.express as px import plotly.io as pio +import narwhals.stable.v1 as nw import numpy as np +import pandas as pd +import polars as pl +import pyarrow as pa import pytest from itertools import permutations +constructors = (pd.DataFrame, pl.DataFrame, pa.table) -def test_scatter(): - iris = px.data.iris() - fig = px.scatter(iris, x="sepal_width", y="sepal_length") + +@pytest.mark.parametrize("constructor", constructors) +def test_scatter(constructor): + data = px.data.iris().to_dict(orient="list") + iris = nw.from_native(constructor(data)) + fig = px.scatter(iris.to_native(), x="sepal_width", y="sepal_length") assert fig.data[0].type == "scatter" - assert np.all(fig.data[0].x == iris.sepal_width) - assert np.all(fig.data[0].y == iris.sepal_length) + assert np.all(fig.data[0].x == iris.get_column("sepal_width").to_numpy()) + assert np.all(fig.data[0].y == iris.get_column("sepal_length").to_numpy()) # test defaults assert fig.data[0].mode == "markers" -def test_custom_data_scatter(): - iris = px.data.iris() +@pytest.mark.parametrize("constructor", constructors) +def test_custom_data_scatter(constructor): + data = px.data.iris().to_dict(orient="list") + iris = nw.from_native(constructor(data)) # No hover, no custom data - fig = px.scatter(iris, x="sepal_width", y="sepal_length", color="species") + fig = px.scatter( + iris.to_native(), x="sepal_width", y="sepal_length", color="species" + ) assert fig.data[0].customdata is None # Hover, no custom data fig = px.scatter( - iris, + iris.to_native(), x="sepal_width", y="sepal_length", color="species", hover_data=["petal_length", "petal_width"], ) for data in fig.data: - assert np.all(np.in1d(data.customdata[:, 1], iris.petal_width)) + assert np.all( + np.in1d(data.customdata[:, 1], iris.get_column("petal_width").to_numpy()) + ) # Hover and custom data, no repeated arguments fig = px.scatter( - iris, + iris.to_native(), x="sepal_width", y="sepal_length", hover_data=["petal_length", "petal_width"], custom_data=["species_id", "species"], ) - assert np.all(fig.data[0].customdata[:, 0] == iris.species_id) + assert np.all( + fig.data[0].customdata[:, 0] == iris.get_column("species_id").to_numpy() + ) assert fig.data[0].customdata.shape[1] == 4 # Hover and custom data, with repeated arguments fig = px.scatter( - iris, + iris.to_native(), x="sepal_width", y="sepal_length", hover_data=["petal_length", "petal_width", "species_id"], custom_data=["species_id", "species"], ) - assert np.all(fig.data[0].customdata[:, 0] == iris.species_id) + assert np.all( + fig.data[0].customdata[:, 0] == iris.get_column("species_id").to_numpy() + ) assert fig.data[0].customdata.shape[1] == 4 assert ( fig.data[0].hovertemplate @@ -56,10 +74,12 @@ def test_custom_data_scatter(): ) -def test_labels(): - tips = px.data.tips() +@pytest.mark.parametrize("constructor", constructors) +def test_labels(constructor): + data = px.data.tips().to_dict(orient="list") + tips = nw.from_native(constructor(data)) fig = px.scatter( - tips, + tips.to_native(), x="total_bill", y="tip", facet_row="time", @@ -80,6 +100,7 @@ def test_labels(): assert fig.layout.annotations[4].text.startswith("TIME") +@pytest.mark.parametrize("constructor", constructors) @pytest.mark.parametrize( ["extra_kwargs", "expected_mode"], [ @@ -88,10 +109,12 @@ def test_labels(): ({"text": "continent"}, "lines+markers+text"), ], ) -def test_line_mode(extra_kwargs, expected_mode): - gapminder = px.data.gapminder() +def test_line_mode(constructor, extra_kwargs, expected_mode): + data = px.data.gapminder().to_dict(orient="list") + gapminder = nw.from_native(constructor(data)) + fig = px.line( - gapminder, + gapminder.to_native(), x="year", y="pop", color="country", @@ -100,11 +123,13 @@ def test_line_mode(extra_kwargs, expected_mode): assert fig.data[0].mode == expected_mode -def test_px_templates(): +@pytest.mark.parametrize("constructor", constructors) +def test_px_templates(constructor): try: import plotly.graph_objects as go - tips = px.data.tips() + data = px.data.tips().to_dict(orient="list") + tips = nw.from_native(constructor(data)) # use the normal defaults fig = px.scatter() @@ -131,7 +156,7 @@ def test_px_templates(): # read colorway from the template fig = px.scatter( - tips, + tips.to_native(), x="total_bill", y="tip", color="sex", @@ -148,14 +173,14 @@ def test_px_templates(): # pio default template colorway fallback pio.templates.default = "seaborn" px.defaults.template = None - fig = px.scatter(tips, x="total_bill", y="tip", color="sex") + fig = px.scatter(tips.to_native(), x="total_bill", y="tip", color="sex") assert fig.data[0].marker.color == pio.templates["seaborn"].layout.colorway[0] assert fig.data[1].marker.color == pio.templates["seaborn"].layout.colorway[1] # pio default template colorway fallback pio.templates.default = "seaborn" px.defaults.template = "ggplot2" - fig = px.scatter(tips, x="total_bill", y="tip", color="sex") + fig = px.scatter(tips.to_native(), x="total_bill", y="tip", color="sex") assert fig.data[0].marker.color == pio.templates["ggplot2"].layout.colorway[0] assert fig.data[1].marker.color == pio.templates["ggplot2"].layout.colorway[1] @@ -173,7 +198,7 @@ def test_px_templates(): pio.templates.default = "none" px.defaults.template = None fig = px.scatter( - tips, + tips.to_native(), x="total_bill", y="tip", marginal_x="histogram", @@ -185,7 +210,7 @@ def test_px_templates(): assert fig.layout.yaxis3.showgrid fig = px.scatter( - tips, + tips.to_native(), x="total_bill", y="tip", marginal_x="histogram", @@ -198,7 +223,7 @@ def test_px_templates(): assert fig.layout.yaxis3.showgrid is None fig = px.scatter( - tips, + tips.to_native(), x="total_bill", y="tip", marginal_x="histogram", @@ -230,11 +255,15 @@ def test_px_defaults(): pio.templates.default = "plotly" -def assert_orderings(days_order, days_check, times_order, times_check): +def assert_orderings(constructor, days_order, days_check, times_order, times_check): symbol_sequence = ["circle", "diamond", "square", "cross", "circle", "diamond"] color_sequence = ["red", "blue", "red", "blue", "red", "blue", "red", "blue"] + + data = px.data.tips().to_dict(orient="list") + tips = nw.from_native(constructor(data)) + fig = px.scatter( - px.data.tips(), + tips.to_native(), x="total_bill", y="tip", facet_row="time", @@ -263,16 +292,20 @@ def assert_orderings(days_order, days_check, times_order, times_check): assert trace.marker.color == color_sequence[i] +@pytest.mark.parametrize("constructor", constructors) @pytest.mark.parametrize("days", permutations(["Sun", "Sat", "Fri", "x"])) @pytest.mark.parametrize("times", permutations(["Lunch", "x"])) -def test_orthogonal_and_missing_orderings(days, times): - assert_orderings(days, list(days) + ["Thur"], times, list(times) + ["Dinner"]) +def test_orthogonal_and_missing_orderings(constructor, days, times): + assert_orderings( + constructor, days, list(days) + ["Thur"], times, list(times) + ["Dinner"] + ) +@pytest.mark.parametrize("constructor", constructors) @pytest.mark.parametrize("days", permutations(["Sun", "Sat", "Fri", "Thur"])) @pytest.mark.parametrize("times", permutations(["Lunch", "Dinner"])) -def test_orthogonal_orderings(days, times): - assert_orderings(days, days, times, times) +def test_orthogonal_orderings(constructor, days, times): + assert_orderings(constructor, days, days, times, times) def test_permissive_defaults(): @@ -281,10 +314,12 @@ def test_permissive_defaults(): px.defaults.should_not_work = "test" -def test_marginal_ranges(): - df = px.data.tips() +@pytest.mark.parametrize("constructor", constructors) +def test_marginal_ranges(constructor): + data = px.data.tips().to_dict(orient="list") + df = nw.from_native(constructor(data)) fig = px.scatter( - df, + df.to_native(), x="total_bill", y="tip", marginal_x="histogram", @@ -296,23 +331,34 @@ def test_marginal_ranges(): assert fig.layout.yaxis3.range is None -def test_render_mode(): - df = px.data.gapminder() - df2007 = df.query("year == 2007") - fig = px.scatter(df2007, x="gdpPercap", y="lifeExp", trendline="ols") +@pytest.mark.parametrize("constructor", constructors) +def test_render_mode(constructor): + data = px.data.gapminder().to_dict(orient="list") + df = nw.from_native(constructor(data)) + df2007 = df.filter(nw.col("year") == 2007) + + fig = px.scatter(df2007.to_native(), x="gdpPercap", y="lifeExp", trendline="ols") assert fig.data[0].type == "scatter" assert fig.data[1].type == "scatter" fig = px.scatter( - df2007, x="gdpPercap", y="lifeExp", trendline="ols", render_mode="webgl" + df2007.to_native(), + x="gdpPercap", + y="lifeExp", + trendline="ols", + render_mode="webgl", ) assert fig.data[0].type == "scattergl" assert fig.data[1].type == "scattergl" - fig = px.scatter(df, x="gdpPercap", y="lifeExp", trendline="ols") + fig = px.scatter(df.to_native(), x="gdpPercap", y="lifeExp", trendline="ols") assert fig.data[0].type == "scattergl" assert fig.data[1].type == "scattergl" - fig = px.scatter(df, x="gdpPercap", y="lifeExp", trendline="ols", render_mode="svg") + fig = px.scatter( + df.to_native(), x="gdpPercap", y="lifeExp", trendline="ols", render_mode="svg" + ) assert fig.data[0].type == "scatter" assert fig.data[1].type == "scatter" - fig = px.density_contour(df, x="gdpPercap", y="lifeExp", trendline="ols") + fig = px.density_contour( + df.to_native(), x="gdpPercap", y="lifeExp", trendline="ols" + ) assert fig.data[0].type == "histogram2dcontour" assert fig.data[1].type == "scatter" diff --git a/packages/python/plotly/plotly/tests/test_optional/test_px/test_px_functions.py b/packages/python/plotly/plotly/tests/test_optional/test_px/test_px_functions.py index cff27c73be..5c69235340 100644 --- a/packages/python/plotly/plotly/tests/test_optional/test_px/test_px_functions.py +++ b/packages/python/plotly/plotly/tests/test_optional/test_px/test_px_functions.py @@ -158,19 +158,32 @@ def test_sunburst_treemap_with_path(constructor): assert fig.data[0].branchvalues == "total" assert fig.data[0].values[-1] == np.sum(values) - # TODO: Fix from here # Error when values cannot be converted to numerical data type - df["values"] = ["1 000", "3 000", "2", "4", "2", "2", "1 000", "4 000"] + df = nw.from_native(df) + native_namespace = df.__native_namespace__() + df = df.with_columns( + values=nw.new_series( + "values", + ["1 000", "3 000", "2", "4", "2", "2", "1 000", "4 000"], + dtype=nw.String(), + native_namespace=native_namespace, + ) + ) msg = "Column `values` of `df` could not be converted to a numerical data type." with pytest.raises(ValueError, match=msg): - fig = px.sunburst(df, path=path, values="values") + fig = px.sunburst(df.to_native(), path=path, values="values") # path is a mixture of column names and array-like - path = [df.total, "regions", df.sectors, "vendors"] - fig = px.sunburst(df, path=path) + path = [ + df.get_column("total").to_native(), + "regions", + df.get_column("sectors").to_native(), + "vendors", + ] + fig = px.sunburst(df.to_native(), path=path) assert fig.data[0].branchvalues == "total" # Continuous colorscale - df["values"] = 1 - fig = px.sunburst(df, path=path, values="values", color="values") + df = df.with_columns(values=nw.lit(1)) + fig = px.sunburst(df.to_native(), path=path, values="values", color="values") assert "coloraxis" in fig.data[0].marker assert np.all(np.array(fig.data[0].marker.colors) == 1) assert fig.data[0].values[-1] == 8 diff --git a/packages/python/plotly/plotly/tests/test_optional/test_px/test_px_wide.py b/packages/python/plotly/plotly/tests/test_optional/test_px/test_px_wide.py index 7d8ce144ae..2686c00252 100644 --- a/packages/python/plotly/plotly/tests/test_optional/test_px/test_px_wide.py +++ b/packages/python/plotly/plotly/tests/test_optional/test_px/test_px_wide.py @@ -1,17 +1,24 @@ import plotly.express as px import plotly.graph_objects as go -import pandas as pd +import narwhals.stable.v1 as nw import numpy as np +import pandas as pd +import polars as pl +import pyarrow as pa from plotly.express._core import build_dataframe, _is_col_list from pandas.testing import assert_frame_equal import pytest import warnings +constructors = (pd.DataFrame, pl.DataFrame, pa.table) + + +@pytest.mark.parametrize("constructor", constructors) +def test_is_col_list(constructor): + df_input = constructor(dict(a=[1, 2], b=[1, 2])) + is_pd_like = nw.dependencies.is_pandas_like_dataframe(df_input) + native_namespace = df_input.__native_namespace__() -def test_is_col_list(): - df_input = pd.DataFrame(dict(a=[1, 2], b=[1, 2])) - is_pd_like = True - native_namespace = pd assert _is_col_list(df_input, ["a"], is_pd_like, native_namespace) assert _is_col_list(df_input, ["a", "b"], is_pd_like, native_namespace) assert _is_col_list(df_input, [[3, 4]], is_pd_like, native_namespace) @@ -24,6 +31,8 @@ def test_is_col_list(): assert not _is_col_list(df_input, ["a", "b", "c"], is_pd_like, native_namespace) assert not _is_col_list(df_input, [1, 2], is_pd_like, native_namespace) + +def test_is_col_list_pandas(): df_input = pd.DataFrame([[1, 2], [1, 2]]) is_pd_like = True native_namespace = pd @@ -62,7 +71,7 @@ def test_is_col_list(): ) @pytest.mark.parametrize("orientation", [None, "v", "h"]) @pytest.mark.parametrize("style", ["implicit", "explicit"]) -def test_wide_mode_external(px_fn, orientation, style): +def test_wide_mode_external(constructor, px_fn, orientation, style): # here we test this feature "black box" style by calling actual PX functions and # inspecting the figure... this is important but clunky, and is mostly a smoke test # allowing us to do more "white box" testing below @@ -294,7 +303,7 @@ def test_wide_x_or_y(tt, df_in, args_in, x, y, color, df_out_exp, transpose): args_in["data_frame"] = df_in args_out = build_dataframe(args_in, tt) df_out = args_out.pop("data_frame") - assert_frame_equal(df_out.to_pandas(), pd.DataFrame(df_out_exp)[df_out.columns]) + assert_frame_equal(df_out.to_native(), pd.DataFrame(df_out_exp)[df_out.columns]) if transpose: args_exp = dict(x=y, y=x, color=color) else: @@ -314,7 +323,7 @@ def test_wide_mode_internal_bar_exception(orientation): args_out = build_dataframe(args_in, go.Bar) df_out = args_out.pop("data_frame") assert_frame_equal( - df_out.to_pandas(), + df_out.to_native(), pd.DataFrame( dict( index=[11, 12, 13, 11, 12, 13], @@ -837,14 +846,16 @@ def test_mixed_input_error(df): ) -def test_mixed_number_input(): - df = pd.DataFrame(dict(a=[1, 2], b=[1.1, 2.1])) +@pytest.mark.parametrize("constructor", constructors) +def test_mixed_number_input(constructor): + df = constructor(dict(a=[1, 2], b=[1.1, 2.1])) fig = px.line(df) assert len(fig.data) == 2 -def test_line_group(): - df = pd.DataFrame( +@pytest.mark.parametrize("constructor", constructors) +def test_line_group(constructor): + df = constructor( data={ "who": ["a", "a", "b", "b"], "x": [0, 1, 0, 1], diff --git a/packages/python/plotly/plotly/tests/test_optional/test_px/test_trendline.py b/packages/python/plotly/plotly/tests/test_optional/test_px/test_trendline.py index b351b410ca..f7160727c0 100644 --- a/packages/python/plotly/plotly/tests/test_optional/test_px/test_trendline.py +++ b/packages/python/plotly/plotly/tests/test_optional/test_px/test_trendline.py @@ -1,10 +1,17 @@ import plotly.express as px +import narwhals.stable.v1 as nw import numpy as np import pandas as pd +import polars as pl +import pyarrow as pa import pytest from datetime import datetime +constructors = (pd.DataFrame, pl.DataFrame, pa.table) + + +@pytest.mark.parametrize("constructor", constructors) @pytest.mark.parametrize( "mode,options", [ @@ -16,10 +23,11 @@ ("ewm", dict(alpha=0.5)), ], ) -def test_trendline_results_passthrough(mode, options): - df = px.data.gapminder().query("continent == 'Oceania'") +def test_trendline_results_passthrough(constructor, mode, options): + data = px.data.gapminder().query("continent == 'Oceania'").to_dict(orient="list") + df = nw.from_native(constructor(data)) fig = px.scatter( - df, + df.to_native(), x="year", y="pop", color="country", @@ -97,6 +105,7 @@ def test_trendline_enough_values(mode, options): assert len(fig.data[1].x) == 2 +@pytest.mark.parametrize("constructor", constructors) @pytest.mark.parametrize( "mode,options", [ @@ -109,12 +118,18 @@ def test_trendline_enough_values(mode, options): ("ewm", dict(alpha=0.5)), ], ) -def test_trendline_nan_values(mode, options): - df = px.data.gapminder().query("continent == 'Oceania'") +def test_trendline_nan_values(constructor, mode, options): + data = px.data.gapminder().query("continent == 'Oceania'").to_dict(orient="list") + df = nw.from_native(constructor(data)) start_date = 1970 - df["pop"][df["year"] < start_date] = np.nan + df = df.with_columns( + pop=nw.when(nw.col("year") >= start_date) + .then(nw.col("pop")) + .otherwise(nw.lit(None)) + ) + fig = px.scatter( - df, + df.to_native(), x="year", y="pop", color="country", @@ -170,6 +185,7 @@ def test_ols_trendline_slopes(): assert "y = 0 * x + 1.5
" in fig.data[1].hovertemplate +@pytest.mark.parametrize("constructor", constructors) @pytest.mark.parametrize( "mode,options", [ @@ -182,18 +198,25 @@ def test_ols_trendline_slopes(): ("ewm", dict(alpha=0.5)), ], ) -def test_trendline_on_timeseries(mode, options): - df = px.data.stocks() +def test_trendline_on_timeseries(constructor, mode, options): + df = nw.from_native(constructor(px.data.stocks().to_dict(orient="list"))) with pytest.raises(ValueError) as err_msg: - px.scatter(df, x="date", y="GOOG", trendline=mode, trendline_options=options) + px.scatter( + df.to_native(), + x="date", + y="GOOG", + trendline=mode, + trendline_options=options, + ) assert "Could not convert value of 'x' ('date') into a numeric type." in str( err_msg.value ) - df["date"] = pd.to_datetime(df["date"]) - df["date"] = df["date"].dt.tz_localize("CET") # force a timezone - fig = px.scatter(df, x="date", y="GOOG", trendline=mode, trendline_options=options) + df = df.with_columns(date=nw.col("date").cast(nw.Datetime(time_zone="CET"))) + fig = px.scatter( + df.to_native(), x="date", y="GOOG", trendline=mode, trendline_options=options + ) assert len(fig.data) == 2 assert len(fig.data[0].x) == len(fig.data[1].x) assert isinstance(fig.data[0].x[0], datetime) @@ -202,16 +225,17 @@ def test_trendline_on_timeseries(mode, options): assert str(fig.data[0].x[0]) == str(fig.data[1].x[0]) -def test_overall_trendline(): - df = px.data.tips() - fig1 = px.scatter(df, x="total_bill", y="tip", trendline="ols") +@pytest.mark.parametrize("constructor", constructors) +def test_overall_trendline(constructor): + df = nw.from_native(constructor(px.data.tips().to_dict(orient="list"))) + fig1 = px.scatter(df.to_native(), x="total_bill", y="tip", trendline="ols") assert len(fig1.data) == 2 assert "trendline" in fig1.data[1].hovertemplate results1 = px.get_trendline_results(fig1) params1 = results1["px_fit_results"].iloc[0].params fig2 = px.scatter( - df, + df.to_native(), x="total_bill", y="tip", color="sex", @@ -226,7 +250,7 @@ def test_overall_trendline(): assert np.all(np.array_equal(params1, params2)) fig3 = px.scatter( - df, + df.to_native(), x="total_bill", y="tip", facet_row="sex", From 1bb244860484bd282c8ef0a2eae5db6dd53047e2 Mon Sep 17 00:00:00 2001 From: FBruzzesi Date: Wed, 2 Oct 2024 23:59:29 +0200 Subject: [PATCH 023/106] added some comments for fixme --- .../test_optional/test_px/test_facets.py | 1 + .../test_px/test_px_functions.py | 2 + .../test_optional/test_px/test_px_input.py | 53 +++++++++++++------ .../test_optional/test_px/test_px_wide.py | 2 +- .../test_optional/test_px/test_trendline.py | 7 +-- 5 files changed, 43 insertions(+), 22 deletions(-) diff --git a/packages/python/plotly/plotly/tests/test_optional/test_px/test_facets.py b/packages/python/plotly/plotly/tests/test_optional/test_px/test_facets.py index aeddef1237..dbd303211e 100644 --- a/packages/python/plotly/plotly/tests/test_optional/test_px/test_facets.py +++ b/packages/python/plotly/plotly/tests/test_optional/test_px/test_facets.py @@ -53,6 +53,7 @@ def test_facets(constructor): assert fig.layout.yaxis4.domain[0] - fig.layout.yaxis.domain[1] == approx(0.08) +# TODO: Figure out how to deal with multiple occurrences of same column name @pytest.mark.parametrize("constructor", constructors) def test_facets_with_marginals(constructor): data = px.data.tips().to_dict(orient="list") diff --git a/packages/python/plotly/plotly/tests/test_optional/test_px/test_px_functions.py b/packages/python/plotly/plotly/tests/test_optional/test_px/test_px_functions.py index 5c69235340..d9343d1e72 100644 --- a/packages/python/plotly/plotly/tests/test_optional/test_px/test_px_functions.py +++ b/packages/python/plotly/plotly/tests/test_optional/test_px/test_px_functions.py @@ -169,6 +169,7 @@ def test_sunburst_treemap_with_path(constructor): native_namespace=native_namespace, ) ) + # TODO: Different exception is raise, but it is raised! msg = "Column `values` of `df` could not be converted to a numerical data type." with pytest.raises(ValueError, match=msg): fig = px.sunburst(df.to_native(), path=path, values="values") @@ -320,6 +321,7 @@ def test_sunburst_treemap_column_parent(constructor): px.sunburst(df, path=path, values="values") +# FIXME: They are not raising @pytest.mark.parametrize("constructor", constructors) def test_sunburst_treemap_with_path_non_rectangular(constructor): print(str(constructor)) diff --git a/packages/python/plotly/plotly/tests/test_optional/test_px/test_px_input.py b/packages/python/plotly/plotly/tests/test_optional/test_px/test_px_input.py index 946b5984da..c09b456281 100644 --- a/packages/python/plotly/plotly/tests/test_optional/test_px/test_px_input.py +++ b/packages/python/plotly/plotly/tests/test_optional/test_px/test_px_input.py @@ -87,30 +87,46 @@ def test_series(constructor): @pytest.mark.parametrize("constructor", constructors) def test_several_dataframes(constructor): - df = constructor(dict(x=[0, 1], y=[1, 10], z=[0.1, 0.8])) - df2 = constructor(dict(time=[23, 26], money=[100, 200])) - fig = px.scatter(df, x="z", y=df2["money"], size="x") + df = nw.from_native(constructor(dict(x=[0, 1], y=[1, 10], z=[0.1, 0.8]))) + df2 = nw.from_native(constructor(dict(time=[23, 26], money=[100, 200]))) + fig = px.scatter( + df.to_native(), x="z", y=df2.get_column("money").to_native(), size="x" + ) assert ( fig.data[0].hovertemplate == "z=%{x}
y=%{y}
x=%{marker.size}" ) - fig = px.scatter(df2, x=df["z"], y=df2["money"], size=df.z) + fig = px.scatter( + df2.to_native(), + x=df.get_column("z").to_native(), + y=df2.get_column("money").to_native(), + size=df.get_column("z").to_native(), + ) assert ( fig.data[0].hovertemplate == "x=%{x}
money=%{y}
size=%{marker.size}" ) # Name conflict with pytest.raises(NameError) as err_msg: - fig = px.scatter(df, x="z", y=df2["money"], size="y") + fig = px.scatter( + df.to_native(), x="z", y=df2.get_column("money").to_native(), size="y" + ) assert "A name conflict was encountered for argument 'y'" in str(err_msg.value) with pytest.raises(NameError) as err_msg: - fig = px.scatter(df, x="z", y=df2["money"], size=df.y) + fig = px.scatter( + df.to_native(), + x="z", + y=df2.get_column("money").to_native(), + size=df.get_column("y").to_native(), + ) assert "A name conflict was encountered for argument 'y'" in str(err_msg.value) # No conflict when the dataframe is not given, fields are used - df = constructor(dict(x=[0, 1], y=[3, 4])) - df2 = constructor(dict(x=[3, 5], y=[23, 24])) - fig = px.scatter(x=df["y"], y=df2["y"]) + df = nw.from_native(constructor(dict(x=[0, 1], y=[3, 4]))) + df2 = nw.from_native(constructor(dict(x=[3, 5], y=[23, 24]))) + fig = px.scatter( + x=df.get_column("y").to_native(), y=df2.get_column("y").to_native() + ) assert np.all(fig.data[0].x == np.array([3, 4])) assert np.all(fig.data[0].y == np.array([23, 24])) assert fig.data[0].hovertemplate == "x=%{x}
y=%{y}" @@ -140,8 +156,13 @@ def test_several_dataframes(constructor): @pytest.mark.parametrize("constructor", constructors) def test_name_heuristics(constructor): - df = constructor(dict(x=[0, 1], y=[3, 4], z=[0.1, 0.2])) - fig = px.scatter(df, x=df.y, y=df.x, size=df.y) + df = nw.from_native(constructor(dict(x=[0, 1], y=[3, 4], z=[0.1, 0.2]))) + fig = px.scatter( + df.to_native(), + x=df.get_column("y").to_native(), + y=df.get_column("x").to_native(), + size=df.get_column("y").to_native(), + ) assert np.all(fig.data[0].x == np.array([3, 4])) assert np.all(fig.data[0].y == np.array([0, 1])) assert fig.data[0].hovertemplate == "y=%{marker.size}
x=%{y}" @@ -241,12 +262,11 @@ def test_multiindex_raise_error(): assert "pandas MultiIndex is not supported by plotly express" in str(err_msg.value) -@pytest.mark.parametrize("constructor", constructors) def test_build_df_from_lists(constructor): # Just lists args = dict(x=[1, 2, 3], y=[2, 3, 4], color=[1, 3, 9]) output = {key: key for key in args} - df = constructor(args) + df = pd.DataFrame(args) args["data_frame"] = None out = build_dataframe(args, go.Scatter) df_out = out.pop("data_frame").to_native() @@ -398,9 +418,10 @@ def test_build_df_with_hover_data_from_vaex_and_polars(test_lib, hover_data): @pytest.mark.parametrize("constructor", constructors) def test_timezones(constructor): - df = constructor({"date": ["2015-04-04 19:31:30+1:00"], "value": [3]}) - df["date"] = pd.to_datetime(df["date"]) - args = dict(data_frame=df, x="date", y="value") + df = nw.from_native( + constructor({"date": ["2015-04-04 19:31:30+1:00"], "value": [3]}) + ).with_columns(nw.col("date").cast(nw.Datetime(time_zone="Europe/London"))) + args = dict(data_frame=df.to_native(), x="date", y="value") out = build_dataframe(args, go.Scatter) assert str(out["data_frame"].item(row=0, column="date")) == str( diff --git a/packages/python/plotly/plotly/tests/test_optional/test_px/test_px_wide.py b/packages/python/plotly/plotly/tests/test_optional/test_px/test_px_wide.py index 2686c00252..93c0ba99bb 100644 --- a/packages/python/plotly/plotly/tests/test_optional/test_px/test_px_wide.py +++ b/packages/python/plotly/plotly/tests/test_optional/test_px/test_px_wide.py @@ -71,7 +71,7 @@ def test_is_col_list_pandas(): ) @pytest.mark.parametrize("orientation", [None, "v", "h"]) @pytest.mark.parametrize("style", ["implicit", "explicit"]) -def test_wide_mode_external(constructor, px_fn, orientation, style): +def test_wide_mode_external(px_fn, orientation, style): # here we test this feature "black box" style by calling actual PX functions and # inspecting the figure... this is important but clunky, and is mostly a smoke test # allowing us to do more "white box" testing below diff --git a/packages/python/plotly/plotly/tests/test_optional/test_px/test_trendline.py b/packages/python/plotly/plotly/tests/test_optional/test_px/test_trendline.py index f7160727c0..4ed558a5b9 100644 --- a/packages/python/plotly/plotly/tests/test_optional/test_px/test_trendline.py +++ b/packages/python/plotly/plotly/tests/test_optional/test_px/test_trendline.py @@ -122,11 +122,7 @@ def test_trendline_nan_values(constructor, mode, options): data = px.data.gapminder().query("continent == 'Oceania'").to_dict(orient="list") df = nw.from_native(constructor(data)) start_date = 1970 - df = df.with_columns( - pop=nw.when(nw.col("year") >= start_date) - .then(nw.col("pop")) - .otherwise(nw.lit(None)) - ) + df = df.with_columns(pop=nw.when(nw.col("year") >= start_date).then(nw.col("pop"))) fig = px.scatter( df.to_native(), @@ -213,6 +209,7 @@ def test_trendline_on_timeseries(constructor, mode, options): err_msg.value ) + # TODO: This conversion requires to be fixed in narwhals df = df.with_columns(date=nw.col("date").cast(nw.Datetime(time_zone="CET"))) fig = px.scatter( df.to_native(), x="date", y="GOOG", trendline=mode, trendline_options=options From f45addfe2e40a28dc9bb2fcecad0b2e0bae0e15e Mon Sep 17 00:00:00 2001 From: FBruzzesi Date: Thu, 3 Oct 2024 13:50:54 +0200 Subject: [PATCH 024/106] to_py_scalar and more tests --- packages/python/plotly/plotly/express/_core.py | 18 ++++++++++++++++-- .../tests/test_optional/test_px/test_px.py | 4 +++- .../test_optional/test_px/test_px_wide.py | 6 ++++-- .../test_optional/test_px/test_trendline.py | 18 +++++++++++------- 4 files changed, 34 insertions(+), 12 deletions(-) diff --git a/packages/python/plotly/plotly/express/_core.py b/packages/python/plotly/plotly/express/_core.py index 65fcb1c151..b73e6a39e2 100644 --- a/packages/python/plotly/plotly/express/_core.py +++ b/packages/python/plotly/plotly/express/_core.py @@ -316,7 +316,7 @@ def make_trace_kwargs(args, trace_spec, trace_data, mapping_labels, sizeref): and ( trace_spec.constructor != go.Parcats or (attr_value is not None and name in attr_value) - or df.get_column(name).n_unique() + or to_py_scalar(df.get_column(name).n_unique()) <= args["dimensions_max_cardinality"] ) ] @@ -2033,7 +2033,9 @@ def infer_config(args, constructor, trace_patch, layout_patch): # Compute sizeref sizeref = 0 if "size" in args and args["size"]: - sizeref = df.get_column(args["size"]).max() / args["size_max"] ** 2 + sizeref = ( + to_py_scalar(df.get_column(args["size"]).max()) / args["size_max"] ** 2 + ) # Compute color attributes and grouping attributes if "color" in args: @@ -2270,6 +2272,7 @@ def get_groups_and_orders(args, grouper): # figure out orders and what the single group name would be if there were one single_group_name = [] unique_cache = dict() + for col in grouper: if col == one_group: single_group_name.append("") @@ -2289,6 +2292,7 @@ def get_groups_and_orders(args, grouper): groups = {tuple(single_group_name): df} else: required_grouper = [g for g in grouper if g != one_group] + # required_grouper = list(set(g for g in grouper if g != one_group)) grouped = dict(df.group_by(required_grouper).__iter__()) sorted_group_names = list(grouped.keys()) @@ -2741,3 +2745,13 @@ def is_into_series(df) -> bool: or nw.dependencies.is_pyarrow_chunked_array(df) or nw.dependencies.is_pandas_like_series(df) ) + + +def to_py_scalar(scalar_like): + """If scalar is not python native, tries to convert it to python native.""" + if hasattr(scalar_like, "as_py"): + return scalar_like.as_py() + elif hasattr(scalar_like, "item"): + return scalar_like.item() + else: + return scalar_like diff --git a/packages/python/plotly/plotly/tests/test_optional/test_px/test_px.py b/packages/python/plotly/plotly/tests/test_optional/test_px/test_px.py index 84d9ecb7c0..54d27bf972 100644 --- a/packages/python/plotly/plotly/tests/test_optional/test_px/test_px.py +++ b/packages/python/plotly/plotly/tests/test_optional/test_px/test_px.py @@ -166,7 +166,9 @@ def test_px_templates(constructor): assert fig.data[1].marker.color == "blue" # default colorway fallback - fig = px.scatter(tips, x="total_bill", y="tip", color="sex", template=dict()) + fig = px.scatter( + tips.to_native(), x="total_bill", y="tip", color="sex", template=dict() + ) assert fig.data[0].marker.color == px.colors.qualitative.D3[0] assert fig.data[1].marker.color == px.colors.qualitative.D3[1] diff --git a/packages/python/plotly/plotly/tests/test_optional/test_px/test_px_wide.py b/packages/python/plotly/plotly/tests/test_optional/test_px/test_px_wide.py index 93c0ba99bb..15134f1a02 100644 --- a/packages/python/plotly/plotly/tests/test_optional/test_px/test_px_wide.py +++ b/packages/python/plotly/plotly/tests/test_optional/test_px/test_px_wide.py @@ -15,10 +15,12 @@ @pytest.mark.parametrize("constructor", constructors) def test_is_col_list(constructor): - df_input = constructor(dict(a=[1, 2], b=[1, 2])) - is_pd_like = nw.dependencies.is_pandas_like_dataframe(df_input) + df_input = nw.from_native(constructor(dict(a=[1, 2], b=[1, 2]))) native_namespace = df_input.__native_namespace__() + df_input = df_input.to_native() + is_pd_like = nw.dependencies.is_pandas_like_dataframe(df_input) + assert _is_col_list(df_input, ["a"], is_pd_like, native_namespace) assert _is_col_list(df_input, ["a", "b"], is_pd_like, native_namespace) assert _is_col_list(df_input, [[3, 4]], is_pd_like, native_namespace) diff --git a/packages/python/plotly/plotly/tests/test_optional/test_px/test_trendline.py b/packages/python/plotly/plotly/tests/test_optional/test_px/test_trendline.py index 4ed558a5b9..874b8e103f 100644 --- a/packages/python/plotly/plotly/tests/test_optional/test_px/test_trendline.py +++ b/packages/python/plotly/plotly/tests/test_optional/test_px/test_trendline.py @@ -181,7 +181,10 @@ def test_ols_trendline_slopes(): assert "y = 0 * x + 1.5
" in fig.data[1].hovertemplate -@pytest.mark.parametrize("constructor", constructors) +@pytest.mark.parametrize( + ("constructor", "from_pandas_fn"), + zip(constructors, (lambda x: x, pl.from_pandas, pa.Table.from_pandas)), +) @pytest.mark.parametrize( "mode,options", [ @@ -194,7 +197,7 @@ def test_ols_trendline_slopes(): ("ewm", dict(alpha=0.5)), ], ) -def test_trendline_on_timeseries(constructor, mode, options): +def test_trendline_on_timeseries(constructor, from_pandas_fn, mode, options): df = nw.from_native(constructor(px.data.stocks().to_dict(orient="list"))) with pytest.raises(ValueError) as err_msg: @@ -209,11 +212,12 @@ def test_trendline_on_timeseries(constructor, mode, options): err_msg.value ) - # TODO: This conversion requires to be fixed in narwhals - df = df.with_columns(date=nw.col("date").cast(nw.Datetime(time_zone="CET"))) - fig = px.scatter( - df.to_native(), x="date", y="GOOG", trendline=mode, trendline_options=options - ) + # TODO: This conversion requires new functionalities in narwhals + df = df.to_pandas() + df["date"] = pd.to_datetime(df["date"]) + df["date"] = df["date"].dt.tz_localize("CET") # force a timezone + df = from_pandas_fn(df) + fig = px.scatter(df, x="date", y="GOOG", trendline=mode, trendline_options=options) assert len(fig.data) == 2 assert len(fig.data[0].x) == len(fig.data[1].x) assert isinstance(fig.data[0].x[0], datetime) From 5341759d4682e11a2be2a674db6d7313267602af Mon Sep 17 00:00:00 2001 From: FBruzzesi Date: Thu, 3 Oct 2024 17:29:54 +0200 Subject: [PATCH 025/106] dealing with exceptions and tests --- .../python/plotly/plotly/express/_core.py | 71 +++++++++---------- .../python/plotly/plotly/express/_imshow.py | 7 +- .../express/trendline_functions/__init__.py | 2 +- .../test_px/test_px_functions.py | 31 +++++--- .../test_optional/test_px/test_px_input.py | 4 +- .../test_optional/test_px/test_trendline.py | 17 +++-- 6 files changed, 70 insertions(+), 62 deletions(-) diff --git a/packages/python/plotly/plotly/express/_core.py b/packages/python/plotly/plotly/express/_core.py index b73e6a39e2..9fc34469a0 100644 --- a/packages/python/plotly/plotly/express/_core.py +++ b/packages/python/plotly/plotly/express/_core.py @@ -11,17 +11,6 @@ import math import narwhals.stable.v1 as nw -from narwhals.dtypes import Datetime -from narwhals.dtypes import Float32 -from narwhals.dtypes import Float64 -from narwhals.dtypes import Int8 -from narwhals.dtypes import Int16 -from narwhals.dtypes import Int32 -from narwhals.dtypes import Int64 -from narwhals.dtypes import UInt8 -from narwhals.dtypes import UInt16 -from narwhals.dtypes import UInt32 -from narwhals.dtypes import UInt64 from plotly._subplots import ( make_subplots, @@ -31,16 +20,16 @@ NO_COLOR = "px_no_color_constant" NW_NUMERIC_DTYPES = { - Float32, - Float64, - Int8, - Int16, - Int32, - Int64, - UInt8, - UInt16, - UInt32, - UInt64, + nw.Float32, + nw.Float64, + nw.Int8, + nw.Int16, + nw.Int32, + nw.Int64, + nw.UInt8, + nw.UInt16, + nw.UInt32, + nw.UInt64, } trendline_functions = dict( @@ -358,15 +347,16 @@ def make_trace_kwargs(args, trace_spec, trace_data, mapping_labels, sizeref): y = sorted_trace_data.get_column(args["y"]) x = sorted_trace_data.get_column(args["x"]) - if isinstance(x.dtype, Datetime): + if x.dtype == nw.Datetime: # convert to unix epoch seconds x = ( x.to_frame() .select( **{ args["x"]: nw.when(~x.is_null()) - .then(x.cast(Int64())) - .otherwise(nw.lit(None, Int64())) + .then(x.cast(nw.Int64())) + .otherwise(nw.lit(None, nw.Int64())) + # Pyarrow breaks due to https://github.com/narwhals-dev/narwhals/issues/1117 } ) .get_column(args["x"]) @@ -374,7 +364,7 @@ def make_trace_kwargs(args, trace_spec, trace_data, mapping_labels, sizeref): ) elif x.dtype not in NW_NUMERIC_DTYPES: try: - x = x.cast(Float64()) + x = x.cast(nw.Float64()) except ValueError: raise ValueError( "Could not convert value of 'x' ('%s') into a numeric type. " @@ -384,7 +374,7 @@ def make_trace_kwargs(args, trace_spec, trace_data, mapping_labels, sizeref): if y.dtype not in NW_NUMERIC_DTYPES: try: - y = y.cast(Float64()) + y = y.cast(nw.Float64()) except ValueError: raise ValueError( "Could not convert value of 'y' into a numeric type." @@ -1732,13 +1722,18 @@ def build_dataframe(args, constructor): def _check_dataframe_all_leaves(df: nw.DataFrame) -> None: cols = df.columns - df_sorted = df.sort(by=cols) + # FIXME: Requires (https://github.com/narwhals-dev/narwhals/pull/1124) to be merged and released + # df_sorted = df.sort(by=cols, descending=False, nulls_last=True) + df_sorted = df.sort(by=cols, descending=False) null_mask = df_sorted.select(*[nw.col(c).is_null() for c in cols]) df_sorted = df_sorted.with_columns(nw.col(*cols).cast(nw.String())) - null_indices = null_mask.select( + null_indices_mask = null_mask.select( null_mask=nw.any_horizontal(nw.col(cols)) ).get_column("null_mask") - for row_idx, row in zip(null_indices, null_mask.filter(null_indices).iter_rows()): + null_indices = null_indices_mask.arg_true() + for row_idx, row in zip( + null_indices_mask, null_mask.filter(null_indices_mask).iter_rows() + ): i = row.index(True) @@ -1768,7 +1763,7 @@ def _check_dataframe_all_leaves(df: nw.DataFrame) -> None: for i, (current_row, next_row) in enumerate( zip(row_strings[:-1], row_strings[1:]), start=1 ): - if next_row in current_row and (i) in null_indices: + if (i in null_indices) and (next_row in current_row): raise ValueError( "Non-leaves rows are not permitted in the dataframe \n", df_sorted.row(i), @@ -1801,14 +1796,14 @@ def process_dataframe_hierarchy(args): args["values"], str ): df = df.with_columns( - **{c: nw.col(c).cast(Float64()) for c in args["values"]} + **{c: nw.col(c).cast(nw.Float64()) for c in args["values"]} ) else: df = df.with_columns( - **{args["values"]: nw.col(args["values"]).cast(Float64())} + **{args["values"]: nw.col(args["values"]).cast(nw.Float64())} ) - except ValueError: + except Exception: # pandas, Polars and pyarrow exception types are different raise ValueError( "Column `%s` of `df` could not be converted to a numerical data type." % args["values"] @@ -1943,7 +1938,9 @@ def post_agg( ) # strip "/" if at the end of the string, equivalent to `.str.rstrip` - df_tree = df_tree.with_columns(parent=nw.col("parent").str.replace("/?$", "")) + df_tree = df_tree.with_columns( + parent=nw.col("parent").str.replace("/?$", "").str.replace("^/?", "") + ) all_trees.append(df_tree.select(*["labels", "parent", "id", *cols])) @@ -1987,9 +1984,9 @@ def process_dataframe_timeline(args): try: df: nw.DataFrame = args["data_frame"] - x_start = df.get_column(args["x_start"]).cast(Datetime) - x_end = df.get_column(args["x_end"]).cast(Datetime) - except (ValueError, TypeError): + x_start = df.get_column(args["x_start"]).cast(nw.Datetime()) + x_end = df.get_column(args["x_end"]).cast(nw.Datetime()) + except Exception: raise TypeError( "Both x_start and x_end must refer to data convertible to datetimes." ) diff --git a/packages/python/plotly/plotly/express/_imshow.py b/packages/python/plotly/plotly/express/_imshow.py index 72750d7540..d60a47b5f4 100644 --- a/packages/python/plotly/plotly/express/_imshow.py +++ b/packages/python/plotly/plotly/express/_imshow.py @@ -321,13 +321,12 @@ def imshow( aspect = "equal" # --- Set the value of binary_string (forbidden for pandas) - # TODO: Should this be generic for all dataframes? - if (pd := nw.dependencies.get_pandas()) is not None and isinstance( - img, pd.DataFrame - ): + img = nw.from_native(img, strict=False) + if isinstance(img, nw.DataFrame): if binary_string: raise ValueError("Binary strings cannot be used with pandas arrays") is_dataframe = True + img = img.to_numpy() else: is_dataframe = False diff --git a/packages/python/plotly/plotly/express/trendline_functions/__init__.py b/packages/python/plotly/plotly/express/trendline_functions/__init__.py index c7dd2ca8fc..367c36d2b5 100644 --- a/packages/python/plotly/plotly/express/trendline_functions/__init__.py +++ b/packages/python/plotly/plotly/express/trendline_functions/__init__.py @@ -123,7 +123,7 @@ def _pandas(mode, trendline_options, x_raw, y, non_missing): series = pd.Series(y, index=x_raw.to_pandas()) - # TODO: If narwhals were to sopport rolling, ewm and expanding then we could go around these + # TODO: If narwhals were to support rolling, ewm and expanding then we could go around these agg = getattr(series, mode) # e.g. series.rolling agg_obj = agg(**trendline_options) # e.g. series.rolling(**opts) function = getattr(agg_obj, function_name) # e.g. series.rolling(**opts).mean diff --git a/packages/python/plotly/plotly/tests/test_optional/test_px/test_px_functions.py b/packages/python/plotly/plotly/tests/test_optional/test_px/test_px_functions.py index d9343d1e72..98e8fae5a4 100644 --- a/packages/python/plotly/plotly/tests/test_optional/test_px/test_px_functions.py +++ b/packages/python/plotly/plotly/tests/test_optional/test_px/test_px_functions.py @@ -169,7 +169,6 @@ def test_sunburst_treemap_with_path(constructor): native_namespace=native_namespace, ) ) - # TODO: Different exception is raise, but it is raised! msg = "Column `values` of `df` could not be converted to a numerical data type." with pytest.raises(ValueError, match=msg): fig = px.sunburst(df.to_native(), path=path, values="values") @@ -250,37 +249,48 @@ def test_sunburst_treemap_with_path_color(constructor): total=total, calls=calls, ) - df = constructor(data) + df = nw.from_native(constructor(data)) path = ["total", "regions", "sectors", "vendors"] - fig = px.sunburst(df, path=path, values="values", color="calls") + fig = px.sunburst(df.to_native(), path=path, values="values", color="calls") colors = fig.data[0].marker.colors - assert np.all(np.array(colors[:8]) == np.array(calls)) - fig = px.sunburst(df, path=path, color="calls") + assert np.all( + np.array(colors[:8]) == np.array(calls) + ) # TODO: Fails because polars has `maintain_order=False` in group by + fig = px.sunburst(df.to_native(), path=path, color="calls") colors = fig.data[0].marker.colors assert np.all(np.array(colors[:8]) == np.array(calls)) # Hover info - df = nw.from_native(df).with_columns(hover=hover).to_native() - fig = px.sunburst(df, path=path, color="calls", hover_data=["hover"]) + df = df.with_columns( + hover=nw.new_series( + name="hover", + values=hover, + dtype=nw.String(), + native_namespace=df.__native_namespace__(), + ) + ) + fig = px.sunburst(df.to_native(), path=path, color="calls", hover_data=["hover"]) custom = fig.data[0].customdata assert np.all(custom[:8, 0] == hover) assert np.all(custom[8:, 0] == "(?)") assert np.all(custom[:8, 1] == calls) # Discrete color - fig = px.sunburst(df, path=path, color="vendors") + fig = px.sunburst(df.to_native(), path=path, color="vendors") assert len(np.unique(fig.data[0].marker.colors)) == 9 # Discrete color and color_discrete_map cmap = {"Tech": "yellow", "Finance": "magenta", "(?)": "black"} - fig = px.sunburst(df, path=path, color="sectors", color_discrete_map=cmap) + fig = px.sunburst( + df.to_native(), path=path, color="sectors", color_discrete_map=cmap + ) assert np.all(np.in1d(fig.data[0].marker.colors, list(cmap.values()))) # Numerical column in path df = ( nw.from_native(df) .with_columns( - regions=nw.when(nw.col("region") == "North") + regions=nw.when(nw.col("regions") == "North") .then(1) .otherwise(2) .cast(nw.Int64()) @@ -321,7 +331,6 @@ def test_sunburst_treemap_column_parent(constructor): px.sunburst(df, path=path, values="values") -# FIXME: They are not raising @pytest.mark.parametrize("constructor", constructors) def test_sunburst_treemap_with_path_non_rectangular(constructor): print(str(constructor)) diff --git a/packages/python/plotly/plotly/tests/test_optional/test_px/test_px_input.py b/packages/python/plotly/plotly/tests/test_optional/test_px/test_px_input.py index c09b456281..f7bcb7c1bf 100644 --- a/packages/python/plotly/plotly/tests/test_optional/test_px/test_px_input.py +++ b/packages/python/plotly/plotly/tests/test_optional/test_px/test_px_input.py @@ -262,7 +262,7 @@ def test_multiindex_raise_error(): assert "pandas MultiIndex is not supported by plotly express" in str(err_msg.value) -def test_build_df_from_lists(constructor): +def test_build_df_from_lists(): # Just lists args = dict(x=[1, 2, 3], y=[2, 3, 4], color=[1, 3, 9]) output = {key: key for key in args} @@ -277,7 +277,7 @@ def test_build_df_from_lists(constructor): # Arrays args = dict(x=np.array([1, 2, 3]), y=np.array([2, 3, 4]), color=[1, 3, 9]) output = {key: key for key in args} - df = constructor(args) + df = pd.DataFrame(args) args["data_frame"] = None out = build_dataframe(args, go.Scatter) df_out = out.pop("data_frame").to_native() diff --git a/packages/python/plotly/plotly/tests/test_optional/test_px/test_trendline.py b/packages/python/plotly/plotly/tests/test_optional/test_px/test_trendline.py index 874b8e103f..e3832c1adb 100644 --- a/packages/python/plotly/plotly/tests/test_optional/test_px/test_trendline.py +++ b/packages/python/plotly/plotly/tests/test_optional/test_px/test_trendline.py @@ -42,7 +42,7 @@ def test_trendline_results_passthrough(constructor, mode, options): if mode == "ols": assert "R2" in trendline.hovertemplate results = px.get_trendline_results(fig) - if mode == "ols": + if mode == "ols": # Somehow this is flaky for polars? assert len(results) == 2 assert results["country"].values[0] == "Australia" au_result = results["px_fit_results"].values[0] @@ -200,7 +200,10 @@ def test_ols_trendline_slopes(): def test_trendline_on_timeseries(constructor, from_pandas_fn, mode, options): df = nw.from_native(constructor(px.data.stocks().to_dict(orient="list"))) - with pytest.raises(ValueError) as err_msg: + pd_err_msg = "Could not convert value of 'x' \('date'\) into a numeric type." + pl_err_msg = "conversion from `str` to `f64` failed in column 'date'" + + with pytest.raises(Exception, match=rf"({pd_err_msg}|{pl_err_msg})"): px.scatter( df.to_native(), x="date", @@ -208,20 +211,20 @@ def test_trendline_on_timeseries(constructor, from_pandas_fn, mode, options): trendline=mode, trendline_options=options, ) - assert "Could not convert value of 'x' ('date') into a numeric type." in str( - err_msg.value - ) # TODO: This conversion requires new functionalities in narwhals + # for now here is a workaround df = df.to_pandas() df["date"] = pd.to_datetime(df["date"]) df["date"] = df["date"].dt.tz_localize("CET") # force a timezone df = from_pandas_fn(df) + fig = px.scatter(df, x="date", y="GOOG", trendline=mode, trendline_options=options) + assert len(fig.data) == 2 assert len(fig.data[0].x) == len(fig.data[1].x) - assert isinstance(fig.data[0].x[0], datetime) - assert isinstance(fig.data[1].x[0], datetime) + assert isinstance(fig.data[0].x[0], (datetime, np.datetime64)) + assert isinstance(fig.data[1].x[0], (datetime, np.datetime64)) assert np.all(fig.data[0].x == fig.data[1].x) assert str(fig.data[0].x[0]) == str(fig.data[1].x[0]) From dfc957c6a4ea2fa320247b91e9d7b44c14cf8278 Mon Sep 17 00:00:00 2001 From: FBruzzesi Date: Fri, 4 Oct 2024 23:20:08 +0200 Subject: [PATCH 026/106] bump version, sort(...,nulls_last=True) --- packages/python/plotly/optional-requirements.txt | 2 +- packages/python/plotly/plotly/express/_core.py | 12 +++++------- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/packages/python/plotly/optional-requirements.txt b/packages/python/plotly/optional-requirements.txt index dc29447175..a74c9124da 100644 --- a/packages/python/plotly/optional-requirements.txt +++ b/packages/python/plotly/optional-requirements.txt @@ -39,7 +39,7 @@ ipython ## pandas deps for some matplotlib functionality ## pandas -narwhals>=1.9.0 +narwhals>=1.9.1 ## scipy deps for some FigureFactory functions ## scipy diff --git a/packages/python/plotly/plotly/express/_core.py b/packages/python/plotly/plotly/express/_core.py index 9fc34469a0..ce7bfda508 100644 --- a/packages/python/plotly/plotly/express/_core.py +++ b/packages/python/plotly/plotly/express/_core.py @@ -343,7 +343,7 @@ def make_trace_kwargs(args, trace_spec, trace_data, mapping_labels, sizeref): > 1 ): # sorting is bad but trace_specs with "trendline" have no other attrs - sorted_trace_data = trace_data.sort(by=args["x"]) + sorted_trace_data = trace_data.sort(by=args["x"], nulls_last=True) y = sorted_trace_data.get_column(args["y"]) x = sorted_trace_data.get_column(args["x"]) @@ -1722,9 +1722,7 @@ def build_dataframe(args, constructor): def _check_dataframe_all_leaves(df: nw.DataFrame) -> None: cols = df.columns - # FIXME: Requires (https://github.com/narwhals-dev/narwhals/pull/1124) to be merged and released - # df_sorted = df.sort(by=cols, descending=False, nulls_last=True) - df_sorted = df.sort(by=cols, descending=False) + df_sorted = df.sort(by=cols, descending=False, nulls_last=True) null_mask = df_sorted.select(*[nw.col(c).is_null() for c in cols]) df_sorted = df_sorted.with_columns(nw.col(*cols).cast(nw.String())) null_indices_mask = null_mask.select( @@ -1953,7 +1951,7 @@ def post_agg( sort_col_name += "0" df_all_trees = df_all_trees.with_columns( **{sort_col_name: df_all_trees.get_column(args["color"]).cast(nw.String())} - ).sort(by=sort_col_name) + ).sort(by=sort_col_name, nulls_last=True) # Now modify arguments args["data_frame"] = df_all_trees @@ -2484,13 +2482,13 @@ def make_figure(args, constructor, trace_patch=None, layout_patch=None): base = args["x"] if args["orientation"] == "v" else args["y"] var = args["x"] if args["orientation"] == "h" else args["y"] ascending = args.get("ecdfmode", "standard") != "reversed" - group = group.sort(by=base, descending=not ascending) + group = group.sort(by=base, descending=not ascending, nulls_last=True) group_sum = group.get_column( var ).sum() # compute here before next line mutates group = group.with_columns(**{var: nw.col(var).cum_sum()}) if not ascending: - group = group.sort(by=base, descending=False) + group = group.sort(by=base, descending=False, nulls_last=True) if args.get("ecdfmode", "standard") == "complementary": group = group.with_columns( From 90f26670e23d651aca363d756eddedec8ab9be70 Mon Sep 17 00:00:00 2001 From: FBruzzesi Date: Sat, 5 Oct 2024 00:47:50 +0200 Subject: [PATCH 027/106] We did it: no more dups in group by :D --- .../python/plotly/plotly/express/_core.py | 23 +++++++++++-------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/packages/python/plotly/plotly/express/_core.py b/packages/python/plotly/plotly/express/_core.py index ce7bfda508..2f4180b790 100644 --- a/packages/python/plotly/plotly/express/_core.py +++ b/packages/python/plotly/plotly/express/_core.py @@ -2267,8 +2267,9 @@ def get_groups_and_orders(args, grouper): # figure out orders and what the single group name would be if there were one single_group_name = [] unique_cache = dict() + grp_to_idx = dict() - for col in grouper: + for i, col in enumerate(grouper): if col == one_group: single_group_name.append("") else: @@ -2282,12 +2283,13 @@ def get_groups_and_orders(args, grouper): else: orders[col] = list(OrderedDict.fromkeys(list(orders[col]) + uniques)) + grp_to_idx = {k: i for i, k in enumerate(orders)} + if len(single_group_name) == len(grouper): # we have a single group, so we can skip all group-by operations! groups = {tuple(single_group_name): df} else: - required_grouper = [g for g in grouper if g != one_group] - # required_grouper = list(set(g for g in grouper if g != one_group)) + required_grouper = list(orders.keys()) grouped = dict(df.group_by(required_grouper).__iter__()) sorted_group_names = list(grouped.keys()) @@ -2298,12 +2300,15 @@ def get_groups_and_orders(args, grouper): ) # calculate the full group_names by inserting "" in the tuple index for one_group groups - full_sorted_group_names = [list(t) for t in sorted_group_names] - for i, col in enumerate(grouper): - if col == one_group: - for g in full_sorted_group_names: - g.insert(i, "") - full_sorted_group_names = [tuple(g) for g in full_sorted_group_names] + full_sorted_group_names = [ + tuple( + [ + "" if col == one_group else sub_group_names[grp_to_idx[col]] + for col in grouper + ] + ) + for sub_group_names in sorted_group_names + ] groups = { sf: grouped[s] for sf, s in zip(full_sorted_group_names, sorted_group_names) From fb58d1bd8f3cf5bcd948dc5cd8b615c243f36fd7 Mon Sep 17 00:00:00 2001 From: FBruzzesi Date: Sat, 5 Oct 2024 12:00:18 +0200 Subject: [PATCH 028/106] concat_str --- .../python/plotly/plotly/express/_core.py | 65 ++++++++++--------- .../test_optional/test_px/test_px_wide.py | 48 +++++++------- 2 files changed, 60 insertions(+), 53 deletions(-) diff --git a/packages/python/plotly/plotly/express/_core.py b/packages/python/plotly/plotly/express/_core.py index 2f4180b790..f08b48e0e2 100644 --- a/packages/python/plotly/plotly/express/_core.py +++ b/packages/python/plotly/plotly/express/_core.py @@ -11,6 +11,7 @@ import math import narwhals.stable.v1 as nw +from narwhals.utils import generate_unique_token from plotly._subplots import ( make_subplots, @@ -353,10 +354,10 @@ def make_trace_kwargs(args, trace_spec, trace_data, mapping_labels, sizeref): x.to_frame() .select( **{ - args["x"]: nw.when(~x.is_null()) - .then(x.cast(nw.Int64())) - .otherwise(nw.lit(None, nw.Int64())) - # Pyarrow breaks due to https://github.com/narwhals-dev/narwhals/issues/1117 + args["x"]: nw.when(~x.is_null()).then( + x.cast(nw.Int64()) + ) + # .otherwise(nw.lit(None, nw.Int64())) } ) .get_column(args["x"]) @@ -1753,11 +1754,10 @@ def _check_dataframe_all_leaves(df: nw.DataFrame) -> None: for c in cols } ) - row_strings = reduce( - lambda x, y: x + y, - [df_sorted.get_column(c).cast(nw.String()) for c in cols], - "", - ) + row_strings = df_sorted.select( + row_strings=nw.concat_str(cols, separator="", ignore_nulls=False) + ).get_column("row_strings") + for i, (current_row, next_row) in enumerate( zip(row_strings[:-1], row_strings[1:]), start=1 ): @@ -1893,7 +1893,7 @@ def post_agg( return dframe.with_columns( **{c: nw.col(c) / nw.col(count_colname) for c in continuous_aggs}, **{ - c: nw.when(nw.col(f"{c}__n_unique__") == nw.lit(1)) + c: nw.when(nw.col(f"{c}__n_unique__") == 1) .then(nw.col(c)) .otherwise(nw.lit("(?)")) for c in discrete_aggs @@ -1909,30 +1909,37 @@ def post_agg( ) # Path label massaging - df_tree = dfg.clone().with_columns( + df_tree = dfg.with_columns( *cols, labels=nw.col(level).cast(nw.String()), parent=nw.lit(""), id=nw.col(level).cast(nw.String()), ) if i < len(path) - 1: - j = i + 1 - - path_j_col = reduce( - lambda c1, c2: c1 + "/" + c2, - ( - dfg.get_column(path[j]).cast(nw.String()) - for j in range(len(path) - 1, j, -1) - ), - "", - ) - parent_col = df_tree.get_column("parent").cast(nw.String()) - id_col = df_tree.get_column("id").cast(nw.String()) - df_tree = df_tree.with_columns( - **{ - "parent": path_j_col + "/" + parent_col, - "id": path_j_col + "/" + id_col, - } + token = generate_unique_token(n_bytes=8, columns=df_tree.columns) + df_tree = ( + df_tree.with_columns( + **{ + token: nw.concat_str( + [ + nw.col(path[j]).cast(nw.String()) + for j in range(len(path) - 1, i, -1) + ], + separator="/", + ) + } + ) + .with_columns( + **{ + "parent": nw.concat_str( + [nw.col(token), nw.col("parent")], separator="/" + ), + "id": nw.concat_str( + [nw.col(token), nw.col("id")], separator="/" + ), + } + ) + .drop(token) ) # strip "/" if at the end of the string, equivalent to `.str.rstrip` @@ -1950,7 +1957,7 @@ def post_agg( while sort_col_name in df_all_trees.columns: sort_col_name += "0" df_all_trees = df_all_trees.with_columns( - **{sort_col_name: df_all_trees.get_column(args["color"]).cast(nw.String())} + **{sort_col_name: nw.col(args["color"]).cast(nw.String())} ).sort(by=sort_col_name, nulls_last=True) # Now modify arguments diff --git a/packages/python/plotly/plotly/tests/test_optional/test_px/test_px_wide.py b/packages/python/plotly/plotly/tests/test_optional/test_px/test_px_wide.py index 15134f1a02..2a33342bfa 100644 --- a/packages/python/plotly/plotly/tests/test_optional/test_px/test_px_wide.py +++ b/packages/python/plotly/plotly/tests/test_optional/test_px/test_px_wide.py @@ -17,38 +17,38 @@ def test_is_col_list(constructor): df_input = nw.from_native(constructor(dict(a=[1, 2], b=[1, 2]))) native_namespace = df_input.__native_namespace__() - + columns = df_input.columns df_input = df_input.to_native() is_pd_like = nw.dependencies.is_pandas_like_dataframe(df_input) - - assert _is_col_list(df_input, ["a"], is_pd_like, native_namespace) - assert _is_col_list(df_input, ["a", "b"], is_pd_like, native_namespace) - assert _is_col_list(df_input, [[3, 4]], is_pd_like, native_namespace) - assert _is_col_list(df_input, [[3, 4], [3, 4]], is_pd_like, native_namespace) - assert not _is_col_list(df_input, pytest, is_pd_like, native_namespace) - assert not _is_col_list(df_input, False, is_pd_like, native_namespace) - assert not _is_col_list(df_input, ["a", 1], is_pd_like, native_namespace) - assert not _is_col_list(df_input, "a", is_pd_like, native_namespace) - assert not _is_col_list(df_input, 1, is_pd_like, native_namespace) - assert not _is_col_list(df_input, ["a", "b", "c"], is_pd_like, native_namespace) - assert not _is_col_list(df_input, [1, 2], is_pd_like, native_namespace) + assert _is_col_list(columns, ["a"], is_pd_like, native_namespace) + assert _is_col_list(columns, ["a", "b"], is_pd_like, native_namespace) + assert _is_col_list(columns, [[3, 4]], is_pd_like, native_namespace) + assert _is_col_list(columns, [[3, 4], [3, 4]], is_pd_like, native_namespace) + assert not _is_col_list(columns, pytest, is_pd_like, native_namespace) + assert not _is_col_list(columns, False, is_pd_like, native_namespace) + assert not _is_col_list(columns, ["a", 1], is_pd_like, native_namespace) + assert not _is_col_list(columns, "a", is_pd_like, native_namespace) + assert not _is_col_list(columns, 1, is_pd_like, native_namespace) + assert not _is_col_list(columns, ["a", "b", "c"], is_pd_like, native_namespace) + assert not _is_col_list(columns, [1, 2], is_pd_like, native_namespace) def test_is_col_list_pandas(): df_input = pd.DataFrame([[1, 2], [1, 2]]) is_pd_like = True native_namespace = pd - assert _is_col_list(df_input, [0], is_pd_like, native_namespace) - assert _is_col_list(df_input, [0, 1], is_pd_like, native_namespace) - assert _is_col_list(df_input, [[3, 4]], is_pd_like, native_namespace) - assert _is_col_list(df_input, [[3, 4], [3, 4]], is_pd_like, native_namespace) - assert not _is_col_list(df_input, pytest, is_pd_like, native_namespace) - assert not _is_col_list(df_input, False, is_pd_like, native_namespace) - assert not _is_col_list(df_input, ["a", 1], is_pd_like, native_namespace) - assert not _is_col_list(df_input, "a", is_pd_like, native_namespace) - assert not _is_col_list(df_input, 1, is_pd_like, native_namespace) - assert not _is_col_list(df_input, [0, 1, 2], is_pd_like, native_namespace) - assert not _is_col_list(df_input, ["a", "b"], is_pd_like, native_namespace) + columns = list(df_input.columns) + assert _is_col_list(columns, [0], is_pd_like, native_namespace) + assert _is_col_list(columns, [0, 1], is_pd_like, native_namespace) + assert _is_col_list(columns, [[3, 4]], is_pd_like, native_namespace) + assert _is_col_list(columns, [[3, 4], [3, 4]], is_pd_like, native_namespace) + assert not _is_col_list(columns, pytest, is_pd_like, native_namespace) + assert not _is_col_list(columns, False, is_pd_like, native_namespace) + assert not _is_col_list(columns, ["a", 1], is_pd_like, native_namespace) + assert not _is_col_list(columns, "a", is_pd_like, native_namespace) + assert not _is_col_list(columns, 1, is_pd_like, native_namespace) + assert not _is_col_list(columns, [0, 1, 2], is_pd_like, native_namespace) + assert not _is_col_list(columns, ["a", "b"], is_pd_like, native_namespace) df_input = None is_pd_like = False From ddb3b3583d70578630ee026378bb7944fb4e47bb Mon Sep 17 00:00:00 2001 From: FBruzzesi Date: Sat, 5 Oct 2024 13:13:53 +0200 Subject: [PATCH 029/106] fix test_several_dataframes --- .../test_optional/test_px/test_px_input.py | 24 ++++++++++++------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/packages/python/plotly/plotly/tests/test_optional/test_px/test_px_input.py b/packages/python/plotly/plotly/tests/test_optional/test_px/test_px_input.py index f7bcb7c1bf..8a6a1a23a0 100644 --- a/packages/python/plotly/plotly/tests/test_optional/test_px/test_px_input.py +++ b/packages/python/plotly/plotly/tests/test_optional/test_px/test_px_input.py @@ -131,10 +131,14 @@ def test_several_dataframes(constructor): assert np.all(fig.data[0].y == np.array([23, 24])) assert fig.data[0].hovertemplate == "x=%{x}
y=%{y}" - df = constructor(dict(x=[0, 1], y=[3, 4])) - df2 = constructor(dict(x=[3, 5], y=[23, 24])) - df3 = constructor(dict(y=[0.1, 0.2])) - fig = px.scatter(x=df.y, y=df2.y, size=df3.y) + df = nw.from_native(constructor(dict(x=[0, 1], y=[3, 4]))) + df2 = nw.from_native(constructor(dict(x=[3, 5], y=[23, 24]))) + df3 = nw.from_native(constructor(dict(y=[0.1, 0.2]))) + fig = px.scatter( + x=df.get_column("y").to_native(), + y=df2.get_column("y").to_native(), + size=df3.get_column("y").to_native(), + ) assert np.all(fig.data[0].x == np.array([3, 4])) assert np.all(fig.data[0].y == np.array([23, 24])) assert ( @@ -142,10 +146,14 @@ def test_several_dataframes(constructor): == "x=%{x}
y=%{y}
size=%{marker.size}" ) - df = constructor(dict(x=[0, 1], y=[3, 4])) - df2 = constructor(dict(x=[3, 5], y=[23, 24])) - df3 = constructor(dict(y=[0.1, 0.2])) - fig = px.scatter(x=df.y, y=df2.y, hover_data=[df3.y]) + df = nw.from_native(constructor(dict(x=[0, 1], y=[3, 4]))) + df2 = nw.from_native(constructor(dict(x=[3, 5], y=[23, 24]))) + df3 = nw.from_native(constructor(dict(y=[0.1, 0.2]))) + fig = px.scatter( + x=df.get_column("y").to_native(), + y=df2.get_column("y").to_native(), + hover_data=[df3.get_column("y").to_native()], + ) assert np.all(fig.data[0].x == np.array([3, 4])) assert np.all(fig.data[0].y == np.array([23, 24])) assert ( From 37ce3028f1dcb572cb870cf088b15694a4bc1f28 Mon Sep 17 00:00:00 2001 From: FBruzzesi Date: Sat, 5 Oct 2024 14:40:03 +0200 Subject: [PATCH 030/106] dedups customdata --- .../python/plotly/plotly/express/_core.py | 32 ++++++++++++++----- .../test_optional/test_px/test_facets.py | 6 +++- .../test_optional/test_px/test_marginals.py | 6 +++- .../tests/test_optional/test_px/test_px.py | 6 +++- .../test_px/test_px_functions.py | 6 +++- .../test_optional/test_px/test_px_hover.py | 10 ++++-- .../test_optional/test_px/test_px_input.py | 6 +++- .../test_optional/test_px/test_px_wide.py | 6 +++- .../test_optional/test_px/test_trendline.py | 6 +++- 9 files changed, 66 insertions(+), 18 deletions(-) diff --git a/packages/python/plotly/plotly/express/_core.py b/packages/python/plotly/plotly/express/_core.py index f08b48e0e2..763cca3a06 100644 --- a/packages/python/plotly/plotly/express/_core.py +++ b/packages/python/plotly/plotly/express/_core.py @@ -461,7 +461,12 @@ def make_trace_kwargs(args, trace_spec, trace_data, mapping_labels, sizeref): if len(customdata_cols) > 0: # here we store a data frame in customdata, and it's serialized # as a list of row lists, which is what we want - trace_patch["customdata"] = trace_data[customdata_cols] + + # dict.fromkeys(customdata_cols) allows to deduplicate column + # names, yet maintaining the original order. + trace_patch["customdata"] = trace_data.select( + [nw.col(c) for c in dict.fromkeys(customdata_cols)] + ) elif attr_name == "color": if trace_spec.constructor in [ go.Choropleth, @@ -1836,12 +1841,14 @@ def process_dataframe_hierarchy(args): discrete_aggs.append(args["color"]) discrete_color = True - # Hack: In theory, we should have a way to do nw.col(x).unique() and - # successively do: + # Hack: In theory, we should have a way to do `.agg(nw.col(x).unique())` and + # successively unpack/parse it as: + # ``` # (nw.when(nw.col(x).list.len()==1) # .then(nw.col(x).list.first()) # .otherwise(nw.lit("(?)")) # ) + # ``` # which replicates: # ``` # def discrete_agg(x): @@ -1988,9 +1995,15 @@ def process_dataframe_timeline(args): raise ValueError("Both x_start and x_end are required") try: + # FIXME: naive cast is most likely not enough in this context + # Related issue: https://github.com/narwhals-dev/narwhals/issues/1120 df: nw.DataFrame = args["data_frame"] - x_start = df.get_column(args["x_start"]).cast(nw.Datetime()) - x_end = df.get_column(args["x_end"]).cast(nw.Datetime()) + df = df.with_columns( + **{ + args["x_start"]: nw.col(args["x_start"]).cast(nw.Datetime()), + args["x_end"]: nw.col(args["x_end"]).cast(nw.Datetime()), + } + ) except Exception: raise TypeError( "Both x_start and x_end must refer to data convertible to datetimes." @@ -1998,12 +2011,15 @@ def process_dataframe_timeline(args): # note that we are not adding any columns to the data frame here, so no risk of overwrite args["data_frame"] = df.with_columns( - **{args["x_end"]: (x_end - x_start).dt.total_milliseconds()} + **{ + args["x_end"]: ( + nw.col(args["x_end"]) - nw.col(args["x_start"]) + ).dt.total_milliseconds() + } ) args["x"] = args["x_end"] - del args["x_end"] args["base"] = args["x_start"] - del args["x_start"] + del args["x_start"], args["x_end"] return args diff --git a/packages/python/plotly/plotly/tests/test_optional/test_px/test_facets.py b/packages/python/plotly/plotly/tests/test_optional/test_px/test_facets.py index dbd303211e..42918f5acc 100644 --- a/packages/python/plotly/plotly/tests/test_optional/test_px/test_facets.py +++ b/packages/python/plotly/plotly/tests/test_optional/test_px/test_facets.py @@ -6,7 +6,11 @@ import pytest import random -constructors = (pd.DataFrame, pl.DataFrame, pa.table) +constructors = ( + pd.DataFrame, + pl.DataFrame, + pa.table, +) @pytest.mark.parametrize("constructor", constructors) diff --git a/packages/python/plotly/plotly/tests/test_optional/test_px/test_marginals.py b/packages/python/plotly/plotly/tests/test_optional/test_px/test_marginals.py index 9be3bbb2c0..fc8ea31d52 100644 --- a/packages/python/plotly/plotly/tests/test_optional/test_px/test_marginals.py +++ b/packages/python/plotly/plotly/tests/test_optional/test_px/test_marginals.py @@ -4,7 +4,11 @@ import plotly.express as px import pytest -constructors = (pd.DataFrame, pl.DataFrame, pa.table) +constructors = ( + pd.DataFrame, + pl.DataFrame, + pa.table, +) @pytest.mark.parametrize("constructor", constructors) diff --git a/packages/python/plotly/plotly/tests/test_optional/test_px/test_px.py b/packages/python/plotly/plotly/tests/test_optional/test_px/test_px.py index 54d27bf972..6f1210d48d 100644 --- a/packages/python/plotly/plotly/tests/test_optional/test_px/test_px.py +++ b/packages/python/plotly/plotly/tests/test_optional/test_px/test_px.py @@ -8,7 +8,11 @@ import pytest from itertools import permutations -constructors = (pd.DataFrame, pl.DataFrame, pa.table) +constructors = ( + pd.DataFrame, + pl.DataFrame, + pa.table, +) @pytest.mark.parametrize("constructor", constructors) diff --git a/packages/python/plotly/plotly/tests/test_optional/test_px/test_px_functions.py b/packages/python/plotly/plotly/tests/test_optional/test_px/test_px_functions.py index 98e8fae5a4..1da92860d7 100644 --- a/packages/python/plotly/plotly/tests/test_optional/test_px/test_px_functions.py +++ b/packages/python/plotly/plotly/tests/test_optional/test_px/test_px_functions.py @@ -9,7 +9,11 @@ import pytest -constructors = (pd.DataFrame, pl.DataFrame, pa.table) +constructors = ( + pd.DataFrame, + pl.DataFrame, + pa.table, +) def _compare_figures(go_trace, px_fig): diff --git a/packages/python/plotly/plotly/tests/test_optional/test_px/test_px_hover.py b/packages/python/plotly/plotly/tests/test_optional/test_px/test_px_hover.py index 463c250361..63bb2dd073 100644 --- a/packages/python/plotly/plotly/tests/test_optional/test_px/test_px_hover.py +++ b/packages/python/plotly/plotly/tests/test_optional/test_px/test_px_hover.py @@ -8,7 +8,11 @@ from collections import OrderedDict # an OrderedDict is needed for Python 2 -constructors = (pd.DataFrame, pl.DataFrame, pa.table) +constructors = ( + pd.DataFrame, + pl.DataFrame, + pa.table, +) @pytest.mark.parametrize("constructor", constructors) @@ -202,7 +206,7 @@ def test_sunburst_hoverdict_color(constructor): @pytest.mark.parametrize("constructor", constructors) def test_date_in_hover(constructor): df = nw.from_native( - constructor({"date": ["2015-04-04 19:31:30+1:00"], "value": [3]}) - ).with_columns(date=nw.col("date").cast(nw.Datetime())) + constructor({"date": ["2015-04-04 19:31:30+01:00"], "value": [3]}) + ).with_columns(date=nw.col("date").cast(nw.Datetime(time_zone="Europe/Berlin"))) fig = px.scatter(df, x="value", y="value", hover_data=["date"]) assert str(fig.data[0].customdata[0][0]) == str(df["date"][0]) diff --git a/packages/python/plotly/plotly/tests/test_optional/test_px/test_px_input.py b/packages/python/plotly/plotly/tests/test_optional/test_px/test_px_input.py index 8a6a1a23a0..81e8150879 100644 --- a/packages/python/plotly/plotly/tests/test_optional/test_px/test_px_input.py +++ b/packages/python/plotly/plotly/tests/test_optional/test_px/test_px_input.py @@ -13,7 +13,11 @@ import sys import warnings -constructors = (pd.DataFrame, pl.DataFrame, pa.table) +constructors = ( + pd.DataFrame, + pl.DataFrame, + pa.table, +) # Fixtures # -------- diff --git a/packages/python/plotly/plotly/tests/test_optional/test_px/test_px_wide.py b/packages/python/plotly/plotly/tests/test_optional/test_px/test_px_wide.py index 2a33342bfa..42c85e203f 100644 --- a/packages/python/plotly/plotly/tests/test_optional/test_px/test_px_wide.py +++ b/packages/python/plotly/plotly/tests/test_optional/test_px/test_px_wide.py @@ -10,7 +10,11 @@ import pytest import warnings -constructors = (pd.DataFrame, pl.DataFrame, pa.table) +constructors = ( + pd.DataFrame, + pl.DataFrame, + pa.table, +) @pytest.mark.parametrize("constructor", constructors) diff --git a/packages/python/plotly/plotly/tests/test_optional/test_px/test_trendline.py b/packages/python/plotly/plotly/tests/test_optional/test_px/test_trendline.py index e3832c1adb..54d8cf7e0f 100644 --- a/packages/python/plotly/plotly/tests/test_optional/test_px/test_trendline.py +++ b/packages/python/plotly/plotly/tests/test_optional/test_px/test_trendline.py @@ -8,7 +8,11 @@ from datetime import datetime -constructors = (pd.DataFrame, pl.DataFrame, pa.table) +constructors = ( + pd.DataFrame, + pl.DataFrame, + pa.table, +) @pytest.mark.parametrize("constructor", constructors) From 4da876866fdf198a8682fd141114324b29ce1507 Mon Sep 17 00:00:00 2001 From: FBruzzesi Date: Sun, 6 Oct 2024 17:43:59 +0200 Subject: [PATCH 031/106] getting there --- packages/python/plotly/plotly/express/_core.py | 16 ++++++++++++---- .../test_optional/test_px/test_px_functions.py | 15 ++++++--------- .../tests/test_optional/test_px/test_px_hover.py | 4 ++-- 3 files changed, 20 insertions(+), 15 deletions(-) diff --git a/packages/python/plotly/plotly/express/_core.py b/packages/python/plotly/plotly/express/_core.py index 763cca3a06..90feb6305b 100644 --- a/packages/python/plotly/plotly/express/_core.py +++ b/packages/python/plotly/plotly/express/_core.py @@ -499,6 +499,7 @@ def make_trace_kwargs(args, trace_spec, trace_data, mapping_labels, sizeref): else: mapping = {} for cat in trace_data.get_column(attr_value): + cat = to_py_scalar(cat) if mapping.get(cat) is None: mapping[cat] = args["color_discrete_sequence"][ len(mapping) % len(args["color_discrete_sequence"]) @@ -1734,7 +1735,7 @@ def _check_dataframe_all_leaves(df: nw.DataFrame) -> None: null_indices_mask = null_mask.select( null_mask=nw.any_horizontal(nw.col(cols)) ).get_column("null_mask") - null_indices = null_indices_mask.arg_true() + for row_idx, row in zip( null_indices_mask, null_mask.filter(null_indices_mask).iter_rows() ): @@ -1759,10 +1760,17 @@ def _check_dataframe_all_leaves(df: nw.DataFrame) -> None: for c in cols } ) - row_strings = df_sorted.select( - row_strings=nw.concat_str(cols, separator="", ignore_nulls=False) - ).get_column("row_strings") + # Conversion to list is due to python native vs pyarrow scalars + row_strings = ( + df_sorted.select( + row_strings=nw.concat_str(cols, separator="", ignore_nulls=False) + ) + .get_column("row_strings") + .to_list() + ) + + null_indices = null_indices_mask.arg_true().to_list() for i, (current_row, next_row) in enumerate( zip(row_strings[:-1], row_strings[1:]), start=1 ): diff --git a/packages/python/plotly/plotly/tests/test_optional/test_px/test_px_functions.py b/packages/python/plotly/plotly/tests/test_optional/test_px/test_px_functions.py index 1da92860d7..ae1fe69840 100644 --- a/packages/python/plotly/plotly/tests/test_optional/test_px/test_px_functions.py +++ b/packages/python/plotly/plotly/tests/test_optional/test_px/test_px_functions.py @@ -193,7 +193,6 @@ def test_sunburst_treemap_with_path(constructor): assert fig.data[0].values[-1] == 8 -# TODO: Fix duplicate column, pyarrow cannot sum strings to concatenate them @pytest.mark.parametrize("constructor", constructors) def test_sunburst_treemap_with_path_and_hover(constructor): data = px.data.tips().to_dict(orient="list") @@ -257,12 +256,10 @@ def test_sunburst_treemap_with_path_color(constructor): path = ["total", "regions", "sectors", "vendors"] fig = px.sunburst(df.to_native(), path=path, values="values", color="calls") colors = fig.data[0].marker.colors - assert np.all( - np.array(colors[:8]) == np.array(calls) - ) # TODO: Fails because polars has `maintain_order=False` in group by + assert np.all(np.array(np.sort(colors[:8])) == np.array(sorted(calls))) fig = px.sunburst(df.to_native(), path=path, color="calls") colors = fig.data[0].marker.colors - assert np.all(np.array(colors[:8]) == np.array(calls)) + assert np.all(np.sort(colors[:8]) == np.array(sorted(calls))) # Hover info df = df.with_columns( @@ -275,9 +272,9 @@ def test_sunburst_treemap_with_path_color(constructor): ) fig = px.sunburst(df.to_native(), path=path, color="calls", hover_data=["hover"]) custom = fig.data[0].customdata - assert np.all(custom[:8, 0] == hover) - assert np.all(custom[8:, 0] == "(?)") - assert np.all(custom[:8, 1] == calls) + assert np.all(np.sort(custom[:8, 0]) == sorted(hover)) + assert np.all(np.sort(custom[8:, 0]) == "(?)") + assert np.all(np.sort(custom[:8, 1]) == sorted(calls)) # Discrete color fig = px.sunburst(df.to_native(), path=path, color="vendors") @@ -304,7 +301,7 @@ def test_sunburst_treemap_with_path_color(constructor): path = ["total", "regions", "sectors", "vendors"] fig = px.sunburst(df, path=path, values="values", color="calls") colors = fig.data[0].marker.colors - assert np.all(np.array(colors[:8]) == np.array(calls)) + assert np.all(np.sort(colors[:8]) == sorted(calls)) @pytest.mark.parametrize("constructor", constructors) diff --git a/packages/python/plotly/plotly/tests/test_optional/test_px/test_px_hover.py b/packages/python/plotly/plotly/tests/test_optional/test_px/test_px_hover.py index 63bb2dd073..253adb5704 100644 --- a/packages/python/plotly/plotly/tests/test_optional/test_px/test_px_hover.py +++ b/packages/python/plotly/plotly/tests/test_optional/test_px/test_px_hover.py @@ -207,6 +207,6 @@ def test_sunburst_hoverdict_color(constructor): def test_date_in_hover(constructor): df = nw.from_native( constructor({"date": ["2015-04-04 19:31:30+01:00"], "value": [3]}) - ).with_columns(date=nw.col("date").cast(nw.Datetime(time_zone="Europe/Berlin"))) - fig = px.scatter(df, x="value", y="value", hover_data=["date"]) + ).with_columns(date=nw.col("date").str.to_datetime(format="%Y-%m-%d %H:%M:%S")) + fig = px.scatter(df.to_native(), x="value", y="value", hover_data=["date"]) assert str(fig.data[0].customdata[0][0]) == str(df["date"][0]) From 210e01a0a310f55336248684fce7d65b1565f49b Mon Sep 17 00:00:00 2001 From: FBruzzesi Date: Sun, 6 Oct 2024 18:38:58 +0200 Subject: [PATCH 032/106] xfail pyarrow chunked-array because name-less --- packages/python/plotly/plotly/express/_core.py | 5 +---- .../test_optional/test_px/test_px_input.py | 17 ++++++++++++++--- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/packages/python/plotly/plotly/express/_core.py b/packages/python/plotly/plotly/express/_core.py index 90feb6305b..d60ad62b6f 100644 --- a/packages/python/plotly/plotly/express/_core.py +++ b/packages/python/plotly/plotly/express/_core.py @@ -2,7 +2,6 @@ import plotly.io as pio from collections import namedtuple, OrderedDict from collections.abc import Sequence -from functools import reduce from ._special_inputs import IdentityMap, Constant, Range from .trendline_functions import ols, lowess, rolling, expanding, ewm @@ -2270,9 +2269,7 @@ def infer_config(args, constructor, trace_patch, layout_patch): args["histnorm"] = args["ecdfnorm"] # Compute applicable grouping attributes - for k in group_attrables: - if k in args: - grouped_attrs.append(k) + grouped_attrs.extend([k for k in group_attrables if k in args]) # Create grouped mappings grouped_mappings = [make_mapping(args, a) for a in grouped_attrs] diff --git a/packages/python/plotly/plotly/tests/test_optional/test_px/test_px_input.py b/packages/python/plotly/plotly/tests/test_optional/test_px/test_px_input.py index 81e8150879..b40a51046c 100644 --- a/packages/python/plotly/plotly/tests/test_optional/test_px/test_px_input.py +++ b/packages/python/plotly/plotly/tests/test_optional/test_px/test_px_input.py @@ -73,10 +73,15 @@ def test_with_index(): @pytest.mark.parametrize("constructor", constructors) -def test_series(constructor): +def test_series(request, constructor): + if constructor is pa.table: + request.applymarker(pytest.mark.xfail) + data = px.data.tips().to_dict(orient="list") tips = nw.from_native(constructor(data)) before_tip = (tips.get_column("total_bill") - tips.get_column("tip")).to_native() + # By converting to native, we lose the name for pyarrow chunked_array and the last + # assertion fails day = tips.get_column("day").to_native() tips = tips.to_native() @@ -90,7 +95,10 @@ def test_series(constructor): @pytest.mark.parametrize("constructor", constructors) -def test_several_dataframes(constructor): +def test_several_dataframes(request, constructor): + if constructor is pa.table: + request.applymarker(pytest.mark.xfail) + df = nw.from_native(constructor(dict(x=[0, 1], y=[1, 10], z=[0.1, 0.8]))) df2 = nw.from_native(constructor(dict(time=[23, 26], money=[100, 200]))) fig = px.scatter( @@ -167,7 +175,10 @@ def test_several_dataframes(constructor): @pytest.mark.parametrize("constructor", constructors) -def test_name_heuristics(constructor): +def test_name_heuristics(request, constructor): + if constructor is pa.table: + request.applymarker(pytest.mark.xfail) + df = nw.from_native(constructor(dict(x=[0, 1], y=[3, 4], z=[0.1, 0.2]))) fig = px.scatter( df.to_native(), From c00525e12ef78961fd687f7e15917da42380ddac Mon Sep 17 00:00:00 2001 From: FBruzzesi Date: Sun, 6 Oct 2024 21:31:58 +0200 Subject: [PATCH 033/106] all green with edge narhwals --- .../python/plotly/plotly/express/_core.py | 4 +- .../test_optional/test_px/test_facets.py | 1 - .../test_px/test_px_functions.py | 5 +- .../test_optional/test_px/test_px_hover.py | 8 ++-- .../test_optional/test_px/test_px_input.py | 46 ++++++++++++------- 5 files changed, 40 insertions(+), 24 deletions(-) diff --git a/packages/python/plotly/plotly/express/_core.py b/packages/python/plotly/plotly/express/_core.py index d60ad62b6f..cdf352e230 100644 --- a/packages/python/plotly/plotly/express/_core.py +++ b/packages/python/plotly/plotly/express/_core.py @@ -2007,8 +2007,8 @@ def process_dataframe_timeline(args): df: nw.DataFrame = args["data_frame"] df = df.with_columns( **{ - args["x_start"]: nw.col(args["x_start"]).cast(nw.Datetime()), - args["x_end"]: nw.col(args["x_end"]).cast(nw.Datetime()), + args["x_start"]: nw.col(args["x_start"]).str.to_datetime(), + args["x_end"]: nw.col(args["x_end"]).str.to_datetime(), } ) except Exception: diff --git a/packages/python/plotly/plotly/tests/test_optional/test_px/test_facets.py b/packages/python/plotly/plotly/tests/test_optional/test_px/test_facets.py index 42918f5acc..c74f3ec8c1 100644 --- a/packages/python/plotly/plotly/tests/test_optional/test_px/test_facets.py +++ b/packages/python/plotly/plotly/tests/test_optional/test_px/test_facets.py @@ -57,7 +57,6 @@ def test_facets(constructor): assert fig.layout.yaxis4.domain[0] - fig.layout.yaxis.domain[1] == approx(0.08) -# TODO: Figure out how to deal with multiple occurrences of same column name @pytest.mark.parametrize("constructor", constructors) def test_facets_with_marginals(constructor): data = px.data.tips().to_dict(orient="list") diff --git a/packages/python/plotly/plotly/tests/test_optional/test_px/test_px_functions.py b/packages/python/plotly/plotly/tests/test_optional/test_px/test_px_functions.py index ae1fe69840..12f7f3d44d 100644 --- a/packages/python/plotly/plotly/tests/test_optional/test_px/test_px_functions.py +++ b/packages/python/plotly/plotly/tests/test_optional/test_px/test_px_functions.py @@ -557,7 +557,10 @@ def check_label(label, fig): @pytest.mark.parametrize("constructor", constructors) -def test_timeline(constructor): +def test_timeline(request, constructor): + if constructor in {pl.DataFrame, pa.table}: + request.applymarker(pytest.mark.xfail) + df = constructor( { "Task": ["Job A", "Job B", "Job C"], diff --git a/packages/python/plotly/plotly/tests/test_optional/test_px/test_px_hover.py b/packages/python/plotly/plotly/tests/test_optional/test_px/test_px_hover.py index 253adb5704..5b16d60a80 100644 --- a/packages/python/plotly/plotly/tests/test_optional/test_px/test_px_hover.py +++ b/packages/python/plotly/plotly/tests/test_optional/test_px/test_px_hover.py @@ -204,9 +204,11 @@ def test_sunburst_hoverdict_color(constructor): @pytest.mark.parametrize("constructor", constructors) -def test_date_in_hover(constructor): +def test_date_in_hover(request, constructor): + if constructor in {pl.DataFrame, pa.table}: + request.applymarker(pytest.mark.xfail) df = nw.from_native( constructor({"date": ["2015-04-04 19:31:30+01:00"], "value": [3]}) - ).with_columns(date=nw.col("date").str.to_datetime(format="%Y-%m-%d %H:%M:%S")) + ).with_columns(date=nw.col("date").str.to_datetime(format="%Y-%m-%d %H:%M:%S%z")) fig = px.scatter(df.to_native(), x="value", y="value", hover_data=["date"]) - assert str(fig.data[0].customdata[0][0]) == str(df["date"][0]) + assert fig.data[0].customdata[0][0] == df.item(row=0, column="date") diff --git a/packages/python/plotly/plotly/tests/test_optional/test_px/test_px_input.py b/packages/python/plotly/plotly/tests/test_optional/test_px/test_px_input.py index b40a51046c..c48a9aa4ca 100644 --- a/packages/python/plotly/plotly/tests/test_optional/test_px/test_px_input.py +++ b/packages/python/plotly/plotly/tests/test_optional/test_px/test_px_input.py @@ -442,8 +442,8 @@ def test_build_df_with_hover_data_from_vaex_and_polars(test_lib, hover_data): @pytest.mark.parametrize("constructor", constructors) def test_timezones(constructor): df = nw.from_native( - constructor({"date": ["2015-04-04 19:31:30+1:00"], "value": [3]}) - ).with_columns(nw.col("date").cast(nw.Datetime(time_zone="Europe/London"))) + constructor({"date": ["2015-04-04 19:31:30+01:00"], "value": [3]}) + ).with_columns(nw.col("date").str.to_datetime(format="%Y-%m-%d %H:%M:%S%z")) args = dict(data_frame=df.to_native(), x="date", y="value") out = build_dataframe(args, go.Scatter) @@ -504,19 +504,23 @@ def test_data_frame_from_dict(): assert np.all(fig.data[0].x == [0, 1]) -def test_arguments_not_modified(): - iris = px.data.iris() # TODO - petal_length = iris.petal_length - hover_data = [iris.sepal_length] - px.scatter(iris, x=petal_length, y="petal_width", hover_data=hover_data) - assert iris.petal_length.equals(petal_length) - assert iris.sepal_length.equals(hover_data[0]) +@pytest.mark.parametrize("constructor", constructors) +def test_arguments_not_modified(constructor): + data = px.data.iris().to_dict(orient="list") + iris = nw.from_native(constructor(data)) + petal_length = iris.get_column("petal_length").to_native() + hover_data = [iris.get_column("sepal_length").to_native()] + px.scatter(iris.to_native(), x=petal_length, y="petal_width", hover_data=hover_data) + assert petal_length.equals(petal_length) + assert iris.get_column("sepal_length").to_native().equals(hover_data[0]) -def test_pass_df_columns(): - tips = px.data.tips() # TODO +@pytest.mark.parametrize("constructor", constructors) +def test_pass_df_columns(constructor): + data = px.data.tips().to_dict(orient="list") + tips = nw.from_native(constructor(data)) fig = px.histogram( - tips, + tips.to_native(), x="total_bill", y="tip", color="sex", @@ -525,13 +529,21 @@ def test_pass_df_columns(): ) # the "- 2" is because we re-use x and y in the hovertemplate where possible assert fig.data[1].hovertemplate.count("customdata") == len(tips.columns) - 2 - tips_copy = px.data.tips() - assert tips_copy.columns.equals(tips.columns) + tips_copy = nw.from_native(constructor(data)) + assert tips_copy.columns == tips.columns -def test_size_column(): - df = px.data.tips() # TODO - fig = px.scatter(df, x=df["size"], y=df.tip) +@pytest.mark.parametrize("constructor", constructors) +def test_size_column(request, constructor): + if constructor is pa.table: + request.applymarker(pytest.mark.xfail) + data = px.data.tips().to_dict(orient="list") + tips = nw.from_native(constructor(data)) + fig = px.scatter( + tips.to_native(), + x=tips.get_column("size").to_native(), + y=tips.get_column("tip").to_native(), + ) assert fig.data[0].hovertemplate == "size=%{x}
tip=%{y}" From 3486a3e861850bdab687d6a59e3d0adf6bfb3c43 Mon Sep 17 00:00:00 2001 From: FBruzzesi Date: Mon, 7 Oct 2024 09:33:31 +0200 Subject: [PATCH 034/106] add pandas nullable constructors in tests --- .../plotly/plotly/tests/test_optional/test_px/test_facets.py | 2 ++ .../plotly/tests/test_optional/test_px/test_marginals.py | 2 ++ .../plotly/plotly/tests/test_optional/test_px/test_px.py | 2 ++ .../plotly/tests/test_optional/test_px/test_px_functions.py | 2 ++ .../plotly/tests/test_optional/test_px/test_px_hover.py | 2 ++ .../plotly/tests/test_optional/test_px/test_px_input.py | 2 ++ .../plotly/plotly/tests/test_optional/test_px/test_px_wide.py | 4 +++- .../plotly/tests/test_optional/test_px/test_trendline.py | 2 ++ 8 files changed, 17 insertions(+), 1 deletion(-) diff --git a/packages/python/plotly/plotly/tests/test_optional/test_px/test_facets.py b/packages/python/plotly/plotly/tests/test_optional/test_px/test_facets.py index c74f3ec8c1..1ffd08914d 100644 --- a/packages/python/plotly/plotly/tests/test_optional/test_px/test_facets.py +++ b/packages/python/plotly/plotly/tests/test_optional/test_px/test_facets.py @@ -10,6 +10,8 @@ pd.DataFrame, pl.DataFrame, pa.table, + lambda d: pd.DataFrame(d).convert_dtypes("pyarrow"), + lambda d: pd.DataFrame(d).convert_dtypes("numpy_nullable"), ) diff --git a/packages/python/plotly/plotly/tests/test_optional/test_px/test_marginals.py b/packages/python/plotly/plotly/tests/test_optional/test_px/test_marginals.py index fc8ea31d52..e09070bc0e 100644 --- a/packages/python/plotly/plotly/tests/test_optional/test_px/test_marginals.py +++ b/packages/python/plotly/plotly/tests/test_optional/test_px/test_marginals.py @@ -8,6 +8,8 @@ pd.DataFrame, pl.DataFrame, pa.table, + lambda d: pd.DataFrame(d).convert_dtypes("pyarrow"), + lambda d: pd.DataFrame(d).convert_dtypes("numpy_nullable"), ) diff --git a/packages/python/plotly/plotly/tests/test_optional/test_px/test_px.py b/packages/python/plotly/plotly/tests/test_optional/test_px/test_px.py index 6f1210d48d..cbbea84433 100644 --- a/packages/python/plotly/plotly/tests/test_optional/test_px/test_px.py +++ b/packages/python/plotly/plotly/tests/test_optional/test_px/test_px.py @@ -12,6 +12,8 @@ pd.DataFrame, pl.DataFrame, pa.table, + lambda d: pd.DataFrame(d).convert_dtypes("pyarrow"), + lambda d: pd.DataFrame(d).convert_dtypes("numpy_nullable"), ) diff --git a/packages/python/plotly/plotly/tests/test_optional/test_px/test_px_functions.py b/packages/python/plotly/plotly/tests/test_optional/test_px/test_px_functions.py index 12f7f3d44d..69ec2be182 100644 --- a/packages/python/plotly/plotly/tests/test_optional/test_px/test_px_functions.py +++ b/packages/python/plotly/plotly/tests/test_optional/test_px/test_px_functions.py @@ -13,6 +13,8 @@ pd.DataFrame, pl.DataFrame, pa.table, + lambda d: pd.DataFrame(d).convert_dtypes("pyarrow"), + lambda d: pd.DataFrame(d).convert_dtypes("numpy_nullable"), ) diff --git a/packages/python/plotly/plotly/tests/test_optional/test_px/test_px_hover.py b/packages/python/plotly/plotly/tests/test_optional/test_px/test_px_hover.py index 5b16d60a80..84b6977afb 100644 --- a/packages/python/plotly/plotly/tests/test_optional/test_px/test_px_hover.py +++ b/packages/python/plotly/plotly/tests/test_optional/test_px/test_px_hover.py @@ -12,6 +12,8 @@ pd.DataFrame, pl.DataFrame, pa.table, + lambda d: pd.DataFrame(d).convert_dtypes("pyarrow"), + lambda d: pd.DataFrame(d).convert_dtypes("numpy_nullable"), ) diff --git a/packages/python/plotly/plotly/tests/test_optional/test_px/test_px_input.py b/packages/python/plotly/plotly/tests/test_optional/test_px/test_px_input.py index c48a9aa4ca..fee268d65a 100644 --- a/packages/python/plotly/plotly/tests/test_optional/test_px/test_px_input.py +++ b/packages/python/plotly/plotly/tests/test_optional/test_px/test_px_input.py @@ -17,6 +17,8 @@ pd.DataFrame, pl.DataFrame, pa.table, + lambda d: pd.DataFrame(d).convert_dtypes("pyarrow"), + lambda d: pd.DataFrame(d).convert_dtypes("numpy_nullable"), ) # Fixtures diff --git a/packages/python/plotly/plotly/tests/test_optional/test_px/test_px_wide.py b/packages/python/plotly/plotly/tests/test_optional/test_px/test_px_wide.py index 42c85e203f..7c668493f0 100644 --- a/packages/python/plotly/plotly/tests/test_optional/test_px/test_px_wide.py +++ b/packages/python/plotly/plotly/tests/test_optional/test_px/test_px_wide.py @@ -14,6 +14,8 @@ pd.DataFrame, pl.DataFrame, pa.table, + lambda d: pd.DataFrame(d).convert_dtypes("pyarrow"), + lambda d: pd.DataFrame(d).convert_dtypes("numpy_nullable"), ) @@ -862,7 +864,7 @@ def test_mixed_number_input(constructor): @pytest.mark.parametrize("constructor", constructors) def test_line_group(constructor): df = constructor( - data={ + { "who": ["a", "a", "b", "b"], "x": [0, 1, 0, 1], "score": [1.0, 2, 3, 4], diff --git a/packages/python/plotly/plotly/tests/test_optional/test_px/test_trendline.py b/packages/python/plotly/plotly/tests/test_optional/test_px/test_trendline.py index 54d8cf7e0f..585b8c4663 100644 --- a/packages/python/plotly/plotly/tests/test_optional/test_px/test_trendline.py +++ b/packages/python/plotly/plotly/tests/test_optional/test_px/test_trendline.py @@ -12,6 +12,8 @@ pd.DataFrame, pl.DataFrame, pa.table, + lambda d: pd.DataFrame(d).convert_dtypes("pyarrow"), + lambda d: pd.DataFrame(d).convert_dtypes("numpy_nullable"), ) From c0ce09351c6e8ae54b77c826eb38e60600531a55 Mon Sep 17 00:00:00 2001 From: FBruzzesi Date: Wed, 9 Oct 2024 13:41:02 +0200 Subject: [PATCH 035/106] bump narwhals and address todos --- .../python/plotly/optional-requirements.txt | 2 +- .../python/plotly/plotly/express/_core.py | 8 +++--- .../test_optional/test_px/test_trendline.py | 25 ++++++++----------- 3 files changed, 16 insertions(+), 19 deletions(-) diff --git a/packages/python/plotly/optional-requirements.txt b/packages/python/plotly/optional-requirements.txt index a74c9124da..f91e618e14 100644 --- a/packages/python/plotly/optional-requirements.txt +++ b/packages/python/plotly/optional-requirements.txt @@ -39,7 +39,7 @@ ipython ## pandas deps for some matplotlib functionality ## pandas -narwhals>=1.9.1 +narwhals>=1.9.2 ## scipy deps for some FigureFactory functions ## scipy diff --git a/packages/python/plotly/plotly/express/_core.py b/packages/python/plotly/plotly/express/_core.py index cdf352e230..ffd90bf95e 100644 --- a/packages/python/plotly/plotly/express/_core.py +++ b/packages/python/plotly/plotly/express/_core.py @@ -1769,11 +1769,11 @@ def _check_dataframe_all_leaves(df: nw.DataFrame) -> None: .to_list() ) - null_indices = null_indices_mask.arg_true().to_list() + null_indices = set(null_indices_mask.arg_true().to_list()) for i, (current_row, next_row) in enumerate( zip(row_strings[:-1], row_strings[1:]), start=1 ): - if (i in null_indices) and (next_row in current_row): + if (next_row in current_row) and (i in null_indices): raise ValueError( "Non-leaves rows are not permitted in the dataframe \n", df_sorted.row(i), @@ -2002,8 +2002,8 @@ def process_dataframe_timeline(args): raise ValueError("Both x_start and x_end are required") try: - # FIXME: naive cast is most likely not enough in this context - # Related issue: https://github.com/narwhals-dev/narwhals/issues/1120 + # TODO(FBruzzesi): We still cannot infer datetime format for pyarrow + # Related issue: https://github.com/narwhals-dev/narwhals/issues/1151 df: nw.DataFrame = args["data_frame"] df = df.with_columns( **{ diff --git a/packages/python/plotly/plotly/tests/test_optional/test_px/test_trendline.py b/packages/python/plotly/plotly/tests/test_optional/test_px/test_trendline.py index 585b8c4663..0f897e8f5f 100644 --- a/packages/python/plotly/plotly/tests/test_optional/test_px/test_trendline.py +++ b/packages/python/plotly/plotly/tests/test_optional/test_px/test_trendline.py @@ -48,7 +48,7 @@ def test_trendline_results_passthrough(constructor, mode, options): if mode == "ols": assert "R2" in trendline.hovertemplate results = px.get_trendline_results(fig) - if mode == "ols": # Somehow this is flaky for polars? + if mode == "ols": # This is flaky for polars? assert len(results) == 2 assert results["country"].values[0] == "Australia" au_result = results["px_fit_results"].values[0] @@ -187,10 +187,7 @@ def test_ols_trendline_slopes(): assert "y = 0 * x + 1.5
" in fig.data[1].hovertemplate -@pytest.mark.parametrize( - ("constructor", "from_pandas_fn"), - zip(constructors, (lambda x: x, pl.from_pandas, pa.Table.from_pandas)), -) +@pytest.mark.parametrize("constructor", constructors) @pytest.mark.parametrize( "mode,options", [ @@ -203,7 +200,7 @@ def test_ols_trendline_slopes(): ("ewm", dict(alpha=0.5)), ], ) -def test_trendline_on_timeseries(constructor, from_pandas_fn, mode, options): +def test_trendline_on_timeseries(constructor, mode, options): df = nw.from_native(constructor(px.data.stocks().to_dict(orient="list"))) pd_err_msg = "Could not convert value of 'x' \('date'\) into a numeric type." @@ -217,15 +214,15 @@ def test_trendline_on_timeseries(constructor, from_pandas_fn, mode, options): trendline=mode, trendline_options=options, ) + df = df.with_columns( + date=nw.col("date") + .str.to_datetime(format="%Y-%m-%d") + .dt.replace_time_zone("CET") + ) - # TODO: This conversion requires new functionalities in narwhals - # for now here is a workaround - df = df.to_pandas() - df["date"] = pd.to_datetime(df["date"]) - df["date"] = df["date"].dt.tz_localize("CET") # force a timezone - df = from_pandas_fn(df) - - fig = px.scatter(df, x="date", y="GOOG", trendline=mode, trendline_options=options) + fig = px.scatter( + df.to_native(), x="date", y="GOOG", trendline=mode, trendline_options=options + ) assert len(fig.data) == 2 assert len(fig.data[0].x) == len(fig.data[1].x) From 0eb6951f5b81b699b7eb9b82da9b2a09c61333b2 Mon Sep 17 00:00:00 2001 From: FBruzzesi Date: Wed, 9 Oct 2024 13:52:45 +0200 Subject: [PATCH 036/106] check narwhals installation --- packages/python/plotly/plotly/express/__init__.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/packages/python/plotly/plotly/express/__init__.py b/packages/python/plotly/plotly/express/__init__.py index 460ce1b998..3b1da0829a 100644 --- a/packages/python/plotly/plotly/express/__init__.py +++ b/packages/python/plotly/plotly/express/__init__.py @@ -2,6 +2,15 @@ `plotly.express` is a terse, consistent, high-level wrapper around `plotly.graph_objects` for rapid data exploration and figure generation. Learn more at https://plotly.com/python/plotly-express/ """ +from plotly import optional_imports + +nw = optional_imports.get_module("narwhals") +if nw is None: + raise ImportError( + """\ +Plotly express requires narwhals to be installed.""" + ) + from ._imshow import imshow from ._chart_types import ( # noqa: F401 scatter, From 844a6a9c2a3ce6752947934319aaf3b048584403 Mon Sep 17 00:00:00 2001 From: FBruzzesi Date: Wed, 9 Oct 2024 14:07:02 +0200 Subject: [PATCH 037/106] rm unused comments --- packages/python/plotly/plotly/express/_core.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/packages/python/plotly/plotly/express/_core.py b/packages/python/plotly/plotly/express/_core.py index ffd90bf95e..b46953512c 100644 --- a/packages/python/plotly/plotly/express/_core.py +++ b/packages/python/plotly/plotly/express/_core.py @@ -356,7 +356,6 @@ def make_trace_kwargs(args, trace_spec, trace_data, mapping_labels, sizeref): args["x"]: nw.when(~x.is_null()).then( x.cast(nw.Int64()) ) - # .otherwise(nw.lit(None, nw.Int64())) } ) .get_column(args["x"]) @@ -1987,8 +1986,6 @@ def post_agg( if not args["hover_data"].get(args["color"]): args["hover_data"][args["color"]] = (True, None) else: - # FIXME: Hover data can become a list with duplicate values, and then we select that! - # In narwhals this raises `ValueError: Expected unique column names, got: Index(['smoker', 'smoker'], dtype='object')` args["hover_data"].append(args["color"]) return args From 0c27789913877828b2c4ebe017bd4dff9b2d22f5 Mon Sep 17 00:00:00 2001 From: FBruzzesi Date: Wed, 9 Oct 2024 14:10:52 +0200 Subject: [PATCH 038/106] rm unused code --- packages/python/plotly/plotly/express/_core.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/packages/python/plotly/plotly/express/_core.py b/packages/python/plotly/plotly/express/_core.py index b46953512c..c2d58ebb5d 100644 --- a/packages/python/plotly/plotly/express/_core.py +++ b/packages/python/plotly/plotly/express/_core.py @@ -2754,15 +2754,6 @@ def _spacing_error_translator(e, direction, facet_arg): return fig -def is_into_eager_dataframe(df) -> bool: - """Check if `df` is a supported narwhals eager dataframe.""" - return ( - nw.dependencies.is_polars_dataframe(df) - or nw.dependencies.is_pyarrow_table(df) - or nw.dependencies.is_pandas_like_dataframe(df) - ) - - def is_into_series(df) -> bool: """Check if `df` is a supported narwhals eager dataframe.""" return ( From 0e6ff78cc5728dcb7664ec5784d3e055a110d4dd Mon Sep 17 00:00:00 2001 From: FBruzzesi Date: Wed, 9 Oct 2024 14:35:21 +0200 Subject: [PATCH 039/106] add pyarrow and narwhals to requirements_39_pandas_2_optional --- .../test_requirements/requirements_39_pandas_2_optional.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/python/plotly/test_requirements/requirements_39_pandas_2_optional.txt b/packages/python/plotly/test_requirements/requirements_39_pandas_2_optional.txt index 06a5a61f3e..bfcf1b0d5f 100644 --- a/packages/python/plotly/test_requirements/requirements_39_pandas_2_optional.txt +++ b/packages/python/plotly/test_requirements/requirements_39_pandas_2_optional.txt @@ -22,3 +22,5 @@ kaleido vaex pydantic<=1.10.11 # for vaex, see https://github.com/vaexio/vaex/issues/2384 polars +pyarrow +narwhals>=1.9.2 From c2337c9a0802470c5d7a54402c2550ea25c1da0a Mon Sep 17 00:00:00 2001 From: FBruzzesi Date: Tue, 15 Oct 2024 21:59:27 +0200 Subject: [PATCH 040/106] requirements, test requirements optional --- packages/python/plotly/requirements.txt | 3 +++ packages/python/plotly/setup.py | 2 +- .../plotly/test_requirements/requirements_310_optional.txt | 2 ++ .../plotly/test_requirements/requirements_311_optional.txt | 2 ++ .../test_requirements/requirements_312_no_numpy_optional.txt | 4 +++- .../plotly/test_requirements/requirements_312_optional.txt | 4 +++- .../plotly/test_requirements/requirements_38_optional.txt | 2 ++ .../plotly/test_requirements/requirements_39_optional.txt | 2 ++ .../test_requirements/requirements_39_pandas_2_optional.txt | 1 - 9 files changed, 18 insertions(+), 4 deletions(-) diff --git a/packages/python/plotly/requirements.txt b/packages/python/plotly/requirements.txt index 2d763d4dae..89baa10f65 100644 --- a/packages/python/plotly/requirements.txt +++ b/packages/python/plotly/requirements.txt @@ -7,3 +7,6 @@ ## retrying requests ## tenacity>=6.2.0 + +## dataframe agnostic layer ## +narwhals>=1.9.2 diff --git a/packages/python/plotly/setup.py b/packages/python/plotly/setup.py index 1122ebb4cb..419bcc01f7 100644 --- a/packages/python/plotly/setup.py +++ b/packages/python/plotly/setup.py @@ -603,7 +603,7 @@ def run(self): data_files=[ ("etc/jupyter/nbconfig/notebook.d", ["jupyterlab-plotly.json"]), ], - install_requires=["tenacity>=6.2.0", "packaging"], + install_requires=["tenacity>=6.2.0", "narwhals>=1.9.2", "packaging"], zip_safe=False, cmdclass=dict( build_py=js_prerelease(versioneer_cmds["build_py"]), diff --git a/packages/python/plotly/test_requirements/requirements_310_optional.txt b/packages/python/plotly/test_requirements/requirements_310_optional.txt index 4beeaa6ffc..0e468bfa9f 100644 --- a/packages/python/plotly/test_requirements/requirements_310_optional.txt +++ b/packages/python/plotly/test_requirements/requirements_310_optional.txt @@ -20,3 +20,5 @@ scikit-image==0.22.0 psutil==5.7.0 kaleido orjson==3.8.12 +polars +pyarrow diff --git a/packages/python/plotly/test_requirements/requirements_311_optional.txt b/packages/python/plotly/test_requirements/requirements_311_optional.txt index c0e4a679d7..0c450275bb 100644 --- a/packages/python/plotly/test_requirements/requirements_311_optional.txt +++ b/packages/python/plotly/test_requirements/requirements_311_optional.txt @@ -20,3 +20,5 @@ scikit-image==0.22.0 psutil==5.7.0 kaleido orjson==3.8.12 +polars +pyarrow diff --git a/packages/python/plotly/test_requirements/requirements_312_no_numpy_optional.txt b/packages/python/plotly/test_requirements/requirements_312_no_numpy_optional.txt index 2fd781846e..caa3adf291 100644 --- a/packages/python/plotly/test_requirements/requirements_312_no_numpy_optional.txt +++ b/packages/python/plotly/test_requirements/requirements_312_no_numpy_optional.txt @@ -18,4 +18,6 @@ matplotlib==3.8.2 scikit-image==0.22.0 psutil==5.9.7 kaleido -orjson==3.9.10 \ No newline at end of file +orjson==3.9.10 +polars +pyarrow diff --git a/packages/python/plotly/test_requirements/requirements_312_optional.txt b/packages/python/plotly/test_requirements/requirements_312_optional.txt index 6a5beaff57..6803db7e97 100644 --- a/packages/python/plotly/test_requirements/requirements_312_optional.txt +++ b/packages/python/plotly/test_requirements/requirements_312_optional.txt @@ -19,4 +19,6 @@ matplotlib==3.8.2 scikit-image==0.22.0 psutil==5.9.7 kaleido -orjson==3.9.10 \ No newline at end of file +orjson==3.9.10 +polars +pyarrow diff --git a/packages/python/plotly/test_requirements/requirements_38_optional.txt b/packages/python/plotly/test_requirements/requirements_38_optional.txt index b77d1a0acb..15c383e8c2 100644 --- a/packages/python/plotly/test_requirements/requirements_38_optional.txt +++ b/packages/python/plotly/test_requirements/requirements_38_optional.txt @@ -20,3 +20,5 @@ matplotlib==3.7.3 scikit-image==0.18.1 psutil==5.7.0 kaleido +polars +pyarrow diff --git a/packages/python/plotly/test_requirements/requirements_39_optional.txt b/packages/python/plotly/test_requirements/requirements_39_optional.txt index 37791cb5a6..2f462f5dd6 100644 --- a/packages/python/plotly/test_requirements/requirements_39_optional.txt +++ b/packages/python/plotly/test_requirements/requirements_39_optional.txt @@ -21,3 +21,5 @@ scikit-image==0.18.1 psutil==5.7.0 kaleido orjson==3.8.12 +polars +pyarrow diff --git a/packages/python/plotly/test_requirements/requirements_39_pandas_2_optional.txt b/packages/python/plotly/test_requirements/requirements_39_pandas_2_optional.txt index bfcf1b0d5f..e8d1303b09 100644 --- a/packages/python/plotly/test_requirements/requirements_39_pandas_2_optional.txt +++ b/packages/python/plotly/test_requirements/requirements_39_pandas_2_optional.txt @@ -23,4 +23,3 @@ vaex pydantic<=1.10.11 # for vaex, see https://github.com/vaexio/vaex/issues/2384 polars pyarrow -narwhals>=1.9.2 From 2cc5d7b701a1d0090bac0efcf96f1417f1778a94 Mon Sep 17 00:00:00 2001 From: FBruzzesi Date: Tue, 15 Oct 2024 22:00:17 +0200 Subject: [PATCH 041/106] refactor tests --- .../test_optional/test_px/test_facets.py | 22 ++++------------- .../test_optional/test_px/test_marginals.py | 13 ---------- .../tests/test_optional/test_px/test_px.py | 20 ---------------- .../test_px/test_px_functions.py | 24 +------------------ .../test_optional/test_px/test_px_hover.py | 20 ++-------------- .../test_optional/test_px/test_px_input.py | 21 ---------------- .../test_optional/test_px/test_px_wide.py | 13 ---------- .../test_optional/test_px/test_trendline.py | 16 ------------- 8 files changed, 7 insertions(+), 142 deletions(-) diff --git a/packages/python/plotly/plotly/tests/test_optional/test_px/test_facets.py b/packages/python/plotly/plotly/tests/test_optional/test_px/test_facets.py index 1ffd08914d..3b467b10f5 100644 --- a/packages/python/plotly/plotly/tests/test_optional/test_px/test_facets.py +++ b/packages/python/plotly/plotly/tests/test_optional/test_px/test_facets.py @@ -1,21 +1,9 @@ -import pandas as pd -import polars as pl -import pyarrow as pa import plotly.express as px from pytest import approx import pytest import random -constructors = ( - pd.DataFrame, - pl.DataFrame, - pa.table, - lambda d: pd.DataFrame(d).convert_dtypes("pyarrow"), - lambda d: pd.DataFrame(d).convert_dtypes("numpy_nullable"), -) - -@pytest.mark.parametrize("constructor", constructors) def test_facets(constructor): data = px.data.tips().to_dict(orient="list") df = constructor(data) @@ -59,7 +47,6 @@ def test_facets(constructor): assert fig.layout.yaxis4.domain[0] - fig.layout.yaxis.domain[1] == approx(0.08) -@pytest.mark.parametrize("constructor", constructors) def test_facets_with_marginals(constructor): data = px.data.tips().to_dict(orient="list") df = constructor(data) @@ -108,12 +95,11 @@ def test_facets_with_marginals(constructor): assert len(fig.data) == 2 # ignore all marginals -@pytest.fixture(params=constructors) -def bad_facet_spacing_df(request): +def bad_facet_spacing_df(constructor_func): NROWS = 101 NDATA = 1000 categories = [n % NROWS for n in range(NDATA)] - df = request.param( + df = constructor_func( { "x": [random.random() for _ in range(NDATA)], "y": [random.random() for _ in range(NDATA)], @@ -123,8 +109,8 @@ def bad_facet_spacing_df(request): return df -def test_bad_facet_spacing_eror(bad_facet_spacing_df): - df = bad_facet_spacing_df +def test_bad_facet_spacing_error(constructor): + df = bad_facet_spacing_df(constructor_func=constructor) with pytest.raises( ValueError, match="Use the facet_row_spacing argument to adjust this spacing." ): diff --git a/packages/python/plotly/plotly/tests/test_optional/test_px/test_marginals.py b/packages/python/plotly/plotly/tests/test_optional/test_px/test_marginals.py index e09070bc0e..a02bad50b8 100644 --- a/packages/python/plotly/plotly/tests/test_optional/test_px/test_marginals.py +++ b/packages/python/plotly/plotly/tests/test_optional/test_px/test_marginals.py @@ -1,19 +1,7 @@ -import pandas as pd -import polars as pl -import pyarrow as pa import plotly.express as px import pytest -constructors = ( - pd.DataFrame, - pl.DataFrame, - pa.table, - lambda d: pd.DataFrame(d).convert_dtypes("pyarrow"), - lambda d: pd.DataFrame(d).convert_dtypes("numpy_nullable"), -) - -@pytest.mark.parametrize("constructor", constructors) @pytest.mark.parametrize("px_fn", [px.scatter, px.density_heatmap, px.density_contour]) @pytest.mark.parametrize("marginal_x", [None, "histogram", "box", "violin"]) @pytest.mark.parametrize("marginal_y", [None, "rug"]) @@ -27,7 +15,6 @@ def test_xy_marginals(constructor, px_fn, marginal_x, marginal_y): assert len(fig.data) == 1 + (marginal_x is not None) + (marginal_y is not None) -@pytest.mark.parametrize("constructor", constructors) @pytest.mark.parametrize("px_fn", [px.histogram, px.ecdf]) @pytest.mark.parametrize("marginal", [None, "rug", "histogram", "box", "violin"]) @pytest.mark.parametrize("orientation", ["h", "v"]) diff --git a/packages/python/plotly/plotly/tests/test_optional/test_px/test_px.py b/packages/python/plotly/plotly/tests/test_optional/test_px/test_px.py index cbbea84433..f46c3f9f6e 100644 --- a/packages/python/plotly/plotly/tests/test_optional/test_px/test_px.py +++ b/packages/python/plotly/plotly/tests/test_optional/test_px/test_px.py @@ -2,22 +2,10 @@ import plotly.io as pio import narwhals.stable.v1 as nw import numpy as np -import pandas as pd -import polars as pl -import pyarrow as pa import pytest from itertools import permutations -constructors = ( - pd.DataFrame, - pl.DataFrame, - pa.table, - lambda d: pd.DataFrame(d).convert_dtypes("pyarrow"), - lambda d: pd.DataFrame(d).convert_dtypes("numpy_nullable"), -) - -@pytest.mark.parametrize("constructor", constructors) def test_scatter(constructor): data = px.data.iris().to_dict(orient="list") iris = nw.from_native(constructor(data)) @@ -29,7 +17,6 @@ def test_scatter(constructor): assert fig.data[0].mode == "markers" -@pytest.mark.parametrize("constructor", constructors) def test_custom_data_scatter(constructor): data = px.data.iris().to_dict(orient="list") iris = nw.from_native(constructor(data)) @@ -80,7 +67,6 @@ def test_custom_data_scatter(constructor): ) -@pytest.mark.parametrize("constructor", constructors) def test_labels(constructor): data = px.data.tips().to_dict(orient="list") tips = nw.from_native(constructor(data)) @@ -106,7 +92,6 @@ def test_labels(constructor): assert fig.layout.annotations[4].text.startswith("TIME") -@pytest.mark.parametrize("constructor", constructors) @pytest.mark.parametrize( ["extra_kwargs", "expected_mode"], [ @@ -129,7 +114,6 @@ def test_line_mode(constructor, extra_kwargs, expected_mode): assert fig.data[0].mode == expected_mode -@pytest.mark.parametrize("constructor", constructors) def test_px_templates(constructor): try: import plotly.graph_objects as go @@ -300,7 +284,6 @@ def assert_orderings(constructor, days_order, days_check, times_order, times_che assert trace.marker.color == color_sequence[i] -@pytest.mark.parametrize("constructor", constructors) @pytest.mark.parametrize("days", permutations(["Sun", "Sat", "Fri", "x"])) @pytest.mark.parametrize("times", permutations(["Lunch", "x"])) def test_orthogonal_and_missing_orderings(constructor, days, times): @@ -309,7 +292,6 @@ def test_orthogonal_and_missing_orderings(constructor, days, times): ) -@pytest.mark.parametrize("constructor", constructors) @pytest.mark.parametrize("days", permutations(["Sun", "Sat", "Fri", "Thur"])) @pytest.mark.parametrize("times", permutations(["Lunch", "Dinner"])) def test_orthogonal_orderings(constructor, days, times): @@ -322,7 +304,6 @@ def test_permissive_defaults(): px.defaults.should_not_work = "test" -@pytest.mark.parametrize("constructor", constructors) def test_marginal_ranges(constructor): data = px.data.tips().to_dict(orient="list") df = nw.from_native(constructor(data)) @@ -339,7 +320,6 @@ def test_marginal_ranges(constructor): assert fig.layout.yaxis3.range is None -@pytest.mark.parametrize("constructor", constructors) def test_render_mode(constructor): data = px.data.gapminder().to_dict(orient="list") df = nw.from_native(constructor(data)) diff --git a/packages/python/plotly/plotly/tests/test_optional/test_px/test_px_functions.py b/packages/python/plotly/plotly/tests/test_optional/test_px/test_px_functions.py index 69ec2be182..0dea495d0f 100644 --- a/packages/python/plotly/plotly/tests/test_optional/test_px/test_px_functions.py +++ b/packages/python/plotly/plotly/tests/test_optional/test_px/test_px_functions.py @@ -3,21 +3,9 @@ from numpy.testing import assert_array_equal import narwhals.stable.v1 as nw import numpy as np -import pandas as pd -import polars as pl -import pyarrow as pa import pytest -constructors = ( - pd.DataFrame, - pl.DataFrame, - pa.table, - lambda d: pd.DataFrame(d).convert_dtypes("pyarrow"), - lambda d: pd.DataFrame(d).convert_dtypes("numpy_nullable"), -) - - def _compare_figures(go_trace, px_fig): """Compare a figure created with a go trace and a figure created with a px function call. Check that all values inside the go Figure are the @@ -130,7 +118,6 @@ def test_sunburst_treemap_colorscales(): assert list(fig.layout[colorway]) == color_seq -@pytest.mark.parametrize("constructor", constructors) def test_sunburst_treemap_with_path(constructor): vendors = ["A", "B", "C", "D", "E", "F", "G", "H"] sectors = [ @@ -195,7 +182,6 @@ def test_sunburst_treemap_with_path(constructor): assert fig.data[0].values[-1] == 8 -@pytest.mark.parametrize("constructor", constructors) def test_sunburst_treemap_with_path_and_hover(constructor): data = px.data.tips().to_dict(orient="list") df = constructor(data) @@ -227,7 +213,6 @@ def test_sunburst_treemap_with_path_and_hover(constructor): assert "%{hovertext}" not in fig.data[0].hovertemplate -@pytest.mark.parametrize("constructor", constructors) def test_sunburst_treemap_with_path_color(constructor): vendors = ["A", "B", "C", "D", "E", "F", "G", "H"] sectors = [ @@ -306,7 +291,6 @@ def test_sunburst_treemap_with_path_color(constructor): assert np.all(np.sort(colors[:8]) == sorted(calls)) -@pytest.mark.parametrize("constructor", constructors) def test_sunburst_treemap_column_parent(constructor): vendors = ["A", "B", "C", "D", "E", "F", "G", "H"] sectors = [ @@ -334,9 +318,7 @@ def test_sunburst_treemap_column_parent(constructor): px.sunburst(df, path=path, values="values") -@pytest.mark.parametrize("constructor", constructors) def test_sunburst_treemap_with_path_non_rectangular(constructor): - print(str(constructor)) vendors = ["A", "B", "C", "D", None, "E", "F", "G", "H", None] sectors = [ "Tech", @@ -427,7 +409,6 @@ def test_funnel(): assert len(fig.data) == 2 -@pytest.mark.parametrize("constructor", constructors) def test_parcats_dimensions_max(constructor): data = px.data.tips().to_dict(orient="list") df = constructor(data) @@ -462,7 +443,6 @@ def test_parcats_dimensions_max(constructor): assert [d.label for d in fig.data[0].dimensions] == ["sex", "smoker", "day", "size"] -@pytest.mark.parametrize("constructor", constructors) @pytest.mark.parametrize("histfunc,y", [(None, None), ("count", "tip")]) def test_histfunc_hoverlabels_univariate(constructor, histfunc, y): def check_label(label, fig): @@ -496,7 +476,6 @@ def check_label(label, fig): check_label("%s (normalized as %s)" % (histnorm, barnorm), fig) -@pytest.mark.parametrize("constructor", constructors) def test_histfunc_hoverlabels_bivariate(constructor): def check_label(label, fig): assert fig.layout.yaxis.title.text == label @@ -558,9 +537,8 @@ def check_label(label, fig): check_label("density of max of tip", fig) -@pytest.mark.parametrize("constructor", constructors) def test_timeline(request, constructor): - if constructor in {pl.DataFrame, pa.table}: + if "pyarrow_table" in str(constructor) or "polars_eager" in str(constructor): request.applymarker(pytest.mark.xfail) df = constructor( diff --git a/packages/python/plotly/plotly/tests/test_optional/test_px/test_px_hover.py b/packages/python/plotly/plotly/tests/test_optional/test_px/test_px_hover.py index 84b6977afb..fca0c3e0ef 100644 --- a/packages/python/plotly/plotly/tests/test_optional/test_px/test_px_hover.py +++ b/packages/python/plotly/plotly/tests/test_optional/test_px/test_px_hover.py @@ -2,22 +2,10 @@ import narwhals.stable.v1 as nw import numpy as np import pandas as pd -import polars as pl -import pyarrow as pa import pytest from collections import OrderedDict # an OrderedDict is needed for Python 2 -constructors = ( - pd.DataFrame, - pl.DataFrame, - pa.table, - lambda d: pd.DataFrame(d).convert_dtypes("pyarrow"), - lambda d: pd.DataFrame(d).convert_dtypes("numpy_nullable"), -) - - -@pytest.mark.parametrize("constructor", constructors) def test_skip_hover(constructor): data = px.data.iris().to_dict(orient="list") df = constructor(data) @@ -31,7 +19,6 @@ def test_skip_hover(constructor): assert fig.data[0].hovertemplate == "species_id=%{marker.size}" -@pytest.mark.parametrize("constructor", constructors) def test_hover_data_string_column(constructor): data = px.data.tips().to_dict(orient="list") df = constructor(data) @@ -44,7 +31,6 @@ def test_hover_data_string_column(constructor): assert "sex" in fig.data[0].hovertemplate -@pytest.mark.parametrize("constructor", constructors) def test_composite_hover(constructor): data = px.data.tips().to_dict(orient="list") df = constructor(data) @@ -105,7 +91,6 @@ def test_newdatain_hover_data(): ) -@pytest.mark.parametrize("constructor", constructors) def test_formatted_hover_and_labels(constructor): data = px.data.tips().to_dict(orient="list") df = constructor(data) @@ -191,7 +176,6 @@ def test_fail_wrong_column(): ) -@pytest.mark.parametrize("constructor", constructors) def test_sunburst_hoverdict_color(constructor): data = px.data.gapminder().query("year == 2007").to_dict(orient="list") df = constructor(data) @@ -205,10 +189,10 @@ def test_sunburst_hoverdict_color(constructor): assert "color" in fig.data[0].hovertemplate -@pytest.mark.parametrize("constructor", constructors) def test_date_in_hover(request, constructor): - if constructor in {pl.DataFrame, pa.table}: + if "pyarrow_table" in str(constructor) or "polars_eager" in str(constructor): request.applymarker(pytest.mark.xfail) + df = nw.from_native( constructor({"date": ["2015-04-04 19:31:30+01:00"], "value": [3]}) ).with_columns(date=nw.col("date").str.to_datetime(format="%Y-%m-%d %H:%M:%S%z")) diff --git a/packages/python/plotly/plotly/tests/test_optional/test_px/test_px_input.py b/packages/python/plotly/plotly/tests/test_optional/test_px/test_px_input.py index fee268d65a..71ecbf7d41 100644 --- a/packages/python/plotly/plotly/tests/test_optional/test_px/test_px_input.py +++ b/packages/python/plotly/plotly/tests/test_optional/test_px/test_px_input.py @@ -13,13 +13,6 @@ import sys import warnings -constructors = ( - pd.DataFrame, - pl.DataFrame, - pa.table, - lambda d: pd.DataFrame(d).convert_dtypes("pyarrow"), - lambda d: pd.DataFrame(d).convert_dtypes("numpy_nullable"), -) # Fixtures # -------- @@ -74,7 +67,6 @@ def test_with_index(): assert fig.data[0]["hovertemplate"] == "item=%{x}
total_bill=%{y}" -@pytest.mark.parametrize("constructor", constructors) def test_series(request, constructor): if constructor is pa.table: request.applymarker(pytest.mark.xfail) @@ -96,7 +88,6 @@ def test_series(request, constructor): assert fig.data[0].hovertemplate == "day=%{x}
tip=%{y}" -@pytest.mark.parametrize("constructor", constructors) def test_several_dataframes(request, constructor): if constructor is pa.table: request.applymarker(pytest.mark.xfail) @@ -176,7 +167,6 @@ def test_several_dataframes(request, constructor): ) -@pytest.mark.parametrize("constructor", constructors) def test_name_heuristics(request, constructor): if constructor is pa.table: request.applymarker(pytest.mark.xfail) @@ -193,7 +183,6 @@ def test_name_heuristics(request, constructor): assert fig.data[0].hovertemplate == "y=%{marker.size}
x=%{y}" -@pytest.mark.parametrize("constructor", constructors) def test_repeated_name(constructor): data = px.data.iris().to_dict(orient="list") iris = constructor(data) @@ -207,7 +196,6 @@ def test_repeated_name(constructor): assert fig.data[0].customdata.shape[1] == 4 -@pytest.mark.parametrize("constructor", constructors) def test_arrayattrable_numpy(constructor): data = px.data.tips().to_dict(orient="list") tips = constructor(data) @@ -251,7 +239,6 @@ def test_wrong_dimensions_of_array(): assert "All arguments should have the same length." in str(err_msg.value) -@pytest.mark.parametrize("constructor", constructors) def test_wrong_dimensions_mixed_case(constructor): with pytest.raises(ValueError) as err_msg: df = constructor(dict(time=[1, 2, 3], temperature=[20, 30, 25])) @@ -259,7 +246,6 @@ def test_wrong_dimensions_mixed_case(constructor): assert "All arguments should have the same length." in str(err_msg.value) -@pytest.mark.parametrize("constructor", constructors) def test_wrong_dimensions(constructor): data = px.data.tips().to_dict(orient="list") df = constructor(data) @@ -441,7 +427,6 @@ def test_build_df_with_hover_data_from_vaex_and_polars(test_lib, hover_data): ) -@pytest.mark.parametrize("constructor", constructors) def test_timezones(constructor): df = nw.from_native( constructor({"date": ["2015-04-04 19:31:30+01:00"], "value": [3]}) @@ -474,7 +459,6 @@ def test_non_matching_index(): assert_frame_equal(expected, out["data_frame"].to_pandas()) -@pytest.mark.parametrize("constructor", constructors) def test_splom_case(constructor): data = px.data.iris().to_dict(orient="list") iris = constructor(data) @@ -488,7 +472,6 @@ def test_splom_case(constructor): assert np.all(fig.data[0].dimensions[0].values == ar[:, 0]) -@pytest.mark.parametrize("constructor", constructors) def test_int_col_names(constructor): # DataFrame with int column names lengths = constructor({"0": np.random.random(100)}) @@ -506,7 +489,6 @@ def test_data_frame_from_dict(): assert np.all(fig.data[0].x == [0, 1]) -@pytest.mark.parametrize("constructor", constructors) def test_arguments_not_modified(constructor): data = px.data.iris().to_dict(orient="list") iris = nw.from_native(constructor(data)) @@ -517,7 +499,6 @@ def test_arguments_not_modified(constructor): assert iris.get_column("sepal_length").to_native().equals(hover_data[0]) -@pytest.mark.parametrize("constructor", constructors) def test_pass_df_columns(constructor): data = px.data.tips().to_dict(orient="list") tips = nw.from_native(constructor(data)) @@ -535,7 +516,6 @@ def test_pass_df_columns(constructor): assert tips_copy.columns == tips.columns -@pytest.mark.parametrize("constructor", constructors) def test_size_column(request, constructor): if constructor is pa.table: request.applymarker(pytest.mark.xfail) @@ -671,7 +651,6 @@ def test_auto_histfunc(): assert px.density_heatmap(x=a, y=a, z=a, histfunc="avg").data[0].histfunc == "avg" -@pytest.mark.parametrize("constructor", constructors) @pytest.mark.parametrize( "fn,mode", [(px.violin, "violinmode"), (px.box, "boxmode"), (px.strip, "boxmode")] ) diff --git a/packages/python/plotly/plotly/tests/test_optional/test_px/test_px_wide.py b/packages/python/plotly/plotly/tests/test_optional/test_px/test_px_wide.py index 7c668493f0..db4221dfc1 100644 --- a/packages/python/plotly/plotly/tests/test_optional/test_px/test_px_wide.py +++ b/packages/python/plotly/plotly/tests/test_optional/test_px/test_px_wide.py @@ -3,23 +3,12 @@ import narwhals.stable.v1 as nw import numpy as np import pandas as pd -import polars as pl -import pyarrow as pa from plotly.express._core import build_dataframe, _is_col_list from pandas.testing import assert_frame_equal import pytest import warnings -constructors = ( - pd.DataFrame, - pl.DataFrame, - pa.table, - lambda d: pd.DataFrame(d).convert_dtypes("pyarrow"), - lambda d: pd.DataFrame(d).convert_dtypes("numpy_nullable"), -) - -@pytest.mark.parametrize("constructor", constructors) def test_is_col_list(constructor): df_input = nw.from_native(constructor(dict(a=[1, 2], b=[1, 2]))) native_namespace = df_input.__native_namespace__() @@ -854,14 +843,12 @@ def test_mixed_input_error(df): ) -@pytest.mark.parametrize("constructor", constructors) def test_mixed_number_input(constructor): df = constructor(dict(a=[1, 2], b=[1.1, 2.1])) fig = px.line(df) assert len(fig.data) == 2 -@pytest.mark.parametrize("constructor", constructors) def test_line_group(constructor): df = constructor( { diff --git a/packages/python/plotly/plotly/tests/test_optional/test_px/test_trendline.py b/packages/python/plotly/plotly/tests/test_optional/test_px/test_trendline.py index 0f897e8f5f..aeded40e07 100644 --- a/packages/python/plotly/plotly/tests/test_optional/test_px/test_trendline.py +++ b/packages/python/plotly/plotly/tests/test_optional/test_px/test_trendline.py @@ -1,23 +1,10 @@ import plotly.express as px import narwhals.stable.v1 as nw import numpy as np -import pandas as pd -import polars as pl -import pyarrow as pa import pytest from datetime import datetime -constructors = ( - pd.DataFrame, - pl.DataFrame, - pa.table, - lambda d: pd.DataFrame(d).convert_dtypes("pyarrow"), - lambda d: pd.DataFrame(d).convert_dtypes("numpy_nullable"), -) - - -@pytest.mark.parametrize("constructor", constructors) @pytest.mark.parametrize( "mode,options", [ @@ -111,7 +98,6 @@ def test_trendline_enough_values(mode, options): assert len(fig.data[1].x) == 2 -@pytest.mark.parametrize("constructor", constructors) @pytest.mark.parametrize( "mode,options", [ @@ -187,7 +173,6 @@ def test_ols_trendline_slopes(): assert "y = 0 * x + 1.5
" in fig.data[1].hovertemplate -@pytest.mark.parametrize("constructor", constructors) @pytest.mark.parametrize( "mode,options", [ @@ -232,7 +217,6 @@ def test_trendline_on_timeseries(constructor, mode, options): assert str(fig.data[0].x[0]) == str(fig.data[1].x[0]) -@pytest.mark.parametrize("constructor", constructors) def test_overall_trendline(constructor): df = nw.from_native(constructor(px.data.tips().to_dict(orient="list"))) fig1 = px.scatter(df.to_native(), x="total_bill", y="tip", trendline="ols") From 1b274874325394ccbbcb2c7e1bf51ad8aa4abf37 Mon Sep 17 00:00:00 2001 From: FBruzzesi Date: Tue, 15 Oct 2024 22:00:47 +0200 Subject: [PATCH 042/106] address feedbacks --- packages/python/plotly/plotly/express/__init__.py | 9 --------- packages/python/plotly/plotly/express/_core.py | 13 ++++++++----- packages/python/plotly/plotly/express/_doc.py | 2 +- 3 files changed, 9 insertions(+), 15 deletions(-) diff --git a/packages/python/plotly/plotly/express/__init__.py b/packages/python/plotly/plotly/express/__init__.py index 3b1da0829a..460ce1b998 100644 --- a/packages/python/plotly/plotly/express/__init__.py +++ b/packages/python/plotly/plotly/express/__init__.py @@ -2,15 +2,6 @@ `plotly.express` is a terse, consistent, high-level wrapper around `plotly.graph_objects` for rapid data exploration and figure generation. Learn more at https://plotly.com/python/plotly-express/ """ -from plotly import optional_imports - -nw = optional_imports.get_module("narwhals") -if nw is None: - raise ImportError( - """\ -Plotly express requires narwhals to be installed.""" - ) - from ._imshow import imshow from ._chart_types import ( # noqa: F401 scatter, diff --git a/packages/python/plotly/plotly/express/_core.py b/packages/python/plotly/plotly/express/_core.py index c2d58ebb5d..77336cff92 100644 --- a/packages/python/plotly/plotly/express/_core.py +++ b/packages/python/plotly/plotly/express/_core.py @@ -1148,7 +1148,7 @@ def to_unindexed_series(x, name=None, native_namespace=None): elif (pd := nw.dependencies.get_pandas()) is not None: return nw.new_series(name=name, values=x, native_namespace=pd) else: - msg = "Pandas is required" + msg = "Pandas installation is required if no namespace is provided." raise NotImplementedError(msg) @@ -1381,7 +1381,7 @@ def process_args_into_dataframe( df_output.update( { - # constant is single value. repeat by len to avoid creating NaN on concating + # constant is single value. repeat by len to avoid creating NaN on concatenating col_name: nw.new_series( name=col_name, values=[constants[col_name]] * length, @@ -2645,10 +2645,13 @@ def make_figure(args, constructor, trace_patch=None, layout_patch=None): trendline_rows.append(dict(px_fit_results=fit_results)) if trendline_rows: - if (pd := nw.dependencies.get_pandas()) is None: - msg = "Trendlines require pandas to be installed" + try: + import pandas as pd + + fig._px_trendlines = pd.DataFrame(trendline_rows) + except ImportError: + msg = "Trendlines require pandas to be installed." raise NotImplementedError(msg) - fig._px_trendlines = pd.DataFrame(trendline_rows) else: fig._px_trendlines = [] diff --git a/packages/python/plotly/plotly/express/_doc.py b/packages/python/plotly/plotly/express/_doc.py index 75f09b6fab..b0c41b40fd 100644 --- a/packages/python/plotly/plotly/express/_doc.py +++ b/packages/python/plotly/plotly/express/_doc.py @@ -502,7 +502,7 @@ zoom=["int (default `8`)", "Between 0 and 20.", "Sets map zoom level."], orientation=[ "str, one of `'h'` for horizontal or `'v'` for vertical. ", - "(default `'v'` if `x` and `y` are provided and both continous or both categorical, ", + "(default `'v'` if `x` and `y` are provided and both continuous or both categorical, ", "otherwise `'v'`(`'h'`) if `x`(`y`) is categorical and `y`(`x`) is continuous, ", "otherwise `'v'`(`'h'`) if only `x`(`y`) is provided) ", ], From 23a23be146d309cf4ccf5f5c3b8e2ab33b96e711 Mon Sep 17 00:00:00 2001 From: FBruzzesi Date: Tue, 15 Oct 2024 22:01:06 +0200 Subject: [PATCH 043/106] typos --- .../tests/test_optional/test_tools/test_figure_factory.py | 6 +++--- .../plotly/tests/test_optional/test_utils/test_utils.py | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/python/plotly/plotly/tests/test_optional/test_tools/test_figure_factory.py b/packages/python/plotly/plotly/tests/test_optional/test_tools/test_figure_factory.py index 22dfa89199..bcebfa2914 100644 --- a/packages/python/plotly/plotly/tests/test_optional/test_tools/test_figure_factory.py +++ b/packages/python/plotly/plotly/tests/test_optional/test_tools/test_figure_factory.py @@ -903,7 +903,7 @@ def test_simple_annotated_heatmap(self): def test_annotated_heatmap_kwargs(self): # we should be able to create an annotated heatmap with x and y axes - # lables, a defined colorscale, and supplied text. + # labels, a defined colorscale, and supplied text. z = [[1, 0], [0.25, 0.75], [0.45, 0.5]] text = [["first", "second"], ["third", "fourth"], ["fifth", "sixth"]] @@ -999,7 +999,7 @@ def test_annotated_heatmap_kwargs(self): def test_annotated_heatmap_reversescale(self): # we should be able to create an annotated heatmap with x and y axes - # lables, a defined colorscale, and supplied text. + # labels, a defined colorscale, and supplied text. z = [[1, 0], [0.25, 0.75], [0.45, 0.5]] text = [["first", "second"], ["third", "fourth"], ["fifth", "sixth"]] @@ -1222,7 +1222,7 @@ def test_fontcolor_input(self): def test_simple_table(self): - # we should be able to create a striped table by suppling a text matrix + # we should be able to create a striped table by supplying a text matrix text = [ ["Country", "Year", "Population"], diff --git a/packages/python/plotly/plotly/tests/test_optional/test_utils/test_utils.py b/packages/python/plotly/plotly/tests/test_optional/test_utils/test_utils.py index cf32e1bdff..000c79d57f 100644 --- a/packages/python/plotly/plotly/tests/test_optional/test_utils/test_utils.py +++ b/packages/python/plotly/plotly/tests/test_optional/test_utils/test_utils.py @@ -62,7 +62,7 @@ def test_encode_as_plotly(self): utils.NotEncodable, utils.PlotlyJSONEncoder.encode_as_plotly, obj ) - # should return without exception when obj has `to_plotly_josn` attr + # should return without exception when obj has `to_plotly_json` attr expected_res = "wedidit" class ObjWithAttr(object): @@ -274,7 +274,7 @@ def test_encode_customdata_datetime_series(self): ) ) - def test_encode_customdata_datetime_homogenous_dataframe(self): + def test_encode_customdata_datetime_homogeneous_dataframe(self): df = pd.DataFrame( dict( t1=pd.to_datetime(["2010-01-01", "2010-01-02"]), @@ -297,7 +297,7 @@ def test_encode_customdata_datetime_homogenous_dataframe(self): ) ) - def test_encode_customdata_datetime_inhomogenous_dataframe(self): + def test_encode_customdata_datetime_inhomogeneous_dataframe(self): df = pd.DataFrame( dict( t=pd.to_datetime(["2010-01-01", "2010-01-02"]), From 7968cffca1e8244eed08f97297a6d096c05460be Mon Sep 17 00:00:00 2001 From: FBruzzesi Date: Tue, 15 Oct 2024 22:01:15 +0200 Subject: [PATCH 044/106] conftest --- .../python/plotly/plotly/tests/conftest.py | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 packages/python/plotly/plotly/tests/conftest.py diff --git a/packages/python/plotly/plotly/tests/conftest.py b/packages/python/plotly/plotly/tests/conftest.py new file mode 100644 index 0000000000..27e4899fbe --- /dev/null +++ b/packages/python/plotly/plotly/tests/conftest.py @@ -0,0 +1,46 @@ +from typing import Any +from typing import Callable + +import pandas as pd +import polars as pl +import pyarrow as pa +import pytest + +from narwhals.typing import IntoDataFrame +from narwhals.utils import parse_version + + +def pandas_constructor(obj: dict[str, Any]) -> IntoDataFrame: + return pd.DataFrame(obj) # type: ignore[no-any-return] + + +def pandas_nullable_constructor(obj: dict[str, Any]) -> IntoDataFrame: + return pd.DataFrame(obj).convert_dtypes(dtype_backend="numpy_nullable") # type: ignore[no-any-return] + + +def pandas_pyarrow_constructor(obj: dict[str, Any]) -> IntoDataFrame: + return pd.DataFrame(obj).convert_dtypes(dtype_backend="pyarrow") # type: ignore[no-any-return] + + +def polars_eager_constructor(obj: dict[str, Any]) -> IntoDataFrame: + return pl.DataFrame(obj) + + +def pyarrow_table_constructor(obj: dict[str, Any]) -> IntoDataFrame: + return pa.table(obj) # type: ignore[no-any-return] + + +constructors = [polars_eager_constructor, pyarrow_table_constructor, pandas_constructor] + +if parse_version(pd.__version__) >= parse_version("2.0.0"): + constructors = [ + pandas_nullable_constructor, + pandas_pyarrow_constructor, + ] + + +@pytest.fixture(params=constructors) +def constructor( + request: pytest.FixtureRequest, +) -> Callable[[dict[str, Any]], IntoDataFrame]: + return request.param # type: ignore[no-any-return] From 91db84bec524727f9ed70b85e3d88488318063a8 Mon Sep 17 00:00:00 2001 From: FBruzzesi Date: Tue, 15 Oct 2024 22:20:11 +0200 Subject: [PATCH 045/106] mock interchange --- .../python/plotly/plotly/express/_core.py | 2 +- .../test_optional/test_px/test_px_input.py | 124 ++++++------------ 2 files changed, 38 insertions(+), 88 deletions(-) diff --git a/packages/python/plotly/plotly/express/_core.py b/packages/python/plotly/plotly/express/_core.py index 4dbc189169..5f3d7421aa 100644 --- a/packages/python/plotly/plotly/express/_core.py +++ b/packages/python/plotly/plotly/express/_core.py @@ -1468,7 +1468,7 @@ def build_dataframe(args, constructor): args["data_frame"] = nw.from_native( nw.from_native( args["data_frame"], eager_or_interchange_only=True - ).to_pandas(), + ).to_pandas(), # Converts to pandas eager_only=True, ) columns = args["data_frame"].columns diff --git a/packages/python/plotly/plotly/tests/test_optional/test_px/test_px_input.py b/packages/python/plotly/plotly/tests/test_optional/test_px/test_px_input.py index 71ecbf7d41..3dbf3c254d 100644 --- a/packages/python/plotly/plotly/tests/test_optional/test_px/test_px_input.py +++ b/packages/python/plotly/plotly/tests/test_optional/test_px/test_px_input.py @@ -3,8 +3,6 @@ import narwhals.stable.v1 as nw import numpy as np import pandas as pd -import polars as pl -import pyarrow as pa import pytest from packaging import version import unittest.mock as mock @@ -14,21 +12,6 @@ import warnings -# Fixtures -# -------- -@pytest.fixture -def add_interchange_module_for_old_pandas(): - if not hasattr(pd.api, "interchange"): - with mock.patch.object(pd.api, "interchange", mock.MagicMock(), create=True): - # to make the following import work: `import pandas.api.interchange` - with mock.patch.dict( - "sys.modules", {"pandas.api.interchange": pd.api.interchange} - ): - yield - else: - yield - - def test_numpy(): fig = px.scatter(x=[1, 2, 3], y=[2, 3, 4], color=[1, 3, 9]) assert np.all(fig.data[0].x == np.array([1, 2, 3])) @@ -68,7 +51,7 @@ def test_with_index(): def test_series(request, constructor): - if constructor is pa.table: + if "pyarrow_table" in str(constructor): request.applymarker(pytest.mark.xfail) data = px.data.tips().to_dict(orient="list") @@ -89,7 +72,7 @@ def test_series(request, constructor): def test_several_dataframes(request, constructor): - if constructor is pa.table: + if "pyarrow_table" in str(constructor): request.applymarker(pytest.mark.xfail) df = nw.from_native(constructor(dict(x=[0, 1], y=[1, 10], z=[0.1, 0.8]))) @@ -168,7 +151,7 @@ def test_several_dataframes(request, constructor): def test_name_heuristics(request, constructor): - if constructor is pa.table: + if "pyarrow_table" in str(constructor): request.applymarker(pytest.mark.xfail) df = nw.from_native(constructor(dict(x=[0, 1], y=[3, 4], z=[0.1, 0.2]))) @@ -305,72 +288,39 @@ def test_build_df_with_index(): ) -# TODO: Changed how this is accomplished in the first place -# @pytest.mark.parametrize("column_names_as_generator", [False, True]) -# def test_build_df_using_interchange_protocol_mock( -# add_interchange_module_for_old_pandas, column_names_as_generator -# ): -# class InterchangeDataFrame: -# def __init__(self, columns): -# self._columns = columns - -# if column_names_as_generator: - -# def column_names(self): -# for col in self._columns: -# yield col - -# else: - -# def column_names(self): -# return self._columns - -# interchange_dataframe = InterchangeDataFrame( -# ["petal_width", "sepal_length", "sepal_width"] -# ) -# interchange_dataframe_reduced = InterchangeDataFrame( -# ["petal_width", "sepal_length"] -# ) -# interchange_dataframe.select_columns_by_name = mock.MagicMock( -# return_value=interchange_dataframe_reduced -# ) -# interchange_dataframe_reduced.select_columns_by_name = mock.MagicMock( -# return_value=interchange_dataframe_reduced -# ) - -# class CustomDataFrame: -# def __dataframe__(self): -# return interchange_dataframe - -# class CustomDataFrameReduced: -# def __dataframe__(self): -# return interchange_dataframe_reduced - -# input_dataframe = CustomDataFrame() -# input_dataframe_reduced = CustomDataFrameReduced() - -# iris_pandas = px.data.iris() - -# with mock.patch("pandas.__version__", "2.0.2"): -# args = dict(data_frame=input_dataframe, x="petal_width", y="sepal_length") -# with mock.patch( -# "pandas.api.interchange.from_dataframe", return_value=iris_pandas -# ) as mock_from_dataframe: -# build_dataframe(args, go.Scatter) -# mock_from_dataframe.assert_called_once_with(interchange_dataframe_reduced) -# assert set(interchange_dataframe.select_columns_by_name.call_args[0][0]) == { -# "petal_width", -# "sepal_length", -# } - -# args = dict(data_frame=input_dataframe_reduced, color=None) -# with mock.patch( -# "pandas.api.interchange.from_dataframe", -# return_value=iris_pandas[["petal_width", "sepal_length"]], -# ) as mock_from_dataframe: -# build_dataframe(args, go.Scatter) -# mock_from_dataframe.assert_called_once_with(interchange_dataframe_reduced) -# interchange_dataframe_reduced.select_columns_by_name.assert_not_called() +def test_build_df_using_interchange_protocol_mock(): + class InterchangeDataFrame: + def __init__(self, columns): + self._columns = columns + + def column_names(self): + return self._columns + + interchange_dataframe = InterchangeDataFrame( + ["petal_width", "sepal_length", "sepal_width"] + ) + + class CustomDataFrame: + def __dataframe__(self): + return interchange_dataframe + + input_dataframe = CustomDataFrame() + + iris_pandas = px.data.iris() + + args = dict(data_frame=input_dataframe, x="petal_width", y="sepal_length") + with mock.patch( + "narwhals._interchange.dataframe.InterchangeFrame.to_pandas", + return_value=iris_pandas, + ) as mock_from_dataframe: + out = build_dataframe(args, go.Scatter) + + mock_from_dataframe.assert_called_once() + + assert_frame_equal( + iris_pandas.reset_index()[out["data_frame"].columns], + out["data_frame"].to_pandas(), + ) @pytest.mark.skipif( @@ -517,7 +467,7 @@ def test_pass_df_columns(constructor): def test_size_column(request, constructor): - if constructor is pa.table: + if "pyarrow_table" in str(constructor): request.applymarker(pytest.mark.xfail) data = px.data.tips().to_dict(orient="list") tips = nw.from_native(constructor(data)) From 5c6772e9ef9b1450fd9710a1c1be9fcc12d5b053 Mon Sep 17 00:00:00 2001 From: FBruzzesi Date: Tue, 15 Oct 2024 22:27:54 +0200 Subject: [PATCH 046/106] optional requirements --- .../plotly/test_requirements/requirements_310_optional.txt | 1 + .../plotly/test_requirements/requirements_311_optional.txt | 1 + .../test_requirements/requirements_312_no_numpy_optional.txt | 1 + .../plotly/test_requirements/requirements_312_optional.txt | 1 + .../python/plotly/test_requirements/requirements_38_optional.txt | 1 + .../python/plotly/test_requirements/requirements_39_optional.txt | 1 + .../test_requirements/requirements_39_pandas_2_optional.txt | 1 + 7 files changed, 7 insertions(+) diff --git a/packages/python/plotly/test_requirements/requirements_310_optional.txt b/packages/python/plotly/test_requirements/requirements_310_optional.txt index 0e468bfa9f..813a938bf8 100644 --- a/packages/python/plotly/test_requirements/requirements_310_optional.txt +++ b/packages/python/plotly/test_requirements/requirements_310_optional.txt @@ -22,3 +22,4 @@ kaleido orjson==3.8.12 polars pyarrow +narwhals>=1.9.2 diff --git a/packages/python/plotly/test_requirements/requirements_311_optional.txt b/packages/python/plotly/test_requirements/requirements_311_optional.txt index 0c450275bb..989fc28fd6 100644 --- a/packages/python/plotly/test_requirements/requirements_311_optional.txt +++ b/packages/python/plotly/test_requirements/requirements_311_optional.txt @@ -22,3 +22,4 @@ kaleido orjson==3.8.12 polars pyarrow +narwhals>=1.9.2 diff --git a/packages/python/plotly/test_requirements/requirements_312_no_numpy_optional.txt b/packages/python/plotly/test_requirements/requirements_312_no_numpy_optional.txt index caa3adf291..d850f5ea42 100644 --- a/packages/python/plotly/test_requirements/requirements_312_no_numpy_optional.txt +++ b/packages/python/plotly/test_requirements/requirements_312_no_numpy_optional.txt @@ -21,3 +21,4 @@ kaleido orjson==3.9.10 polars pyarrow +narwhals>=1.9.2 diff --git a/packages/python/plotly/test_requirements/requirements_312_optional.txt b/packages/python/plotly/test_requirements/requirements_312_optional.txt index 6803db7e97..cfd9f6ac86 100644 --- a/packages/python/plotly/test_requirements/requirements_312_optional.txt +++ b/packages/python/plotly/test_requirements/requirements_312_optional.txt @@ -22,3 +22,4 @@ kaleido orjson==3.9.10 polars pyarrow +narwhals>=1.9.2 diff --git a/packages/python/plotly/test_requirements/requirements_38_optional.txt b/packages/python/plotly/test_requirements/requirements_38_optional.txt index 15c383e8c2..1c8ce7a480 100644 --- a/packages/python/plotly/test_requirements/requirements_38_optional.txt +++ b/packages/python/plotly/test_requirements/requirements_38_optional.txt @@ -22,3 +22,4 @@ psutil==5.7.0 kaleido polars pyarrow +narwhals>=1.9.2 diff --git a/packages/python/plotly/test_requirements/requirements_39_optional.txt b/packages/python/plotly/test_requirements/requirements_39_optional.txt index 2f462f5dd6..3e304118da 100644 --- a/packages/python/plotly/test_requirements/requirements_39_optional.txt +++ b/packages/python/plotly/test_requirements/requirements_39_optional.txt @@ -23,3 +23,4 @@ kaleido orjson==3.8.12 polars pyarrow +narwhals>=1.9.2 diff --git a/packages/python/plotly/test_requirements/requirements_39_pandas_2_optional.txt b/packages/python/plotly/test_requirements/requirements_39_pandas_2_optional.txt index e8d1303b09..bfcf1b0d5f 100644 --- a/packages/python/plotly/test_requirements/requirements_39_pandas_2_optional.txt +++ b/packages/python/plotly/test_requirements/requirements_39_pandas_2_optional.txt @@ -23,3 +23,4 @@ vaex pydantic<=1.10.11 # for vaex, see https://github.com/vaexio/vaex/issues/2384 polars pyarrow +narwhals>=1.9.2 From 9ec3f9e61f96834b8bbaed893ac3e4a3814048aa Mon Sep 17 00:00:00 2001 From: FBruzzesi Date: Tue, 15 Oct 2024 22:35:37 +0200 Subject: [PATCH 047/106] move conftest in express folder --- .../{ => test_optional/test_px}/conftest.py | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) rename packages/python/plotly/plotly/tests/{ => test_optional/test_px}/conftest.py (63%) diff --git a/packages/python/plotly/plotly/tests/conftest.py b/packages/python/plotly/plotly/tests/test_optional/test_px/conftest.py similarity index 63% rename from packages/python/plotly/plotly/tests/conftest.py rename to packages/python/plotly/plotly/tests/test_optional/test_px/conftest.py index 27e4899fbe..585ff9e7a3 100644 --- a/packages/python/plotly/plotly/tests/conftest.py +++ b/packages/python/plotly/plotly/tests/test_optional/test_px/conftest.py @@ -1,6 +1,3 @@ -from typing import Any -from typing import Callable - import pandas as pd import polars as pl import pyarrow as pa @@ -10,23 +7,23 @@ from narwhals.utils import parse_version -def pandas_constructor(obj: dict[str, Any]) -> IntoDataFrame: +def pandas_constructor(obj) -> IntoDataFrame: return pd.DataFrame(obj) # type: ignore[no-any-return] -def pandas_nullable_constructor(obj: dict[str, Any]) -> IntoDataFrame: +def pandas_nullable_constructor(obj) -> IntoDataFrame: return pd.DataFrame(obj).convert_dtypes(dtype_backend="numpy_nullable") # type: ignore[no-any-return] -def pandas_pyarrow_constructor(obj: dict[str, Any]) -> IntoDataFrame: +def pandas_pyarrow_constructor(obj) -> IntoDataFrame: return pd.DataFrame(obj).convert_dtypes(dtype_backend="pyarrow") # type: ignore[no-any-return] -def polars_eager_constructor(obj: dict[str, Any]) -> IntoDataFrame: +def polars_eager_constructor(obj) -> IntoDataFrame: return pl.DataFrame(obj) -def pyarrow_table_constructor(obj: dict[str, Any]) -> IntoDataFrame: +def pyarrow_table_constructor(obj) -> IntoDataFrame: return pa.table(obj) # type: ignore[no-any-return] @@ -40,7 +37,5 @@ def pyarrow_table_constructor(obj: dict[str, Any]) -> IntoDataFrame: @pytest.fixture(params=constructors) -def constructor( - request: pytest.FixtureRequest, -) -> Callable[[dict[str, Any]], IntoDataFrame]: +def constructor(request: pytest.FixtureRequest): return request.param # type: ignore[no-any-return] From 400a62467ac623bbe1ca6cebaea75f055f5e8e8f Mon Sep 17 00:00:00 2001 From: FBruzzesi Date: Wed, 16 Oct 2024 00:04:40 +0200 Subject: [PATCH 048/106] hotfix and figure_factory hexbin --- .../python/plotly/plotly/express/_core.py | 2 +- .../plotly/figure_factory/_hexbin_mapbox.py | 80 +++++++++++++------ .../test_optional/test_px/test_trendline.py | 2 +- 3 files changed, 57 insertions(+), 27 deletions(-) diff --git a/packages/python/plotly/plotly/express/_core.py b/packages/python/plotly/plotly/express/_core.py index 5f3d7421aa..3601c6741b 100644 --- a/packages/python/plotly/plotly/express/_core.py +++ b/packages/python/plotly/plotly/express/_core.py @@ -2522,7 +2522,7 @@ def make_figure(args, constructor, trace_patch=None, layout_patch=None): if args.get("ecdfmode", "standard") == "complementary": group = group.with_columns( - **{var: -nw.col(var) + nw.lit(group_sum)} + **{var: (nw.col(var) - nw.lit(group_sum)) * (-1)} ) if args["ecdfnorm"] == "probability": diff --git a/packages/python/plotly/plotly/figure_factory/_hexbin_mapbox.py b/packages/python/plotly/plotly/figure_factory/_hexbin_mapbox.py index 38a81d1afc..8b2233d986 100644 --- a/packages/python/plotly/plotly/figure_factory/_hexbin_mapbox.py +++ b/packages/python/plotly/plotly/figure_factory/_hexbin_mapbox.py @@ -1,8 +1,8 @@ from plotly.express._core import build_dataframe from plotly.express._doc import make_docstring from plotly.express._chart_types import choropleth_mapbox, scatter_mapbox +import narwhals.stable.v1 as nw import numpy as np -import pandas as pd def _project_latlon_to_wgs84(lat, lon): @@ -231,6 +231,7 @@ def _compute_wgs84_hexbin( nx=None, agg_func=None, min_count=None, + native_namespace=None, ): """ Computes the lat-lon aggregation at hexagonal bin level. @@ -263,7 +264,7 @@ def _compute_wgs84_hexbin( Lat coordinates of each hexagon (shape M x 6) np.ndarray Lon coordinates of each hexagon (shape M x 6) - pd.Series + nw.Series Unique id for each hexagon, to be used in the geojson data (shape M) np.ndarray Aggregated value in each hexagon (shape M) @@ -288,7 +289,14 @@ def _compute_wgs84_hexbin( # Create unique feature id based on hexagon center centers = centers.astype(str) - hexagons_ids = pd.Series(centers[:, 0]) + "," + pd.Series(centers[:, 1]) + hexagons_ids = ( + nw.from_dict( + {"x1": centers[:, 0], "x2": centers[:, 1]}, + native_namespace=native_namespace, + ) + .select(hexagons_ids=nw.concat_str([nw.col("x1"), nw.col("x2")], separator=",")) + .get_column("hexagons_ids") + ) return hexagons_lats, hexagons_lons, hexagons_ids, agreggated_value @@ -344,22 +352,40 @@ def create_hexbin_mapbox( Returns a figure aggregating scattered points into connected hexagons """ args = build_dataframe(args=locals(), constructor=None) - + native_namespace = nw.get_native_namespace(args["data_frame"]) if agg_func is None: agg_func = np.mean - lat_range = args["data_frame"][args["lat"]].agg(["min", "max"]).values - lon_range = args["data_frame"][args["lon"]].agg(["min", "max"]).values + lat_range = ( + args["data_frame"] + .select( + nw.min(args["lat"]).name.suffix("_min"), + nw.max(args["lat"]).name.suffix("_max"), + ) + .to_numpy() + .squeeze() + ) + + lon_range = ( + args["data_frame"] + .select( + nw.min(args["lon"]).name.suffix("_min"), + nw.max(args["lon"]).name.suffix("_max"), + ) + .to_numpy() + .squeeze() + ) hexagons_lats, hexagons_lons, hexagons_ids, count = _compute_wgs84_hexbin( - lat=args["data_frame"][args["lat"]].values, - lon=args["data_frame"][args["lon"]].values, + lat=args["data_frame"].get_column(args["lat"]).to_numpy(), + lon=args["data_frame"].get_column(args["lon"]).to_numpy(), lat_range=lat_range, lon_range=lon_range, color=None, nx=nx_hexagon, agg_func=agg_func, min_count=min_count, + native_namespace=native_namespace, ) geojson = _hexagons_to_geojson(hexagons_lats, hexagons_lons, hexagons_ids) @@ -381,41 +407,43 @@ def create_hexbin_mapbox( center = dict(lat=lat_range.mean(), lon=lon_range.mean()) if args["animation_frame"] is not None: - groups = args["data_frame"].groupby(args["animation_frame"]).groups + groups = dict(args["data_frame"].group_by(args["animation_frame"]).__iter__()) else: - groups = {0: args["data_frame"].index} + groups = {0: args["data_frame"]} agg_data_frame_list = [] - for frame, index in groups.items(): - df = args["data_frame"].loc[index] + for key, df in groups.items(): _, _, hexagons_ids, aggregated_value = _compute_wgs84_hexbin( - lat=df[args["lat"]].values, - lon=df[args["lon"]].values, + lat=df.get_column(args["lat"]).to_numpy(), + lon=df.get_column(args["lon"]).to_numpy(), lat_range=lat_range, lon_range=lon_range, - color=df[args["color"]].values if args["color"] else None, + color=df.get_column(args["color"]).to_numpy() if args["color"] else None, nx=nx_hexagon, agg_func=agg_func, min_count=min_count, + native_namespace=native_namespace, ) agg_data_frame_list.append( - pd.DataFrame( - np.c_[hexagons_ids, aggregated_value], columns=["locations", "color"] + nw.from_dict( + { + "frame": [key] * len(hexagons_ids), + "locations": hexagons_ids, + "color": aggregated_value, + }, + native_namespace=native_namespace, ) ) - agg_data_frame = ( - pd.concat(agg_data_frame_list, axis=0, keys=groups.keys()) - .rename_axis(index=("frame", "index")) - .reset_index("frame") - ) - agg_data_frame["color"] = pd.to_numeric(agg_data_frame["color"]) + agg_data_frame = nw.concat(agg_data_frame_list, how="vertical").with_columns( + color=nw.col("color").cast(nw.Float64) + ) if range_color is None: range_color = [agg_data_frame["color"].min(), agg_data_frame["color"].max()] fig = choropleth_mapbox( - data_frame=agg_data_frame, + data_frame=agg_data_frame.to_native(), geojson=geojson, locations="locations", color="color", @@ -440,7 +468,9 @@ def create_hexbin_mapbox( if show_original_data: original_fig = scatter_mapbox( data_frame=( - args["data_frame"].sort_values(by=args["animation_frame"]) + args["data_frame"].sort( + by=args["animation_frame"], descending=False, nulls_last=True + ) if args["animation_frame"] is not None else args["data_frame"] ), diff --git a/packages/python/plotly/plotly/tests/test_optional/test_px/test_trendline.py b/packages/python/plotly/plotly/tests/test_optional/test_px/test_trendline.py index aeded40e07..f6bd2cace1 100644 --- a/packages/python/plotly/plotly/tests/test_optional/test_px/test_trendline.py +++ b/packages/python/plotly/plotly/tests/test_optional/test_px/test_trendline.py @@ -188,7 +188,7 @@ def test_ols_trendline_slopes(): def test_trendline_on_timeseries(constructor, mode, options): df = nw.from_native(constructor(px.data.stocks().to_dict(orient="list"))) - pd_err_msg = "Could not convert value of 'x' \('date'\) into a numeric type." + pd_err_msg = r"Could not convert value of 'x' \('date'\) into a numeric type." pl_err_msg = "conversion from `str` to `f64` failed in column 'date'" with pytest.raises(Exception, match=rf"({pd_err_msg}|{pl_err_msg})"): From 1aa5163c28665fd961cd7c0e2b51184a8a7539a2 Mon Sep 17 00:00:00 2001 From: FBruzzesi Date: Wed, 16 Oct 2024 08:21:53 +0200 Subject: [PATCH 049/106] old versions, polars[timezone], hotfix --- packages/python/plotly/plotly/express/_core.py | 4 +--- .../plotly/plotly/figure_factory/_hexbin_mapbox.py | 2 +- .../plotly/tests/test_optional/test_px/test_trendline.py | 9 +++++---- .../test_requirements/requirements_310_optional.txt | 2 +- .../test_requirements/requirements_311_optional.txt | 2 +- .../requirements_312_no_numpy_optional.txt | 2 +- .../test_requirements/requirements_312_optional.txt | 2 +- .../test_requirements/requirements_38_optional.txt | 2 +- .../test_requirements/requirements_39_optional.txt | 2 +- .../requirements_39_pandas_2_optional.txt | 2 +- 10 files changed, 14 insertions(+), 15 deletions(-) diff --git a/packages/python/plotly/plotly/express/_core.py b/packages/python/plotly/plotly/express/_core.py index 3601c6741b..f226f81e87 100644 --- a/packages/python/plotly/plotly/express/_core.py +++ b/packages/python/plotly/plotly/express/_core.py @@ -1896,9 +1896,7 @@ def process_dataframe_hierarchy(args): **{args["color"]: nw.col(args["color"]) * nw.col(count_colname)} ) - def post_agg( - dframe: nw.DataFrame, continuous_aggs: list[str], discrete_aggs: list[str] - ) -> nw.DataFrame: + def post_agg(dframe: nw.DataFrame, continuous_aggs, discrete_aggs) -> nw.DataFrame: """ - continuous_aggs is either [] or [args["color"]] - discrete_aggs is either [args["color"], ] or [] diff --git a/packages/python/plotly/plotly/figure_factory/_hexbin_mapbox.py b/packages/python/plotly/plotly/figure_factory/_hexbin_mapbox.py index 8b2233d986..f3772ac962 100644 --- a/packages/python/plotly/plotly/figure_factory/_hexbin_mapbox.py +++ b/packages/python/plotly/plotly/figure_factory/_hexbin_mapbox.py @@ -436,7 +436,7 @@ def create_hexbin_mapbox( ) agg_data_frame = nw.concat(agg_data_frame_list, how="vertical").with_columns( - color=nw.col("color").cast(nw.Float64) + color=nw.col("color").cast(nw.Int64) ) if range_color is None: diff --git a/packages/python/plotly/plotly/tests/test_optional/test_px/test_trendline.py b/packages/python/plotly/plotly/tests/test_optional/test_px/test_trendline.py index f6bd2cace1..9ddb011b9a 100644 --- a/packages/python/plotly/plotly/tests/test_optional/test_px/test_trendline.py +++ b/packages/python/plotly/plotly/tests/test_optional/test_px/test_trendline.py @@ -35,11 +35,12 @@ def test_trendline_results_passthrough(constructor, mode, options): if mode == "ols": assert "R2" in trendline.hovertemplate results = px.get_trendline_results(fig) - if mode == "ols": # This is flaky for polars? + if mode == "ols": assert len(results) == 2 - assert results["country"].values[0] == "Australia" - au_result = results["px_fit_results"].values[0] - assert len(au_result.params) == 2 + # Polars does not guarantee to maintain order in group by + assert set(results["country"].to_list()) == {"Australia", "New Zealand"} + result = results["px_fit_results"].values[0] + assert len(result.params) == 2 else: assert len(results) == 0 diff --git a/packages/python/plotly/test_requirements/requirements_310_optional.txt b/packages/python/plotly/test_requirements/requirements_310_optional.txt index 813a938bf8..297e1dca46 100644 --- a/packages/python/plotly/test_requirements/requirements_310_optional.txt +++ b/packages/python/plotly/test_requirements/requirements_310_optional.txt @@ -20,6 +20,6 @@ scikit-image==0.22.0 psutil==5.7.0 kaleido orjson==3.8.12 -polars +polars[timezone] pyarrow narwhals>=1.9.2 diff --git a/packages/python/plotly/test_requirements/requirements_311_optional.txt b/packages/python/plotly/test_requirements/requirements_311_optional.txt index 989fc28fd6..e57dd319c0 100644 --- a/packages/python/plotly/test_requirements/requirements_311_optional.txt +++ b/packages/python/plotly/test_requirements/requirements_311_optional.txt @@ -20,6 +20,6 @@ scikit-image==0.22.0 psutil==5.7.0 kaleido orjson==3.8.12 -polars +polars[timezone] pyarrow narwhals>=1.9.2 diff --git a/packages/python/plotly/test_requirements/requirements_312_no_numpy_optional.txt b/packages/python/plotly/test_requirements/requirements_312_no_numpy_optional.txt index d850f5ea42..39fa36d93b 100644 --- a/packages/python/plotly/test_requirements/requirements_312_no_numpy_optional.txt +++ b/packages/python/plotly/test_requirements/requirements_312_no_numpy_optional.txt @@ -19,6 +19,6 @@ scikit-image==0.22.0 psutil==5.9.7 kaleido orjson==3.9.10 -polars +polars[timezone] pyarrow narwhals>=1.9.2 diff --git a/packages/python/plotly/test_requirements/requirements_312_optional.txt b/packages/python/plotly/test_requirements/requirements_312_optional.txt index cfd9f6ac86..7200960580 100644 --- a/packages/python/plotly/test_requirements/requirements_312_optional.txt +++ b/packages/python/plotly/test_requirements/requirements_312_optional.txt @@ -20,6 +20,6 @@ scikit-image==0.22.0 psutil==5.9.7 kaleido orjson==3.9.10 -polars +polars[timezone] pyarrow narwhals>=1.9.2 diff --git a/packages/python/plotly/test_requirements/requirements_38_optional.txt b/packages/python/plotly/test_requirements/requirements_38_optional.txt index 1c8ce7a480..fdb9a5a3cc 100644 --- a/packages/python/plotly/test_requirements/requirements_38_optional.txt +++ b/packages/python/plotly/test_requirements/requirements_38_optional.txt @@ -20,6 +20,6 @@ matplotlib==3.7.3 scikit-image==0.18.1 psutil==5.7.0 kaleido -polars +polars[timezone] pyarrow narwhals>=1.9.2 diff --git a/packages/python/plotly/test_requirements/requirements_39_optional.txt b/packages/python/plotly/test_requirements/requirements_39_optional.txt index 3e304118da..e2af46cebf 100644 --- a/packages/python/plotly/test_requirements/requirements_39_optional.txt +++ b/packages/python/plotly/test_requirements/requirements_39_optional.txt @@ -21,6 +21,6 @@ scikit-image==0.18.1 psutil==5.7.0 kaleido orjson==3.8.12 -polars +polars[timezone] pyarrow narwhals>=1.9.2 diff --git a/packages/python/plotly/test_requirements/requirements_39_pandas_2_optional.txt b/packages/python/plotly/test_requirements/requirements_39_pandas_2_optional.txt index bfcf1b0d5f..96fe3dc854 100644 --- a/packages/python/plotly/test_requirements/requirements_39_pandas_2_optional.txt +++ b/packages/python/plotly/test_requirements/requirements_39_pandas_2_optional.txt @@ -21,6 +21,6 @@ psutil==5.7.0 kaleido vaex pydantic<=1.10.11 # for vaex, see https://github.com/vaexio/vaex/issues/2384 -polars +polars[timezone] pyarrow narwhals>=1.9.2 From 594ded043b8442a6b8ec5eefe73d734f03d4fbf5 Mon Sep 17 00:00:00 2001 From: FBruzzesi Date: Wed, 16 Oct 2024 13:15:40 +0200 Subject: [PATCH 050/106] fix frame value in hexbin --- .../python/plotly/plotly/figure_factory/_hexbin_mapbox.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/python/plotly/plotly/figure_factory/_hexbin_mapbox.py b/packages/python/plotly/plotly/figure_factory/_hexbin_mapbox.py index f3772ac962..8ba8277721 100644 --- a/packages/python/plotly/plotly/figure_factory/_hexbin_mapbox.py +++ b/packages/python/plotly/plotly/figure_factory/_hexbin_mapbox.py @@ -409,7 +409,7 @@ def create_hexbin_mapbox( if args["animation_frame"] is not None: groups = dict(args["data_frame"].group_by(args["animation_frame"]).__iter__()) else: - groups = {0: args["data_frame"]} + groups = {(0,): args["data_frame"]} agg_data_frame_list = [] for key, df in groups.items(): @@ -427,7 +427,7 @@ def create_hexbin_mapbox( agg_data_frame_list.append( nw.from_dict( { - "frame": [key] * len(hexagons_ids), + "frame": [int(key[0])] * len(hexagons_ids), "locations": hexagons_ids, "color": aggregated_value, }, From 6676061152527160b44dbf67ecf6b89e46eef3b2 Mon Sep 17 00:00:00 2001 From: FBruzzesi Date: Wed, 16 Oct 2024 13:55:50 +0200 Subject: [PATCH 051/106] copy numpy array --- .../express/trendline_functions/__init__.py | 15 +++++++++------ .../plotly/figure_factory/_hexbin_mapbox.py | 2 +- .../test_figure_factory/test_figure_factory.py | 2 +- 3 files changed, 11 insertions(+), 8 deletions(-) diff --git a/packages/python/plotly/plotly/express/trendline_functions/__init__.py b/packages/python/plotly/plotly/express/trendline_functions/__init__.py index 367c36d2b5..b2be5be676 100644 --- a/packages/python/plotly/plotly/express/trendline_functions/__init__.py +++ b/packages/python/plotly/plotly/express/trendline_functions/__init__.py @@ -111,17 +111,20 @@ def lowess(trendline_options, x_raw, x, y, x_label, y_label, non_missing): def _pandas(mode, trendline_options, x_raw, y, non_missing): + import numpy as np + + try: + import pandas as pd + except ImportError: + msg = "Trendline requires pandas to be installed" + raise ImportError(msg) + modes = dict(rolling="Rolling", ewm="Exponentially Weighted", expanding="Expanding") trendline_options = trendline_options.copy() function_name = trendline_options.pop("function", "mean") function_args = trendline_options.pop("function_args", dict()) - pd = nw.dependencies.get_pandas() - if pd is None: - msg = "Trendline requires pandas to be installed" - raise ImportError(msg) - - series = pd.Series(y, index=x_raw.to_pandas()) + series = pd.Series(np.copy(y), index=x_raw.to_pandas()) # TODO: If narwhals were to support rolling, ewm and expanding then we could go around these agg = getattr(series, mode) # e.g. series.rolling diff --git a/packages/python/plotly/plotly/figure_factory/_hexbin_mapbox.py b/packages/python/plotly/plotly/figure_factory/_hexbin_mapbox.py index 8ba8277721..0a2f6ff457 100644 --- a/packages/python/plotly/plotly/figure_factory/_hexbin_mapbox.py +++ b/packages/python/plotly/plotly/figure_factory/_hexbin_mapbox.py @@ -427,7 +427,7 @@ def create_hexbin_mapbox( agg_data_frame_list.append( nw.from_dict( { - "frame": [int(key[0])] * len(hexagons_ids), + "frame": [key[0]] * len(hexagons_ids), "locations": hexagons_ids, "color": aggregated_value, }, diff --git a/packages/python/plotly/plotly/tests/test_optional/test_figure_factory/test_figure_factory.py b/packages/python/plotly/plotly/tests/test_optional/test_figure_factory/test_figure_factory.py index 20a1b23f7f..792d8b69de 100644 --- a/packages/python/plotly/plotly/tests/test_optional/test_figure_factory/test_figure_factory.py +++ b/packages/python/plotly/plotly/tests/test_optional/test_figure_factory/test_figure_factory.py @@ -4474,7 +4474,7 @@ def test_build_dataframe(self): lon = np.random.randn(N) color = np.ones(N) frame = np.random.randint(0, n_frames, N) - df = pd.DataFrame( + df = pd.DataFrame( # TODO: Test other constructors? np.c_[lat, lon, color, frame], columns=["Latitude", "Longitude", "Metric", "Frame"], ) From d7d28841008a85c3dd388f98cc3d2bd79c280939 Mon Sep 17 00:00:00 2001 From: FBruzzesi Date: Wed, 16 Oct 2024 16:39:53 +0200 Subject: [PATCH 052/106] hotfix hexbin mapbox --- packages/python/plotly/plotly/figure_factory/_hexbin_mapbox.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/python/plotly/plotly/figure_factory/_hexbin_mapbox.py b/packages/python/plotly/plotly/figure_factory/_hexbin_mapbox.py index 0a2f6ff457..f62513b49b 100644 --- a/packages/python/plotly/plotly/figure_factory/_hexbin_mapbox.py +++ b/packages/python/plotly/plotly/figure_factory/_hexbin_mapbox.py @@ -473,7 +473,7 @@ def create_hexbin_mapbox( ) if args["animation_frame"] is not None else args["data_frame"] - ), + ).to_native(), lat=args["lat"], lon=args["lon"], animation_frame=args["animation_frame"], From c9b626e3fec7bb7a37d12dbf64629079bd8209d3 Mon Sep 17 00:00:00 2001 From: FBruzzesi Date: Thu, 17 Oct 2024 12:12:29 +0200 Subject: [PATCH 053/106] use lazy in process_dataframe_hierarchy --- .../python/plotly/plotly/express/_core.py | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/packages/python/plotly/plotly/express/_core.py b/packages/python/plotly/plotly/express/_core.py index f226f81e87..f30eb1b64c 100644 --- a/packages/python/plotly/plotly/express/_core.py +++ b/packages/python/plotly/plotly/express/_core.py @@ -1787,7 +1787,9 @@ def process_dataframe_hierarchy(args): df: nw.DataFrame = args["data_frame"] path = args["path"][::-1] _check_dataframe_all_leaves(df[path[::-1]]) - discrete_color = False + discrete_color = not _is_continuous(df, args["color"]) if args["color"] else False + + df = df.lazy() new_path = [col_name + "_path_copy" for col_name in path] df = df.with_columns( @@ -1826,10 +1828,9 @@ def process_dataframe_hierarchy(args): else: # we need a count column for the first groupby and the weighted mean of color # trick to be sure the col name is unused: take the sum of existing names + columns = df.collect_schema().names() count_colname = ( - "count" - if "count" not in df.columns - else "".join([str(el) for el in df.columns]) + "count" if "count" not in columns else "".join([str(el) for el in columns]) ) # we can modify df because it's a copy of the px argument df = df.with_columns(**{count_colname: nw.lit(1)}) @@ -1843,10 +1844,9 @@ def process_dataframe_hierarchy(args): continuous_aggs = [] if args["color"]: - if not _is_continuous(df, args["color"]): + if discrete_color: discrete_aggs.append(args["color"]) - discrete_color = True # Hack: In theory, we should have a way to do `.agg(nw.col(x).unique())` and # successively unpack/parse it as: # ``` @@ -1869,12 +1869,11 @@ def process_dataframe_hierarchy(args): else: # This first needs to be multiplied by `count_colname` continuous_aggs.append(args["color"]) - discrete_color = False agg_f[args["color"]] = nw.sum(args["color"]) # Other columns (for color, hover_data, custom_data etc.) - cols = list(set(df.columns).difference(path)) + cols = list(set(df.collect_schema().names()).difference(path)) df = df.with_columns( **{c: nw.col(c).cast(nw.String()) for c in cols if c not in agg_f} ) @@ -1896,7 +1895,7 @@ def process_dataframe_hierarchy(args): **{args["color"]: nw.col(args["color"]) * nw.col(count_colname)} ) - def post_agg(dframe: nw.DataFrame, continuous_aggs, discrete_aggs) -> nw.DataFrame: + def post_agg(dframe: nw.LazyFrame, continuous_aggs, discrete_aggs) -> nw.LazyFrame: """ - continuous_aggs is either [] or [args["color"]] - discrete_aggs is either [args["color"], ] or [] @@ -1960,7 +1959,7 @@ def post_agg(dframe: nw.DataFrame, continuous_aggs, discrete_aggs) -> nw.DataFra all_trees.append(df_tree.select(*["labels", "parent", "id", *cols])) - df_all_trees = nw.maybe_reset_index(nw.concat(all_trees, how="vertical")) + df_all_trees = nw.maybe_reset_index(nw.concat(all_trees, how="vertical").collect()) # we want to make sure than (?) is the first color of the sequence if args["color"] and discrete_color: From 82c114d02a41282a863fd7c4ccf7716f079eef5a Mon Sep 17 00:00:00 2001 From: FBruzzesi Date: Thu, 17 Oct 2024 12:26:02 +0200 Subject: [PATCH 054/106] fix test --- .../tests/test_optional/test_px/test_px_functions.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/python/plotly/plotly/tests/test_optional/test_px/test_px_functions.py b/packages/python/plotly/plotly/tests/test_optional/test_px/test_px_functions.py index 0dea495d0f..5cdfa64094 100644 --- a/packages/python/plotly/plotly/tests/test_optional/test_px/test_px_functions.py +++ b/packages/python/plotly/plotly/tests/test_optional/test_px/test_px_functions.py @@ -3,6 +3,7 @@ from numpy.testing import assert_array_equal import narwhals.stable.v1 as nw import numpy as np +from polars.exceptions import InvalidOperationError import pytest @@ -162,8 +163,12 @@ def test_sunburst_treemap_with_path(constructor): native_namespace=native_namespace, ) ) - msg = "Column `values` of `df` could not be converted to a numerical data type." - with pytest.raises(ValueError, match=msg): + pd_msg = "Column `values` of `df` could not be converted to a numerical data type." + pl_msg = "conversion from `str` to `f64` failed in column 'values'" + + with pytest.raises( + (ValueError, InvalidOperationError), match=f"({pd_msg}|{pl_msg})" + ): fig = px.sunburst(df.to_native(), path=path, values="values") # path is a mixture of column names and array-like path = [ From 87841d1132523a745fbf15f0a3f712c9641127a0 Mon Sep 17 00:00:00 2001 From: FBruzzesi Date: Fri, 18 Oct 2024 23:34:43 +0200 Subject: [PATCH 055/106] fix custom sort in process_dataframe_pie --- packages/python/plotly/plotly/express/_core.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/python/plotly/plotly/express/_core.py b/packages/python/plotly/plotly/express/_core.py index f30eb1b64c..f51cdcc6ac 100644 --- a/packages/python/plotly/plotly/express/_core.py +++ b/packages/python/plotly/plotly/express/_core.py @@ -2037,10 +2037,11 @@ def process_dataframe_pie(args, trace_patch): uniques = df.get_column(names).unique().to_list() order = [x for x in OrderedDict.fromkeys(list(order_in) + uniques) if x in uniques] - # TODO # Original implementation: args["data_frame"] = df.set_index(names).loc[order].reset_index() - # However this is untested, therefore will need some special attention to verify - args["data_frame"] = df[order].select(names) + # However we do not have a way to custom sort a dataframe in narwhals. + args["data_frame"] = nw.concat( + [df.filter(nw.col(names) == value) for value in order], how="vertical" + ) return args, trace_patch From 3ba19ae46eef0b3edc746ff58e995814db5d4e91 Mon Sep 17 00:00:00 2001 From: FBruzzesi Date: Mon, 21 Oct 2024 23:17:56 +0200 Subject: [PATCH 056/106] bump version and adjust core --- .../python/plotly/optional-requirements.txt | 2 +- .../python/plotly/plotly/express/_core.py | 39 +++---------------- packages/python/plotly/requirements.txt | 2 +- packages/python/plotly/setup.py | 2 +- .../requirements_310_optional.txt | 2 +- .../requirements_311_optional.txt | 2 +- .../requirements_312_no_numpy_optional.txt | 2 +- .../requirements_312_optional.txt | 2 +- .../requirements_38_optional.txt | 2 +- .../requirements_39_optional.txt | 2 +- .../requirements_39_pandas_2_optional.txt | 2 +- 11 files changed, 15 insertions(+), 44 deletions(-) diff --git a/packages/python/plotly/optional-requirements.txt b/packages/python/plotly/optional-requirements.txt index f91e618e14..e686089801 100644 --- a/packages/python/plotly/optional-requirements.txt +++ b/packages/python/plotly/optional-requirements.txt @@ -39,7 +39,7 @@ ipython ## pandas deps for some matplotlib functionality ## pandas -narwhals>=1.9.2 +narwhals>=1.10.0 ## scipy deps for some FigureFactory functions ## scipy diff --git a/packages/python/plotly/plotly/express/_core.py b/packages/python/plotly/plotly/express/_core.py index f51cdcc6ac..2be9ecbae8 100644 --- a/packages/python/plotly/plotly/express/_core.py +++ b/packages/python/plotly/plotly/express/_core.py @@ -10,6 +10,7 @@ import math import narwhals.stable.v1 as nw +from narwhals.dependencies import is_into_series from narwhals.utils import generate_unique_token from plotly._subplots import ( @@ -305,7 +306,7 @@ def make_trace_kwargs(args, trace_spec, trace_data, mapping_labels, sizeref): and ( trace_spec.constructor != go.Parcats or (attr_value is not None and name in attr_value) - or to_py_scalar(df.get_column(name).n_unique()) + or nw.to_py_scalar(df.get_column(name).n_unique()) <= args["dimensions_max_cardinality"] ) ] @@ -349,18 +350,7 @@ def make_trace_kwargs(args, trace_spec, trace_data, mapping_labels, sizeref): if x.dtype == nw.Datetime: # convert to unix epoch seconds - x = ( - x.to_frame() - .select( - **{ - args["x"]: nw.when(~x.is_null()).then( - x.cast(nw.Int64()) - ) - } - ) - .get_column(args["x"]) - / 10**9 - ) + x = x.dt.timestamp() / 10**9 elif x.dtype not in NW_NUMERIC_DTYPES: try: x = x.cast(nw.Float64()) @@ -497,7 +487,7 @@ def make_trace_kwargs(args, trace_spec, trace_data, mapping_labels, sizeref): else: mapping = {} for cat in trace_data.get_column(attr_value): - cat = to_py_scalar(cat) + cat = nw.to_py_scalar(cat) if mapping.get(cat) is None: mapping[cat] = args["color_discrete_sequence"][ len(mapping) % len(args["color_discrete_sequence"]) @@ -2054,7 +2044,7 @@ def infer_config(args, constructor, trace_patch, layout_patch): sizeref = 0 if "size" in args and args["size"]: sizeref = ( - to_py_scalar(df.get_column(args["size"]).max()) / args["size_max"] ** 2 + nw.to_py_scalar(df.get_column(args["size"]).max()) / args["size_max"] ** 2 ) # Compute color attributes and grouping attributes @@ -2753,22 +2743,3 @@ def _spacing_error_translator(e, direction, facet_arg): annot.update(font=None) return fig - - -def is_into_series(df) -> bool: - """Check if `df` is a supported narwhals eager dataframe.""" - return ( - nw.dependencies.is_polars_series(df) - or nw.dependencies.is_pyarrow_chunked_array(df) - or nw.dependencies.is_pandas_like_series(df) - ) - - -def to_py_scalar(scalar_like): - """If scalar is not python native, tries to convert it to python native.""" - if hasattr(scalar_like, "as_py"): - return scalar_like.as_py() - elif hasattr(scalar_like, "item"): - return scalar_like.item() - else: - return scalar_like diff --git a/packages/python/plotly/requirements.txt b/packages/python/plotly/requirements.txt index 89baa10f65..b20382a014 100644 --- a/packages/python/plotly/requirements.txt +++ b/packages/python/plotly/requirements.txt @@ -9,4 +9,4 @@ tenacity>=6.2.0 ## dataframe agnostic layer ## -narwhals>=1.9.2 +narwhals>=1.10.0 \ No newline at end of file diff --git a/packages/python/plotly/setup.py b/packages/python/plotly/setup.py index 419bcc01f7..11f42887fb 100644 --- a/packages/python/plotly/setup.py +++ b/packages/python/plotly/setup.py @@ -603,7 +603,7 @@ def run(self): data_files=[ ("etc/jupyter/nbconfig/notebook.d", ["jupyterlab-plotly.json"]), ], - install_requires=["tenacity>=6.2.0", "narwhals>=1.9.2", "packaging"], + install_requires=["tenacity>=6.2.0", "narwhals>=1.10.0", "packaging"], zip_safe=False, cmdclass=dict( build_py=js_prerelease(versioneer_cmds["build_py"]), diff --git a/packages/python/plotly/test_requirements/requirements_310_optional.txt b/packages/python/plotly/test_requirements/requirements_310_optional.txt index 297e1dca46..9f5a9abf9c 100644 --- a/packages/python/plotly/test_requirements/requirements_310_optional.txt +++ b/packages/python/plotly/test_requirements/requirements_310_optional.txt @@ -22,4 +22,4 @@ kaleido orjson==3.8.12 polars[timezone] pyarrow -narwhals>=1.9.2 +narwhals>=1.10.0 diff --git a/packages/python/plotly/test_requirements/requirements_311_optional.txt b/packages/python/plotly/test_requirements/requirements_311_optional.txt index e57dd319c0..ee80d82189 100644 --- a/packages/python/plotly/test_requirements/requirements_311_optional.txt +++ b/packages/python/plotly/test_requirements/requirements_311_optional.txt @@ -22,4 +22,4 @@ kaleido orjson==3.8.12 polars[timezone] pyarrow -narwhals>=1.9.2 +narwhals>=1.10.0 diff --git a/packages/python/plotly/test_requirements/requirements_312_no_numpy_optional.txt b/packages/python/plotly/test_requirements/requirements_312_no_numpy_optional.txt index 39fa36d93b..497a634c7a 100644 --- a/packages/python/plotly/test_requirements/requirements_312_no_numpy_optional.txt +++ b/packages/python/plotly/test_requirements/requirements_312_no_numpy_optional.txt @@ -21,4 +21,4 @@ kaleido orjson==3.9.10 polars[timezone] pyarrow -narwhals>=1.9.2 +narwhals>=1.10.0 diff --git a/packages/python/plotly/test_requirements/requirements_312_optional.txt b/packages/python/plotly/test_requirements/requirements_312_optional.txt index 7200960580..6442b44ef8 100644 --- a/packages/python/plotly/test_requirements/requirements_312_optional.txt +++ b/packages/python/plotly/test_requirements/requirements_312_optional.txt @@ -22,4 +22,4 @@ kaleido orjson==3.9.10 polars[timezone] pyarrow -narwhals>=1.9.2 +narwhals>=1.10.0 diff --git a/packages/python/plotly/test_requirements/requirements_38_optional.txt b/packages/python/plotly/test_requirements/requirements_38_optional.txt index fdb9a5a3cc..30a54339c0 100644 --- a/packages/python/plotly/test_requirements/requirements_38_optional.txt +++ b/packages/python/plotly/test_requirements/requirements_38_optional.txt @@ -22,4 +22,4 @@ psutil==5.7.0 kaleido polars[timezone] pyarrow -narwhals>=1.9.2 +narwhals>=1.10.0 diff --git a/packages/python/plotly/test_requirements/requirements_39_optional.txt b/packages/python/plotly/test_requirements/requirements_39_optional.txt index e2af46cebf..3277dff405 100644 --- a/packages/python/plotly/test_requirements/requirements_39_optional.txt +++ b/packages/python/plotly/test_requirements/requirements_39_optional.txt @@ -23,4 +23,4 @@ kaleido orjson==3.8.12 polars[timezone] pyarrow -narwhals>=1.9.2 +narwhals>=1.10.0 diff --git a/packages/python/plotly/test_requirements/requirements_39_pandas_2_optional.txt b/packages/python/plotly/test_requirements/requirements_39_pandas_2_optional.txt index 96fe3dc854..d1f7dd2b9a 100644 --- a/packages/python/plotly/test_requirements/requirements_39_pandas_2_optional.txt +++ b/packages/python/plotly/test_requirements/requirements_39_pandas_2_optional.txt @@ -23,4 +23,4 @@ vaex pydantic<=1.10.11 # for vaex, see https://github.com/vaexio/vaex/issues/2384 polars[timezone] pyarrow -narwhals>=1.9.2 +narwhals>=1.10.0 From a70146bf81783248b012336235ee89dccb07b7c3 Mon Sep 17 00:00:00 2001 From: FBruzzesi Date: Tue, 22 Oct 2024 17:48:05 +0200 Subject: [PATCH 057/106] use dtype.is_numeric --- .../python/plotly/plotly/express/_core.py | 21 +++++-------------- .../test_optional/test_px/test_trendline.py | 9 ++++---- 2 files changed, 10 insertions(+), 20 deletions(-) diff --git a/packages/python/plotly/plotly/express/_core.py b/packages/python/plotly/plotly/express/_core.py index 2be9ecbae8..06504a5319 100644 --- a/packages/python/plotly/plotly/express/_core.py +++ b/packages/python/plotly/plotly/express/_core.py @@ -20,18 +20,7 @@ ) NO_COLOR = "px_no_color_constant" -NW_NUMERIC_DTYPES = { - nw.Float32, - nw.Float64, - nw.Int8, - nw.Int16, - nw.Int32, - nw.Int64, - nw.UInt8, - nw.UInt16, - nw.UInt32, - nw.UInt64, -} + trendline_functions = dict( lowess=lowess, rolling=rolling, ewm=ewm, expanding=expanding, ols=ols @@ -168,7 +157,7 @@ def invert_label(args, column): def _is_continuous(df: nw.DataFrame, col_name: str) -> bool: - return df.get_column(col_name).dtype in NW_NUMERIC_DTYPES + return df.get_column(col_name).dtype.is_numeric() def get_decorated_label(args, column, role): @@ -351,7 +340,7 @@ def make_trace_kwargs(args, trace_spec, trace_data, mapping_labels, sizeref): if x.dtype == nw.Datetime: # convert to unix epoch seconds x = x.dt.timestamp() / 10**9 - elif x.dtype not in NW_NUMERIC_DTYPES: + elif not x.dtype.is_numeric(): try: x = x.cast(nw.Float64()) except ValueError: @@ -361,7 +350,7 @@ def make_trace_kwargs(args, trace_spec, trace_data, mapping_labels, sizeref): % args["x"] ) - if y.dtype not in NW_NUMERIC_DTYPES: + if not y.dtype.is_numeric(): try: y = y.cast(nw.Float64()) except ValueError: @@ -1648,7 +1637,7 @@ def build_dataframe(args, constructor): dtype = None for v in wide_value_vars: v_dtype = df_output.get_column(v).dtype - v_dtype = "number" if v_dtype in NW_NUMERIC_DTYPES else str(v_dtype) + v_dtype = "number" if v_dtype.is_numeric() else str(v_dtype) if dtype is None: dtype = v_dtype elif dtype != v_dtype: diff --git a/packages/python/plotly/plotly/tests/test_optional/test_px/test_trendline.py b/packages/python/plotly/plotly/tests/test_optional/test_px/test_trendline.py index 9ddb011b9a..e9aa537ecd 100644 --- a/packages/python/plotly/plotly/tests/test_optional/test_px/test_trendline.py +++ b/packages/python/plotly/plotly/tests/test_optional/test_px/test_trendline.py @@ -189,10 +189,10 @@ def test_ols_trendline_slopes(): def test_trendline_on_timeseries(constructor, mode, options): df = nw.from_native(constructor(px.data.stocks().to_dict(orient="list"))) - pd_err_msg = r"Could not convert value of 'x' \('date'\) into a numeric type." - pl_err_msg = "conversion from `str` to `f64` failed in column 'date'" - - with pytest.raises(Exception, match=rf"({pd_err_msg}|{pl_err_msg})"): + with pytest.raises( + ValueError, + match=r"Could not convert value of 'x' \('date'\) into a numeric type.", + ): px.scatter( df.to_native(), x="date", @@ -200,6 +200,7 @@ def test_trendline_on_timeseries(constructor, mode, options): trendline=mode, trendline_options=options, ) + df = df.with_columns( date=nw.col("date") .str.to_datetime(format="%Y-%m-%d") From 0103aa69ff84d752d319cc9763b546e3ab22e8f6 Mon Sep 17 00:00:00 2001 From: FBruzzesi Date: Tue, 22 Oct 2024 18:02:01 +0200 Subject: [PATCH 058/106] revert test --- .../plotly/tests/test_optional/test_px/test_trendline.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/python/plotly/plotly/tests/test_optional/test_px/test_trendline.py b/packages/python/plotly/plotly/tests/test_optional/test_px/test_trendline.py index e9aa537ecd..1ec4263cd9 100644 --- a/packages/python/plotly/plotly/tests/test_optional/test_px/test_trendline.py +++ b/packages/python/plotly/plotly/tests/test_optional/test_px/test_trendline.py @@ -189,10 +189,10 @@ def test_ols_trendline_slopes(): def test_trendline_on_timeseries(constructor, mode, options): df = nw.from_native(constructor(px.data.stocks().to_dict(orient="list"))) - with pytest.raises( - ValueError, - match=r"Could not convert value of 'x' \('date'\) into a numeric type.", - ): + pd_err_msg = r"Could not convert value of 'x' \('date'\) into a numeric type." + pl_err_msg = "conversion from `str` to `f64` failed in column 'date'" + + with pytest.raises(Exception, match=rf"({pd_err_msg}|{pl_err_msg})"): px.scatter( df.to_native(), x="date", From b858ed815b45485e5437489635009a55efa62d6f Mon Sep 17 00:00:00 2001 From: FBruzzesi Date: Wed, 23 Oct 2024 18:43:12 +0200 Subject: [PATCH 059/106] feedback adjustments --- .../python/plotly/plotly/express/_core.py | 115 ++++++++++++------ 1 file changed, 75 insertions(+), 40 deletions(-) diff --git a/packages/python/plotly/plotly/express/_core.py b/packages/python/plotly/plotly/express/_core.py index 06504a5319..b7f711a574 100644 --- a/packages/python/plotly/plotly/express/_core.py +++ b/packages/python/plotly/plotly/express/_core.py @@ -337,9 +337,9 @@ def make_trace_kwargs(args, trace_spec, trace_data, mapping_labels, sizeref): y = sorted_trace_data.get_column(args["y"]) x = sorted_trace_data.get_column(args["x"]) - if x.dtype == nw.Datetime: + if x.dtype == nw.Datetime or x.dtype == nw.Date: # convert to unix epoch seconds - x = x.dt.timestamp() / 10**9 + x = _to_unix_epoch_seconds(x) elif not x.dtype.is_numeric(): try: x = x.cast(nw.Float64()) @@ -1051,11 +1051,10 @@ def _get_reserved_col_names(args): elif isinstance(arg, str): # no need to add ints since kw arg are not ints reserved_names.add(arg) elif is_into_series(arg): - arg_name = nw.from_native(arg, series_only=True).name + arg_series = nw.from_native(arg, series_only=True) + arg_name = arg_series.name if arg_name and arg_name in df.columns: - in_df = ( - nw.from_native(arg, series_only=True) == df.get_column(arg_name) - ).all() + in_df = (arg_series == df.get_column(arg_name)).all() if in_df: reserved_names.add(arg_name) elif arg is nw.maybe_get_index(df) and arg.name is not None: @@ -1114,20 +1113,28 @@ def _escape_col_name(columns, col_name, extra): def to_unindexed_series(x, name=None, native_namespace=None): - """Assuming x is list-like or even an existing Series, returns a new Series with - no index (if pandas-like). Stripping the index from existing pd.Series is + """Assuming x is list-like or even an existing Series, returns a new Series (with + itx index reset if pandas-like). Stripping the index from existing pd.Series is required to get things to match up right in the new DataFrame we're building. """ + x_native = nw.to_native(x, strict=False) + if nw.dependencies.is_pandas_like_series(x_native): + return nw.from_native( + x_native.__class__(x_native, name=name).reset_index(drop=True), + series_only=True, + ) x = nw.from_native(x, series_only=True, strict=False) if isinstance(x, nw.Series): - return nw.maybe_reset_index(x).rename(name) + return x.rename(name) + elif native_namespace is not None: + return nw.new_series(name=name, values=x, native_namespace=native_namespace) else: - if native_namespace is not None: - return nw.new_series(name=name, values=x, native_namespace=native_namespace) - elif (pd := nw.dependencies.get_pandas()) is not None: + try: + import pandas as pd + return nw.new_series(name=name, values=x, native_namespace=pd) - else: - msg = "Pandas installation is required if no namespace is provided." + except ImportError: + msg = "Pandas installation is required if no dataframe is provided." raise NotImplementedError(msg) @@ -1322,7 +1329,7 @@ def process_args_into_dataframe( ) df_output[str(col_name)] = to_unindexed_series( - x=nw.from_native(argument, allow_series=True, strict=False), + x=nw.from_native(argument, series_only=True, strict=False), name=str(col_name), native_namespace=native_namespace, ) @@ -1345,14 +1352,19 @@ def process_args_into_dataframe( length = len(df_output[next(iter(df_output))]) if len(df_output) else 0 + if native_namespace is None: + try: + import pandas as pd + + native_namespace = pd + except ImportError: + msg = "Pandas installation is required if no dataframe is provided." + raise NotImplementedError(msg) + df_output.update( { col_name: nw.new_series( - name=col_name, - values=range(length), - native_namespace=( - native_namespace if df_provided else nw.dependencies.get_pandas() - ), + name=col_name, values=range(length), native_namespace=native_namespace ) for col_name in ranges } @@ -1364,9 +1376,7 @@ def process_args_into_dataframe( col_name: nw.new_series( name=col_name, values=[constants[col_name]] * length, - native_namespace=( - native_namespace if df_provided else nw.dependencies.get_pandas() - ), + native_namespace=native_namespace, ) for col_name in constants } @@ -1375,7 +1385,11 @@ def process_args_into_dataframe( if df_output: df_output = nw.from_dict(df_output) else: - pd = nw.dependencies.get_pandas() + try: + import pandas as pd + except ImportError: + msg = "Pandas installation is required." + raise NotImplementedError(msg) df_output = nw.from_native(pd.DataFrame({}), eager_only=True) return df_output, wide_id_vars @@ -1455,21 +1469,25 @@ def build_dataframe(args, constructor): else: try: - pd = nw.dependencies.get_pandas() - if pd is None: - msg = ( - f"data_frame of type {type(args['data_frame'])} requires Pandas " - "to be installed. Convert it to supported dataframe type or " - "install Pandas." - ) - raise ValueError(msg) + import pandas as pd - args["data_frame"] = nw.from_native(pd.DataFrame(args["data_frame"])) - columns = args["data_frame"].columns - is_pd_like = True - except Exception: - msg = f"Unsupported type: {type(args['data_frame'])}" + try: + args["data_frame"] = nw.from_native( + pd.DataFrame(args["data_frame"]) + ) + columns = args["data_frame"].columns + is_pd_like = True + except Exception: + msg = f"Unsupported type: {type(args['data_frame'])}" + raise NotImplementedError(msg) + except ImportError: + msg = ( + f"data_frame of type {type(args['data_frame'])} requires Pandas " + "to be installed. Convert it to supported dataframe type or " + "install Pandas." + ) raise NotImplementedError(msg) + else: columns = None # no data_frame @@ -1478,9 +1496,7 @@ def build_dataframe(args, constructor): # This is safe since at this point `_compliant_frame` is one of the "full" level # support dataframe(s) - native_namespace = ( - df_input._compliant_frame.__native_namespace__() if df_provided else None - ) + native_namespace = nw.get_native_namespace(df_input) if df_provided else None # now we handle special cases like wide-mode or x-xor-y specification # by rearranging args to tee things up for process_args_into_dataframe to work @@ -2732,3 +2748,22 @@ def _spacing_error_translator(e, direction, facet_arg): annot.update(font=None) return fig + + +def _to_unix_epoch_seconds(s: nw.Series) -> nw.Series: + dtype = s.dtype + if dtype == nw.Date: + return s.dt.timestamp("ms") / 1_000 + if dtype == nw.Datetime: + if dtype.time_unit in ("s", "ms"): + return s.dt.timestamp("ms") / 1_000 + elif dtype.time_unit == "us": + return s.dt.timestamp("us") / 1_000_000 + elif dtype.time_unit == "ns": + return s.dt.timestamp("us") / 1_000_000_000 + else: + msg = "Unexpected dtype, please report a bug" + raise ValueError(msg) + else: + msg = f"Expected Date or Datetime, got {dtype}" + raise TypeError(msg) From 49efae2b3d1008e6fe7cfa844553f81028bec929 Mon Sep 17 00:00:00 2001 From: FBruzzesi Date: Fri, 25 Oct 2024 08:44:02 +0200 Subject: [PATCH 060/106] raise if numpy is missing, conftest fix, typo --- packages/python/plotly/plotly/express/__init__.py | 9 +++++++++ packages/python/plotly/plotly/express/_core.py | 10 +++++----- .../plotly/express/trendline_functions/__init__.py | 2 -- .../plotly/tests/test_optional/test_px/conftest.py | 10 ++++++---- 4 files changed, 20 insertions(+), 11 deletions(-) diff --git a/packages/python/plotly/plotly/express/__init__.py b/packages/python/plotly/plotly/express/__init__.py index 460ce1b998..33db532cd4 100644 --- a/packages/python/plotly/plotly/express/__init__.py +++ b/packages/python/plotly/plotly/express/__init__.py @@ -2,6 +2,15 @@ `plotly.express` is a terse, consistent, high-level wrapper around `plotly.graph_objects` for rapid data exploration and figure generation. Learn more at https://plotly.com/python/plotly-express/ """ +from plotly import optional_imports + +np = optional_imports.get_module("numpy") +if np is None: + raise ImportError( + """\ +Plotly express requires numpy to be installed.""" + ) + from ._imshow import imshow from ._chart_types import ( # noqa: F401 scatter, diff --git a/packages/python/plotly/plotly/express/_core.py b/packages/python/plotly/plotly/express/_core.py index b7f711a574..d0c2715035 100644 --- a/packages/python/plotly/plotly/express/_core.py +++ b/packages/python/plotly/plotly/express/_core.py @@ -1877,8 +1877,8 @@ def process_dataframe_hierarchy(args): # Similar trick as above discrete_aggs.append(col) agg_f[col] = nw.col(col).max() - agg_f[f"{col}__n_unique__"] = ( - nw.col(col).n_unique().alias(f"{col}__n_unique__") + agg_f[f"{col}__plotly_n_unique__"] = ( + nw.col(col).n_unique().alias(f"{col}__plotly_n_unique__") ) # Avoid collisions with reserved names - columns in the path have been copied already cols = list(set(cols) - set(["labels", "parent", "id"])) @@ -1898,12 +1898,12 @@ def post_agg(dframe: nw.LazyFrame, continuous_aggs, discrete_aggs) -> nw.LazyFra return dframe.with_columns( **{c: nw.col(c) / nw.col(count_colname) for c in continuous_aggs}, **{ - c: nw.when(nw.col(f"{c}__n_unique__") == 1) + c: nw.when(nw.col(f"{c}__plotly_n_unique__") == 1) .then(nw.col(c)) .otherwise(nw.lit("(?)")) for c in discrete_aggs }, - ).drop([f"{c}__n_unique__" for c in discrete_aggs]) + ).drop([f"{c}__plotly_n_unique__" for c in discrete_aggs]) for i, level in enumerate(path): @@ -2760,7 +2760,7 @@ def _to_unix_epoch_seconds(s: nw.Series) -> nw.Series: elif dtype.time_unit == "us": return s.dt.timestamp("us") / 1_000_000 elif dtype.time_unit == "ns": - return s.dt.timestamp("us") / 1_000_000_000 + return s.dt.timestamp("ns") / 1_000_000_000 else: msg = "Unexpected dtype, please report a bug" raise ValueError(msg) diff --git a/packages/python/plotly/plotly/express/trendline_functions/__init__.py b/packages/python/plotly/plotly/express/trendline_functions/__init__.py index b2be5be676..ad9131176e 100644 --- a/packages/python/plotly/plotly/express/trendline_functions/__init__.py +++ b/packages/python/plotly/plotly/express/trendline_functions/__init__.py @@ -8,8 +8,6 @@ exposed as part of the public API for documentation purposes. """ -import narwhals.stable.v1 as nw - __all__ = ["ols", "lowess", "rolling", "ewm", "expanding"] diff --git a/packages/python/plotly/plotly/tests/test_optional/test_px/conftest.py b/packages/python/plotly/plotly/tests/test_optional/test_px/conftest.py index 585ff9e7a3..0a25142046 100644 --- a/packages/python/plotly/plotly/tests/test_optional/test_px/conftest.py +++ b/packages/python/plotly/plotly/tests/test_optional/test_px/conftest.py @@ -30,10 +30,12 @@ def pyarrow_table_constructor(obj) -> IntoDataFrame: constructors = [polars_eager_constructor, pyarrow_table_constructor, pandas_constructor] if parse_version(pd.__version__) >= parse_version("2.0.0"): - constructors = [ - pandas_nullable_constructor, - pandas_pyarrow_constructor, - ] + constructors.extend( + [ + pandas_nullable_constructor, + pandas_pyarrow_constructor, + ] + ) @pytest.fixture(params=constructors) From a36bc243ffc15b23783da82004dbe6560325d3e3 Mon Sep 17 00:00:00 2001 From: FBruzzesi Date: Fri, 25 Oct 2024 09:04:21 +0200 Subject: [PATCH 061/106] __plotly_n_unique__ --- out-branch.csv | 3 +++ packages/python/plotly/plotly/express/_core.py | 4 ++-- packages/python/plotly/requirements.txt | 3 --- packages/python/plotly/setup.py | 2 +- perf-branch.csv | 3 +++ perf-master.csv | 3 +++ profile-branch | 1 + 7 files changed, 13 insertions(+), 6 deletions(-) create mode 100644 out-branch.csv create mode 100644 perf-branch.csv create mode 100644 perf-master.csv create mode 100644 profile-branch diff --git a/out-branch.csv b/out-branch.csv new file mode 100644 index 0000000000..252d4abdc3 --- /dev/null +++ b/out-branch.csv @@ -0,0 +1,3 @@ +Chart Type,Time (s) +scatter_pandas,0.5901803081999788 +bar_pandas,1.043395163300056 diff --git a/packages/python/plotly/plotly/express/_core.py b/packages/python/plotly/plotly/express/_core.py index d0c2715035..74c69e4b5e 100644 --- a/packages/python/plotly/plotly/express/_core.py +++ b/packages/python/plotly/plotly/express/_core.py @@ -1858,8 +1858,8 @@ def process_dataframe_hierarchy(args): # ``` # However we cannot do that just yet, therefore a workaround is provided agg_f[args["color"]] = nw.col(args["color"]).max() - agg_f[f'{args["color"]}__n_unique__'] = ( - nw.col(args["color"]).n_unique().alias(f'{args["color"]}__n_unique__') + agg_f[f'{args["color"]}__plotly_n_unique__'] = ( + nw.col(args["color"]).n_unique().alias(f'{args["color"]}__plotly_n_unique__') ) else: # This first needs to be multiplied by `count_colname` diff --git a/packages/python/plotly/requirements.txt b/packages/python/plotly/requirements.txt index b20382a014..ed709d00bd 100644 --- a/packages/python/plotly/requirements.txt +++ b/packages/python/plotly/requirements.txt @@ -5,8 +5,5 @@ ### ### ################################################### -## retrying requests ## -tenacity>=6.2.0 - ## dataframe agnostic layer ## narwhals>=1.10.0 \ No newline at end of file diff --git a/packages/python/plotly/setup.py b/packages/python/plotly/setup.py index 11f42887fb..2433740d47 100644 --- a/packages/python/plotly/setup.py +++ b/packages/python/plotly/setup.py @@ -603,7 +603,7 @@ def run(self): data_files=[ ("etc/jupyter/nbconfig/notebook.d", ["jupyterlab-plotly.json"]), ], - install_requires=["tenacity>=6.2.0", "narwhals>=1.10.0", "packaging"], + install_requires=["narwhals>=1.10.0", "packaging"], zip_safe=False, cmdclass=dict( build_py=js_prerelease(versioneer_cmds["build_py"]), diff --git a/perf-branch.csv b/perf-branch.csv new file mode 100644 index 0000000000..7083e86447 --- /dev/null +++ b/perf-branch.csv @@ -0,0 +1,3 @@ +Chart Type,Time (s) +scatter_pandas,0.4685762626999576 +bar_pandas,0.9242745239999749 diff --git a/perf-master.csv b/perf-master.csv new file mode 100644 index 0000000000..bf59cb2180 --- /dev/null +++ b/perf-master.csv @@ -0,0 +1,3 @@ +Chart Type,Time (s) +scatter_pandas,0.5244665377000274 +bar_pandas,0.8416239924999672 diff --git a/profile-branch b/profile-branch new file mode 100644 index 0000000000..6e963626f5 --- /dev/null +++ b/profile-branch @@ -0,0 +1 @@ +{"$schema":"https://www.speedscope.app/file-format-schema.json","profiles":[{"type":"sampled","name":"Thread 35046 \"\"","unit":"seconds","startValue":0.0,"endValue":18.34,"samples":[[18,5,4,3,8,7,17,5,4,3,8,7,16,5,15,7,5,4,3,8,7,14,10,7,5,4,3,8,7,13,5,4,3,8,7,12,5,4,3,8,7,11,10,7,5,4,3,8,7,9,5,4,3,8,7,6,5,4,3,2,1,0],[18,5,4,3,8,7,17,5,4,3,8,7,16,5,15,7,5,4,3,8,7,14,10,7,5,4,3,8,7,13,5,4,3,8,7,12,5,4,3,8,7,11,10,7,5,4,3,8,7,9,5,4,3,8,7,6,5,4,3,8,7,20,5,19],[18,5,4,3,8,7,17,5,4,3,8,7,16,5,15,7,5,4,3,8,7,14,10,7,5,4,3,8,7,13,5,4,3,8,7,12,5,4,3,8,7,11,10,7,5,4,3,8,7,9,5,4,3,8,7,21,5,4,3,2,1,0],[18,5,4,3,8,7,17,5,4,3,8,7,16,5,15,7,5,4,3,8,7,14,10,7,5,4,3,8,7,13,5,4,3,8,7,12,5,4,3,8,7,11,10,7,5,4,3,8,7,9,5,4,3,8,7,21,5,4,3,2,1,0],[18,5,4,3,8,7,17,5,4,3,8,7,16,5,15,7,5,4,3,8,7,14,10,7,5,4,3,8,7,13,5,4,3,8,7,12,5,4,3,8,7,11,10,7,5,4,3,8,7,9,5,4,3,8,7,21,5,4,3,8,7,23,22],[18,5,4,3,8,7,17,5,4,3,8,7,16,5,15,7,5,4,3,8,7,14,10,7,5,4,3,8,7,13,5,4,3,8,7,25,24],[18,5,4,3,8,7,17,5,4,3,8,7,16,5,15,7,5,4,3,8,7,28,10,7,5,27,26],[18,5,4,3,8,7,17,5,4,3,8,7,16,5,15,7,5,4,3,8,7,36,10,7,5,35,34,33,32,31,30,29],[18,5,4,3,8,7,17,5,4,3,8,7,16,5,15,7,5,4,3,8,7,47,10,7,5,4,3,8,7,46,45,44,43,42,40,41,40,39,38,37],[18,5,4,3,8,7,53,10,7,5,4,3,8,7,52,10,7,5,4,3,8,7,51,5,4,3,2,50,49,48],[18,5,4,3,8,7,53,10,7,5,4,3,8,7,52,10,7,5,4,3,8,7,51,5,4,3,2,50,54],[18,5,4,3,8,7,53,10,7,5,4,3,8,7,61,10,7,5,4,3,8,7,60,45,44,59,58,57,56,55],[18,5,4,3,8,7,53,10,7,5,4,3,8,7,64,10,7,5,4,3,2,63,62],[18,5,4,3,8,7,53,10,7,5,4,3,8,7,67,10,7,5,35,34,33,32,66,65],[18,5,4,3,8,7,71,10,7,5,4,3,8,7,70,10,7,5,4,3,8,7,69,5,4,3,8,7,68,5,15,7,5,35,34,33,32,66,65],[84,5,4,3,8,7,83,10,7,5,4,3,8,7,82,5,4,3,8,7,81,5,4,3,8,7,80,5,4,3,8,7,79,10,7,5,4,3,8,7,78,5,4,3,8,7,77,5,4,3,8,7,76,75,74,73,72],[84,5,4,3,8,7,83,10,7,5,4,3,8,7,82,5,4,3,8,7,81,5,4,3,8,7,80,5,4,3,8,7,86,5,4,3,8,7,85,5,4,3,2,1,0],[84,5,4,3,8,7,83,10,7,5,4,3,8,7,82,5,4,3,8,7,91,5,4,3,8,7,90,5,4,3,8,7,89,5,4,88,87],[84,5,4,3,8,7,83,10,7,5,4,3,8,7,82,5,4,3,8,7,91,5,4,3,8,7,90,5,4,3,8,7,95,5,4,3,8,7,94,93,92],[84,5,4,3,8,7,99,5,4,3,8,7,98,5,4,3,8,7,97,5,35,34,33,32,96],[84,5,4,3,8,7,99,5,4,3,8,7,98,5,4,3,8,7,97,5,4,3,8,7,107,5,4,3,8,7,106,5,4,3,8,7,105,104,103,102,101,100],[84,5,4,3,8,7,99,5,4,3,8,7,111,5,4,3,8,7,110,5,4,3,8,7,109,108],[84,5,4,3,8,7,112],[119,5,4,3,8,7,118,5,4,3,8,7,117,5,4,3,8,7,116,5,4,3,8,7,115,5,4,88,114,113,7],[119,5,4,3,8,7,118,5,4,3,8,7,117,5,4,3,8,7,116,5,4,3,8,7,115,5,4,88,114,113,7],[119,5,4,3,8,7,118,5,4,3,8,7,117,5,4,3,8,7,116,5,4,3,8,7,115,5,4,88,114,113,7],[119,5,4,3,8,7,118,5,4,3,8,7,117,5,4,3,8,7,116,5,4,3,8,7,115,5,4,3,120,7],[119,5,4,3,8,7,118,5,4,3,8,7,117,5,4,3,8,7,116,5,4,3,8,7,115,5,4,3,120,7,5,4,3,8,7,122,10,7,5,4,3,2,1,121],[119,5,4,3,8,7,118,5,4,3,8,7,117,5,4,3,8,7,116,5,4,3,8,7,115,5,4,3,120,7,5,4,3,8,7,122,10,7,5,4,3,8,7,123,5,4,3,2,63,62],[119,5,4,3,8,7,127,5,4,3,8,7,126,5,4,3,8,7,125,5,4,3,120,7,5,4,3,120,7,5,4,3,120,7,5,15,7,5,4,3,8,7,124,10,7,5,4,3,120,7],[119,5,4,3,8,7,127,5,4,3,8,7,126,5,4,3,8,7,125,5,4,3,120,7,5,4,3,120,7,5,4,3,120,7,5,15,7,5,4,3,8,7,124,10,7,5,4,3,120,7,5,35,34,128],[119,5,4,3,8,7,127,5,4,3,8,7,132,5,4,3,8,7,131,130,45,44,43,42,40,39,38,129],[119,5,4,3,8,7,127,5,4,3,8,7,138,5,4,3,8,7,137,5,4,3,8,7,136,5,4,3,8,7,135,5,4,3,8,7,134,10,7,5,4,3,8,7,133,5,4,3,2,1,0],[119,5,4,3,8,7,127,5,4,3,8,7,138,5,4,3,8,7,137,5,4,3,8,7,136,5,4,3,8,7,135,5,4,3,8,7,134,10,7,5,4,3,8,7,133,5,4,3,2,1,0],[119,5,4,3,8,7,127,5,4,3,8,7,138,5,4,3,8,7,137,5,4,3,8,7,136,5,4,3,8,7,135,5,4,3,8,7,134,10,7,5,4,3,8,7,141,140,45,44,43,42,40,41,40,139],[119,5,4,3,8,7,127,5,4,3,8,7,138,5,4,3,8,7,137,5,4,3,8,7,136,5,4,3,8,7,135,5,4,3,8,7,146,145,144,143,142],[119,5,4,3,8,7,127,5,4,3,8,7,138,5,4,3,8,7,137,5,4,3,8,7,151,5,4,3,8,7,150,10,7,5,4,3,8,7,149,5,4,3,8,7,148,10,7,5,35,147],[119,5,4,3,8,7,127,5,4,3,8,7,138,5,4,3,8,7,137,5,4,3,8,7,151,5,4,3,8,7,150,10,7,5,4,3,8,7,149,5,4,3,8,7,148,10,7,5,35,147],[119,5,4,3,8,7,127,5,4,3,8,7,138,5,4,3,8,7,153,5,4,3,8,7,152,10,7,5,4,3,120,7],[119,5,4,3,8,7,127,5,4,3,8,7,138,5,4,3,8,7,153,5,4,3,8,7,154,10,7,5,4,3,2,1,0],[119,5,4,3,8,7,127,5,4,3,8,7,138,5,4,3,8,7,153,5,4,3,8,7,154,10,7,5,4,3,2,1,0],[119,5,4,3,8,7,127,5,4,3,8,7,138,5,4,3,8,7,153,5,4,3,8,7,154,10,7,5,4,3,8,7,155],[119,5,4,3,8,7,127,5,4,3,8,7,158,5,4,3,8,7,157,5,4,3,8,7,156,5,4,3,2,1,0],[119,5,4,3,8,7,127,5,4,3,8,7,158,5,4,3,8,7,157,5,4,3,8,7,156,5,4,3,2,1,0],[119,5,4,3,8,7,127,5,4,3,8,7,158,5,4,3,8,7,157,5,4,3,8,7,156,5,4,3,8,7,167,5,4,3,8,7,166,10,7,5,4,3,8,7,165,5,4,3,8,7,164,5,4,3,8,7,163,5,4,3,8,7,162,161,160,159],[119,5,4,3,8,7,127,5,4,3,8,7,158,5,4,3,8,7,157,5,4,3,8,7,156,5,4,3,8,7,167,5,4,3,8,7,166,10,7,5,4,3,8,7,165,5,4,3,8,7,169,5,4,3,8,7,168,5,4,3,2,1,0],[119,5,4,3,8,7,127,5,4,3,8,7,158,5,4,3,8,7,157,5,4,3,8,7,156,5,4,3,8,7,167,5,4,3,8,7,175,174,173,172,171,170],[119,5,4,3,8,7,127,5,4,3,8,7,158,5,4,3,8,7,157,5,4,3,8,7,156,5,4,3,8,7,178,5,4,3,8,7,177,176],[119,5,4,3,8,7,127,5,4,3,8,7,158,5,4,3,8,7,157,5,4,3,8,7,156,5,4,3,8,7,178,5,4,3,8,7,177,182,181,180,179],[119,5,4,3,8,7,127,5,4,3,8,7,158,5,4,3,8,7,157,5,4,3,8,7,187,10,7,5,4,3,8,7,186,5,4,3,120,7,185,184,183],[119,5,4,3,8,7,193,5,4,3,8,7,192,5,4,3,8,7,191,5,4,3,8,7,190,5,4,3,8,7,189,5,4,3,8,7,188],[119,5,4,3,8,7,193,5,4,3,8,7,192,5,4,3,8,7,194,5,4,3,2,63,62],[119,5,4,3,8,7,196,5,4,3,8,7,195,5,35,34,33,32,66,65],[199,5,4,3,8,7,198,5,4,3,8,7,197,5,4,3,2,1,0],[199,5,4,3,8,7,198,5,4,3,8,7,202,5,4,3,8,7,201,200],[199,5,4,3,8,7,198,5,4,3,8,7,209,5,4,3,8,7,208,10,7,5,4,3,8,7,207,5,4,3,8,7,206,5,4,3,8,7,205,5,4,3,8,7,204,5,4,3,8,7,203,10,7,5,4,3,2,1,0],[199,5,4,3,8,7,198,5,4,3,8,7,209,5,4,3,8,7,208,10,7,5,4,3,8,7,212,5,4,3,8,7,211,5,4,3,8,7,210],[199,5,4,3,8,7,198,5,4,3,8,7,209,5,4,3,8,7,208,10,7,5,4,3,8,7,220,5,4,3,8,7,219,5,4,3,8,7,218,5,4,3,8,7,217,216,215,214,45,44,43,42,40,41,40,213],[199,5,4,3,8,7,198,5,4,3,8,7,209,5,4,3,8,7,208,10,7,5,4,3,8,7,224,5,15,7,5,4,3,8,7,223,5,4,3,8,7,222,5,4,88,87,221],[199,5,4,3,8,7,198,5,4,3,8,7,209,5,4,3,8,7,208,10,7,5,4,3,8,7,224,5,4,3,8,7,226,5,4,3,2,63,225],[199,5,4,3,8,7,233,5,4,3,8,7,232,231,230,229,228,227],[199,5,4,3,8,7,233,5,4,3,8,7,234,231,230,229,228,227],[235],[235],[235],[235],[235],[235],[241,240,239,238,237,236],[241,240,239,238,237,236],[241,240,239,238,237,236],[241,240,239,238,237,236],[241,240,239,238,237,236],[241,240,239,238,237,236],[241,240,239,238,237,236],[241,240,239,238,237,236],[241,240,239,238,237,236],[241,240,239,238,237,236],[241,240,239,238,237,236],[241,240,239,238,237,236],[241,240,239,238,237,236],[241,240,239,238,237,236],[241,240,239,238,237,236],[241,240,239,238,237,236],[241,240,239,238,237,236],[241,240,239,238,237,236],[241,240,239,238,237,236],[241,240,239,238,237,236],[241,240,239,238,237,236],[241,240,239,238,237,236],[241,240,239,238,237,236],[241,240,239,238,237,236],[241,240,239,238,237,236],[241,240,239,238,237,236],[241,240,239,238,237,236],[241,240,239,238,237,236],[241,240,239,238,237,236],[241,240,239,238,237,236],[241,240,239,238,237,236],[241,240,239,238,237,236],[241,240,239,238,237,236],[241,240,239,238,237,236],[241,240,239,238,237,236],[241,240,239,238,237,236],[241,240,239,238,237,236],[241,240,239,238,237,236],[241,240,239,238,237,236],[241,240,239,238,237,236],[241,240,239,238,237,236],[241,240,239,238,237,236],[241,240,239,238,237,236],[241,240,239,238,237,236],[241,240,239,238,237,236],[241,240,239,238,237,236],[241,240,239,238,237,236],[241,240,239,238,237,236],[241,240,239,238,237,236],[241,240,239,238,237,236],[241,240,239,238,237,236],[241,240,239,238,237,236],[241,240,239,238,237,236],[241,240,239,238,237,236],[241,240,239,238,237,236],[241,240,239,238,237,236],[241,240,239,238,237,236],[241,240,239,238,237,236],[241,240,239,238,237,236],[241,240,239,238,237,236],[241,240,239,238,237,236],[241,240,239,238,237,236],[241,240,239,238,237,236],[241,240,239,238,237,236],[248,247,246,245,244,243,242],[248,247,246,245,244,243,242],[248,247,246,245,244,243,242],[248,247,246,245,244,243,242],[248,247,246,245,244,243,242],[248,247,246,245,244,243,242],[248,247,246,245,244,243,242],[248,247,246,245,244,243,242],[248,247,246,245,244,243,242],[248,247,246,245,244,243,242],[248,247,246,245,244,243,242],[248,247,246,245,244,243,242],[248,247,246,252,251,250,249],[248,247,246,252,251,250,249],[248,247,246,252,251,250,249],[248,247,246,252,251,250,249],[248,247,246,252,251,250,249],[248,247,246,252,251,250,249],[248,247,246],[248,247,246],[248,247,246],[248,247,246],[253],[253],[253],[253],[253],[253],[253],[253],[253],[253],[253],[253],[253],[253],[253],[253],[253],[253],[253],[253],[253],[253],[253],[253],[266,265,264,263,262,261,260,259,258,257,256,255,254],[266,265,264,263,262,261,260,270,269,268,267],[266,265,264,263,262,261,260,270,269,268,267],[266,265,264,263,262,261,260,270,269,268,267],[266,265,264,263,262,261,260,270,269,268,267],[266,265,264,263,262,261,260,270,269,268,267],[266,265,264,263,262,261,260,270,269,268,267],[266,265,264,263,262,261,260,270,269,268,267],[266,265,264,263,262,261,260,270,269,268,267],[266,265,264,263,262,261,260,270,277,276,275,274,247,246,245,273,272,271],[266,265,264,263,262,261,260,270,277,276,275,274,247,246,252,251,250,249],[266,265,264,263,262,261,260,270,277,276,275,274,247,246,252,251,250,249],[266,265,264,263,262,261,260,270,277],[266,265,264,263,262,261,292,291,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,264,263,262,261,292,291,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,264,263,262,261,292,291,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,264,263,262,261,302,301,300,299,298,297,296,295,294,293],[266,265,264,263,262,261,302,301,300,299,298,297,296,295,294,293],[266,265,264,263,262,261,302,301,300,299,298,297,296,303],[266,265,264,263,262,261,302,301,300,299,298,297,296,303],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,319,294,293],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,319,294,293],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,322],[266,265,264,263,262,261,302,318,317,316,315,314,313,326,325,324,323],[266,265,264,263,262,261,302,318,317,316,315,314,336,335,334,333,332,331,330,329,328,327],[266,265,264,263,262,261,302,318,317,316,315,314,336,335,334,333,332,331,330,329,328,327],[266,265,264,263,262,261,302,318,317,316,315,314,336,335,334,333,332,331,330,329,328,337],[266,265,264,263,262,261,302,318,317,316,315,314,336,335,334,333,332,331,330,329,328,327],[266,265,264,263,262,261,302,318,317,316,315,314,336,335,334,333,332,331,330,329,328,327],[266,265,264,263,262,261,351,350,349,348,347,346,345,344,343,342,341,340,281,339,338],[266,265,264,263,262,261,364,363,362,361,360,359,358,357,356,355,354,353,352,5,4,3,2,63,62],[266,265,264,263,262,261,364,363,373,372,371,5,15,7,5,4,3,8,7,370,5,4,88,87,369,368,367,49,366,365],[266,265,264,263,262,261,364,363,373,372,371,5,15,7,5,4,3,8,7,370,5,4,3,8,7,383,382,381,353,352,5,4,3,8,7,380,379,353,352,5,15,7,352,5,15,7,352,5,4,3,8,7,378,5,4,3,8,7,377,10,7,5,4,3,8,7,376,10,7,5,4,3,8,7,375,5,4,3,8,7,374,5,4,3,2,1,0],[266,265,264,263,262,261,364,363,373,372,371,5,15,7,5,4,3,8,7,370,5,4,3,8,7,383,382,381,353,352,5,4,3,8,7,380,379,353,352,5,15,7,352,5,15,7,352,5,4,3,8,7,390,5,4,3,8,7,389,5,4,3,8,7,388,10,7,5,4,3,8,7,387,10,7,5,4,3,8,7,386,5,4,3,8,7,385,5,4,3,8,7,384],[266,265,264,263,262,261,364,363,373,372,371,5,15,7,5,4,3,8,7,370,5,4,3,8,7,383,382,381,353,352,5,4,3,8,7,380,379,353,352,5,15,7,352,5,15,7,352,5,4,3,8,7,390,5,4,3,8,7,394,5,4,3,8,7,393,5,4,3,8,7,392,391],[266,265,264,263,262,261,364,363,373,372,371,5,15,7,5,4,3,8,7,370,5,4,3,8,7,383,382,381,353,352,5,4,3,8,7,380,379,353,352,5,15,7,352,5,15,7,352,5,4,3,8,7,390,5,4,3,8,7,400,5,4,3,8,7,399,5,15,7,5,4,3,8,7,398,5,4,3,8,7,397,5,4,3,8,7,396,5,4,3,8,7,395,5,4,3,2,1,0],[266,265,264,263,262,261,364,363,373,372,371,5,15,7,5,4,3,8,7,370,5,4,3,8,7,383,382,381,353,352,5,4,3,8,7,380,379,353,352,5,15,7,352,5,15,7,352,5,4,3,8,7,390,5,4,3,8,7,400,5,4,3,8,7,399,5,15,7,5,4,3,8,7,406,5,4,3,8,7,405,5,4,3,8,7,404,5,4,3,8,7,403,5,4,3,8,7,402,401],[266,265,264,263,262,261,364,363,373,372,371,5,15,7,5,4,3,8,7,370,5,4,3,8,7,383,382,381,353,352,5,4,3,8,7,380,379,353,352,5,15,7,352,5,15,7,352,5,4,3,8,7,390,5,4,3,8,7,400,5,4,3,8,7,417,5,4,3,8,7,416,5,4,3,8,7,415,5,4,3,8,7,414,5,4,3,8,7,413,10,7,5,4,3,8,7,412,5,15,7,5,4,3,8,7,411,10,7,5,4,3,8,7,410,5,4,3,8,7,409,5,4,3,8,7,408,5,4,3,8,7,407,5,4,3,2,63,62],[266,265,264,263,262,261,364,363,373,372,371,5,15,7,5,4,3,8,7,370,5,4,3,8,7,383,382,381,353,352,5,4,3,8,7,380,379,353,352,5,15,7,352,5,15,7,352,5,4,3,8,7,390,5,4,3,8,7,400,5,4,3,8,7,417,5,4,3,8,7,416,5,4,3,8,7,415,5,4,3,8,7,414,5,4,3,8,7,413,10,7,5,4,3,8,7,412,5,15,7,5,4,3,8,7,411,10,7,5,4,3,8,7,410,5,4,3,8,7,409,5,4,3,8,7,418],[266,265,264,263,262,261,364,363,373,372,371,5,15,7,5,4,3,8,7,370,5,4,3,8,7,383,382,381,353,352,5,4,3,8,7,380,379,353,352,5,15,7,352,5,15,7,352,5,4,3,8,7,390,5,4,3,8,7,400,5,4,3,8,7,417,5,4,3,8,7,416,5,4,3,8,7,415,5,4,3,8,7,414,5,4,3,8,7,413,10,7,5,4,3,8,7,422,5,4,3,8,7,421,5,35,34,33,32,420,30,419,65],[266,265,264,263,262,261,364,363,373,372,371,5,15,7,5,4,3,8,7,370,5,4,3,8,7,383,382,381,353,352,5,4,3,8,7,380,379,353,352,5,15,7,352,5,15,7,352,5,4,3,8,7,390,5,4,3,8,7,400,5,4,3,8,7,417,5,4,3,8,7,416,5,4,3,8,7,415,5,4,3,8,7,425,10,7,5,4,3,8,7,424,10,7,5,4,3,8,7,423],[266,265,264,263,262,261,364,363,373,372,371,5,15,7,5,4,3,8,7,370,5,4,3,8,7,383,382,381,353,352,5,4,3,8,7,380,379,353,352,5,15,7,352,5,15,7,352,5,4,3,8,7,390,5,4,3,8,7,400,5,4,3,8,7,417,5,4,3,8,7,416,5,4,3,8,7,415,5,4,3,8,7,425,10,7,5,4,3,8,7,428,10,7,5,35,34,33,32,427,426],[266,265,264,263,262,261,364,363,373,372,371,5,15,7,5,4,3,8,7,370,5,4,3,8,7,383,382,381,353,352,5,4,3,8,7,380,379,353,352,5,15,7,352,5,15,7,352,5,4,3,8,7,390,5,4,3,8,7,400,5,4,3,8,7,433,432,431,430,429],[266,265,264,263,262,261,364,363,444,443,442,441,440,439,438,437,436,355,354,353,352,5,4,3,2,435,434,7],[266,265,264,263,262,261,364,363,444,443,442,441,440,439,438,437,436,355,354,353,352,5,4,3,2,435,434,7],[266,265,264,263,262,261,364,363,444,443,442,441,440,439,438,437,436,355,354,353,352,5,4,3,2,435,434,7],[266,265,264,263,262,261,364,363,444,443,442,441,440,439,438,437,447,446,445,354,353,352,5,4,3,2,63,225],[266,265,264,263,262,261,364,363,444,443,442,441,440,439,438,437,447,446,451,382,450,354,353,449,448],[266,265,264,263,262,261,364,363,444,443,442,441,440,439,438,437,447,446,451,382,450,354,353,352,5,27],[266,265,264,263,262,261,466,443,442,465,464,359,358,357,463,462,461,358,357,356,460,459,458,457,456,358,357,356,455,454,446,445,354,353,352,5,35,34,33,32,453,452],[266,265,264,263,262,261,466,443,442,465,464,359,358,357,463,462,461,358,357,356,460,459,458,457,456,358,357,356,468,454,446,445,467],[266,265,264,263,262,261,466,443,442,465,464,359,358,357,463,462,461,358,357,356,472,459,458,457,471,454,446,445,354,353,352,5,4,88,87,369,368,367,470,469],[266,265,264,263,262,261,466,443,442,465,464,359,358,357,463,462,461,358,357,356,476,459,458,457,475,358,357,356,355,354,353,352,5,4,3,2,474,473],[266,265,264,263,262,261,466,443,442,465,464,359,358,357,463,462,461,358,357,356,479,459,458,457,478,454,446,445,354,353,352,5,4,3,8,7,477],[266,265,264,263,262,261,466,443,442,465,464,359,358,357,463,462,461,358,357,356,483,459,458,457,482,358,357,356,481,358,357,356,355,354,353,352,5,4,3,8,7,480],[266,265,264,263,262,261,466,443,442,465,464,359,358,357,463,462,461,358,357,356,488,459,458,487,486,354,353,352,5,4,3,8,7,485,382,484,354,353,352,5,4,88,87,221],[266,265,264,263,262,261,466,443,442,465,464,359,358,357,463,462,461,358,357,356,492,459,458,457,491,358,357,356,490,358,357,356,355,354,353,352,5,4,3,8,7,489],[266,265,264,263,262,261,466,443,442,465,464,359,358,357,463,462,495,358,357,356,494,359,358,357,356,493],[266,265,264,263,262,261,466,443,442,465,464,359,358,357,463,462,495,358,357,356,494,359,358,357,356,497,358,357,356,355,354,353,352,5,496],[266,265,264,263,262,261,466,443,442,465,464,359,358,357,463,462,495,358,357,356,494,359,358,357,356,497,358,357,356,498],[266,265,264,263,262,261,466,443,442,465,464,359,358,357,463,462,495,358,357,356,500,359,358,357,356,499,454,446,445,354,353,352,5,4,88,87,369,368,367,470],[266,265,264,263,262,261,466,443,442,465,464,359,358,504,502,503,502,503,502,501],[266,265,264,263,262,261],[266,265,264,263,262,261],[266,265,264,263,262],[266,265,264,263,262],[266,265,264,263,262],[266,265,264,263,262],[266,265,264,263,262,261,260,270,269,268,267],[266,265,264,263,262,261,260,270,269,268,267],[266,265,264,263,262,261,260,270,269,268,267],[266,265,264,263,262,261,260,270,269,268,267],[266,265,264,263,262,261,260,270,277,276,275,274,247,246,252,251,250,249],[266,265,264,263,262,261,260,270,277,276,275,274,247,246,252,251,250,249],[266,265,264,263,262,261,292,291,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,264,263,262,261,292,291,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,264,263,262,261,292,291,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,264,263,262,261,302,301,300,299,298,297,296,505],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,506],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,508,507],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,508,509],[266,265,264,263,262,261,302,318,317,316,315,314,512,511,510],[266,265,264,263,262,261,302,318,317,316,315,314,336,335,334,333,332,331,330,329,328,327],[266,265,264,263,262,261,302,318,317,316,315,314,336,335,334,333,332,331,330,329,328,337],[266,265,264,263,262,261,302,318,317,316,315,314,336,335,334,333,332,331,330,329,328,327],[266,265,264,263,262,261,302,318,317,316,315,314,336,335,334,333,332,331,330,329,328,327],[266,265,264,263,262,261,302,318,317,316,315,314,336,335,334,333,332,331,330,329,328,327],[266,265,264,263,262,261,302,318,317,316,315,314,336,516,515,514,513],[266,265,264,263,262,261,519,484,354,353,352,518,108,517],[266,265,264,263,262,261,351,350,349,348,347,346,521,520],[266,265,264,263,262,261,526,525,524,523,502,503,522],[266,265,264,263,262,261,466,443,442,465,464,359,358,357,463,462,495,358,357,356,531,359,348,347,530,529,528,527],[266,265,264,263,262,261,466,443,442,465,464,359,358,357,463,462,495,358,357,356,535,359,358,357,356,534,358,357,356,533,348,347,530,529,532],[266,265,264,263,262,261,466,443,442,465,464,359,358,357,463,462,495,358,357,356,535,359,358,357,356,534,358,357,356,533,348,347,530,529,536,527],[266,265,264,263,262,261,543,542,541,465,440,540,539,538,537],[266,265,264,263,262,261],[266,265,264,263,262],[266,265,264,263,262,261,260,259,258,546,545,544,179],[266,265,264,263,262,261,260,270,269,268,267],[266,265,264,263,262,261,260,270,269,268,267],[266,265,264,263,262,261,260,270,269,268,267],[266,265,264,263,262,261,260,270,277,276,275,274,247,246,252,251,250,547],[266,265,264,263,262,261,260,270,277,276,275,274,247,246,252,251,250,249],[266,265,264,263,262,261,260,270,277,276,275,274,247,246,252,251,250,249],[266,265,264,263,262,261,260,270,277,276,275,274,247,246,252,251,250,249],[266,265,264,263,262,261,260,270,277,276,275,274,247,246,252,251,250,249],[266,265,264,263,262,261,260,270,277],[266,265,264,263,262,261,260,270,277],[266,265,264,263,262,261,292,291,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,264,263,262,261,292,291,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,264,263,262,261,292,291,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,264,263,262,261,292,291,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,264,263,262,261,292,291,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,264,263,262,261,292,291,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,264,263,262,261,292,291,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,264,263,262,261,302,301,300,299,298,297,296,295,294,293],[266,265,264,263,262,261,302,301,300,299,298,297,296,303],[266,265,264,263,262,261,302,301,300,299,298,297,296,303],[266,265,264,263,262,261,302,301,300,299,298,297,296,303],[266,265,264,263,262,261,302,301,300,299,298,297,296,303],[266,265,264,263,262,261,302,301,300,299,298,297,296,303],[266,265,264,263,262,261,302,301,300,299,298,297,296,295,294,293],[266,265,264,263,262,261,302,301,300,299,298,297,296,303],[266,265,264,263,262,261,302,301,300,299,298,297,296,303],[266,265,264,263,262,261,302,301,300,299,298,297,296,303],[266,265,264,263,262,261,302,301,300,299,298,297,296,303],[266,265,264,263,262,261,302,301,300,299,298,297,296,303],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,506],[266,265,264,263,262,261,302,318,317,316,315,314,313,326,325,324,323],[266,265,264,263,262,261,302,318,317,316,315,314,512,511,510],[266,265,264,263,262,261,302,318,317,316,315,314,336,335,334,333,332,331,330,329,328,327],[266,265,264,263,262,261,302,318,317,316,315,314,336,335,334,333,332,331,330,329,328,327],[266,265,264,263,262,261,302,318,317,316,315,314,336,335,334,333,332,331,330,329,328,327],[266,265,264,263,262,261,302,318,317,316,315,314,336,335,334,333,332,331,330,329,328,327],[266,265,264,263,262,261,302,318,317,316,315,314,336,335,334,333,332,331,330,329,328,327],[266,265,264,263,262,261,302,318,317,316,315,314,336,335,334,333,332,331,330,329,328,327],[266,265,264,263,262,261,302,318,317,316,315,314,336,335,334,333,332,331,330,329,328,327],[266,265,264,263,262,261,302,318,317,316,315,314,336,335,334,333,332,331,330,329,328,327],[266,265,264,263,262,261,302,318,317,316,315,314,336,335,334,333,332,331,330,329,328,327],[266,265,264,263,262,261,302,318,317,316,315,314,336,548],[266,265,264,263,262,261,351,350,349,348,347,346,345],[266,265,264,263,262,261,351,350,349,348,347,346,345,344,343,342,341,340,281,280,549],[266,265,264,263,262,261,364,363,362,361,360,359,358,357,356,551,550],[266,265,264,263,262,261,559,443,442,465,558,557,556,555,554,553,539,538,552,537],[266,265,264,263,262,261,559,443,442,465,558,557,562,561,439,438,437,447,560],[266,265,264,263,262,261,466,443,442,465,464,359,358,357,463,462,461,358,357,356,483,459,458,457,482,358,563,560],[266,265,264,263,262,261,466,443,442,465,464,359,358,357,463,462,495,358,357,356,565,359,358,357,356,564,454,560],[266,265,264,263,262,261,466,443,442,465,464,359,358,571,570,569,570,569,570,569,568,567,568,567,566],[266,265,264,263,262,261],[266,265,264,263,262,261],[266,265,264,263,262,261],[266,265,264,263,262],[266,265,264,263,262],[266,265,264,263,262],[266,265,264,263,262,261,260,270,269,268,267],[266,265,264,263,262,261,260,270,277,276,275,274,247,246,252,251,250,249],[266,265,264,263,262,261,260,270,277,276,275,274,247,246,252,251,250,249],[266,265,264,263,262,261,260,270,277,276,275,274,247,246,252,251,250,249],[266,265,264,263,262,261,260,270,277,276,275,274,247,246,252,251,250,249],[266,265,264,263,262,261,260,270,277,276,275,274,247,246,252,251,250,249],[266,265,264,263,262,261,292,291,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,264,263,262,261,292,291,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,264,263,262,261,292,291,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,264,263,262,261,292,291,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,264,263,262,261,302,301,300,299,298,297,296,303],[266,265,264,263,262,261,302,301,300,299,298,297,296,303],[266,265,264,263,262,261,302,301,300,299,298,297,296,295,294,293],[266,265,264,263,262,261,302,301,300,299,298,297,296,303],[266,265,264,263,262,261,302,301,300,299,298,297,296,303],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,506],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,572],[266,265,264,263,262,261,302,318,317,316,315,314,313,326,325,324,323],[266,265,264,263,262,261,302,318,317,316,315,314,512,511,510],[266,265,264,263,262,261,302,318,317,316,315,314,512,511,510],[266,265,264,263,262,261,302,318,317,316,315,314,336,335,334,333,332,331,330,329,328,327],[266,265,264,263,262,261,302,318,317,316,315,314,336,335,334,333,332,331,330,329,328,337],[266,265,264,263,262,261,302,318,317,316,315,314,336,335,334,333,332,331,330,329,328,337],[266,265,264,263,262,261,302,318,317,316,315,314,336,335,334,333,332,331,330,329,328,337],[266,265,264,263,262,261,302,318,317,316,315,314,336,335,334,333,332,331,330,329,328,327],[266,265,264,263,262,261,302,318,317,316,315,314,336,335,334,333,332,331,330,329,328,327],[266,265,264,263,262,261,575,574,350,573,349,348,347,530,529,536,527],[266,265,264,263,262,261,351,350,349,348,347,346,521,520],[266,265,264,263,262,261,364,363,444,443,442,441,579,578,577,576],[266,265,264,263,262,261,364,363,444,443,442,441,579,578,437,580],[266,265,264,263,262,261],[266,265,264,263,262,261],[266,265,264,263,262,261],[266,265,264,263,262],[266,265,264,263,262,261,260,270,277,276,275,274,247,246,252,251,250,249],[266,265,264,263,262,261,260,270,277,276,275,274,247,246,252,251,250,249],[266,265,264,263,262,261,260,270,277,276,275,274,247,246,252,251,250,249],[266,265,264,263,262,261,260,270,277,276,275,274,247,246,252,251,250,249],[266,265,264,263,262,261,260,270,277],[266,265,264,263,262,261,292,291,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,264,263,262,261,292,291,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,264,263,262,261,292,291,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,264,263,262,261,292,291,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,264,263,262,261,292,582,290,289,288,581],[266,265,264,263,262,261,302,301,300,299,298,297,296,295,294,293],[266,265,264,263,262,261,302,301,300,299,298,297,296,295,294,293],[266,265,264,263,262,261,302,301,300,299,298,297,296,303],[266,265,264,263,262,261,302,301,300,299,298,297,296,303],[266,265,264,263,262,261,302,301,300,299,298,297,296,303],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,264,263,262,261,302,318,317,316,315,314,313,326,325,324,323],[266,265,264,263,262,261,302,318,317,316,315,314,336,335,334,333,584,583],[266,265,264,263,262,261,302,318,317,316,315,314,336,335,334,333,332,331,330,329,328,337],[266,265,264,263,262,261,302,318,317,316,315,314,336,335,334,333,332,331,330,329,328,327],[266,265,264,263,262,261,302,318,317,316,315,314,336,335,334,333,332,331,330,329,328,327],[266,265,264,263,262,261,364,363,444,443,442,441,579,587,586,585],[266,265,264,263,262,261,364,363,444,443,442,465,558,557,562,589,588],[266,265,264,263,262,261],[266,265,264,263,262,261],[266,265,264,263,262,261],[266,265,264,263,262],[266,265,264,263,262],[266,265,264,263,262],[266,265,264,263,262],[266,265,264,263,262],[266,265,264,263,262],[266,265,264,263,262],[266,265,264,263,262],[266,265,264,263,262,261,260,270,269,268,267],[266,265,264,263,262,261,260,270,269,268,267],[266,265,264,263,262,261,260,270,269,268,267],[266,265,264,263,262,261,260,270,269,268,267],[266,265,264,263,262,261,260,270,277],[266,265,264,263,262,261,260,270,277],[266,265,264,263,262,261,260,270,277],[266,265,264,263,262,261,292,291,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,264,263,262,261,292,291,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,264,263,262,261,292,291,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,264,263,262,261,292,291,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,264,263,262,261,292,291,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,264,263,262,261,292,291,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,264,263,262,261,292,291,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,264,263,262,261,292,291,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,264,263,262,261,302,301,300,299,298,297,296,295,294,293],[266,265,264,263,262,261,302,301,300,299,298,297,296,303],[266,265,264,263,262,261,302,301,300,299,298,297,296,303],[266,265,264,263,262,261,302,301,300,299,298,297,296,303],[266,265,264,263,262,261,302,301,300,299,298,297,296,295,294,293],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,319,294,293],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,319,294,293],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,319,294,293],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,591,590],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,592],[266,265,264,263,262,261,302,318,317,316,315,314,313,326,325,324,323],[266,265,264,263,262,261,302,318,317,316,315,314,512,511,510],[266,265,264,263,262,261,302,318,317,316,315,314,512,511,510],[266,265,264,263,262,261,302,318,317,316,315,314,336,335,334,333,332,331,330,329,328,327],[266,265,264,263,262,261,302,318,317,316,315,314,336,335,334,333,332,331,330,329,328,327],[266,265,264,263,262,261,302,318,317,316,315,314,336,335,334,333,332,331,330,329,328,327],[266,265,264,263,262,261,302,318,317,316,315,314,336,335,334,333,332,331,330,329,328,337],[266,265,264,263,262,261,302,318,317,316,315,314,336,335,334,333,332,331,330,329,328,327],[266,265,264,263,262,261,302,318,317,316,315,314,336,335,334,333,332,331,330,329,328,327],[266,265,264,263,262,261,302,318,317,316,315,314,336,335,334,333,332,331,330,329,328,327],[266,265,264,263,262,261,302,318,317,316,315,314,336,335,334,333,332,331,330,329,328,327],[266,265,264,263,262,261,597,596,595,594,593],[266,265,264,263,262,261,364,363,444,443,442,441,579,578,577,576,539,599,598],[266,265,264,263,262,261,364,363,444,443,442,441,573,349,348,604,603,602,601,600,437,447,560],[266,265,264,263,262,261,559,443,442,441,605],[266,265,264,263,262,261,466,443,442,465,464,359,358,357,463,462,608,607,502,503,502,503,606],[266,265,264,263,262,261,466,443,442,465,464,359,358,357,463,462,461,358,357,356,614,459,458,457,613,348,347,612,611,536,610,609],[266,265,264,263,262,261,466,443,442,465,464,359,358,504,502,503,502,503,502,616,502,503,615],[266,265,264,263,262,261],[266,265,264,263,262,261],[266,265,264,263,262],[266,265,264,263,262],[266,265,264,263,262,261,260,270,269,268,267],[266,265,264,263,262,261,260,270,277,276,275,274,247,246,252,251,250,249],[266,265,264,263,262,261,260,270,277,276,275,274,247,246,252,251,250,249],[266,265,264,263,262,261,260,270,277,276,275,274,247,246,252,251,250,249],[266,265,264,263,262,261,260,270,277,276,275,274,247,246,252,251,250,249],[266,265,264,263,262,261,260,270,277],[266,265,264,263,262,261,260,270,277],[266,265,264,263,262,261,292,291,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,264,263,262,261,292,291,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,264,263,262,261,302,301,300,299,298,297,296,303],[266,265,264,263,262,261,302,301,300,299,298,297,296,303],[266,265,264,263,262,261,302,301,300,299,298,297,296,303],[266,265,264,263,262,261,302,301,300,299,298,297,296,303],[266,265,264,263,262,261,302,301,300,299,298,297,296,303],[266,265,264,263,262,261,302,301,300,299,298,297,296,303],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,319,294,293],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,264,263,262,261,302,318,317,316,315,314,313,326,325],[266,265,264,263,262,261,302,318,317,316,315,314,336,335,334,333,584,583],[266,265,264,263,262,261,302,318,317,316,315,314,336,335,334,333,332,331,330,329,328,327],[266,265,264,263,262,261,302,318,317,316,315,314,336,335,334,333,332,331,330,329,328,337],[266,265,264,263,262,261,302,318,317,316,315,314,336,335,334,333,332,331,330,329,328,327],[266,265,264,263,262,261,302,318,317,316,315,314,336,335,334,333,332,331,330,329,328,327],[266,265,264,263,262,261,302,318,317,316,315,314,336,335,334,333,332,331,330,329,328,327],[266,265,264,263,262,261,302,318,317,316,315,314,336,335,334,333,332,331,330,329,328,327],[266,265,264,263,262,261,302,318,317,316,315,314,336,335,334,333,332,331,330,329,328,327],[266,265,264,263,262,261,302,318,317,316,315,314,336,335,334,333,332,331,330,329,328,327],[266,265,264,263,262,261,302,318,317,316,315,314,336,335,334,333,332,331,330,329,328,327],[266,265,264,263,262,261,302,318,317,316,315,314,336,335,334,333,332,331,330,329,328,327],[266,265,264,263,262,261,302,318,317,316,315,314,336,548],[266,265,264,263,262,261,302,318,317,316,315,314,336,548],[266,265,264,263,262,261,575,574,350,440,438,436,619,618,617,550],[266,265,264,263,262,261,364,624,465,558,557,556,555,623,622,439,438,577,621,620],[266,265,264,263,262,261],[266,265,264,263,262,261],[266,265,264,263,262,261],[266,265,264,263,262,261],[266,265,264,263,262,261],[266,265,264,263,262,261],[266,265,264,263,262],[266,265,264,263,262,261,260,270,269,268,628,627,626,625],[266,265,264,263,262,261,260,270,269,268,267],[266,265,264,263,262,261,260,270,269,268,267],[266,265,264,263,262,261,292,291,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,264,263,262,261,292,291,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,264,263,262,261,292,291,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,264,263,262,261,292,291,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,264,263,262,261,292,291,290,289,288,632,631,630,629],[266,265,264,263,262,261,302,301,300,299,298,297,296,303],[266,265,264,263,262,261,302,301,300,299,298,297,296,303],[266,265,264,263,262,261,302,301,300,299,298,297,296,303],[266,265,264,263,262,261,302,301,300,299,298,297,296,295,294,293],[266,265,264,263,262,261,302,301,300,299,298,297,296,303],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,592],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,592],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,508,509],[266,265,264,263,262,261,302,318,317,316,315,314,313,326,325,324,323],[266,265,264,263,262,261,302,318,317,316,315,314,512,511,510],[266,265,264,263,262,261,302,318,317,316,315,314,336,335,334,333,332,331,330,329,328,327],[266,265,264,263,262,261,302,318,317,316,315,314,336,335,334,333,332,331,330,329,328,327],[266,265,264,263,262,261,302,318,317,316,315,314,336,335,334,333,332,331,330,329,328,327],[266,265,264,263,262,261,302,318,317,316,315,314,336,335,334,333,332,331,330,329,328,327],[266,265,264,263,262,261,302,318,317,316,315,314,336,335,334,333,332,331,330,329,328,327],[266,265,264,263,262,261,302,318,317,316,315,314,336,335,334,333,332,331,330,329,328,327],[266,265,264,263,262,261,364,363,444,443,442,465,558,557,562,634,437,633],[266,265,264,263,262,261,466,443,442,465,464,359,358,357,635,607,502,503,502,503,502,616,502,503],[266,265,264,263,262,261,466,443,442,465,464,359,358,357,463,462,461,358,357,356,639,459,458,457,638,358,357,356,637,358,357,356,636],[266,265,264,263,262,261,466,443,442,465,464,359,358,357,463,462,495,358,357,356,643,359,358,357,356,642,358,357,356,641,348,347,530,529,640,610],[266,265,264,263,262,261,466,443,442,465,464,359,358,357,463,462,495,358,357,356,643,359,358,357,356,645,358,357,356,644,348,347,530,529,640,527],[266,265,264,263,262,261],[266,265,264,263,262,261],[266,265,264,263,262,261],[266,265,264,263,262,261],[266,265,264,263,262,261],[266,265,264,263,262],[266,265,264,263,262],[266,265,264,263,262],[266,265,264,263,262,261,260,270,269,268,267],[266,265,264,263,262,261,260,270,269,268,267],[266,265,264,263,262,261,260,270,277,276,275,274,247,246,252,251,250,249],[266,265,264,263,262,261,260,270,277,276,275,274,247,246,252,251,250,249],[266,265,264,263,262,261,260,270,277],[266,265,264,263,262,261,260,270,277],[266,265,264,263,262,261,292,291,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,264,263,262,261,292,291,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,264,263,262,261,292,291,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,264,263,262,261,292,291,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,264,263,262,261,292,291,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,264,263,262,261,292,291,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,264,263,262,261,302,301,300,299,298,297,296,303],[266,265,264,263,262,261,302,301,300,299,298,297,296,303],[266,265,264,263,262,261,302,301,300,299,298,297,296,295,294,293],[266,265,264,263,262,261,302,301,300,299,298,297,296,295,294,293],[266,265,264,263,262,261,302,301,300,299,298,297,296,295,294,293],[266,265,264,263,262,261,302,301,300,299,298,297,296,303],[266,265,264,263,262,261,302,301,300,299,298,297,296,303],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,319,294,293],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,319,294,293],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,506],[266,265,264,263,262,261,302,318,317,316,315,314,313,326,325],[266,265,264,263,262,261,302,318,317,316,315,314,313,326,325],[266,265,264,263,262,261,302,318,317,316,315,314,336,335,334,333,584,646],[266,265,264,263,262,261,302,318,317,316,315,314,336,335,334,333,332,331,330,329,328,327],[266,265,264,263,262,261,302,318,317,316,315,314,336,335,334,333,332,331,330,329,328,327],[266,265,264,263,262,261,302,318,317,316,315,314,336,335,334,333,332,331,330,329,328,327],[266,265,264,263,262,261,302,318,317,316,315,314,336,335,334,333,332,331,330,329,328,327],[266,265,264,263,262,261,302,318,317,316,315,314,336,335,334,333,332,331,330,329,328,327],[266,265,264,263,262,261,302,318,317,316,315,314,336,335,334,333,332,331,330,329,328,327],[266,265,264,263,262,261,302,318,317,316,315,314,336,335,334,333,332,647],[266,265,264,263,262,261,575,574,350,579,587,651,650,649,648],[266,265,264,263,262,261,526,525,654,653,652,348,347,346,520],[266,265,264,263,262,261,526,525,654,653,655,348,347,346,520],[266,265,264,263,262,261,526,525,654,653,657,348,347,656,527],[266,265,264,263,262,261,526,525,524,523,502,503,522],[266,265,264,263,262,261,466,443,442,465,464,359,358,504,502,503,502,503,502,501],[266,265,264,263,262,261,543,542,541,465,558,557,556,555,623,659,658],[266,265,264,263,262,261,543,542,541,465,558,557,556,555,623,660,539,651],[266,265,264,263,262,261],[266,265,264,263,262,261],[266,265,264,263,262,261],[266,265,264,263,262,261],[266,265,264,263,262],[266,265,264,263,262],[266,265,264,263,262],[266,265,264,263,262],[266,265,264,263,262,261,260,270,269,268,267],[266,265,264,263,262,261,260,270,269,268,267],[266,265,264,263,262,261,260,270,269,268,267],[266,265,264,263,262,261,260,270,277],[266,265,264,263,262,261,260,270,277],[266,265,264,263,262,261,292,291,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,264,263,262,261,292,291,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,264,263,262,261,302,301,300,299,298,297,296,303],[266,265,264,263,262,261,302,301,300,299,298,297,296,303],[266,265,264,263,262,261,302,301,300,299,298,297,296,303],[266,265,264,263,262,261,302,301,300,299,298,297,296,295,294,293],[266,265,264,263,262,261,302,301,300,299,298,297,296,303],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,319,294,293],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,319,294,293],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,508,509],[266,265,264,263,262,261,302,318,317,316,315,314,313,326,325,324,323],[266,265,264,263,262,261,302,318,317,316,315,314,336,335,334,333,584,583],[266,265,264,263,262,261,302,318,317,316,315,314,336,335,334,333,332,331,330,329,328,327],[266,265,264,263,262,261,302,318,317,316,315,314,336,335,334,333,332,331,330,329,328,327],[266,265,264,263,262,261,302,318,317,316,315,314,336,335,334,333,332,331,330,329,328,327],[266,265,264,263,262,261,351,350,349,348,347,346,345,344,343,342,341,340,281,280,549],[266,265,264,263,262,261,526,525,654,653,655,348,347,346,520],[266,265,264,263,262,261,526,525,524,523,502,503,522],[266,265,264,263,262,261,466,443,442,465,464,359,358,357,635,607,502,503,502,503,606],[266,265,264,263,262,261,466,443,442,465,464,359,358,357,463,462,495,358,357,356,643,359,358,357,356,642,358,357,356,661],[266,265,264,263,262,261,543,542,666,665,664,663,662],[266,265,264,263,262,261,543,542,666,665,668,667],[266,265,264,263,262,261,543,542,669,465,440,438,436],[266,265,264,263,262,261],[266,265,264,263,262,261],[266,265,264,263,262,261],[266,265,264,263,262],[266,265,679,263,678,677,676,675,674,673,447,446,672,353,352,185,671,670],[266,265,679,263,678,677,260,270,269,268,267],[266,265,679,263,678,677,260,270,269,268,267],[266,265,679,263,678,677,260,270,269,268,267],[266,265,679,263,678,677,260,270,269,268,267],[266,265,679,263,678,677,260,270,269,268,267],[266,265,679,263,678,677,260,270,269,268,267],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,251,250,249],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,251,250,249],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,681,680],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,685],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,685],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,685],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,685],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,685],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,685],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,685],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,685],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,685],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,685],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252],[266,265,679,263,678,677,260,270,277],[266,265,679,263,678,677,260,270,277],[266,265,679,263,678,677,292,291,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,679,263,678,677,292,291,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,679,263,678,677,292,291,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,679,263,678,677,292,291,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,679,263,678,677,292,582,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,679,263,678,677,292,582,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,679,263,678,677,302,301,300,299,298,297,296,295,294,293],[266,265,679,263,678,677,302,301,300,299,298,297,296,295,294,293],[266,265,679,263,678,677,302,301,300,299,298,297,296,295,294,293],[266,265,679,263,678,677,302,301,300,299,298,297,296,295,294,293],[266,265,679,263,678,677,302,301,300,299,298,297,296,295,294,293],[266,265,679,263,678,677,302,301,300,299,298,297,296,295,294,293],[266,265,679,263,678,677,302,301,300,299,298,297,296,303],[266,265,679,263,678,677,302,301,300,299,298,297,296,303],[266,265,679,263,678,677,302,301,300,299,298,297,296,295,294,293],[266,265,679,263,678,677,302,301,300,299,298,297,296,303],[266,265,679,263,678,677,302,301,300,299,298,297,296,303],[266,265,679,263,678,677,302,301,300,299,298,297,296,303],[266,265,679,263,678,677,302,301,300,299,298,297,296,303],[266,265,679,263,678,677,302,301,300,299,298,297,296,303],[266,265,679,263,678,677,302,301,300,299,298,297,296,303],[266,265,679,263,678,677,302,301,300,299,298,297,296,303],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,508,509],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,508,509],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,508,509],[266,265,679,263,678,677,302,318,317,316,315,314,313,326,325,324,323],[266,265,679,263,678,677,302,318,317,316,315,314,313,326,325,324,323],[266,265,679,263,678,677,302,318,317,316,315,314,512,511,510],[266,265,679,263,678,677,302,318,317,316,315,314,512,511,510],[266,265,679,263,678,677,302,318,317,316,315,314,512,511,510],[266,265,679,263,678,677,302,318,317,316,315,314,512,511,510],[266,265,679,263,678,677,302,318,317,316,315,314,336,335,334,333,584,583],[266,265,679,263,678,677,302,318,317,316,315,314,336,335,334,333,332,331,330,329,328,327],[266,265,679,263,678,677,302,318,317,316,315,314,336,335,334,333,332,331,330,329,328,327],[266,265,679,263,678,677,687,686,438,447,446,445,354,353,352,5,4,88,87,221],[266,265,679,263,678,677,351,350,349,348,347,689,688,344,343,342,341,340,281,280,279,278],[266,265,679,263,678,677,351,350,349,348,347,346,521,690],[266,265,679,263,678,677,351,350,349,348,347,689,688,344,343,342,341,340,281,280,279,278],[266,265,679,263,678,677,575,574,350,440,438,691],[266,265,679,263,678,677,351,350,349,348,347,689,688],[266,265,679,263,678,677,351,350,349,348,347,346,345,344,343,342,341,340,281,280,279,278],[266,265,679,263,678,677,351,350,349,348,347,346,345,344,343,342,341,340,281,280,279,278],[266,265,679,263,678,677,351,350,349,348,347,346,345,344,343,342,341,340,281,280,279,278],[266,265,679,263,678,677,351,350,349,348,347,689,688,344,343,342,341,340,281,280,279,278],[266,265,679,263,678,677,351,350,349,348,347,689,688],[266,265,679,263,678,677,351,350,349,348,347,346,345,344,343,342,341,340,281,280,279,278],[266,265,679,263,678,677,351,350,349,348,347,346,345,344,343,342,341,340,281,280,279,278],[266,265,679,263,678,677,364,363,373,372,697,696,695,694,693,359,692,607,502,503,502,503,502,616,502,503,502],[266,265,679,263,678,677,364,363,444,443,442,441,440,439,438,437,436,698],[266,265,679,263,678,677,526,525,524,523,502,503,522,699],[266,265,679,263,678,677,526,525,524,523,502,503,522],[266,265,679,263,678,677,526,525,524,523,502,503,522,700],[266,265,679,263,678,677,526,525,524,523,502,503,522,701],[266,265,679,263,678,677,526,525,524,523,502,503,522,702],[266,265,679,263,678,677,526,525,524,523,502,503,522,703],[266,265,679,263,678,677,526,525,524,523,502,503,522],[266,265,679,263,678,677,526,525,524,523,502,503,522,703],[266,265,679,263,678,677,526,525,524,523,502,503,522,703],[266,265,679,263,678,677,526,525,524,523,502,503,522],[266,265,679,263,678,677,526,525,524,523,502,503,522],[266,265,679,263,678,677,526,525,524,523,502,503,522,704],[266,265,679,263,678,677,526,525,524,523,502,503,522,703],[266,265,679,263,678,677,526,525,524,523,502,503,522,502],[266,265,679,263,678,677,526,525,524,523,502,503,522,705],[266,265,679,263,678,677,526,525,524,523,502,503,522],[266,265,679,263,678,677,526,525,524,523,502,503,522,702],[266,265,679,263,678,677,526,525,524,523,502,503,522,703],[266,265,679,263,678,677,526,525,524,523,502,503,522,702],[266,265,679,263,678,677,526,525,524,523,502,503,522,703],[266,265,679,263,678,677,526,525,524,523,502,503,522,502],[266,265,679,263,678,677,526,525,524,523,502,503,522,702],[266,265,679,263,678,677,526,525,524,523,502,503,522,705],[266,265,679,263,678,677,526,525,524,523,502,503,522],[266,265,679,263,678,677,526,525,524,523,502,503,522,703],[266,265,679,263,678,677,526,525,524,523,502,503,522,699],[266,265,679,263,678,677,526,525,524,523,502,503,522,706],[266,265,679,263,678,677,526,525,524,523,502,503,522,702],[266,265,679,263,678,677,526,525,524,523,502,503,522,702],[266,265,679,263,678,677,526,525,524,523,502,503,522],[266,265,679,263,678,677,526,525,524,523,502,503,522,699],[266,265,679,263,678,677,526,525,524,523,502,503,522,703],[266,265,679,263,678,677,526,525,524,523,502,503,522],[266,265,679,263,678,677,526,525,524,523,502,503,522,703],[266,265,679,263,678,677,526,525,524,523,502,503,522],[266,265,679,263,678,677,526,525,524,523,502,503,522,703],[266,265,679,263,678,677,466,443,442,465,464,359,358,357,463,462,461,358,357,356,479,459,458,457,478,348,347,708,707,536,610,609],[266,265,679,263,678,677],[266,265,679,263,678,677],[266,265,679,263,678,677,260,270,269,268,267],[266,265,679,263,678,677,260,270,269,268,267],[266,265,679,263,678,677,260,270,269,268,267],[266,265,679,263,678,677,260,270,269,268,267],[266,265,679,263,678,677,260,270,269,268,267],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,251,250,547],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,251,250,249],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,251,250,249],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,251,250,249],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,681,680],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,681,680],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,681,680],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,681,680],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,681,680],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,681,680],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,681,680],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,681,680],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,681,680],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,685],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,685],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,685],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,685],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,685],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,685],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,685],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,685],[266,265,679,263,678,677,260,270,277],[266,265,679,263,678,677,260,270,277],[266,265,679,263,678,677,292,291,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,679,263,678,677,292,291,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,679,263,678,677,292,291,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,679,263,678,677,292,291,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,679,263,678,677,292,291,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,679,263,678,677,292,291,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,679,263,678,677,292,291,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,679,263,678,677,292,582,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,679,263,678,677,292,582,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,679,263,678,677,292,582,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,679,263,678,677,292,582,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,679,263,678,677,292,582,290,289,288,287,286,285,284,283,282,281,339,710,709],[266,265,679,263,678,677,302,301,300,299,298,297,296,295,294,293],[266,265,679,263,678,677,302,301,300,299,298,297,296,303],[266,265,679,263,678,677,302,301,300,299,298,297,296,303],[266,265,679,263,678,677,302,301,300,299,298,297,296,303],[266,265,679,263,678,677,302,301,300,299,298,297,296,303],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,319,294,293],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,319,294,293],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,319,294,293],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,319,294,293],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,319,294,293],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,679,263,678,677,302,318,317,316,315,314,313,326,325,324,323],[266,265,679,263,678,677,302,318,317,316,315,314,313,326,325,324,323],[266,265,679,263,678,677,302,318,317,316,315,314,313,326,325,324,323],[266,265,679,263,678,677,302,318,317,316,315,314,336,335,334,333,332,331,330,329,328,337],[266,265,679,263,678,677,302,318,317,316,315,314,336,335,334,333,332,331,330,329,328,337],[266,265,679,263,678,677,302,318,317,316,315,314,336,335,334,333,332,331,330,329,328,337],[266,265,679,263,678,677,302,318,317,316,315,314,336,335,334,333,332,331,330,329,328,327],[266,265,679,263,678,677,302,318,317,316,315,314,336,335,334,333,332,331,330,329,328,327],[266,265,679,263,678,677,302,318,317,316,315,314,336,335,334,333,332,331,330,329,328,327],[266,265,679,263,678,677,351,350,349,348,347,689,688,344,343,342,341,340,281,280,279,278],[266,265,679,263,678,677,351,350,349,348,347,689,688,344,343,342,341,340,281,280,279,278],[266,265,679,263,678,677,351,350,349,348,347,346,345,344,343,342,341,340,281,280,279,278],[266,265,679,263,678,677,526,525,654,653,713,358,357,356,712,358,357,711,694],[266,265,679,263,678,677,526,525,654,653,714,348,347,346,690],[266,265,679,263,678,677,526,525,524,523,502,503,522,702],[266,265,679,263,678,677,526,525,524,523,502,503,522,699],[266,265,679,263,678,677,526,525,524,523,502,503,522,704],[266,265,679,263,678,677,526,525,524,523,502,503,522,704],[266,265,679,263,678,677,526,525,524,523,502,503,522,704],[266,265,679,263,678,677,526,525,524,523,502,503,522,699],[266,265,679,263,678,677,526,525,524,523,502,503,522,706],[266,265,679,263,678,677,526,525,524,523,502,503,522],[266,265,679,263,678,677,526,525,524,523,502,503,522,502],[266,265,679,263,678,677,526,525,524,523,502,503,522,502,715],[266,265,679,263,678,677,526,525,524,523,502,503,522,716],[266,265,679,263,678,677,526,525,524,523,502,503,522,703],[266,265,679,263,678,677,526,525,524,523,502,503,522,502],[266,265,679,263,678,677,526,525,524,523,502,503,522,703],[266,265,679,263,678,677,526,525,524,523,502,503,522,701],[266,265,679,263,678,677,526,525,524,523,502,503,522,702],[266,265,679,263,678,677,526,525,524,523,502,503,522,703],[266,265,679,263,678,677,526,525,524,523,502,503,522,699],[266,265,679,263,678,677,526,525,524,523,502,503,522],[266,265,679,263,678,677,526,525,524,523,502,503,522,502],[266,265,679,263,678,677,526,525,524,523,502,503,522],[266,265,679,263,678,677,526,525,524,523,502,503,522],[266,265,679,263,678,677,526,525,524,523,502,503,522,702],[266,265,679,263,678,677,526,525,524,523,502,503,522,702],[266,265,679,263,678,677,526,525,524,523,502,503,522,700],[266,265,679,263,678,677,526,525,524,523,502,503,522,502],[266,265,679,263,678,677,526,525,524,523,502,503,522,706],[266,265,679,263,678,677,526,525,524,523,502,503,522,700],[266,265,679,263,678,677,526,525,524,523,502,503,522],[266,265,679,263,678,677,526,525,524,523,502,503,522,502],[266,265,679,263,678,677,526,525,524,523,502,503,522,703],[266,265,679,263,678,677,526,525,524,523,502,503,522],[266,265,679,263,678,677,526,525,524,523,502,503,522,502],[266,265,679,263,678,677,526,525,717],[266,265,679,263,678,677,526,525,717],[266,265,679,263,678,677,466,443,442,465,464,359,358,357,463,462,461,358,357,356,721,459,720,719,502,503,502,616,502,718],[266,265,679,263,678,677],[266,265,679,263,678,677],[266,265,679,263,678,677],[266,265,679,263,678],[266,265,679,263,678],[266,265,679,263,678,677,260,270,269,268,628,723,722],[266,265,679,263,678,677,260,270,269,268,267],[266,265,679,263,678,677,260,270,269,268,267],[266,265,679,263,678,677,260,270,269,268,267],[266,265,679,263,678,677,260,270,269,268,267],[266,265,679,263,678,677,260,270,269,268,267],[266,265,679,263,678,677,260,270,269,268,267],[266,265,679,263,678,677,260,270,269,268,267],[266,265,679,263,678,677,260,270,269,268,267,724],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,251,250,249],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,681,680],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,681,680],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,685],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,685],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,685],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,685],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,685],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252],[266,265,679,263,678,677,260,270,277],[266,265,679,263,678,677,260,270,277],[266,265,679,263,678,677,292,291,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,679,263,678,677,292,582,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,679,263,678,677,302,301,300,299,298,297,296,295,294,293],[266,265,679,263,678,677,302,301,300,299,298,297,296,303],[266,265,679,263,678,677,302,301,300,299,298,297,296,303],[266,265,679,263,678,677,302,301,300,299,298,297,296,303],[266,265,679,263,678,677,302,301,300,299,298,297,296,303],[266,265,679,263,678,677,302,301,300,299,298,297,296,303],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,679,263,678,677,302,318,317,316,315,314,512,511,510],[266,265,679,263,678,677,302,318,317,316,315,314,336,335,334,333,584,583],[266,265,679,263,678,677,302,318,317,316,315,314,336,335,334,333,332,331,330,329,328,327],[266,265,679,263,678,677,727,484,354,353,352,518,108,726,725],[266,265,679,263,678,677,730,729,349,454,446,728],[266,265,679,263,678,677,351,350,349,348,347,689,688,344,343,342,341,340,281,280,279,278],[266,265,679,263,678,677,351,350,349,348,347,689,688,344,343,342,341,340,281,280,279,278],[266,265,679,263,678,677,351,350,349,348,347,346,345,344,343,342,341,340,281,280,279,278],[266,265,679,263,678,677,351,350,349,348,347,346,345],[266,265,679,263,678,677,351,350,349,348,347,346,521,732,379,353,352,185,671,726,731],[266,265,679,263,678,677,351,350,349,348,347,689,688,344,343,342,341,340,281,280,279,278],[266,265,679,263,678,677,351,350,349,348,347,689,688,344,343,342,341,340,281,280,279,278],[266,265,679,263,678,677,351,350,349,348,347,346,345,344,343,342,341,340,281,280,279,278],[266,265,679,263,678,677,351,350,349,348,347,689,688,344,343,342,341,340,281,280,279,278],[266,265,679,263,678,677,351,350,349,348,347,346,345,344,343,342,341,340,281,280,279,278],[266,265,679,263,678,677,351,350,349,348,347,346,345,344,343,342,341,340,281,280,279,278],[266,265,679,263,678,677,351,350,349,348,347,689,688,344,343,342,341,340,281,280,279,278],[266,265,679,263,678,677,364,363,362,361,738,737,736,357,356,735,348,347,734,733],[266,265,679,263,678,677,364,363,742,741,740,577,576,739],[266,265,679,263,678,677,526,525,524,523,502,503,522,502],[266,265,679,263,678,677,526,525,524,523,502,503,522],[266,265,679,263,678,677,526,525,524,523,502,503,522,716],[266,265,679,263,678,677,526,525,524,523,502,503,522,716],[266,265,679,263,678,677,526,525,524,523,502,503,522,703],[266,265,679,263,678,677,526,525,524,523,502,503,522,701],[266,265,679,263,678,677,526,525,524,523,502,503,522,705],[266,265,679,263,678,677,526,525,524,523,502,503,522,702],[266,265,679,263,678,677,526,525,524,523,502,503,522,502,715],[266,265,679,263,678,677,526,525,524,523,502,503,522,702],[266,265,679,263,678,677,526,525,524,523,502,503,522],[266,265,679,263,678,677,526,525,524,523,502,503,522,702],[266,265,679,263,678,677,526,525,524,523,502,503,522,703],[266,265,679,263,678,677,526,525,524,523,502,503,522,704],[266,265,679,263,678,677,526,525,524,523,502,503,522,705],[266,265,679,263,678,677,526,525,524,523,502,503,522],[266,265,679,263,678,677,526,525,524,523,502,503,522,702],[266,265,679,263,678,677,526,525,524,523,502,503,522,703],[266,265,679,263,678,677,526,525,524,523,502,503,522,502],[266,265,679,263,678,677,526,525,524,523,502,503,522,699],[266,265,679,263,678,677,526,525,524,523,502,503,522,699],[266,265,679,263,678,677,526,525,524,523,502,503,522,502],[266,265,679,263,678,677,526,525,524,523,502,503,522,502],[266,265,679,263,678,677,526,525,524,523,502,503,522,502,743],[266,265,679,263,678,677,526,525,524,523,502,503,522,502],[266,265,679,263,678,677,526,525,524,523,502,503,522,704],[266,265,679,263,678,677,526,525,524,523,502,503,522,704],[266,265,679,263,678,677,526,525,524,523,502,503,522,703],[266,265,679,263,678,677,526,525,524,523,502,503,522,705],[266,265,679,263,678,677,526,525,524,523,502,503,522,702],[266,265,679,263,678,677,526,525,524,523,502,503,522],[266,265,679,263,678,677,559,443,442,465,558,557,562,634,437,744,447,446],[266,265,679,263,678,677,466,443,442,465,464,359,358,357,463,462,461,358,357,356,748,459,458,457,747,358,357,356,746,358,357,356,745,454,560],[266,265,679,263,678,677,466,443,442,465,464,359,358,571,570,569,570,569,568,567,570,569,568,567,568,567,749],[266,265,679,263,678,677],[266,265,679,263,678,677],[266,265,679,263,678,677],[266,265,679,263,678,677],[266,265,679,263,678,677],[266,265,679,263,678,677],[266,265,679,263,678,677],[266,265,679,263,678,677],[266,265,679,263,678],[266,265,679,263,678],[266,265,679,263,678],[266,265,679,263,678],[266,265,679,263,678],[266,265,679,263,678],[266,265,679,263,678],[266,265,679,263,678],[266,265,679,263,678,677,260,270,269,268,267],[266,265,679,263,678,677,260,270,269,268,267],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,251,250,249],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,251,250,547],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,251,250,547],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,251,250,249],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,681,680],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,681,680],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,681,680],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,681,680],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,681,680],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,681,680],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,681,680],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,681,680],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,685],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,685],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,685],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,685],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,685],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,685],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,685],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,685],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,685],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,685],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,685],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,685],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252],[266,265,679,263,678,677,292,291,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,679,263,678,677,292,291,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,679,263,678,677,292,291,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,679,263,678,677,292,291,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,679,263,678,677,292,291,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,679,263,678,677,292,291,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,679,263,678,677,292,291,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,679,263,678,677,292,291,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,679,263,678,677,292,291,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,679,263,678,677,292,582,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,679,263,678,677,292,582,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,679,263,678,677,292,582,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,679,263,678,677,292,582,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,679,263,678,677,292,582,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,679,263,678,677,302,301,300,299,298,297,296,303],[266,265,679,263,678,677,302,301,300,299,298,297,296,303],[266,265,679,263,678,677,302,301,300,299,298,297,296,303],[266,265,679,263,678,677,302,301,300,299,298,297,296,303],[266,265,679,263,678,677,302,301,300,299,298,297,296,303],[266,265,679,263,678,677,302,301,300,299,298,297,296,295,294,293],[266,265,679,263,678,677,302,301,300,299,298,297,296,295,294,293],[266,265,679,263,678,677,302,301,300,299,298,297,296,303],[266,265,679,263,678,677,302,301,300,299,298,297,296,303],[266,265,679,263,678,677,302,301,300,299,298,297,296,303],[266,265,679,263,678,677,302,301,300,299,298,297,296,303],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,319,294,293],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,319,294,293],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,322],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,508,509],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,508,509],[266,265,679,263,678,677,302,318,317,316,315,314,512,511,510],[266,265,679,263,678,677,302,318,317,316,315,314,336,335,334,333,332,331,330,329,328,337],[266,265,679,263,678,677,302,318,317,316,315,314,336,335,334,333,332,331,330,329,328,327],[266,265,679,263,678,677,302,318,317,316,315,314,336,335,334,333,332,331,330,329,328,327],[266,265,679,263,678,677,302,318,317,316,315,314,336,335,334,333,332,331,330,329,328,327],[266,265,679,263,678,677,302,318,317,316,315,314,336,335,334,333,332,331,330,329,328,327],[266,265,679,263,678,677,302,318,317,316,315,314,336,335,334,333,332,331,330,329,328,327],[266,265,679,263,678,677,351,350,349,348,347,689,688,344,343,342,341,340,281,280,279,278],[266,265,679,263,678,677,351,350,349,348,347,346,345,344,343,342,341,340,281,280,279,278],[266,265,679,263,678,677,351,350,349,348,347,346,345,344,343,342,341,340,281,280,279,278],[266,265,679,263,678,677,351,350,349,348,347,689,688,344,343,342,341,340,281,280,279,278],[266,265,679,263,678,677,351,350,349,348,347,689,688,344,343,342,341,340,281,280,279,278],[266,265,679,263,678,677,750,729,349,454,560],[266,265,679,263,678,677,351,350,349,348,347,346,345,344,343,342,341,340,281,280,279,278],[266,265,679,263,678,677,364,363,444,443,442,441,440,439,438,577,576,539,599,751],[266,265,679,263,678,677,364,363,444,443,442,441,579,587,752],[266,265,679,263,678,677,526,525,654,653,753,348,347,656,610],[266,265,679,263,678,677,526,525,524,523,502,503,522,699],[266,265,679,263,678,677,526,525,524,523,502,503,522,701],[266,265,679,263,678,677,526,525,524,523,502,503,522,705],[266,265,679,263,678,677,526,525,524,523,502,503,522,702],[266,265,679,263,678,677,526,525,524,523,502,503,522,703],[266,265,679,263,678,677,526,525,524,523,502,503,522,704],[266,265,679,263,678,677,526,525,524,523,502,503,522,502],[266,265,679,263,678,677,526,525,524,523,502,503,522,699],[266,265,679,263,678,677,526,525,524,523,502,503,522,502],[266,265,679,263,678,677,526,525,524,523,502,503,522,699],[266,265,679,263,678,677,526,525,524,523,502,503,522,700],[266,265,679,263,678,677,526,525,524,523,502,503,522,705],[266,265,679,263,678,677,526,525,524,523,502,503,522,703],[266,265,679,263,678,677,526,525,524,523,502,503,522,701],[266,265,679,263,678,677,526,525,524,523,502,503,522,699],[266,265,679,263,678,677,526,525,524,523,502,503,522],[266,265,679,263,678,677,526,525,524,523,502,503,522,705],[266,265,679,263,678,677,526,525,524,523,502,503,522,703],[266,265,679,263,678,677,526,525,524,523,502,503,522,502],[266,265,679,263,678,677,526,525,524,523,502,503,522,702],[266,265,679,263,678,677,526,525,524,523,502,503,522,699],[266,265,679,263,678,677,526,525,524,523,502,503,522,701],[266,265,679,263,678,677,526,525,524,523,502,503,522,702],[266,265,679,263,678,677,526,525,524,523,502,503,522,705],[266,265,679,263,678,677,526,525,524,523,502,503,522,699],[266,265,679,263,678,677,526,525,717],[266,265,679,263,678,677],[266,265,679,263,678,677],[266,265,679,263,678],[266,265,679,263,678],[266,265,679,263,678,677,260,270,269,268,267],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,251,250,249],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,251,250,249],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,251,250,249],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,681,680],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,681,680],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,681,680],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,681,680],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,685],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,685],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,685],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,685],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,685],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,685],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,685],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,685],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,685],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252],[266,265,679,263,678,677,260,270,277],[266,265,679,263,678,677,260,270,277],[266,265,679,263,678,677,292,291,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,679,263,678,677,292,582,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,679,263,678,677,292,582,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,679,263,678,677,292,582,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,679,263,678,677,292,582,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,679,263,678,677,302,301,300,299,298,297,296,295,294,293],[266,265,679,263,678,677,302,301,300,299,298,297,296,295,294,293],[266,265,679,263,678,677,302,301,300,299,298,297,296,295,294,293],[266,265,679,263,678,677,302,318,317,316,758,757,45,756,755,754],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,319,294,759],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,319,294,293],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,319,294,293],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,679,263,678,677,302,318,317,316,315,314,512,511,510],[266,265,679,263,678,677,302,318,317,316,315,314,336,335,334,333,332,331,330,329,328,327],[266,265,679,263,678,677,302,318,317,316,315,314,336,335,334,333,332,331,330,329,328,327],[266,265,679,263,678,677,302,318,317,316,315,314,336,335,334,333,332,331,330,329,328,327],[266,265,679,263,678,677,302,318,317,316,315,314,336,335,334,333,332,331,330,329,328,327],[266,265,679,263,678,677,302,318,317,316,315,314,336,335,334,333,332,331,330,329,328,327],[266,265,679,263,678,677,302,318,317,316,315,314,336,335,334,333,332,331,330,329,328,327],[266,265,679,263,678,677,351,350,349,348,347,689,688,344,343,342,341,340,281,280,279,278],[266,265,679,263,678,677,575,574,350,573,349,764,578,763,761,762,761,760],[266,265,679,263,678,677,597,767,766,765],[266,265,679,263,678,677,351,350,349,348,347,689,768],[266,265,679,263,678,677,351,350,349,348,347,346,345,344,343,342,341,340,281,280,279,278],[266,265,679,263,678,677,351,350,349,348,347,689,688,344,343,342,341,340,281,280,279,278],[266,265,679,263,678,677,351,350,349,348,347,346,345,344,343,342,341,340,281,280,279,278],[266,265,679,263,678,677,351,350,349,348,347,346,345,344,343,342,341,340,281,280,279,278],[266,265,679,263,678,677,351,350,349,348,347,346,345,344,343,342,772,771,770,769],[266,265,679,263,678,677,364,363,776,359,459,458,457,775,348,347,774,773,179],[266,265,679,263,678,677,526,525,524,523,502,503,522,706],[266,265,679,263,678,677,526,525,524,523,502,503,522,704],[266,265,679,263,678,677,526,525,524,523,502,503,522,502],[266,265,679,263,678,677,526,525,524,523,502,503,522,699],[266,265,679,263,678,677,526,525,524,523,502,503,522,702],[266,265,679,263,678,677,526,525,524,523,502,503,522,703],[266,265,679,263,678,677,526,525,524,523,502,503,522,702],[266,265,679,263,678,677,526,525,524,523,502,503,522,705],[266,265,679,263,678,677,526,525,524,523,502,503,522,702],[266,265,679,263,678,677,526,525,524,523,502,503,522],[266,265,679,263,678,677,526,525,524,523,502,503,522,702],[266,265,679,263,678,677,526,525,524,523,502,503,522],[266,265,679,263,678,677,526,525,524,523,502,503,522],[266,265,679,263,678,677,526,525,524,523,502,503,522,702],[266,265,679,263,678,677,526,525,524,523,502,503,522,502,743],[266,265,679,263,678,677,526,525,524,523,502,503,522,705],[266,265,679,263,678,677,526,525,524,523,502,503,522,699],[266,265,679,263,678,677,526,525,524,523,502,503,522,702],[266,265,679,263,678,677,526,525,524,523,502,503,522],[266,265,679,263,678,677,526,525,524,523,502,503,522],[266,265,679,263,678,677,526,525,524,523,502,503,522,706],[266,265,679,263,678,677,526,525,524,523,502,503,522,702],[266,265,679,263,678,677,526,525,524,523,502,503,522,703],[266,265,679,263,678,677,526,525,524,523,502,503,522,699],[266,265,679,263,678,677,526,525,524,523,502,503,522,703],[266,265,679,263,678,677,526,525,524,523,502,503,522],[266,265,679,263,678,677,526,525,524,523,502,503,522,703],[266,265,679,263,678,677,466,443,442,465,464,359,358,777,502,503,502,503,502,616,502,503,502,616,502],[266,265,679,263,678,677],[266,265,679,263,678,677],[266,265,679,263,678,677,260,270,269,268,267],[266,265,679,263,678,677,260,270,269,268,267],[266,265,679,263,678,677,260,270,269,268,267],[266,265,679,263,678,677,260,270,269,268,267],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,251,250,547],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,251,250,547],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,251,250,547],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,251,250,547],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,251,250,547],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,251,250,249],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,251,250,249],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,251,250,249],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,251,250,249],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,681,680],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,681,680],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,685],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,685],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,685],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,685],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,685],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,685],[266,265,679,263,678,677,260,270,277],[266,265,679,263,678,677,260,270,277],[266,265,679,263,678,677,292,291,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,679,263,678,677,292,291,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,679,263,678,677,292,291,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,679,263,678,677,292,291,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,679,263,678,677,292,582,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,679,263,678,677,292,582,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,679,263,678,677,292,582,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,679,263,678,677,302,301,300,299,298,297,296,295,294,293],[266,265,679,263,678,677,302,301,300,299,298,297,296,303],[266,265,679,263,678,677,302,301,300,299,298,297,296,303],[266,265,679,263,678,677,302,301,300,299,298,297,296,303],[266,265,679,263,678,677,302,301,300,299,298,297,296,303],[266,265,679,263,678,677,302,301,300,299,298,297,296,303],[266,265,679,263,678,677,302,301,300,299,298,297,296,303],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,319,294,293],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,319,294,293],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,506],[266,265,679,263,678,677,302,318,317,316,315,314,313,326,325,324,323],[266,265,679,263,678,677,302,318,317,316,315,314,313,326,325,324,323],[266,265,679,263,678,677,302,318,317,316,315,314,313,326,325],[266,265,679,263,678,677,302,318,317,316,315,314,313,326,325],[266,265,679,263,678,677,302,318,317,316,315,314,512,511,510],[266,265,679,263,678,677,302,318,317,316,315,314,336,335,334,333,332,331,330,329,328,327],[266,265,679,263,678,677,302,318,317,316,315,314,336,335,334,333,332,331,330,329,328,327],[266,265,679,263,678,677,302,318,317,316,315,314,336,335,334,333,332,331,330,329,328,327],[266,265,679,263,678,677,302,318,317,316,315,314,336,335,334,333,332,331,330,329,328,327],[266,265,679,263,678,677,302,318,317,316,315,314,336,335,334,333,332,331,330,329,328,327],[266,265,679,263,678,677,351,350,349,348,347,689,688,344,343,342,341,340,281,280,279,278],[266,265,679,263,678,677,351,350,349,348,347,346,345,344,343,342,341,340,281,280,279,278],[266,265,679,263,678,677,351,350,349,348,347,346,345,344,343,342,341,340,281,280,279,278],[266,265,679,263,678,677,597,767,766],[266,265,679,263,678,677,351,350,349,348,347,346,345,344,343,342,341,340,281,280,279,278],[266,265,679,263,678,677,351,350,349,348,347,346,345,344,343,342,341,340,281,280,279,278],[266,265,679,263,678,677,351,350,349,348,347,346,345,344,343,342,341,340,281,280,279,278],[266,265,679,263,678,677,351,350,349,348,347,346,345,344,343,342,341,340,281,280,279,278],[266,265,679,263,678,677,351,350,349,348,347,689,688],[266,265,679,263,678,677,351,350,349,348,347,689,688,344,343,342,341,340,281,280,279,278],[266,265,679,263,678,677,351,350,349,348,347,346,345,344,343,342,341,340,281,339,778],[266,265,679,263,678,677,351,350,349,348,347,346,345],[266,265,679,263,678,677,364,363,444,443,442,441,579,587,586],[266,265,679,263,678,677,364,363,444,443,442,441,440,439,438,577,621],[266,265,679,263,678,677,364,363,444,443,442,441,440,439,438,437,779,539,651],[266,265,679,263,678,677,526,525,524,523,502,503,522,703],[266,265,679,263,678,677,526,525,524,523,502,503,522,703],[266,265,679,263,678,677,526,525,524,523,502,503,522,701],[266,265,679,263,678,677,526,525,524,523,502,503,522,705],[266,265,679,263,678,677,526,525,524,523,502,503,522,700],[266,265,679,263,678,677,526,525,524,523,502,503,522],[266,265,679,263,678,677,526,525,524,523,502,503,522,704],[266,265,679,263,678,677,526,525,524,523,502,503,522,705],[266,265,679,263,678,677,526,525,524,523,502,503,522,705],[266,265,679,263,678,677,526,525,524,523,502,503,522,606],[266,265,679,263,678,677,526,525,524,523,502,503,522,703],[266,265,679,263,678,677,526,525,524,523,502,503,522,716],[266,265,679,263,678,677,526,525,524,523,502,503,522,502],[266,265,679,263,678,677,526,525,524,523,502,503,522,502],[266,265,679,263,678,677,526,525,524,523,502,503,522,502],[266,265,679,263,678,677,526,525,524,523,502,503,522,502],[266,265,679,263,678,677,526,525,524,523,502,503,522,502],[266,265,679,263,678,677,526,525,524,523,502,503,522],[266,265,679,263,678,677,526,525,524,523,502,503,522,703],[266,265,679,263,678,677,526,525,524,523,502,503,522],[266,265,679,263,678,677,526,525,524,523,502,503,522,702],[266,265,679,263,678,677,526,525,524,523,502,503,522,699],[266,265,679,263,678,677,526,525,524,523,502,503,522,702],[266,265,679,263,678,677,526,525,524,523,502,503,522,606],[266,265,679,263,678,677,526,525,524,523,502,503,522,606],[266,265,679,263,678,677,526,525,524,523,502,503,522,703],[266,265,679,263,678,677,526,525,524,523,502,503,522,704],[266,265,679,263,678,677,526,525,524,523,502,503,522,699],[266,265,679,263,678,677,526,525,524,523,502,503,522],[266,265,679,263,678,677,526,525,524,523,502,503,522,702],[266,265,679,263,678,677,526,525,524,523,502,503,522,502],[266,265,679,263,678,677,526,525,524,523,502,503,522,703],[266,265,679,263,678,677,526,525,524,523,502,503,522,705],[266,265,679,263,678,677,526,525,524,523,502,503,522,703],[266,265,679,263,678,677,526,525,524,523,502,503,522,703],[266,265,679,263,678,677,526,525,524,523,502,503,522,703],[266,265,679,263,678,677,526,525,524,523,502,503,522,703],[266,265,679,263,678,677,526,525,524,523,502,503,522,502,743],[266,265,679,263,678,677,526,525,524,523,502,503,522,502],[266,265,679,263,678,677,526,525,524,523,502,503,522,702],[266,265,679,263,678,677,526,525,524,523,502,503,522,699],[266,265,679,263,678,677,526,525,524,523,502,503,522,703],[266,265,679,263,678,677,526,525,524,523,502,503,522,502],[266,265,679,263,678,677,526,525,524,523,502,503,522,705],[266,265,679,263,678,677,526,525,524,523,502,503,522],[266,265,679,263,678,677,526,525,524,523,502,503,522],[266,265,679,263,678,677,543,542,666,665,664,663,662],[266,265,679,263,678,677],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,251,250,249],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,681,680],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,681,680],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,681,680],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,681,680],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,685],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,685],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,685],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,685],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,685],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,685],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,685],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252],[266,265,679,263,678,677,292,291,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,679,263,678,677,292,291,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,679,263,678,677,292,291,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,679,263,678,677,292,291,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,679,263,678,677,292,582,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,679,263,678,677,292,582,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,679,263,678,677,292,582,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,679,263,678,677,302,301,300,299,298,297,296,295,294,293],[266,265,679,263,678,677,302,301,300,299,298,297,296,303],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,319,294,293],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,508,507],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,508,780],[266,265,679,263,678,677,302,318,317,316,315,314,313,326,325,324,323],[266,265,679,263,678,677,302,318,317,316,315,314,336,335,334,333,584,583],[266,265,679,263,678,677,302,318,317,316,315,314,336,335,334,333,783,782,781],[266,265,679,263,678,677,302,318,317,316,315,314,336,335,334,333,332,331,330,329,328,327],[266,265,679,263,678,677,302,318,317,316,315,314,336,335,334,333,332,331,330,329,328,327],[266,265,679,263,678,677,302,318,317,316,315,314,336,335,334,333,332,331,330,329,328,327],[266,265,679,263,678,677,575,574,350,573,440,540,539,651,650,649,648],[266,265,679,263,678,677,351,350,349,348,347,689,688,344,343,342,341,340,281,280,279,278],[266,265,679,263,678,677,575,574,350,573,579,578,763,761,784,446,728],[266,265,679,263,678,677,351,350,349,348,347,689,688,344,343,342,341,340,281,280,279,278],[266,265,679,263,678,677,351,350,349,348,347,689,688,344,343,342,341,340,281,280,279,278],[266,265,679,263,678,677,351,350,349,348,347,689,688],[266,265,679,263,678,677,351,350,349,348,347,689,688],[266,265,679,263,678,677,364,363,444,443,442,441,579,587,538,552],[266,265,679,263,678,677,364,363,444,443,442,441,440,785,621,620],[266,265,679,263,678,677,364,363,444,443,442,441,579,587,786],[266,265,679,263,678,677,364,363,444,443,442,465,558,557,556,555,623,660,539,651,650,649,648],[266,265,679,263,678,677,526,525,524,523,502,503,522,606],[266,265,679,263,678,677,526,525,524,523,502,503,522],[266,265,679,263,678,677,526,525,524,523,502,503,522,704],[266,265,679,263,678,677,526,525,524,523,502,503,522],[266,265,679,263,678,677,526,525,524,523,502,503,522,703],[266,265,679,263,678,677,526,525,524,523,502,503,522,706],[266,265,679,263,678,677,526,525,524,523,502,503,522,702],[266,265,679,263,678,677,526,525,524,523,502,503,522,705],[266,265,679,263,678,677,526,525,524,523,502,503,522,706],[266,265,679,263,678,677,526,525,524,523,502,503,522,502,715],[266,265,679,263,678,677,526,525,524,523,502,503,522],[266,265,679,263,678,677,526,525,524,523,502,503,522,502],[266,265,679,263,678,677,526,525,524,523,502,503,522],[266,265,679,263,678,677,526,525,524,523,502,503,522,702],[266,265,679,263,678,677,526,525,524,523,502,503,522,702],[266,265,679,263,678,677,526,525,524,523,502,503,522,705],[266,265,679,263,678,677,526,525,524,523,502,503,522,701],[266,265,679,263,678,677,526,525,524,523,502,503,522,699],[266,265,679,263,678,677,526,525,524,523,502,503,522,706],[266,265,679,263,678,677,526,525,524,523,502,503,522,502],[266,265,679,263,678,677,526,525,524,523,502,503,522,702],[266,265,679,263,678,677,526,525,524,523,502,503,522,702],[266,265,679,263,678,677,526,525,524,523,502,503,522],[266,265,679,263,678,677,526,525,524,523,502,503,522,699],[266,265,679,263,678,677,526,525,524,523,502,503,522,702],[266,265,679,263,678,677,526,525,524,523,502,503,522,702],[266,265,679,263,678,677,466,443,442,465,464,359,358,357,463,462,461,358,357,356,472,459,720,719,502,503,716],[266,265,679,263,678,677],[266,265,679,263,678],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,681,680],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,681,680],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,681,680],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,685],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,685],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,685],[266,265,679,263,678,677,260,270,277],[266,265,679,263,678,677,260,270,277],[266,265,679,263,678,677,260,270,277],[266,265,679,263,678,677,260,270,277],[266,265,679,263,678,677,260,270,277],[266,265,679,263,678,677,260,270,277],[266,265,679,263,678,677,260,270,277],[266,265,679,263,678,677,260,270,277],[266,265,679,263,678,677,260,270,277],[266,265,679,263,678,677,292,291,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,679,263,678,677,292,291,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,679,263,678,677,292,291,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,679,263,678,677,292,582,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,679,263,678,677,292,582,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,679,263,678,677,292,582,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,679,263,678,677,302,301,300,299,298,297,296,295,294,293],[266,265,679,263,678,677,302,301,300,299,298,297,296,295,294,293],[266,265,679,263,678,677,302,301,300,299,298,297,296,295,294,293],[266,265,679,263,678,677,302,301,300,299,298,297,296,303],[266,265,679,263,678,677,302,301,300,299,298,297,296,295,294,293],[266,265,679,263,678,677,302,301,300,299,298,297,296,303],[266,265,679,263,678,677,302,301,300,299,298,297,296,303],[266,265,679,263,678,677,302,301,300,299,298,297,296,303],[266,265,679,263,678,677,302,301,300,299,298,297,296,303],[266,265,679,263,678,677,302,301,300,299,298,297,296,303],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,319,294,293],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,679,263,678,677,302,318,317,316,315,314,313,326,325],[266,265,679,263,678,677,302,318,317,316,315,314,336,335,334,333,332,331,330,329,328,327],[266,265,679,263,678,677,302,318,317,316,315,314,336,335,334,333,332,331,330,329,328,327],[266,265,679,263,678,677,302,318,317,316,315,314,336,335,334,333,332,331,330,329,328,327],[266,265,679,263,678,677,302,318,317,316,315,314,336,335,334,333,332,331,330,329,328,327],[266,265,679,263,678,677,302,318,317,316,315,314,336,335,334,333,332,331,330,329,328,327],[266,265,679,263,678,677,302,318,317,316,315,314,336,335,334,333,332,331,330,329,328,327],[266,265,679,263,678,677,302,318,317,316,315,314,336,335,334,333,332,331,330,329,328,327],[266,265,679,263,678,677,302,318,317,316,315,314,336,335,334,333,332,331,330,329,328,327],[266,265,679,263,678,677,302,318,317,316,315,314,336,548],[266,265,679,263,678,677,351,350,349,348,347,689,688,344,343,342,341,340,281,280,279,278],[266,265,679,263,678,677,351,350,349,348,347,689,688,344,343,342,341,340,281,280,279,278],[266,265,679,263,678,677,351,350,349,348,347,689,688,344,343,342,341,340,281,280,279,278],[266,265,679,263,678,677,526,525,524,523,502,503,522,703],[266,265,679,263,678,677,526,525,524,523,502,503,522],[266,265,679,263,678,677,526,525,524,523,502,503,522,703],[266,265,679,263,678,677,526,525,524,523,502,503,522,701],[266,265,679,263,678,677,526,525,524,523,502,503,522,502,743],[266,265,679,263,678,677,526,525,524,523,502,503,522,699],[266,265,679,263,678,677,526,525,524,523,502,503,522,703],[266,265,679,263,678,677,526,525,524,523,502,503,522],[266,265,679,263,678,677,526,525,524,523,502,503,522,704],[266,265,679,263,678,677,526,525,524,523,502,503,522],[266,265,679,263,678,677,526,525,524,523,502,503,522,703],[266,265,679,263,678,677,526,525,524,523,502,503,522,702],[266,265,679,263,678,677,526,525,524,523,502,503,522,700],[266,265,679,263,678,677,526,525,524,523,502,503,522],[266,265,679,263,678,677,526,525,524,523,502,503,522,703],[266,265,679,263,678,677,526,525,524,523,502,503,522,699],[266,265,679,263,678,677,526,525,524,523,502,503,522,502],[266,265,679,263,678,677],[266,265,679,263,678,677],[266,265,679,263,678],[266,265,679,263,678,677,676,790,789,788,761,787,439,438,437,447,560],[266,265,679,263,678,677,260,270,269,268,267],[266,265,679,263,678,677,260,270,269,268,267],[266,265,679,263,678,677,260,270,269,268,267],[266,265,679,263,678,677,260,270,269,268,267],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,251,250,547],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,251,250,249],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,251,250,249],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,251,250,249],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,681,680],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,681,680],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,681,680],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,681,680],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,681,680],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,681,680],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,681,680],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,685],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,685],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,685],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,685],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,685],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,685],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252],[266,265,679,263,678,677,260,270,277],[266,265,679,263,678,677,292,291,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,679,263,678,677,292,291,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,679,263,678,677,292,291,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,679,263,678,677,292,582,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,679,263,678,677,292,582,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,679,263,678,677,292,582,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,679,263,678,677,302,301,300,299,298,297,296,295,294,293],[266,265,679,263,678,677,302,301,300,299,298,297,296,295,294,293],[266,265,679,263,678,677,302,301,300,299,298,297,296,303],[266,265,679,263,678,677,302,301,300,299,298,297,296,303],[266,265,679,263,678,677,302,301,300,299,298,297,296,303],[266,265,679,263,678,677,302,301,300,299,298,297,296,303],[266,265,679,263,678,677,302,301,300,299,298,297,296,303],[266,265,679,263,678,677,302,301,300,299,298,297,296,303],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,319,294,293],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,319,294,293],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,319,294,293],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,319,294,293],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,679,263,678,677,302,318,317,316,315,314,336,335,334,333,584,646],[266,265,679,263,678,677,302,318,317,316,315,314,336,335,334,333,584,583],[266,265,679,263,678,677,302,318,317,316,315,314,336,335,334,333,584,583],[266,265,679,263,678,677,302,318,317,316,315,314,336,335,334,333,584,583],[266,265,679,263,678,677,302,318,317,316,315,314,336,335,334,333,332,331,330,329,328,337],[266,265,679,263,678,677,302,318,317,316,315,314,336,335,334,333,332,331,330,329,328,327],[266,265,679,263,678,677,302,318,317,316,315,314,336,335,334,333,332,331,330,329,328,327],[266,265,679,263,678,677,302,318,317,316,315,314,336,335,334,333,332,331,330,329,328,327],[266,265,679,263,678,677,302,318,317,316,315,314,336,335,334,333,332,331,330,329,328,327],[266,265,679,263,678,677,351,350,349,348,347,689,688,344,343,342,341,340,281,280,279,278],[266,265,679,263,678,677,351,350,349,348,347,689,688,344,343,342,341,340,281,280,279,278],[266,265,679,263,678,677,351,350,349,348,347,689,688,344,343,342,341,340,281,280,279,278],[266,265,679,263,678,677,351,350,349,348,347,689,688,344,343,342,341,340,281,280,279,278],[266,265,679,263,678,677,351,350,349,348,347,346,345,344,343,342,341,340,281,280,279,278],[266,265,679,263,678,677,351,350,349,348,347,346,345,344,343,342,341,340,281,280,279,278],[266,265,679,263,678,677,351,350,349,348,347,346,345],[266,265,679,263,678,677,351,350,349,348,347,346,345,344,343,342,341,340,281,280,279,278],[266,265,679,263,678,677,351,350,349,348,347,346,345,344,343,342,341,340,281,280,279,278],[266,265,679,263,678,677,351,350,349,348,347,346,345,344,343,342,341,340,281,339,791],[266,265,679,263,678,677,526,525,524,523,502,503,522],[266,265,679,263,678,677,526,525,524,523,502,503,522],[266,265,679,263,678,677,526,525,524,523,502,503,522,703],[266,265,679,263,678,677,526,525,524,523,502,503,522,502],[266,265,679,263,678,677,526,525,524,523,502,503,522,701],[266,265,679,263,678,677,526,525,524,523,502,503,522],[266,265,679,263,678,677,526,525,524,523,502,503,522,716],[266,265,679,263,678,677,526,525,524,523,502,503,522,704],[266,265,679,263,678,677,526,525,524,523,502,503,522],[266,265,679,263,678,677,526,525,524,523,502,503,522,606],[266,265,679,263,678,677,526,525,524,523,502,503,522,704],[266,265,679,263,678,677,526,525,524,523,502,503,522,704],[266,265,679,263,678,677,526,525,524,523,502,503,522,703],[266,265,679,263,678,677,526,525,524,523,502,503,522,502],[266,265,679,263,678,677,526,525,524,523,502,503,522,703],[266,265,679,263,678,677,526,525,524,523,502,503,522,606],[266,265,679,263,678,677,526,525,524,523,502,503,522,702],[266,265,679,263,678,677,526,525,524,523,502,503,522],[266,265,679,263,678,677,526,525,524,523,502,503,522,704],[266,265,679,263,678,677,526,525,524,523,502,503,522],[266,265,679,263,678,677,526,525,524,523,502,503,522],[266,265,679,263,678,677,526,525,524,523,502,503,522],[266,265,679,263,678,677,526,525,524,523,502,503,522,502],[266,265,679,263,678,677,526,525,524,523,502,503,522],[266,265,679,263,678,677],[266,265,679,263,678,677],[266,265,679,263,678,677],[266,265,679,263,678,677],[266,265,679,263,678,677],[266,265,679,263,678,677],[266,265,679,263,678,677],[266,265,679,263,678],[266,265,679,263,678,677,260,270,794,595,594,793,792],[266,265,679,263,678,677,260,270,269,268,267],[266,265,679,263,678,677,260,270,269,268,267],[266,265,679,263,678,677,260,270,269,268,267],[266,265,679,263,678,677,260,270,269,268,267],[266,265,679,263,678,677,260,270,269,268,267],[266,265,679,263,678,677,260,270,269,268,267],[266,265,679,263,678,677,260,270,269,268,267],[266,265,679,263,678,677,260,270,269,268,267],[266,265,679,263,678,677,260,270,269,268,267],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,251,250,249],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,251,250,249],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,681,680],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,685],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,685],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,685],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,685],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,685],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,685],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252],[266,265,679,263,678,677,292,291,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,679,263,678,677,292,291,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,679,263,678,677,292,291,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,679,263,678,677,292,291,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,679,263,678,677,292,291,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,679,263,678,677,292,582,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,679,263,678,677,292,582,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,679,263,678,677,292,582,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,679,263,678,677,302,301,300,299,298,297,296,295,294,293],[266,265,679,263,678,677,302,301,300,299,298,297,296,295,294,293],[266,265,679,263,678,677,302,301,300,299,298,297,296,303],[266,265,679,263,678,677,302,301,300,299,298,297,296,303],[266,265,679,263,678,677,302,301,300,299,298,297,296,303],[266,265,679,263,678,677,302,301,300,299,298,297,296,303],[266,265,679,263,678,677,302,301,300,299,298,297,296,303],[266,265,679,263,678,677,302,301,300,299,298,297,296,295,294,293],[266,265,679,263,678,677,302,301,300,299,298,297,296,303],[266,265,679,263,678,677,302,301,300,299,298,297,296,303],[266,265,679,263,678,677,302,301,300,299,298,297,296,303],[266,265,679,263,678,677,302,301,300,299,298,297,296,303],[266,265,679,263,678,677,302,301,300,299,298,297,296,303],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,319,294,293],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,679,263,678,677,302,318,317,316,315,314,313,326,325,324,323],[266,265,679,263,678,677,302,318,317,316,315,314,313,326,325,324,323],[266,265,679,263,678,677,302,318,317,316,315,314,313,326,325,324,323],[266,265,679,263,678,677,302,318,317,316,315,314,313,326,325,324,323],[266,265,679,263,678,677,302,318,317,316,315,314,313,326,325],[266,265,679,263,678,677,302,318,317,316,315,314,313,326,325],[266,265,679,263,678,677,302,318,317,316,315,314,313,326,325],[266,265,679,263,678,677,302,318,317,316,315,314,336,335,334,333,332,331,330,329,328,337],[266,265,679,263,678,677,302,318,317,316,315,314,336,335,334,333,332,331,330,329,328,337],[266,265,679,263,678,677,302,318,317,316,315,314,336,335,334,333,332,331,330,329,328,327],[266,265,679,263,678,677,796,484,354,353,352,5,27,795],[266,265,679,263,678,677,351,350,349,348,347,689,688,344,343,342,341,340,281,280,279,278],[266,265,679,263,678,677,351,350,349,348,347,689,688,344,343,342,341,340,281,280,279,278],[266,265,679,263,678,677,351,350,349,348,347,689],[266,265,679,263,678,677,351,350,349,348,347,689,688,344,343,342,341,340,281,280,279,278],[266,265,679,263,678,677,351,350,349,348,347,689,688,344,343,342,341,340,281,280,279,278],[266,265,679,263,678,677,351,350,349,348,347,689,688,344,343,342,341,340,281,280,279,278],[266,265,679,263,678,677,351,350,349,348,347,689,688,344,343,342,341,340,281,280,279,278],[266,265,679,263,678,677,351,350,349,348,347,346,345,344,343,342,341,340,281,280,279,278],[266,265,679,263,678,677,526,525,524,523,502,503,522,705],[266,265,679,263,678,677,526,525,524,523,502,503,522,702],[266,265,679,263,678,677,526,525,524,523,502,503,522,699],[266,265,679,263,678,677,526,525,524,523,502,503,522,705],[266,265,679,263,678,677,526,525,524,523,502,503,522,705],[266,265,679,263,678,677,526,525,524,523,502,503,522,702],[266,265,679,263,678,677,526,525,524,523,502,503,522,703],[266,265,679,263,678,677,526,525,524,523,502,503,522,502,715],[266,265,679,263,678,677,526,525,524,523,502,503,522,703],[266,265,679,263,678,677,526,525,524,523,502,503,522,703],[266,265,679,263,678,677,526,525,524,523,502,503,522,702],[266,265,679,263,678,677,526,525,524,523,502,503,522],[266,265,679,263,678,677,526,525,524,523,502,503,522],[266,265,679,263,678,677,526,525,524,523,502,503,522,702],[266,265,679,263,678,677,526,525,524,523,502,503,522,705],[266,265,679,263,678,677,526,525,524,523,502,503,522,705],[266,265,679,263,678,677,526,525,524,523,502,503,522,703],[266,265,679,263,678,677,526,525,524,523,502,503,522,502],[266,265,679,263,678,677,526,525,524,523,502,503,522],[266,265,679,263,678,677,526,525,524,523,502,503,522,606],[266,265,679,263,678,677,526,525,524,523,502,503,522,502,743],[266,265,679,263,678,677,526,525,524,523,502,503,522],[266,265,679,263,678,677,526,525,524,523,502,503,522],[266,265,679,263,678,677,526,525,524,523,502,503,522,702],[266,265,679,263,678,677,526,525,524,523,502,503,522,702],[266,265,679,263,678,677,526,525,524,523,502,503,522,606],[266,265,679,263,678,677,526,525,524,523,502,503,522,699],[266,265,679,263,678,677,466,443,442,465,464,359,358,357,463,462,461,358,357,356,798,797],[266,265,679,263,678,677,466,443,442,465,464,359,358,357,463,462,461,358,357,356,748,459,458,457,747,358,357,356,746,358,357,356,800,799],[266,265,679,263,678,677],[266,265,679,263,678,677],[266,265,679,263,678,677],[266,265,679,263,678,677],[266,265,679,263,678,677],[266,802,801],[266,802,801],[266,802,801],[],[],[],[],[],[],[],[],[],[],[],[],[],[],[]],"weights":[0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01]}],"shared":{"frames":[{"name":"_compile_bytecode","file":"","line":647,"col":null},{"name":"get_code","file":"","line":978,"col":null},{"name":"exec_module","file":"","line":846,"col":null},{"name":"_load_unlocked","file":"","line":680,"col":null},{"name":"_find_and_load_unlocked","file":"","line":986,"col":null},{"name":"_find_and_load","file":"","line":1007,"col":null},{"name":"","file":"/home/fbruzzesi/.local/share/uv/python/cpython-3.9.19-linux-x86_64-gnu/lib/python3.9/pathlib.py","line":4,"col":null},{"name":"_call_with_frames_removed","file":"","line":228,"col":null},{"name":"exec_module","file":"","line":850,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/numpy/compat/py3k.py","line":21,"col":null},{"name":"_handle_fromlist","file":"","line":1058,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/numpy/compat/__init__.py","line":14,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/numpy/core/_type_aliases.py","line":20,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/numpy/core/numerictypes.py","line":105,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/numpy/core/__init__.py","line":72,"col":null},{"name":"_find_and_load_unlocked","file":"","line":972,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/numpy/__config__.py","line":4,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/numpy/__init__.py","line":130,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/t.py","line":1,"col":null},{"name":"_find_and_load_unlocked","file":"","line":984,"col":null},{"name":"","file":"/home/fbruzzesi/.local/share/uv/python/cpython-3.9.19-linux-x86_64-gnu/lib/python3.9/ntpath.py","line":259,"col":null},{"name":"","file":"/home/fbruzzesi/.local/share/uv/python/cpython-3.9.19-linux-x86_64-gnu/lib/python3.9/pathlib.py","line":13,"col":null},{"name":"namedtuple","file":"/home/fbruzzesi/.local/share/uv/python/cpython-3.9.19-linux-x86_64-gnu/lib/python3.9/collections/__init__.py","line":372,"col":null},{"name":"","file":"/home/fbruzzesi/.local/share/uv/python/cpython-3.9.19-linux-x86_64-gnu/lib/python3.9/urllib/parse.py","line":256,"col":null},{"name":"_add_integer_aliases","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/numpy/core/_type_aliases.py","line":133,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/numpy/core/_type_aliases.py","line":142,"col":null},{"name":"release","file":"","line":118,"col":null},{"name":"__exit__","file":"","line":161,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/numpy/core/__init__.py","line":100,"col":null},{"name":"_path_is_mode_type","file":"","line":151,"col":null},{"name":"_path_isfile","file":"","line":156,"col":null},{"name":"find_spec","file":"","line":1555,"col":null},{"name":"_get_spec","file":"","line":1395,"col":null},{"name":"find_spec","file":"","line":1423,"col":null},{"name":"_find_spec","file":"","line":925,"col":null},{"name":"_find_and_load_unlocked","file":"","line":982,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/numpy/core/__init__.py","line":101,"col":null},{"name":"__next","file":"/home/fbruzzesi/.local/share/uv/python/cpython-3.9.19-linux-x86_64-gnu/lib/python3.9/sre_parse.py","line":235,"col":null},{"name":"get","file":"/home/fbruzzesi/.local/share/uv/python/cpython-3.9.19-linux-x86_64-gnu/lib/python3.9/sre_parse.py","line":257,"col":null},{"name":"_parse","file":"/home/fbruzzesi/.local/share/uv/python/cpython-3.9.19-linux-x86_64-gnu/lib/python3.9/sre_parse.py","line":512,"col":null},{"name":"_parse_sub","file":"/home/fbruzzesi/.local/share/uv/python/cpython-3.9.19-linux-x86_64-gnu/lib/python3.9/sre_parse.py","line":444,"col":null},{"name":"_parse","file":"/home/fbruzzesi/.local/share/uv/python/cpython-3.9.19-linux-x86_64-gnu/lib/python3.9/sre_parse.py","line":841,"col":null},{"name":"parse","file":"/home/fbruzzesi/.local/share/uv/python/cpython-3.9.19-linux-x86_64-gnu/lib/python3.9/sre_parse.py","line":955,"col":null},{"name":"compile","file":"/home/fbruzzesi/.local/share/uv/python/cpython-3.9.19-linux-x86_64-gnu/lib/python3.9/sre_compile.py","line":788,"col":null},{"name":"_compile","file":"/home/fbruzzesi/.local/share/uv/python/cpython-3.9.19-linux-x86_64-gnu/lib/python3.9/re.py","line":304,"col":null},{"name":"compile","file":"/home/fbruzzesi/.local/share/uv/python/cpython-3.9.19-linux-x86_64-gnu/lib/python3.9/re.py","line":252,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/numpy/core/_internal.py","line":146,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/numpy/core/__init__.py","line":104,"col":null},{"name":"_path_split","file":"","line":132,"col":null},{"name":"cache_from_source","file":"","line":387,"col":null},{"name":"get_code","file":"","line":930,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/numpy/lib/index_tricks.py","line":12,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/numpy/lib/__init__.py","line":23,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/numpy/__init__.py","line":149,"col":null},{"name":"cache_from_source","file":"","line":388,"col":null},{"name":"_optimize_charset","file":"/home/fbruzzesi/.local/share/uv/python/cpython-3.9.19-linux-x86_64-gnu/lib/python3.9/sre_compile.py","line":421,"col":null},{"name":"_compile","file":"/home/fbruzzesi/.local/share/uv/python/cpython-3.9.19-linux-x86_64-gnu/lib/python3.9/sre_compile.py","line":136,"col":null},{"name":"_compile","file":"/home/fbruzzesi/.local/share/uv/python/cpython-3.9.19-linux-x86_64-gnu/lib/python3.9/sre_compile.py","line":164,"col":null},{"name":"_code","file":"/home/fbruzzesi/.local/share/uv/python/cpython-3.9.19-linux-x86_64-gnu/lib/python3.9/sre_compile.py","line":631,"col":null},{"name":"compile","file":"/home/fbruzzesi/.local/share/uv/python/cpython-3.9.19-linux-x86_64-gnu/lib/python3.9/sre_compile.py","line":792,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/numpy/lib/utils.py","line":751,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/numpy/lib/__init__.py","line":32,"col":null},{"name":"get_data","file":"","line":1040,"col":null},{"name":"get_code","file":"","line":941,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/numpy/lib/__init__.py","line":33,"col":null},{"name":"_path_stat","file":"","line":142,"col":null},{"name":"find_spec","file":"","line":1522,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/numpy/lib/__init__.py","line":34,"col":null},{"name":"","file":"/home/fbruzzesi/.local/share/uv/python/cpython-3.9.19-linux-x86_64-gnu/lib/python3.9/inspect.py","line":39,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/numpy/ma/core.py","line":24,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/numpy/ma/__init__.py","line":42,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/numpy/__init__.py","line":159,"col":null},{"name":"__init__","file":"/home/fbruzzesi/.local/share/uv/python/cpython-3.9.19-linux-x86_64-gnu/lib/python3.9/typing.py","line":740,"col":null},{"name":"Literal","file":"/home/fbruzzesi/.local/share/uv/python/cpython-3.9.19-linux-x86_64-gnu/lib/python3.9/typing.py","line":513,"col":null},{"name":"inner","file":"/home/fbruzzesi/.local/share/uv/python/cpython-3.9.19-linux-x86_64-gnu/lib/python3.9/typing.py","line":274,"col":null},{"name":"__getitem__","file":"/home/fbruzzesi/.local/share/uv/python/cpython-3.9.19-linux-x86_64-gnu/lib/python3.9/typing.py","line":361,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/polars/_typing.py","line":152,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/polars/functions/eager.py","line":11,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/polars/functions/__init__.py","line":28,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/polars/dataframe/frame.py","line":31,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/polars/dataframe/__init__.py","line":1,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/polars/_reexport.py","line":3,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/polars/api.py","line":6,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/polars/__init__.py","line":21,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/t.py","line":3,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/polars/_utils/construction/__init__.py","line":1,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/polars/dataframe/frame.py","line":37,"col":null},{"name":"module_from_spec","file":"","line":571,"col":null},{"name":"_load_unlocked","file":"","line":666,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/polars/series/series.py","line":83,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/polars/series/__init__.py","line":1,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/polars/_reexport.py","line":6,"col":null},{"name":"_expr_lookup","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/polars/series/utils.py","line":84,"col":null},{"name":"expr_dispatch","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/polars/series/utils.py","line":39,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/polars/series/datetime.py","line":29,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/polars/series/series.py","line":101,"col":null},{"name":"find_spec","file":"","line":1527,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/polars/io/database/__init__.py","line":1,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/polars/io/__init__.py","line":6,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/polars/__init__.py","line":154,"col":null},{"name":"__init__","file":"/home/fbruzzesi/.local/share/uv/python/cpython-3.9.19-linux-x86_64-gnu/lib/python3.9/typing.py","line":528,"col":null},{"name":"_type_convert","file":"/home/fbruzzesi/.local/share/uv/python/cpython-3.9.19-linux-x86_64-gnu/lib/python3.9/typing.py","line":133,"col":null},{"name":"_type_check","file":"/home/fbruzzesi/.local/share/uv/python/cpython-3.9.19-linux-x86_64-gnu/lib/python3.9/typing.py","line":155,"col":null},{"name":"","file":"/home/fbruzzesi/.local/share/uv/python/cpython-3.9.19-linux-x86_64-gnu/lib/python3.9/typing.py","line":1956,"col":null},{"name":"__new__","file":"/home/fbruzzesi/.local/share/uv/python/cpython-3.9.19-linux-x86_64-gnu/lib/python3.9/typing.py","line":1955,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/polars/io/database/_arrow_registry.py","line":6,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/polars/io/database/_executor.py","line":18,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/polars/io/database/functions.py","line":9,"col":null},{"name":"_lock_unlock_module","file":"","line":209,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/polars/io/spreadsheet/functions.py","line":10,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/polars/io/spreadsheet/__init__.py","line":1,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/polars/io/__init__.py","line":14,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/polars/__init__.py","line":187,"col":null},{"name":"create_module","file":"","line":1173,"col":null},{"name":"module_from_spec","file":"","line":565,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pyarrow/__init__.py","line":65,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/compat/pyarrow.py","line":8,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/compat/__init__.py","line":27,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/__init__.py","line":26,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/t.py","line":4,"col":null},{"name":"exec_module","file":"","line":1181,"col":null},{"name":"_compile_bytecode","file":"","line":651,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/cloudpickle/__init__.py","line":1,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/cloudpickle/cloudpickle.py","line":63,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/_libs/tslibs/__init__.py","line":39,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/_libs/__init__.py","line":18,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/api.py","line":1,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/__init__.py","line":49,"col":null},{"name":"__exit__","file":"","line":880,"col":null},{"name":"__next","file":"/home/fbruzzesi/.local/share/uv/python/cpython-3.9.19-linux-x86_64-gnu/lib/python3.9/sre_parse.py","line":240,"col":null},{"name":"IntervalDtype","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/dtypes/dtypes.py","line":1200,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/dtypes/dtypes.py","line":1165,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/api.py","line":9,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pyarrow/vendored/docscrape.py","line":13,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pyarrow/compute.py","line":104,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/arrays/arrow/accessors.py","line":23,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/arrays/arrow/__init__.py","line":1,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/arrays/__init__.py","line":1,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/api.py","line":28,"col":null},{"name":"_parse","file":"/home/fbruzzesi/.local/share/uv/python/cpython-3.9.19-linux-x86_64-gnu/lib/python3.9/sre_parse.py","line":500,"col":null},{"name":"NumpyDocString","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pyarrow/vendored/docscrape.py","line":274,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pyarrow/vendored/docscrape.py","line":118,"col":null},{"name":"__init__","file":"/home/fbruzzesi/.local/share/uv/python/cpython-3.9.19-linux-x86_64-gnu/lib/python3.9/inspect.py","line":2791,"col":null},{"name":"_make_signature","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pyarrow/compute.py","line":288,"col":null},{"name":"_wrap_function","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pyarrow/compute.py","line":302,"col":null},{"name":"_make_global_functions","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pyarrow/compute.py","line":333,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pyarrow/compute.py","line":336,"col":null},{"name":"_find_spec","file":"","line":917,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/ops/array_ops.py","line":18,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/ops/__init__.py","line":8,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/arrays/arrow/array.py","line":50,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/arrays/arrow/__init__.py","line":5,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/arrays/datetimes.py","line":17,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/arrays/__init__.py","line":9,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/arrays/datetimes.py","line":62,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/arrays/datetimelike.py","line":54,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/groupby/generic.py","line":68,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/groupby/__init__.py","line":1,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/api.py","line":47,"col":null},{"name":"update_wrapper","file":"/home/fbruzzesi/.local/share/uv/python/cpython-3.9.19-linux-x86_64-gnu/lib/python3.9/functools.py","line":52,"col":null},{"name":"_forbid_nonstring_types","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/strings/accessor.py","line":130,"col":null},{"name":"StringMethods","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/strings/accessor.py","line":2349,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/strings/accessor.py","line":157,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/indexes/base.py","line":190,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/indexes/api.py","line":20,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/indexing.py","line":79,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/generic.py","line":152,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/frame.py","line":149,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/indexes/datetimes.py","line":47,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/indexes/api.py","line":28,"col":null},{"name":"__init__","file":"/home/fbruzzesi/.local/share/uv/python/cpython-3.9.19-linux-x86_64-gnu/lib/python3.9/inspect.py","line":2500,"col":null},{"name":"replace","file":"/home/fbruzzesi/.local/share/uv/python/cpython-3.9.19-linux-x86_64-gnu/lib/python3.9/inspect.py","line":2580,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/util/_decorators.py","line":308,"col":null},{"name":"decorate","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/util/_decorators.py","line":307,"col":null},{"name":"NDFrame","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/generic.py","line":2723,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/generic.py","line":244,"col":null},{"name":"Series","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/series.py","line":381,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/series.py","line":263,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/frame.py","line":188,"col":null},{"name":"sub","file":"/home/fbruzzesi/.local/share/uv/python/cpython-3.9.19-linux-x86_64-gnu/lib/python3.9/re.py","line":210,"col":null},{"name":"dedent","file":"/home/fbruzzesi/.local/share/uv/python/cpython-3.9.19-linux-x86_64-gnu/lib/python3.9/textwrap.py","line":461,"col":null},{"name":"__call__","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/util/_decorators.py","line":488,"col":null},{"name":"Series","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/series.py","line":874,"col":null},{"name":"acquire","file":"","line":97,"col":null},{"name":"__enter__","file":"","line":158,"col":null},{"name":"_find_and_load","file":"","line":1004,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/groupby/ops.py","line":25,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/groupby/generic.py","line":69,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/computation/scope.py","line":121,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/computation/ops.py","line":30,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/computation/engines.py","line":15,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/computation/eval.py","line":15,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/computation/api.py","line":2,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/__init__.py","line":119,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/computation/eval.py","line":16,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/io/api.py","line":22,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/__init__.py","line":142,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/express/_imshow.py","line":2,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/express/__init__.py","line":5,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/t.py","line":7,"col":null},{"name":"namedtuple","file":"/home/fbruzzesi/.local/share/uv/python/cpython-3.9.19-linux-x86_64-gnu/lib/python3.9/collections/__init__.py","line":396,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/express/_core.py","line":125,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/express/_imshow.py","line":3,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/xarray/coding/variables.py","line":13,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/xarray/coding/times.py","line":14,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/xarray/coding/cftimeindex.py","line":54,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/xarray/coding/cftime_offsets.py","line":55,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/xarray/groupers.py","line":17,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/xarray/__init__.py","line":3,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/express/_imshow.py","line":11,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/xarray/core/dataset.py","line":32,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/xarray/core/dataarray.py","line":43,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/xarray/groupers.py","line":19,"col":null},{"name":"_parse","file":"/home/fbruzzesi/.local/share/uv/python/cpython-3.9.19-linux-x86_64-gnu/lib/python3.9/sre_parse.py","line":510,"col":null},{"name":"insert_doc_addendum","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/xarray/core/datatree_ops.py","line":233,"col":null},{"name":"_wrap_then_attach_to_cls","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/xarray/core/datatree_ops.py","line":214,"col":null},{"name":"MappedDatasetMethodsMixin","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/xarray/core/datatree_ops.py","line":276,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/xarray/core/datatree_ops.py","line":270,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/xarray/core/datatree.py","line":30,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/xarray/testing/assertions.py","line":15,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/xarray/testing/__init__.py","line":2,"col":null},{"name":"_init_module_attrs","file":"","line":535,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/xarray/backends/file_manager.py","line":13,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/xarray/backends/__init__.py","line":8,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/xarray/tutorial.py","line":17,"col":null},{"name":"get_data","file":"","line":1039,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/xarray/backends/api.py","line":31,"col":null},{"name":"_split","file":"/home/fbruzzesi/.local/share/uv/python/cpython-3.9.19-linux-x86_64-gnu/lib/python3.9/textwrap.py","line":176,"col":null},{"name":"_split_chunks","file":"/home/fbruzzesi/.local/share/uv/python/cpython-3.9.19-linux-x86_64-gnu/lib/python3.9/textwrap.py","line":338,"col":null},{"name":"wrap","file":"/home/fbruzzesi/.local/share/uv/python/cpython-3.9.19-linux-x86_64-gnu/lib/python3.9/textwrap.py","line":351,"col":null},{"name":"fill","file":"/home/fbruzzesi/.local/share/uv/python/cpython-3.9.19-linux-x86_64-gnu/lib/python3.9/textwrap.py","line":363,"col":null},{"name":"make_docstring","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/express/_doc.py","line":625,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/express/_chart_types.py","line":267,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/express/__init__.py","line":6,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/express/_chart_types.py","line":1837,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/t.py","line":39,"col":null},{"name":"numpy_to_pyseries","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/polars/_utils/construction/series.py","line":454,"col":null},{"name":"__init__","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/polars/series/series.py","line":301,"col":null},{"name":"_expand_dict_values","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/polars/_utils/construction/dataframe.py","line":389,"col":null},{"name":"dict_to_pydf","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/polars/_utils/construction/dataframe.py","line":160,"col":null},{"name":"__init__","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/polars/dataframe/frame.py","line":365,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/t.py","line":44,"col":null},{"name":"_sanitize_str_dtypes","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/construction.py","line":752,"col":null},{"name":"sanitize_array","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/construction.py","line":664,"col":null},{"name":"_homogenize","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/internals/construction.py","line":629,"col":null},{"name":"arrays_to_mgr","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/internals/construction.py","line":119,"col":null},{"name":"dict_to_mgr","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/internals/construction.py","line":503,"col":null},{"name":"__init__","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/frame.py","line":778,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/t.py","line":45,"col":null},{"name":"_stack_arrays","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/internals/managers.py","line":2254,"col":null},{"name":"_form_blocks","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/internals/managers.py","line":2212,"col":null},{"name":"create_block_manager_from_column_arrays","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/internals/managers.py","line":2139,"col":null},{"name":"arrays_to_mgr","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/internals/construction.py","line":152,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/t.py","line":46,"col":null},{"name":"__contains__","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/indexes/base.py","line":5360,"col":null},{"name":"_can_hold_identifiers_and_holds_name","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/indexes/base.py","line":5452,"col":null},{"name":"__getattr__","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/generic.py","line":6296,"col":null},{"name":"_from_native_impl","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/narwhals/translate.py","line":396,"col":null},{"name":"from_native","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/narwhals/stable/v1/__init__.py","line":830,"col":null},{"name":"build_dataframe","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/express/_core.py","line":1448,"col":null},{"name":"make_figure","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/express/_core.py","line":2342,"col":null},{"name":"scatter","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/express/_chart_types.py","line":66,"col":null},{"name":"figure_generation_scatter","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/t.py","line":52,"col":null},{"name":"wrapper","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/t.py","line":20,"col":null},{"name":"test_all_charts","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/t.py","line":82,"col":null},{"name":"run_and_save_results","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/t.py","line":94,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/t.py","line":113,"col":null},{"name":"reset_index","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/series.py","line":1754,"col":null},{"name":"to_unindexed_series","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/express/_core.py","line":1123,"col":null},{"name":"process_args_into_dataframe","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/express/_core.py","line":1294,"col":null},{"name":"build_dataframe","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/express/_core.py","line":1608,"col":null},{"name":"reindex","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/generic.py","line":5592,"col":null},{"name":"reindex","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/series.py","line":5153,"col":null},{"name":"_homogenize","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/internals/construction.py","line":611,"col":null},{"name":"from_dict","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/frame.py","line":1917,"col":null},{"name":"_from_dict_impl","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/narwhals/functions.py","line":384,"col":null},{"name":"from_dict","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/narwhals/stable/v1/__init__.py","line":2428,"col":null},{"name":"process_args_into_dataframe","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/express/_core.py","line":1386,"col":null},{"name":"_isna_string_dtype","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/dtypes/missing.py","line":313,"col":null},{"name":"_isna_array","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/dtypes/missing.py","line":292,"col":null},{"name":"_isna","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/dtypes/missing.py","line":216,"col":null},{"name":"isna","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/dtypes/missing.py","line":178,"col":null},{"name":"notna","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/dtypes/missing.py","line":457,"col":null},{"name":"notna","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/generic.py","line":8821,"col":null},{"name":"notna","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/series.py","line":5788,"col":null},{"name":"_find_valid_index","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/generic.py","line":12786,"col":null},{"name":"first_valid_index","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/generic.py","line":12866,"col":null},{"name":"native_to_narwhals_dtype","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/narwhals/_pandas_like/utils.py","line":287,"col":null},{"name":"dtype","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/narwhals/_pandas_like/series.py","line":178,"col":null},{"name":"dtype","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/narwhals/series.py","line":356,"col":null},{"name":"_is_continuous","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/express/_core.py","line":160,"col":null},{"name":"infer_config","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/express/_core.py","line":2061,"col":null},{"name":"make_figure","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/express/_core.py","line":2351,"col":null},{"name":"_check_object_for_strings","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/algorithms.py","line":297,"col":null},{"name":"_get_hashtable_algo","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/algorithms.py","line":275,"col":null},{"name":"unique_with_mask","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/algorithms.py","line":436,"col":null},{"name":"unique","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/algorithms.py","line":401,"col":null},{"name":"unique","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/base.py","line":1025,"col":null},{"name":"unique","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/series.py","line":2407,"col":null},{"name":"unique","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/narwhals/_pandas_like/series.py","line":472,"col":null},{"name":"unique","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/narwhals/series.py","line":1045,"col":null},{"name":"get_groups_and_orders","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/express/_core.py","line":2295,"col":null},{"name":"make_figure","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/express/_core.py","line":2355,"col":null},{"name":"unique_with_mask","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/algorithms.py","line":440,"col":null},{"name":"_isna","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/dtypes/missing.py","line":207,"col":null},{"name":"factorize","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/algorithms.py","line":789,"col":null},{"name":"_codes_and_uniques","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/groupby/grouper.py","line":835,"col":null},{"name":"codes","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/groupby/grouper.py","line":691,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/groupby/ops.py","line":690,"col":null},{"name":"codes","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/groupby/ops.py","line":690,"col":null},{"name":"_get_compressed_codes","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/groupby/ops.py","line":764,"col":null},{"name":"group_info","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/groupby/ops.py","line":745,"col":null},{"name":"_get_splitter","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/groupby/ops.py","line":629,"col":null},{"name":"get_iterator","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/groupby/ops.py","line":618,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/narwhals/_pandas_like/group_by.py","line":108,"col":null},{"name":"__iter__","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/narwhals/_pandas_like/group_by.py","line":108,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/narwhals/group_by.py","line":115,"col":null},{"name":"__iter__","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/narwhals/group_by.py","line":115,"col":null},{"name":"get_groups_and_orders","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/express/_core.py","line":2311,"col":null},{"name":"factorize_array","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/algorithms.py","line":592,"col":null},{"name":"factorize","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/algorithms.py","line":795,"col":null},{"name":"factorize_array","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/algorithms.py","line":595,"col":null},{"name":"get_group_index","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/sorting.py","line":198,"col":null},{"name":"get_group_index_sorter","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/sorting.py","line":677,"col":null},{"name":"_sort_idx","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/groupby/ops.py","line":944,"col":null},{"name":"_sorted_ids","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/groupby/ops.py","line":950,"col":null},{"name":"_get_splitter","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/groupby/ops.py","line":634,"col":null},{"name":"_take_nd_ndarray","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/array_algos/take.py","line":162,"col":null},{"name":"take_nd","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/array_algos/take.py","line":117,"col":null},{"name":"take_nd","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/internals/blocks.py","line":1307,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/internals/managers.py","line":688,"col":null},{"name":"reindex_indexer","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/internals/managers.py","line":687,"col":null},{"name":"take","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/internals/managers.py","line":894,"col":null},{"name":"take","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/generic.py","line":4133,"col":null},{"name":"_sorted_data","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/groupby/ops.py","line":1164,"col":null},{"name":"__iter__","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/groupby/ops.py","line":1150,"col":null},{"name":"get_iterator","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/groupby/ops.py","line":620,"col":null},{"name":"_take_nd_ndarray","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/array_algos/take.py","line":157,"col":null},{"name":"__init__","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/series.py","line":410,"col":null},{"name":"_isna","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/dtypes/missing.py","line":218,"col":null},{"name":"isna","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/generic.py","line":8754,"col":null},{"name":"isna","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/series.py","line":5775,"col":null},{"name":"to_numpy","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/narwhals/_pandas_like/series.py","line":508,"col":null},{"name":"__array__","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/narwhals/_pandas_like/series.py","line":501,"col":null},{"name":"__array__","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/narwhals/series.py","line":58,"col":null},{"name":"copy_to_readonly_numpy_array","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/_plotly_utils/basevalidators.py","line":129,"col":null},{"name":"validate_coerce","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/_plotly_utils/basevalidators.py","line":408,"col":null},{"name":"_set_prop","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":5199,"col":null},{"name":"__setitem__","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":4860,"col":null},{"name":"_perform_update","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":3926,"col":null},{"name":"update","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":5125,"col":null},{"name":"make_figure","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/express/_core.py","line":2531,"col":null},{"name":"_gcd_import","file":"","line":1030,"col":null},{"name":"import_module","file":"/home/fbruzzesi/.local/share/uv/python/cpython-3.9.19-linux-x86_64-gnu/lib/python3.9/importlib/__init__.py","line":127,"col":null},{"name":"__getattr__","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/_plotly_utils/importers.py","line":36,"col":null},{"name":"data_class","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/_plotly_utils/basevalidators.py","line":2482,"col":null},{"name":"validate_coerce","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/_plotly_utils/basevalidators.py","line":2512,"col":null},{"name":"_set_compound_prop","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":5263,"col":null},{"name":"__setitem__","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":4852,"col":null},{"name":"__setitem__","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":5898,"col":null},{"name":"_init_subplot_xy","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/_subplots.py","line":989,"col":null},{"name":"_init_subplot","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/_subplots.py","line":1124,"col":null},{"name":"make_subplots","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/_subplots.py","line":752,"col":null},{"name":"init_figure","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/express/_core.py","line":2727,"col":null},{"name":"make_figure","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/express/_core.py","line":2591,"col":null},{"name":"","file":"","line":129,"col":null},{"name":"_path_split","file":"","line":129,"col":null},{"name":"_get_cached","file":"","line":494,"col":null},{"name":"cached","file":"","line":391,"col":null},{"name":"_init_module_attrs","file":"","line":550,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/offline/__init__.py","line":6,"col":null},{"name":"__init__","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":578,"col":null},{"name":"__init__","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/graph_objs/_figure.py","line":641,"col":null},{"name":"make_subplots","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/_subplots.py","line":880,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/stack_data/__init__.py","line":1,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/IPython/core/ultratb.py","line":104,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/IPython/core/crashhandler.py","line":27,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/IPython/core/application.py","line":26,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/IPython/__init__.py","line":53,"col":null},{"name":"get_module","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/_plotly_utils/optional_imports.py","line":28,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/tools.py","line":64,"col":null},{"name":"__getattr__","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/_plotly_utils/importers.py","line":29,"col":null},{"name":"_handle_fromlist","file":"","line":1055,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/offline/offline.py","line":11,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/IPython/core/display.py","line":717,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/IPython/display.py","line":16,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/IPython/core/page.py","line":28,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/IPython/core/oinspect.py","line":37,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/IPython/core/magic.py","line":20,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/IPython/terminal/embed.py","line":14,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/IPython/__init__.py","line":54,"col":null},{"name":"__new__","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/traitlets/traitlets.py","line":963,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/IPython/core/alias.py","line":192,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/IPython/core/interactiveshell.py","line":85,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/IPython/terminal/embed.py","line":15,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/prompt_toolkit/key_binding/defaults.py","line":19,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/prompt_toolkit/application/application.py","line":56,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/prompt_toolkit/application/__init__.py","line":3,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/prompt_toolkit/__init__.py","line":26,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/IPython/terminal/interactiveshell.py","line":31,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/IPython/terminal/embed.py","line":16,"col":null},{"name":"__new__","file":"/home/fbruzzesi/.local/share/uv/python/cpython-3.9.19-linux-x86_64-gnu/lib/python3.9/abc.py","line":107,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/prompt_toolkit/shortcuts/progress_bar/formatters.py","line":251,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/prompt_toolkit/shortcuts/progress_bar/base.py","line":57,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/prompt_toolkit/shortcuts/progress_bar/__init__.py","line":3,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/prompt_toolkit/shortcuts/__init__.py","line":12,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/prompt_toolkit/__init__.py","line":28,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/jedi/inference/gradual/typing.py","line":21,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/jedi/inference/gradual/stub_value.py","line":5,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/jedi/inference/gradual/typeshed.py","line":12,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/jedi/inference/imports.py","line":29,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/jedi/inference/__init__.py","line":70,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/jedi/api/classes.py","line":24,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/jedi/api/__init__.py","line":21,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/jedi/__init__.py","line":32,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/IPython/core/completer.py","line":250,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/IPython/terminal/debugger.py","line":6,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/IPython/terminal/interactiveshell.py","line":48,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/jedi/inference/gradual/stub_value.py","line":79,"col":null},{"name":"_path_is_mode_type","file":"","line":148,"col":null},{"name":"find_spec","file":"","line":1541,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/jedi/api/keywords.py","line":8,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/jedi/api/classes.py","line":31,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/jedi/plugins/stdlib.py","line":353,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/jedi/plugins/registry.py","line":5,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/jedi/__init__.py","line":41,"col":null},{"name":"_get_spec","file":"","line":1511,"col":null},{"name":"find_spec","file":"","line":1556,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/jedi/plugins/registry.py","line":6,"col":null},{"name":"getmembers","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/traitlets/traitlets.py","line":245,"col":null},{"name":"setup_class","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/traitlets/traitlets.py","line":985,"col":null},{"name":"setup_class","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/traitlets/traitlets.py","line":1002,"col":null},{"name":"__init__","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/traitlets/traitlets.py","line":970,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/IPython/terminal/interactiveshell.py","line":188,"col":null},{"name":"source_to_code","file":"","line":913,"col":null},{"name":"get_code","file":"","line":983,"col":null},{"name":"__getitem__","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":4714,"col":null},{"name":"__getitem__","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":5869,"col":null},{"name":"__contains__","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":4791,"col":null},{"name":"__contains__","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":5876,"col":null},{"name":"_perform_update","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":3872,"col":null},{"name":"update","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":5122,"col":null},{"name":"update_layout","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":1392,"col":null},{"name":"update_layout","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/graph_objs/_figure.py","line":787,"col":null},{"name":"make_subplots","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/_subplots.py","line":881,"col":null},{"name":"get_validator","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/validator_cache.py","line":28,"col":null},{"name":"_get_validator","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":4330,"col":null},{"name":"__getitem__","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":4709,"col":null},{"name":"_resolve_name","file":"","line":889,"col":null},{"name":"_gcd_import","file":"","line":1029,"col":null},{"name":"__getattr__","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/graph_objects/__init__.py","line":303,"col":null},{"name":"get_validator","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/validator_cache.py","line":20,"col":null},{"name":"_verbose_message","file":"","line":236,"col":null},{"name":"find_spec","file":"","line":1553,"col":null},{"name":"__setitem__","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":4848,"col":null},{"name":"__init__","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/graph_objs/carpet/_baxis.py","line":2313,"col":null},{"name":"__init__","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/graph_objs/_carpet.py","line":1906,"col":null},{"name":"validate_coerce","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/_plotly_utils/basevalidators.py","line":2604,"col":null},{"name":"_set_array_prop","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":5337,"col":null},{"name":"__setitem__","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":4856,"col":null},{"name":"__init__","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/graph_objs/layout/template/_data.py","line":1611,"col":null},{"name":"__init__","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/graph_objs/layout/_template.py","line":327,"col":null},{"name":"validate_coerce","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/_plotly_utils/basevalidators.py","line":2516,"col":null},{"name":"validate_coerce","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/_plotly_utils/basevalidators.py","line":2805,"col":null},{"name":"_perform_update","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":3896,"col":null},{"name":"update","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":5123,"col":null},{"name":"make_figure","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/express/_core.py","line":2614,"col":null},{"name":"__init__","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/validators/carpet/baxis/_minorgridcolor.py","line":8,"col":null},{"name":"__init__","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/graph_objs/carpet/_baxis.py","line":2325,"col":null},{"name":"_path_join","file":"","line":123,"col":null},{"name":"cache_from_source","file":"","line":429,"col":null},{"name":"__init__","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/graph_objs/_choropleth.py","line":2172,"col":null},{"name":"__init__","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/graph_objs/layout/template/_data.py","line":1623,"col":null},{"name":"_classify_pyc","file":"","line":577,"col":null},{"name":"get_code","file":"","line":950,"col":null},{"name":"__init__","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/graph_objs/_histogram2dcontour.py","line":3057,"col":null},{"name":"__init__","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/graph_objs/layout/template/_data.py","line":1659,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/validators/histogram2d/_colorscale.py","line":4,"col":null},{"name":"__init__","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/graph_objs/_histogram2d.py","line":2914,"col":null},{"name":"__init__","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/graph_objs/layout/template/_data.py","line":1663,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/graph_objs/scattercarpet/marker/_colorbar.py","line":5,"col":null},{"name":"__init__","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/graph_objs/scattercarpet/_marker.py","line":1622,"col":null},{"name":"__init__","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/graph_objs/_scattercarpet.py","line":2364,"col":null},{"name":"__init__","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/graph_objs/layout/template/_data.py","line":1715,"col":null},{"name":"__getattr__","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/graph_objs/__init__.py","line":303,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/graph_objs/layout/template/data/_scattergeo.py","line":1,"col":null},{"name":"data_class","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/_plotly_utils/basevalidators.py","line":2588,"col":null},{"name":"validate_coerce","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/_plotly_utils/basevalidators.py","line":2601,"col":null},{"name":"__init__","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/graph_objs/layout/template/_data.py","line":1719,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/graph_objs/scattergl/marker/_colorbar.py","line":5,"col":null},{"name":"__init__","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/graph_objs/scattergl/_marker.py","line":1445,"col":null},{"name":"__init__","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/graph_objs/_scattergl.py","line":2883,"col":null},{"name":"__init__","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/graph_objs/layout/template/_data.py","line":1723,"col":null},{"name":"__init__","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/graph_objs/layout/_ternary.py","line":1036,"col":null},{"name":"__init__","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/graph_objs/_layout.py","line":7128,"col":null},{"name":"__init__","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/graph_objs/layout/_template.py","line":331,"col":null},{"name":"_find_and_load_unlocked","file":"","line":969,"col":null},{"name":"__init__","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/graph_objs/layout/_ternary.py","line":1038,"col":null},{"name":"__init__","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/graph_objs/layout/ternary/_caxis.py","line":1751,"col":null},{"name":"__init__","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/graph_objs/layout/_xaxis.py","line":4465,"col":null},{"name":"__init__","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/graph_objs/_layout.py","line":7188,"col":null},{"name":"_deepcopy_list","file":"/home/fbruzzesi/.local/share/uv/python/cpython-3.9.19-linux-x86_64-gnu/lib/python3.9/copy.py","line":204,"col":null},{"name":"deepcopy","file":"/home/fbruzzesi/.local/share/uv/python/cpython-3.9.19-linux-x86_64-gnu/lib/python3.9/copy.py","line":146,"col":null},{"name":"_deepcopy_dict","file":"/home/fbruzzesi/.local/share/uv/python/cpython-3.9.19-linux-x86_64-gnu/lib/python3.9/copy.py","line":230,"col":null},{"name":"_set_compound_prop","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":5274,"col":null},{"name":"unique_with_mask","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/algorithms.py","line":438,"col":null},{"name":"get_group_index","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/sorting.py","line":193,"col":null},{"name":"compress_group_index","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/sorting.py","line":695,"col":null},{"name":"_get_compressed_codes","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/groupby/ops.py","line":765,"col":null},{"name":"compress_group_index","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/sorting.py","line":710,"col":null},{"name":"get_flattened_list","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/sorting.py","line":605,"col":null},{"name":"group_keys_seq","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/groupby/ops.py","line":648,"col":null},{"name":"get_iterator","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/groupby/ops.py","line":619,"col":null},{"name":"attrs","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/generic.py","line":399,"col":null},{"name":"__finalize__","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/generic.py","line":6255,"col":null},{"name":"_chop","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/groupby/ops.py","line":1188,"col":null},{"name":"__iter__","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/groupby/ops.py","line":1160,"col":null},{"name":"_get_module_lock","file":"","line":177,"col":null},{"name":"_find_and_load","file":"","line":1014,"col":null},{"name":"make_figure","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/express/_core.py","line":2422,"col":null},{"name":"copy_to_readonly_numpy_array","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/_plotly_utils/basevalidators.py","line":149,"col":null},{"name":"copy_to_readonly_numpy_array","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/_plotly_utils/basevalidators.py","line":128,"col":null},{"name":"deepcopy","file":"/home/fbruzzesi/.local/share/uv/python/cpython-3.9.19-linux-x86_64-gnu/lib/python3.9/copy.py","line":153,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":2250,"col":null},{"name":"add_traces","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":2250,"col":null},{"name":"add_traces","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/graph_objs/_figure.py","line":992,"col":null},{"name":"make_figure","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/express/_core.py","line":2611,"col":null},{"name":"fullmatch","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/_plotly_utils/basevalidators.py","line":18,"col":null},{"name":"perform_validate_coerce","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/_plotly_utils/basevalidators.py","line":1476,"col":null},{"name":"vc_scalar","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/_plotly_utils/basevalidators.py","line":1439,"col":null},{"name":"validate_coerce","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/_plotly_utils/basevalidators.py","line":1409,"col":null},{"name":"__init__","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/graph_objs/_layout.py","line":7044,"col":null},{"name":"perform_validate_coerce","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/_plotly_utils/basevalidators.py","line":1470,"col":null},{"name":"__init__","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/graph_objs/layout/polar/_angularaxis.py","line":2094,"col":null},{"name":"__init__","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/graph_objs/layout/_polar.py","line":1086,"col":null},{"name":"__init__","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/graph_objs/_layout.py","line":7056,"col":null},{"name":"perform_validate_coerce","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/_plotly_utils/basevalidators.py","line":1473,"col":null},{"name":"split_multichar","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/_plotly_utils/utils.py","line":408,"col":null},{"name":"_str_to_dict_path_full","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":70,"col":null},{"name":"_str_to_dict_path","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":1847,"col":null},{"name":"__contains__","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":4776,"col":null},{"name":"configure_cartesian_axes","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/express/_core.py","line":680,"col":null},{"name":"configure_axes","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/express/_core.py","line":573,"col":null},{"name":"make_figure","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/express/_core.py","line":2651,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/narwhals/utils.py","line":132,"col":null},{"name":"parse_version","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/narwhals/utils.py","line":132,"col":null},{"name":"_from_native_impl","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/narwhals/translate.py","line":477,"col":null},{"name":"_stack_arrays","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/internals/managers.py","line":2252,"col":null},{"name":"__iter__","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/groupby/ops.py","line":1157,"col":null},{"name":"_isna_array","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/dtypes/missing.py","line":300,"col":null},{"name":"__setattr__","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":4914,"col":null},{"name":"__init__","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/graph_objs/layout/_xaxis.py","line":4497,"col":null},{"name":"split_multichar","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/_plotly_utils/utils.py","line":409,"col":null},{"name":"_set_in","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":1881,"col":null},{"name":"_perform_plotly_relayout","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":2656,"col":null},{"name":"_perform_plotly_update","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":2964,"col":null},{"name":"plotly_update","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":2916,"col":null},{"name":"batch_update","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":3063,"col":null},{"name":"__exit__","file":"/home/fbruzzesi/.local/share/uv/python/cpython-3.9.19-linux-x86_64-gnu/lib/python3.9/contextlib.py","line":126,"col":null},{"name":"make_figure","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/express/_core.py","line":2612,"col":null},{"name":"_get_validator","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":4328,"col":null},{"name":"_dispatch_layout_change_callbacks","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":2808,"col":null},{"name":"plotly_update","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":2941,"col":null},{"name":"_set_compound_prop","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":5262,"col":null},{"name":"__init__","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/graph_objs/layout/_hoverlabel.py","line":435,"col":null},{"name":"__init__","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/graph_objs/_layout.py","line":6976,"col":null},{"name":"_vals_equal","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":5647,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":5655,"col":null},{"name":"_vals_equal","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":5655,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":5662,"col":null},{"name":"_vals_equal","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":5662,"col":null},{"name":"_set_compound_prop","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":5290,"col":null},{"name":"get_group_index","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/sorting.py","line":196,"col":null},{"name":"_perform_update","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":3905,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/express/_core.py","line":244,"col":null},{"name":"make_figure","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/express/_core.py","line":2433,"col":null},{"name":"_strip_subplot_suffix_of_1","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":5826,"col":null},{"name":"__getitem__","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":5868,"col":null},{"name":"_check_path_in_prop_tree","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":185,"col":null},{"name":"_perform_update","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":3880,"col":null},{"name":"__getitem__","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":4711,"col":null},{"name":"native_to_narwhals_dtype","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/narwhals/_pandas_like/utils.py","line":222,"col":null},{"name":"infer_config","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/express/_core.py","line":2116,"col":null},{"name":"maybe_convert_indices","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/indexers/utils.py","line":280,"col":null},{"name":"take","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/internals/managers.py","line":891,"col":null},{"name":"_make_underscore_key","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":86,"col":null},{"name":"_str_to_dict_path_full","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":113,"col":null},{"name":"_check_path_in_prop_tree","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":179,"col":null},{"name":"_build_dispatch_plan","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":2780,"col":null},{"name":"_dispatch_layout_change_callbacks","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":2803,"col":null},{"name":"_int64_cut_off","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/sorting.py","line":160,"col":null},{"name":"get_group_index","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/sorting.py","line":182,"col":null},{"name":"get_group_index","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/sorting.py","line":186,"col":null},{"name":"__getitem__","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/frame.py","line":4070,"col":null},{"name":"get_column","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/narwhals/_pandas_like/dataframe.py","line":107,"col":null},{"name":"get_column","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/narwhals/dataframe.py","line":705,"col":null},{"name":"make_trace_kwargs","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/express/_core.py","line":525,"col":null},{"name":"make_figure","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/express/_core.py","line":2528,"col":null},{"name":"_make_hyphen_key","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":81,"col":null},{"name":"_str_to_dict_path_full","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":88,"col":null},{"name":"yaxis","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/graph_objs/_layout.py","line":5578,"col":null},{"name":"_prop_set_child","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":5428,"col":null},{"name":"_relayout_child","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":5462,"col":null},{"name":"_send_prop_set","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":5683,"col":null},{"name":"_set_prop","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":5233,"col":null},{"name":"_perform_update","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":3861,"col":null},{"name":"deepcopy","file":"/home/fbruzzesi/.local/share/uv/python/cpython-3.9.19-linux-x86_64-gnu/lib/python3.9/copy.py","line":134,"col":null},{"name":"to_plotly_json","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":5594,"col":null},{"name":"__init__","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/graph_objs/layout/_template.py","line":306,"col":null},{"name":"match","file":"/home/fbruzzesi/.local/share/uv/python/cpython-3.9.19-linux-x86_64-gnu/lib/python3.9/re.py","line":191,"col":null},{"name":"fullmatch","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/_plotly_utils/basevalidators.py","line":22,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/_plotly_utils/basevalidators.py","line":1666,"col":null},{"name":"validate_coerce","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/_plotly_utils/basevalidators.py","line":1657,"col":null},{"name":"__init__","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/graph_objs/_contour.py","line":3207,"col":null},{"name":"__init__","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/graph_objs/layout/template/_data.py","line":1635,"col":null},{"name":"deepcopy","file":"/home/fbruzzesi/.local/share/uv/python/cpython-3.9.19-linux-x86_64-gnu/lib/python3.9/copy.py","line":176,"col":null},{"name":"_deepcopy_list","file":"/home/fbruzzesi/.local/share/uv/python/cpython-3.9.19-linux-x86_64-gnu/lib/python3.9/copy.py","line":205,"col":null},{"name":"__init__","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":4308,"col":null},{"name":"__init__","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":5933,"col":null},{"name":"__init__","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/graph_objs/scattergl/_marker.py","line":1377,"col":null},{"name":"_subplot_re_match","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/graph_objs/_layout.py","line":63,"col":null},{"name":"_strip_subplot_suffix_of_1","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":5836,"col":null},{"name":"_is_key_path_compatible","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":2683,"col":null},{"name":"_perform_plotly_relayout","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":2645,"col":null},{"name":"init_figure","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/express/_core.py","line":2748,"col":null},{"name":"is_hashable","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/dtypes/inference.py","line":365,"col":null},{"name":"maybe_extract_name","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/indexes/base.py","line":7698,"col":null},{"name":"__init__","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/series.py","line":487,"col":null},{"name":"reset_index","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/series.py","line":1753,"col":null},{"name":"is_bool_indexer","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/common.py","line":125,"col":null},{"name":"_getitem_axis","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/indexing.py","line":1412,"col":null},{"name":"__getitem__","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/indexing.py","line":1191,"col":null},{"name":"native_to_narwhals_dtype","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/narwhals/_pandas_like/utils.py","line":288,"col":null},{"name":"__getitem__","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":4712,"col":null},{"name":"_dispatch_layout_change_callbacks","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":2809,"col":null},{"name":"validate_coerce","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/_plotly_utils/basevalidators.py","line":2799,"col":null},{"name":"__init__","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/graph_objs/scatter3d/marker/_colorbar.py","line":2216,"col":null},{"name":"__init__","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/graph_objs/scatter3d/_marker.py","line":1263,"col":null},{"name":"__init__","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/graph_objs/_scatter3d.py","line":2697,"col":null},{"name":"__init__","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/graph_objs/layout/template/_data.py","line":1711,"col":null},{"name":"perform_validate_coerce","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/_plotly_utils/basevalidators.py","line":1481,"col":null},{"name":"__init__","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/graph_objs/layout/scene/_xaxis.py","line":2600,"col":null},{"name":"__init__","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/graph_objs/layout/_scene.py","line":1773,"col":null},{"name":"__init__","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/graph_objs/_layout.py","line":7068,"col":null},{"name":"__init__","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/graph_objs/layout/scene/_yaxis.py","line":2584,"col":null},{"name":"__init__","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/graph_objs/layout/_scene.py","line":1777,"col":null},{"name":"maybe_convert_indices","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/indexers/utils.py","line":274,"col":null},{"name":"reindex_indexer","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/internals/managers.py","line":704,"col":null},{"name":"chomp_empty_strings","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/_plotly_utils/utils.py","line":512,"col":null},{"name":"chomp_empty_strings","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/_plotly_utils/utils.py","line":506,"col":null},{"name":"_split_and_chomp","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":99,"col":null},{"name":"_str_to_dict_path_full","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":106,"col":null},{"name":"__init__","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/graph_objs/_scattergl.py","line":2967,"col":null},{"name":"validate_coerce","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/_plotly_utils/basevalidators.py","line":2721,"col":null},{"name":"add_traces","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":2191,"col":null},{"name":"__init__","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/graph_objs/_scattergl.py","line":3003,"col":null},{"name":"validate_coerce","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/_plotly_utils/basevalidators.py","line":1807,"col":null},{"name":"__init__","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/graph_objs/_scattergl.py","line":3011,"col":null},{"name":"__getitem__","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":741,"col":null},{"name":"layout","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":2545,"col":null},{"name":"_is_key_path_compatible","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":2673,"col":null},{"name":"__init__","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/graph_objs/layout/scene/_xaxis.py","line":2643,"col":null},{"name":"key","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/_plotly_utils/utils.py","line":374,"col":null},{"name":"_natural_sort_strings","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/_plotly_utils/utils.py","line":380,"col":null},{"name":"_select_layout_subplots_by_prefix","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":1440,"col":null},{"name":"select_yaxes","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/graph_objs/_figure.py","line":23186,"col":null},{"name":"configure_cartesian_axes","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/express/_core.py","line":672,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":1442,"col":null},{"name":"_select_layout_subplots_by_prefix","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":1442,"col":null},{"name":"configure_cartesian_axes","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/express/_core.py","line":673,"col":null},{"name":"_get_module_lock","file":"","line":185,"col":null},{"name":"__enter__","file":"","line":157,"col":null},{"name":"get_validator","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/validator_cache.py","line":29,"col":null},{"name":"shape","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/graph_objs/bar/marker/_pattern.py","line":252,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/express/_core.py","line":1016,"col":null},{"name":"apply_default_cascade","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/express/_core.py","line":1015,"col":null},{"name":"make_figure","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/express/_core.py","line":2340,"col":null},{"name":"bar","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/express/_chart_types.py","line":373,"col":null},{"name":"figure_generation_bar","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/t.py","line":65,"col":null},{"name":"test_all_charts","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/t.py","line":83,"col":null},{"name":"vstack","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/numpy/core/shape_base.py","line":289,"col":null},{"name":"_merge_blocks","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/internals/managers.py","line":2294,"col":null},{"name":"_consolidate","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/internals/managers.py","line":2269,"col":null},{"name":"_consolidate_inplace","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/internals/managers.py","line":1788,"col":null},{"name":"create_block_manager_from_column_arrays","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/internals/managers.py","line":2144,"col":null},{"name":"_merge_blocks","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/internals/managers.py","line":2301,"col":null},{"name":"_subplot_type_for_trace_type","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/_subplots.py","line":1073,"col":null},{"name":"make_figure","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/express/_core.py","line":2377,"col":null},{"name":"is_homogeneous_array","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/_plotly_utils/basevalidators.py","line":201,"col":null},{"name":"validate_coerce","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/_plotly_utils/basevalidators.py","line":407,"col":null},{"name":"copy_to_readonly_numpy_array","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/_plotly_utils/basevalidators.py","line":152,"col":null},{"name":"__getitem__","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":4719,"col":null},{"name":"__setitem__","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":4867,"col":null},{"name":"template","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/graph_objs/_layout.py","line":3828,"col":null},{"name":"__setattr__","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":4916,"col":null},{"name":"__setattr__","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":5912,"col":null},{"name":"_initialize_layout_template","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":2532,"col":null},{"name":"__init__","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":630,"col":null},{"name":"__init__","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/graph_objs/layout/_geo.py","line":1412,"col":null},{"name":"deepcopy","file":"/home/fbruzzesi/.local/share/uv/python/cpython-3.9.19-linux-x86_64-gnu/lib/python3.9/copy.py","line":142,"col":null},{"name":"deepcopy","file":"/home/fbruzzesi/.local/share/uv/python/cpython-3.9.19-linux-x86_64-gnu/lib/python3.9/copy.py","line":178,"col":null},{"name":"deepcopy","file":"/home/fbruzzesi/.local/share/uv/python/cpython-3.9.19-linux-x86_64-gnu/lib/python3.9/copy.py","line":139,"col":null},{"name":"deepcopy","file":"/home/fbruzzesi/.local/share/uv/python/cpython-3.9.19-linux-x86_64-gnu/lib/python3.9/copy.py","line":144,"col":null},{"name":"deepcopy","file":"/home/fbruzzesi/.local/share/uv/python/cpython-3.9.19-linux-x86_64-gnu/lib/python3.9/copy.py","line":138,"col":null},{"name":"deepcopy","file":"/home/fbruzzesi/.local/share/uv/python/cpython-3.9.19-linux-x86_64-gnu/lib/python3.9/copy.py","line":175,"col":null},{"name":"deepcopy","file":"/home/fbruzzesi/.local/share/uv/python/cpython-3.9.19-linux-x86_64-gnu/lib/python3.9/copy.py","line":137,"col":null},{"name":"deepcopy","file":"/home/fbruzzesi/.local/share/uv/python/cpython-3.9.19-linux-x86_64-gnu/lib/python3.9/copy.py","line":145,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/_plotly_utils/basevalidators.py","line":1675,"col":null},{"name":"validate_coerce","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/_plotly_utils/basevalidators.py","line":1674,"col":null},{"name":"__init__","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/generic.py","line":282,"col":null},{"name":"__init__","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/series.py","line":592,"col":null},{"name":"validate_coerce","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/_plotly_utils/basevalidators.py","line":2523,"col":null},{"name":"__init__","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/graph_objs/bar/_marker.py","line":1215,"col":null},{"name":"__init__","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/graph_objs/_bar.py","line":3313,"col":null},{"name":"__init__","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/graph_objs/_bar.py","line":3425,"col":null},{"name":"_deepcopy_atomic","file":"/home/fbruzzesi/.local/share/uv/python/cpython-3.9.19-linux-x86_64-gnu/lib/python3.9/copy.py","line":182,"col":null},{"name":"deepcopy","file":"/home/fbruzzesi/.local/share/uv/python/cpython-3.9.19-linux-x86_64-gnu/lib/python3.9/copy.py","line":128,"col":null},{"name":"add_traces","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":2255,"col":null},{"name":"_deepcopy_list","file":"/home/fbruzzesi/.local/share/uv/python/cpython-3.9.19-linux-x86_64-gnu/lib/python3.9/copy.py","line":201,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":5348,"col":null},{"name":"_set_array_prop","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":5348,"col":null},{"name":"__init__","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/graph_objs/layout/template/_data.py","line":1655,"col":null},{"name":"allows_duplicate_labels","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/flags.py","line":90,"col":null},{"name":"__finalize__","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/generic.py","line":6262,"col":null},{"name":"dtype","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/series.py","line":718,"col":null},{"name":"__init__","file":"","line":63,"col":null},{"name":"_get_module_lock","file":"","line":183,"col":null},{"name":"make_figure","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/express/_core.py","line":2407,"col":null},{"name":"get_validator","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/validator_cache.py","line":12,"col":null},{"name":"update","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":5126,"col":null},{"name":"make_figure","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/express/_core.py","line":2423,"col":null},{"name":"__init__","file":"","line":61,"col":null},{"name":"copy_to_readonly_numpy_array","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/_plotly_utils/basevalidators.py","line":73,"col":null},{"name":"validate_coerce","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/_plotly_utils/basevalidators.py","line":806,"col":null},{"name":"validate_coerce","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/_plotly_utils/basevalidators.py","line":2240,"col":null},{"name":"__init__","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/graph_objs/layout/_yaxis.py","line":4128,"col":null},{"name":"_set_subplotid_prop","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":5805,"col":null},{"name":"__setitem__","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":5901,"col":null},{"name":"_init_subplot_xy","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/_subplots.py","line":990,"col":null},{"name":"_str_to_dict_path","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":1837,"col":null},{"name":"update_axis_matches","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/_subplots.py","line":916,"col":null},{"name":"_configure_shared_axes","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/_subplots.py","line":963,"col":null},{"name":"make_subplots","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/_subplots.py","line":757,"col":null},{"name":"_deepcopy_atomic","file":"/home/fbruzzesi/.local/share/uv/python/cpython-3.9.19-linux-x86_64-gnu/lib/python3.9/copy.py","line":183,"col":null},{"name":"__getitem__","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":4750,"col":null},{"name":"__init__","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/graph_objs/barpolar/marker/_pattern.py","line":560,"col":null},{"name":"__init__","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/graph_objs/barpolar/_marker.py","line":1176,"col":null},{"name":"__init__","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/graph_objs/_barpolar.py","line":1924,"col":null},{"name":"__init__","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/graph_objs/layout/template/_data.py","line":1595,"col":null},{"name":"_vals_equal","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":5665,"col":null},{"name":"make_figure","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/express/_core.py","line":2418,"col":null},{"name":"_make_hyphen_key","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":83,"col":null},{"name":"_str_to_dict_path_full","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":114,"col":null},{"name":"__init__","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/graph_objs/_bar.py","line":3469,"col":null},{"name":"value","file":"/home/fbruzzesi/.local/share/uv/python/cpython-3.9.19-linux-x86_64-gnu/lib/python3.9/enum.py","line":795,"col":null},{"name":"__get__","file":"/home/fbruzzesi/.local/share/uv/python/cpython-3.9.19-linux-x86_64-gnu/lib/python3.9/types.py","line":178,"col":null},{"name":"_compile","file":"/home/fbruzzesi/.local/share/uv/python/cpython-3.9.19-linux-x86_64-gnu/lib/python3.9/re.py","line":292,"col":null},{"name":"filterwarnings","file":"/home/fbruzzesi/.local/share/uv/python/cpython-3.9.19-linux-x86_64-gnu/lib/python3.9/warnings.py","line":155,"col":null},{"name":"__iter__","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/narwhals/_pandas_like/group_by.py","line":102,"col":null},{"name":"_check_object_for_strings","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/algorithms.py","line":292,"col":null},{"name":"_get_child_props","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":4450,"col":null},{"name":"_props","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":4429,"col":null},{"name":"_get_child_props","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":4458,"col":null},{"name":"__getitem__","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":4734,"col":null},{"name":"__setitem__","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":4891,"col":null},{"name":"get_label","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/express/_core.py","line":144,"col":null},{"name":"get_decorated_label","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/express/_core.py","line":164,"col":null},{"name":"make_trace_kwargs","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/express/_core.py","line":288,"col":null},{"name":"is_homogeneous_array","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/_plotly_utils/basevalidators.py","line":203,"col":null},{"name":"_any","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/numpy/core/_methods.py","line":58,"col":null},{"name":"nanany","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/nanops.py","line":520,"col":null},{"name":"_reduce","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/series.py","line":6457,"col":null},{"name":"any","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/series.py","line":6471,"col":null},{"name":"perform_replacemenet","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/_plotly_utils/basevalidators.py","line":518,"col":null},{"name":"validate_coerce","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/_plotly_utils/basevalidators.py","line":617,"col":null},{"name":"__init__","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/graph_objs/layout/_annotation.py","line":2100,"col":null},{"name":"make_subplots","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/_subplots.py","line":838,"col":null},{"name":"_set_compound_prop","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":5269,"col":null},{"name":"__init__","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/series.py","line":574,"col":null},{"name":"__getitem__","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":4690,"col":null},{"name":"compress_group_index","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/sorting.py","line":705,"col":null},{"name":"_amax","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/numpy/core/_methods.py","line":41,"col":null},{"name":"take","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/indexes/range.py","line":1168,"col":null},{"name":"take","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/internals/managers.py","line":893,"col":null},{"name":"_get_child_props","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":4455,"col":null},{"name":"__contains__","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":5875,"col":null},{"name":"_str_to_dict_path_full","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":68,"col":null},{"name":"_get_child_props","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":4449,"col":null},{"name":"__getitem__","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":4735,"col":null},{"name":"sequential","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/graph_objs/layout/_colorscale.py","line":98,"col":null},{"name":"apply_default_cascade","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/express/_core.py","line":974,"col":null},{"name":"__init__","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/series.py","line":521,"col":null},{"name":"_get_item_cache","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/frame.py","line":4628,"col":null},{"name":"__getitem__","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/frame.py","line":4078,"col":null},{"name":"process_args_into_dataframe","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/express/_core.py","line":1295,"col":null},{"name":"release","file":"","line":123,"col":null},{"name":"make_figure","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/express/_core.py","line":2406,"col":null},{"name":"__init__","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":5678,"col":null},{"name":"__init__","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/graph_objs/layout/template/_data.py","line":1563,"col":null},{"name":"copy","file":"/home/fbruzzesi/.local/share/uv/python/cpython-3.9.19-linux-x86_64-gnu/lib/python3.9/copy.py","line":76,"col":null},{"name":"__init__","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/graph_objs/barpolar/marker/_pattern.py","line":509,"col":null},{"name":"write_csv","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/polars/dataframe/frame.py","line":2896,"col":null},{"name":"run_and_save_results","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/t.py","line":101,"col":null}]},"activeProfileIndex":null,"exporter":"py-spy@0.3.14","name":"py-spy profile"} From 7416407483859d1fa64c4ffa1bde243abfb7e3a6 Mon Sep 17 00:00:00 2001 From: FBruzzesi Date: Fri, 25 Oct 2024 09:08:29 +0200 Subject: [PATCH 062/106] format --- packages/python/plotly/plotly/express/_core.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/python/plotly/plotly/express/_core.py b/packages/python/plotly/plotly/express/_core.py index 74c69e4b5e..30906c5745 100644 --- a/packages/python/plotly/plotly/express/_core.py +++ b/packages/python/plotly/plotly/express/_core.py @@ -1859,7 +1859,9 @@ def process_dataframe_hierarchy(args): # However we cannot do that just yet, therefore a workaround is provided agg_f[args["color"]] = nw.col(args["color"]).max() agg_f[f'{args["color"]}__plotly_n_unique__'] = ( - nw.col(args["color"]).n_unique().alias(f'{args["color"]}__plotly_n_unique__') + nw.col(args["color"]) + .n_unique() + .alias(f'{args["color"]}__plotly_n_unique__') ) else: # This first needs to be multiplied by `count_colname` From 1867f6f25acafbef90910c42f88d1725471df75e Mon Sep 17 00:00:00 2001 From: FBruzzesi Date: Fri, 25 Oct 2024 09:09:06 +0200 Subject: [PATCH 063/106] format --- out-branch.csv | 3 --- perf-branch.csv | 3 --- perf-master.csv | 3 --- profile-branch | 1 - 4 files changed, 10 deletions(-) delete mode 100644 out-branch.csv delete mode 100644 perf-branch.csv delete mode 100644 perf-master.csv delete mode 100644 profile-branch diff --git a/out-branch.csv b/out-branch.csv deleted file mode 100644 index 252d4abdc3..0000000000 --- a/out-branch.csv +++ /dev/null @@ -1,3 +0,0 @@ -Chart Type,Time (s) -scatter_pandas,0.5901803081999788 -bar_pandas,1.043395163300056 diff --git a/perf-branch.csv b/perf-branch.csv deleted file mode 100644 index 7083e86447..0000000000 --- a/perf-branch.csv +++ /dev/null @@ -1,3 +0,0 @@ -Chart Type,Time (s) -scatter_pandas,0.4685762626999576 -bar_pandas,0.9242745239999749 diff --git a/perf-master.csv b/perf-master.csv deleted file mode 100644 index bf59cb2180..0000000000 --- a/perf-master.csv +++ /dev/null @@ -1,3 +0,0 @@ -Chart Type,Time (s) -scatter_pandas,0.5244665377000274 -bar_pandas,0.8416239924999672 diff --git a/profile-branch b/profile-branch deleted file mode 100644 index 6e963626f5..0000000000 --- a/profile-branch +++ /dev/null @@ -1 +0,0 @@ -{"$schema":"https://www.speedscope.app/file-format-schema.json","profiles":[{"type":"sampled","name":"Thread 35046 \"\"","unit":"seconds","startValue":0.0,"endValue":18.34,"samples":[[18,5,4,3,8,7,17,5,4,3,8,7,16,5,15,7,5,4,3,8,7,14,10,7,5,4,3,8,7,13,5,4,3,8,7,12,5,4,3,8,7,11,10,7,5,4,3,8,7,9,5,4,3,8,7,6,5,4,3,2,1,0],[18,5,4,3,8,7,17,5,4,3,8,7,16,5,15,7,5,4,3,8,7,14,10,7,5,4,3,8,7,13,5,4,3,8,7,12,5,4,3,8,7,11,10,7,5,4,3,8,7,9,5,4,3,8,7,6,5,4,3,8,7,20,5,19],[18,5,4,3,8,7,17,5,4,3,8,7,16,5,15,7,5,4,3,8,7,14,10,7,5,4,3,8,7,13,5,4,3,8,7,12,5,4,3,8,7,11,10,7,5,4,3,8,7,9,5,4,3,8,7,21,5,4,3,2,1,0],[18,5,4,3,8,7,17,5,4,3,8,7,16,5,15,7,5,4,3,8,7,14,10,7,5,4,3,8,7,13,5,4,3,8,7,12,5,4,3,8,7,11,10,7,5,4,3,8,7,9,5,4,3,8,7,21,5,4,3,2,1,0],[18,5,4,3,8,7,17,5,4,3,8,7,16,5,15,7,5,4,3,8,7,14,10,7,5,4,3,8,7,13,5,4,3,8,7,12,5,4,3,8,7,11,10,7,5,4,3,8,7,9,5,4,3,8,7,21,5,4,3,8,7,23,22],[18,5,4,3,8,7,17,5,4,3,8,7,16,5,15,7,5,4,3,8,7,14,10,7,5,4,3,8,7,13,5,4,3,8,7,25,24],[18,5,4,3,8,7,17,5,4,3,8,7,16,5,15,7,5,4,3,8,7,28,10,7,5,27,26],[18,5,4,3,8,7,17,5,4,3,8,7,16,5,15,7,5,4,3,8,7,36,10,7,5,35,34,33,32,31,30,29],[18,5,4,3,8,7,17,5,4,3,8,7,16,5,15,7,5,4,3,8,7,47,10,7,5,4,3,8,7,46,45,44,43,42,40,41,40,39,38,37],[18,5,4,3,8,7,53,10,7,5,4,3,8,7,52,10,7,5,4,3,8,7,51,5,4,3,2,50,49,48],[18,5,4,3,8,7,53,10,7,5,4,3,8,7,52,10,7,5,4,3,8,7,51,5,4,3,2,50,54],[18,5,4,3,8,7,53,10,7,5,4,3,8,7,61,10,7,5,4,3,8,7,60,45,44,59,58,57,56,55],[18,5,4,3,8,7,53,10,7,5,4,3,8,7,64,10,7,5,4,3,2,63,62],[18,5,4,3,8,7,53,10,7,5,4,3,8,7,67,10,7,5,35,34,33,32,66,65],[18,5,4,3,8,7,71,10,7,5,4,3,8,7,70,10,7,5,4,3,8,7,69,5,4,3,8,7,68,5,15,7,5,35,34,33,32,66,65],[84,5,4,3,8,7,83,10,7,5,4,3,8,7,82,5,4,3,8,7,81,5,4,3,8,7,80,5,4,3,8,7,79,10,7,5,4,3,8,7,78,5,4,3,8,7,77,5,4,3,8,7,76,75,74,73,72],[84,5,4,3,8,7,83,10,7,5,4,3,8,7,82,5,4,3,8,7,81,5,4,3,8,7,80,5,4,3,8,7,86,5,4,3,8,7,85,5,4,3,2,1,0],[84,5,4,3,8,7,83,10,7,5,4,3,8,7,82,5,4,3,8,7,91,5,4,3,8,7,90,5,4,3,8,7,89,5,4,88,87],[84,5,4,3,8,7,83,10,7,5,4,3,8,7,82,5,4,3,8,7,91,5,4,3,8,7,90,5,4,3,8,7,95,5,4,3,8,7,94,93,92],[84,5,4,3,8,7,99,5,4,3,8,7,98,5,4,3,8,7,97,5,35,34,33,32,96],[84,5,4,3,8,7,99,5,4,3,8,7,98,5,4,3,8,7,97,5,4,3,8,7,107,5,4,3,8,7,106,5,4,3,8,7,105,104,103,102,101,100],[84,5,4,3,8,7,99,5,4,3,8,7,111,5,4,3,8,7,110,5,4,3,8,7,109,108],[84,5,4,3,8,7,112],[119,5,4,3,8,7,118,5,4,3,8,7,117,5,4,3,8,7,116,5,4,3,8,7,115,5,4,88,114,113,7],[119,5,4,3,8,7,118,5,4,3,8,7,117,5,4,3,8,7,116,5,4,3,8,7,115,5,4,88,114,113,7],[119,5,4,3,8,7,118,5,4,3,8,7,117,5,4,3,8,7,116,5,4,3,8,7,115,5,4,88,114,113,7],[119,5,4,3,8,7,118,5,4,3,8,7,117,5,4,3,8,7,116,5,4,3,8,7,115,5,4,3,120,7],[119,5,4,3,8,7,118,5,4,3,8,7,117,5,4,3,8,7,116,5,4,3,8,7,115,5,4,3,120,7,5,4,3,8,7,122,10,7,5,4,3,2,1,121],[119,5,4,3,8,7,118,5,4,3,8,7,117,5,4,3,8,7,116,5,4,3,8,7,115,5,4,3,120,7,5,4,3,8,7,122,10,7,5,4,3,8,7,123,5,4,3,2,63,62],[119,5,4,3,8,7,127,5,4,3,8,7,126,5,4,3,8,7,125,5,4,3,120,7,5,4,3,120,7,5,4,3,120,7,5,15,7,5,4,3,8,7,124,10,7,5,4,3,120,7],[119,5,4,3,8,7,127,5,4,3,8,7,126,5,4,3,8,7,125,5,4,3,120,7,5,4,3,120,7,5,4,3,120,7,5,15,7,5,4,3,8,7,124,10,7,5,4,3,120,7,5,35,34,128],[119,5,4,3,8,7,127,5,4,3,8,7,132,5,4,3,8,7,131,130,45,44,43,42,40,39,38,129],[119,5,4,3,8,7,127,5,4,3,8,7,138,5,4,3,8,7,137,5,4,3,8,7,136,5,4,3,8,7,135,5,4,3,8,7,134,10,7,5,4,3,8,7,133,5,4,3,2,1,0],[119,5,4,3,8,7,127,5,4,3,8,7,138,5,4,3,8,7,137,5,4,3,8,7,136,5,4,3,8,7,135,5,4,3,8,7,134,10,7,5,4,3,8,7,133,5,4,3,2,1,0],[119,5,4,3,8,7,127,5,4,3,8,7,138,5,4,3,8,7,137,5,4,3,8,7,136,5,4,3,8,7,135,5,4,3,8,7,134,10,7,5,4,3,8,7,141,140,45,44,43,42,40,41,40,139],[119,5,4,3,8,7,127,5,4,3,8,7,138,5,4,3,8,7,137,5,4,3,8,7,136,5,4,3,8,7,135,5,4,3,8,7,146,145,144,143,142],[119,5,4,3,8,7,127,5,4,3,8,7,138,5,4,3,8,7,137,5,4,3,8,7,151,5,4,3,8,7,150,10,7,5,4,3,8,7,149,5,4,3,8,7,148,10,7,5,35,147],[119,5,4,3,8,7,127,5,4,3,8,7,138,5,4,3,8,7,137,5,4,3,8,7,151,5,4,3,8,7,150,10,7,5,4,3,8,7,149,5,4,3,8,7,148,10,7,5,35,147],[119,5,4,3,8,7,127,5,4,3,8,7,138,5,4,3,8,7,153,5,4,3,8,7,152,10,7,5,4,3,120,7],[119,5,4,3,8,7,127,5,4,3,8,7,138,5,4,3,8,7,153,5,4,3,8,7,154,10,7,5,4,3,2,1,0],[119,5,4,3,8,7,127,5,4,3,8,7,138,5,4,3,8,7,153,5,4,3,8,7,154,10,7,5,4,3,2,1,0],[119,5,4,3,8,7,127,5,4,3,8,7,138,5,4,3,8,7,153,5,4,3,8,7,154,10,7,5,4,3,8,7,155],[119,5,4,3,8,7,127,5,4,3,8,7,158,5,4,3,8,7,157,5,4,3,8,7,156,5,4,3,2,1,0],[119,5,4,3,8,7,127,5,4,3,8,7,158,5,4,3,8,7,157,5,4,3,8,7,156,5,4,3,2,1,0],[119,5,4,3,8,7,127,5,4,3,8,7,158,5,4,3,8,7,157,5,4,3,8,7,156,5,4,3,8,7,167,5,4,3,8,7,166,10,7,5,4,3,8,7,165,5,4,3,8,7,164,5,4,3,8,7,163,5,4,3,8,7,162,161,160,159],[119,5,4,3,8,7,127,5,4,3,8,7,158,5,4,3,8,7,157,5,4,3,8,7,156,5,4,3,8,7,167,5,4,3,8,7,166,10,7,5,4,3,8,7,165,5,4,3,8,7,169,5,4,3,8,7,168,5,4,3,2,1,0],[119,5,4,3,8,7,127,5,4,3,8,7,158,5,4,3,8,7,157,5,4,3,8,7,156,5,4,3,8,7,167,5,4,3,8,7,175,174,173,172,171,170],[119,5,4,3,8,7,127,5,4,3,8,7,158,5,4,3,8,7,157,5,4,3,8,7,156,5,4,3,8,7,178,5,4,3,8,7,177,176],[119,5,4,3,8,7,127,5,4,3,8,7,158,5,4,3,8,7,157,5,4,3,8,7,156,5,4,3,8,7,178,5,4,3,8,7,177,182,181,180,179],[119,5,4,3,8,7,127,5,4,3,8,7,158,5,4,3,8,7,157,5,4,3,8,7,187,10,7,5,4,3,8,7,186,5,4,3,120,7,185,184,183],[119,5,4,3,8,7,193,5,4,3,8,7,192,5,4,3,8,7,191,5,4,3,8,7,190,5,4,3,8,7,189,5,4,3,8,7,188],[119,5,4,3,8,7,193,5,4,3,8,7,192,5,4,3,8,7,194,5,4,3,2,63,62],[119,5,4,3,8,7,196,5,4,3,8,7,195,5,35,34,33,32,66,65],[199,5,4,3,8,7,198,5,4,3,8,7,197,5,4,3,2,1,0],[199,5,4,3,8,7,198,5,4,3,8,7,202,5,4,3,8,7,201,200],[199,5,4,3,8,7,198,5,4,3,8,7,209,5,4,3,8,7,208,10,7,5,4,3,8,7,207,5,4,3,8,7,206,5,4,3,8,7,205,5,4,3,8,7,204,5,4,3,8,7,203,10,7,5,4,3,2,1,0],[199,5,4,3,8,7,198,5,4,3,8,7,209,5,4,3,8,7,208,10,7,5,4,3,8,7,212,5,4,3,8,7,211,5,4,3,8,7,210],[199,5,4,3,8,7,198,5,4,3,8,7,209,5,4,3,8,7,208,10,7,5,4,3,8,7,220,5,4,3,8,7,219,5,4,3,8,7,218,5,4,3,8,7,217,216,215,214,45,44,43,42,40,41,40,213],[199,5,4,3,8,7,198,5,4,3,8,7,209,5,4,3,8,7,208,10,7,5,4,3,8,7,224,5,15,7,5,4,3,8,7,223,5,4,3,8,7,222,5,4,88,87,221],[199,5,4,3,8,7,198,5,4,3,8,7,209,5,4,3,8,7,208,10,7,5,4,3,8,7,224,5,4,3,8,7,226,5,4,3,2,63,225],[199,5,4,3,8,7,233,5,4,3,8,7,232,231,230,229,228,227],[199,5,4,3,8,7,233,5,4,3,8,7,234,231,230,229,228,227],[235],[235],[235],[235],[235],[235],[241,240,239,238,237,236],[241,240,239,238,237,236],[241,240,239,238,237,236],[241,240,239,238,237,236],[241,240,239,238,237,236],[241,240,239,238,237,236],[241,240,239,238,237,236],[241,240,239,238,237,236],[241,240,239,238,237,236],[241,240,239,238,237,236],[241,240,239,238,237,236],[241,240,239,238,237,236],[241,240,239,238,237,236],[241,240,239,238,237,236],[241,240,239,238,237,236],[241,240,239,238,237,236],[241,240,239,238,237,236],[241,240,239,238,237,236],[241,240,239,238,237,236],[241,240,239,238,237,236],[241,240,239,238,237,236],[241,240,239,238,237,236],[241,240,239,238,237,236],[241,240,239,238,237,236],[241,240,239,238,237,236],[241,240,239,238,237,236],[241,240,239,238,237,236],[241,240,239,238,237,236],[241,240,239,238,237,236],[241,240,239,238,237,236],[241,240,239,238,237,236],[241,240,239,238,237,236],[241,240,239,238,237,236],[241,240,239,238,237,236],[241,240,239,238,237,236],[241,240,239,238,237,236],[241,240,239,238,237,236],[241,240,239,238,237,236],[241,240,239,238,237,236],[241,240,239,238,237,236],[241,240,239,238,237,236],[241,240,239,238,237,236],[241,240,239,238,237,236],[241,240,239,238,237,236],[241,240,239,238,237,236],[241,240,239,238,237,236],[241,240,239,238,237,236],[241,240,239,238,237,236],[241,240,239,238,237,236],[241,240,239,238,237,236],[241,240,239,238,237,236],[241,240,239,238,237,236],[241,240,239,238,237,236],[241,240,239,238,237,236],[241,240,239,238,237,236],[241,240,239,238,237,236],[241,240,239,238,237,236],[241,240,239,238,237,236],[241,240,239,238,237,236],[241,240,239,238,237,236],[241,240,239,238,237,236],[241,240,239,238,237,236],[241,240,239,238,237,236],[241,240,239,238,237,236],[248,247,246,245,244,243,242],[248,247,246,245,244,243,242],[248,247,246,245,244,243,242],[248,247,246,245,244,243,242],[248,247,246,245,244,243,242],[248,247,246,245,244,243,242],[248,247,246,245,244,243,242],[248,247,246,245,244,243,242],[248,247,246,245,244,243,242],[248,247,246,245,244,243,242],[248,247,246,245,244,243,242],[248,247,246,245,244,243,242],[248,247,246,252,251,250,249],[248,247,246,252,251,250,249],[248,247,246,252,251,250,249],[248,247,246,252,251,250,249],[248,247,246,252,251,250,249],[248,247,246,252,251,250,249],[248,247,246],[248,247,246],[248,247,246],[248,247,246],[253],[253],[253],[253],[253],[253],[253],[253],[253],[253],[253],[253],[253],[253],[253],[253],[253],[253],[253],[253],[253],[253],[253],[253],[266,265,264,263,262,261,260,259,258,257,256,255,254],[266,265,264,263,262,261,260,270,269,268,267],[266,265,264,263,262,261,260,270,269,268,267],[266,265,264,263,262,261,260,270,269,268,267],[266,265,264,263,262,261,260,270,269,268,267],[266,265,264,263,262,261,260,270,269,268,267],[266,265,264,263,262,261,260,270,269,268,267],[266,265,264,263,262,261,260,270,269,268,267],[266,265,264,263,262,261,260,270,269,268,267],[266,265,264,263,262,261,260,270,277,276,275,274,247,246,245,273,272,271],[266,265,264,263,262,261,260,270,277,276,275,274,247,246,252,251,250,249],[266,265,264,263,262,261,260,270,277,276,275,274,247,246,252,251,250,249],[266,265,264,263,262,261,260,270,277],[266,265,264,263,262,261,292,291,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,264,263,262,261,292,291,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,264,263,262,261,292,291,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,264,263,262,261,302,301,300,299,298,297,296,295,294,293],[266,265,264,263,262,261,302,301,300,299,298,297,296,295,294,293],[266,265,264,263,262,261,302,301,300,299,298,297,296,303],[266,265,264,263,262,261,302,301,300,299,298,297,296,303],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,319,294,293],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,319,294,293],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,322],[266,265,264,263,262,261,302,318,317,316,315,314,313,326,325,324,323],[266,265,264,263,262,261,302,318,317,316,315,314,336,335,334,333,332,331,330,329,328,327],[266,265,264,263,262,261,302,318,317,316,315,314,336,335,334,333,332,331,330,329,328,327],[266,265,264,263,262,261,302,318,317,316,315,314,336,335,334,333,332,331,330,329,328,337],[266,265,264,263,262,261,302,318,317,316,315,314,336,335,334,333,332,331,330,329,328,327],[266,265,264,263,262,261,302,318,317,316,315,314,336,335,334,333,332,331,330,329,328,327],[266,265,264,263,262,261,351,350,349,348,347,346,345,344,343,342,341,340,281,339,338],[266,265,264,263,262,261,364,363,362,361,360,359,358,357,356,355,354,353,352,5,4,3,2,63,62],[266,265,264,263,262,261,364,363,373,372,371,5,15,7,5,4,3,8,7,370,5,4,88,87,369,368,367,49,366,365],[266,265,264,263,262,261,364,363,373,372,371,5,15,7,5,4,3,8,7,370,5,4,3,8,7,383,382,381,353,352,5,4,3,8,7,380,379,353,352,5,15,7,352,5,15,7,352,5,4,3,8,7,378,5,4,3,8,7,377,10,7,5,4,3,8,7,376,10,7,5,4,3,8,7,375,5,4,3,8,7,374,5,4,3,2,1,0],[266,265,264,263,262,261,364,363,373,372,371,5,15,7,5,4,3,8,7,370,5,4,3,8,7,383,382,381,353,352,5,4,3,8,7,380,379,353,352,5,15,7,352,5,15,7,352,5,4,3,8,7,390,5,4,3,8,7,389,5,4,3,8,7,388,10,7,5,4,3,8,7,387,10,7,5,4,3,8,7,386,5,4,3,8,7,385,5,4,3,8,7,384],[266,265,264,263,262,261,364,363,373,372,371,5,15,7,5,4,3,8,7,370,5,4,3,8,7,383,382,381,353,352,5,4,3,8,7,380,379,353,352,5,15,7,352,5,15,7,352,5,4,3,8,7,390,5,4,3,8,7,394,5,4,3,8,7,393,5,4,3,8,7,392,391],[266,265,264,263,262,261,364,363,373,372,371,5,15,7,5,4,3,8,7,370,5,4,3,8,7,383,382,381,353,352,5,4,3,8,7,380,379,353,352,5,15,7,352,5,15,7,352,5,4,3,8,7,390,5,4,3,8,7,400,5,4,3,8,7,399,5,15,7,5,4,3,8,7,398,5,4,3,8,7,397,5,4,3,8,7,396,5,4,3,8,7,395,5,4,3,2,1,0],[266,265,264,263,262,261,364,363,373,372,371,5,15,7,5,4,3,8,7,370,5,4,3,8,7,383,382,381,353,352,5,4,3,8,7,380,379,353,352,5,15,7,352,5,15,7,352,5,4,3,8,7,390,5,4,3,8,7,400,5,4,3,8,7,399,5,15,7,5,4,3,8,7,406,5,4,3,8,7,405,5,4,3,8,7,404,5,4,3,8,7,403,5,4,3,8,7,402,401],[266,265,264,263,262,261,364,363,373,372,371,5,15,7,5,4,3,8,7,370,5,4,3,8,7,383,382,381,353,352,5,4,3,8,7,380,379,353,352,5,15,7,352,5,15,7,352,5,4,3,8,7,390,5,4,3,8,7,400,5,4,3,8,7,417,5,4,3,8,7,416,5,4,3,8,7,415,5,4,3,8,7,414,5,4,3,8,7,413,10,7,5,4,3,8,7,412,5,15,7,5,4,3,8,7,411,10,7,5,4,3,8,7,410,5,4,3,8,7,409,5,4,3,8,7,408,5,4,3,8,7,407,5,4,3,2,63,62],[266,265,264,263,262,261,364,363,373,372,371,5,15,7,5,4,3,8,7,370,5,4,3,8,7,383,382,381,353,352,5,4,3,8,7,380,379,353,352,5,15,7,352,5,15,7,352,5,4,3,8,7,390,5,4,3,8,7,400,5,4,3,8,7,417,5,4,3,8,7,416,5,4,3,8,7,415,5,4,3,8,7,414,5,4,3,8,7,413,10,7,5,4,3,8,7,412,5,15,7,5,4,3,8,7,411,10,7,5,4,3,8,7,410,5,4,3,8,7,409,5,4,3,8,7,418],[266,265,264,263,262,261,364,363,373,372,371,5,15,7,5,4,3,8,7,370,5,4,3,8,7,383,382,381,353,352,5,4,3,8,7,380,379,353,352,5,15,7,352,5,15,7,352,5,4,3,8,7,390,5,4,3,8,7,400,5,4,3,8,7,417,5,4,3,8,7,416,5,4,3,8,7,415,5,4,3,8,7,414,5,4,3,8,7,413,10,7,5,4,3,8,7,422,5,4,3,8,7,421,5,35,34,33,32,420,30,419,65],[266,265,264,263,262,261,364,363,373,372,371,5,15,7,5,4,3,8,7,370,5,4,3,8,7,383,382,381,353,352,5,4,3,8,7,380,379,353,352,5,15,7,352,5,15,7,352,5,4,3,8,7,390,5,4,3,8,7,400,5,4,3,8,7,417,5,4,3,8,7,416,5,4,3,8,7,415,5,4,3,8,7,425,10,7,5,4,3,8,7,424,10,7,5,4,3,8,7,423],[266,265,264,263,262,261,364,363,373,372,371,5,15,7,5,4,3,8,7,370,5,4,3,8,7,383,382,381,353,352,5,4,3,8,7,380,379,353,352,5,15,7,352,5,15,7,352,5,4,3,8,7,390,5,4,3,8,7,400,5,4,3,8,7,417,5,4,3,8,7,416,5,4,3,8,7,415,5,4,3,8,7,425,10,7,5,4,3,8,7,428,10,7,5,35,34,33,32,427,426],[266,265,264,263,262,261,364,363,373,372,371,5,15,7,5,4,3,8,7,370,5,4,3,8,7,383,382,381,353,352,5,4,3,8,7,380,379,353,352,5,15,7,352,5,15,7,352,5,4,3,8,7,390,5,4,3,8,7,400,5,4,3,8,7,433,432,431,430,429],[266,265,264,263,262,261,364,363,444,443,442,441,440,439,438,437,436,355,354,353,352,5,4,3,2,435,434,7],[266,265,264,263,262,261,364,363,444,443,442,441,440,439,438,437,436,355,354,353,352,5,4,3,2,435,434,7],[266,265,264,263,262,261,364,363,444,443,442,441,440,439,438,437,436,355,354,353,352,5,4,3,2,435,434,7],[266,265,264,263,262,261,364,363,444,443,442,441,440,439,438,437,447,446,445,354,353,352,5,4,3,2,63,225],[266,265,264,263,262,261,364,363,444,443,442,441,440,439,438,437,447,446,451,382,450,354,353,449,448],[266,265,264,263,262,261,364,363,444,443,442,441,440,439,438,437,447,446,451,382,450,354,353,352,5,27],[266,265,264,263,262,261,466,443,442,465,464,359,358,357,463,462,461,358,357,356,460,459,458,457,456,358,357,356,455,454,446,445,354,353,352,5,35,34,33,32,453,452],[266,265,264,263,262,261,466,443,442,465,464,359,358,357,463,462,461,358,357,356,460,459,458,457,456,358,357,356,468,454,446,445,467],[266,265,264,263,262,261,466,443,442,465,464,359,358,357,463,462,461,358,357,356,472,459,458,457,471,454,446,445,354,353,352,5,4,88,87,369,368,367,470,469],[266,265,264,263,262,261,466,443,442,465,464,359,358,357,463,462,461,358,357,356,476,459,458,457,475,358,357,356,355,354,353,352,5,4,3,2,474,473],[266,265,264,263,262,261,466,443,442,465,464,359,358,357,463,462,461,358,357,356,479,459,458,457,478,454,446,445,354,353,352,5,4,3,8,7,477],[266,265,264,263,262,261,466,443,442,465,464,359,358,357,463,462,461,358,357,356,483,459,458,457,482,358,357,356,481,358,357,356,355,354,353,352,5,4,3,8,7,480],[266,265,264,263,262,261,466,443,442,465,464,359,358,357,463,462,461,358,357,356,488,459,458,487,486,354,353,352,5,4,3,8,7,485,382,484,354,353,352,5,4,88,87,221],[266,265,264,263,262,261,466,443,442,465,464,359,358,357,463,462,461,358,357,356,492,459,458,457,491,358,357,356,490,358,357,356,355,354,353,352,5,4,3,8,7,489],[266,265,264,263,262,261,466,443,442,465,464,359,358,357,463,462,495,358,357,356,494,359,358,357,356,493],[266,265,264,263,262,261,466,443,442,465,464,359,358,357,463,462,495,358,357,356,494,359,358,357,356,497,358,357,356,355,354,353,352,5,496],[266,265,264,263,262,261,466,443,442,465,464,359,358,357,463,462,495,358,357,356,494,359,358,357,356,497,358,357,356,498],[266,265,264,263,262,261,466,443,442,465,464,359,358,357,463,462,495,358,357,356,500,359,358,357,356,499,454,446,445,354,353,352,5,4,88,87,369,368,367,470],[266,265,264,263,262,261,466,443,442,465,464,359,358,504,502,503,502,503,502,501],[266,265,264,263,262,261],[266,265,264,263,262,261],[266,265,264,263,262],[266,265,264,263,262],[266,265,264,263,262],[266,265,264,263,262],[266,265,264,263,262,261,260,270,269,268,267],[266,265,264,263,262,261,260,270,269,268,267],[266,265,264,263,262,261,260,270,269,268,267],[266,265,264,263,262,261,260,270,269,268,267],[266,265,264,263,262,261,260,270,277,276,275,274,247,246,252,251,250,249],[266,265,264,263,262,261,260,270,277,276,275,274,247,246,252,251,250,249],[266,265,264,263,262,261,292,291,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,264,263,262,261,292,291,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,264,263,262,261,292,291,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,264,263,262,261,302,301,300,299,298,297,296,505],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,506],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,508,507],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,508,509],[266,265,264,263,262,261,302,318,317,316,315,314,512,511,510],[266,265,264,263,262,261,302,318,317,316,315,314,336,335,334,333,332,331,330,329,328,327],[266,265,264,263,262,261,302,318,317,316,315,314,336,335,334,333,332,331,330,329,328,337],[266,265,264,263,262,261,302,318,317,316,315,314,336,335,334,333,332,331,330,329,328,327],[266,265,264,263,262,261,302,318,317,316,315,314,336,335,334,333,332,331,330,329,328,327],[266,265,264,263,262,261,302,318,317,316,315,314,336,335,334,333,332,331,330,329,328,327],[266,265,264,263,262,261,302,318,317,316,315,314,336,516,515,514,513],[266,265,264,263,262,261,519,484,354,353,352,518,108,517],[266,265,264,263,262,261,351,350,349,348,347,346,521,520],[266,265,264,263,262,261,526,525,524,523,502,503,522],[266,265,264,263,262,261,466,443,442,465,464,359,358,357,463,462,495,358,357,356,531,359,348,347,530,529,528,527],[266,265,264,263,262,261,466,443,442,465,464,359,358,357,463,462,495,358,357,356,535,359,358,357,356,534,358,357,356,533,348,347,530,529,532],[266,265,264,263,262,261,466,443,442,465,464,359,358,357,463,462,495,358,357,356,535,359,358,357,356,534,358,357,356,533,348,347,530,529,536,527],[266,265,264,263,262,261,543,542,541,465,440,540,539,538,537],[266,265,264,263,262,261],[266,265,264,263,262],[266,265,264,263,262,261,260,259,258,546,545,544,179],[266,265,264,263,262,261,260,270,269,268,267],[266,265,264,263,262,261,260,270,269,268,267],[266,265,264,263,262,261,260,270,269,268,267],[266,265,264,263,262,261,260,270,277,276,275,274,247,246,252,251,250,547],[266,265,264,263,262,261,260,270,277,276,275,274,247,246,252,251,250,249],[266,265,264,263,262,261,260,270,277,276,275,274,247,246,252,251,250,249],[266,265,264,263,262,261,260,270,277,276,275,274,247,246,252,251,250,249],[266,265,264,263,262,261,260,270,277,276,275,274,247,246,252,251,250,249],[266,265,264,263,262,261,260,270,277],[266,265,264,263,262,261,260,270,277],[266,265,264,263,262,261,292,291,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,264,263,262,261,292,291,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,264,263,262,261,292,291,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,264,263,262,261,292,291,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,264,263,262,261,292,291,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,264,263,262,261,292,291,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,264,263,262,261,292,291,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,264,263,262,261,302,301,300,299,298,297,296,295,294,293],[266,265,264,263,262,261,302,301,300,299,298,297,296,303],[266,265,264,263,262,261,302,301,300,299,298,297,296,303],[266,265,264,263,262,261,302,301,300,299,298,297,296,303],[266,265,264,263,262,261,302,301,300,299,298,297,296,303],[266,265,264,263,262,261,302,301,300,299,298,297,296,303],[266,265,264,263,262,261,302,301,300,299,298,297,296,295,294,293],[266,265,264,263,262,261,302,301,300,299,298,297,296,303],[266,265,264,263,262,261,302,301,300,299,298,297,296,303],[266,265,264,263,262,261,302,301,300,299,298,297,296,303],[266,265,264,263,262,261,302,301,300,299,298,297,296,303],[266,265,264,263,262,261,302,301,300,299,298,297,296,303],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,506],[266,265,264,263,262,261,302,318,317,316,315,314,313,326,325,324,323],[266,265,264,263,262,261,302,318,317,316,315,314,512,511,510],[266,265,264,263,262,261,302,318,317,316,315,314,336,335,334,333,332,331,330,329,328,327],[266,265,264,263,262,261,302,318,317,316,315,314,336,335,334,333,332,331,330,329,328,327],[266,265,264,263,262,261,302,318,317,316,315,314,336,335,334,333,332,331,330,329,328,327],[266,265,264,263,262,261,302,318,317,316,315,314,336,335,334,333,332,331,330,329,328,327],[266,265,264,263,262,261,302,318,317,316,315,314,336,335,334,333,332,331,330,329,328,327],[266,265,264,263,262,261,302,318,317,316,315,314,336,335,334,333,332,331,330,329,328,327],[266,265,264,263,262,261,302,318,317,316,315,314,336,335,334,333,332,331,330,329,328,327],[266,265,264,263,262,261,302,318,317,316,315,314,336,335,334,333,332,331,330,329,328,327],[266,265,264,263,262,261,302,318,317,316,315,314,336,335,334,333,332,331,330,329,328,327],[266,265,264,263,262,261,302,318,317,316,315,314,336,548],[266,265,264,263,262,261,351,350,349,348,347,346,345],[266,265,264,263,262,261,351,350,349,348,347,346,345,344,343,342,341,340,281,280,549],[266,265,264,263,262,261,364,363,362,361,360,359,358,357,356,551,550],[266,265,264,263,262,261,559,443,442,465,558,557,556,555,554,553,539,538,552,537],[266,265,264,263,262,261,559,443,442,465,558,557,562,561,439,438,437,447,560],[266,265,264,263,262,261,466,443,442,465,464,359,358,357,463,462,461,358,357,356,483,459,458,457,482,358,563,560],[266,265,264,263,262,261,466,443,442,465,464,359,358,357,463,462,495,358,357,356,565,359,358,357,356,564,454,560],[266,265,264,263,262,261,466,443,442,465,464,359,358,571,570,569,570,569,570,569,568,567,568,567,566],[266,265,264,263,262,261],[266,265,264,263,262,261],[266,265,264,263,262,261],[266,265,264,263,262],[266,265,264,263,262],[266,265,264,263,262],[266,265,264,263,262,261,260,270,269,268,267],[266,265,264,263,262,261,260,270,277,276,275,274,247,246,252,251,250,249],[266,265,264,263,262,261,260,270,277,276,275,274,247,246,252,251,250,249],[266,265,264,263,262,261,260,270,277,276,275,274,247,246,252,251,250,249],[266,265,264,263,262,261,260,270,277,276,275,274,247,246,252,251,250,249],[266,265,264,263,262,261,260,270,277,276,275,274,247,246,252,251,250,249],[266,265,264,263,262,261,292,291,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,264,263,262,261,292,291,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,264,263,262,261,292,291,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,264,263,262,261,292,291,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,264,263,262,261,302,301,300,299,298,297,296,303],[266,265,264,263,262,261,302,301,300,299,298,297,296,303],[266,265,264,263,262,261,302,301,300,299,298,297,296,295,294,293],[266,265,264,263,262,261,302,301,300,299,298,297,296,303],[266,265,264,263,262,261,302,301,300,299,298,297,296,303],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,506],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,572],[266,265,264,263,262,261,302,318,317,316,315,314,313,326,325,324,323],[266,265,264,263,262,261,302,318,317,316,315,314,512,511,510],[266,265,264,263,262,261,302,318,317,316,315,314,512,511,510],[266,265,264,263,262,261,302,318,317,316,315,314,336,335,334,333,332,331,330,329,328,327],[266,265,264,263,262,261,302,318,317,316,315,314,336,335,334,333,332,331,330,329,328,337],[266,265,264,263,262,261,302,318,317,316,315,314,336,335,334,333,332,331,330,329,328,337],[266,265,264,263,262,261,302,318,317,316,315,314,336,335,334,333,332,331,330,329,328,337],[266,265,264,263,262,261,302,318,317,316,315,314,336,335,334,333,332,331,330,329,328,327],[266,265,264,263,262,261,302,318,317,316,315,314,336,335,334,333,332,331,330,329,328,327],[266,265,264,263,262,261,575,574,350,573,349,348,347,530,529,536,527],[266,265,264,263,262,261,351,350,349,348,347,346,521,520],[266,265,264,263,262,261,364,363,444,443,442,441,579,578,577,576],[266,265,264,263,262,261,364,363,444,443,442,441,579,578,437,580],[266,265,264,263,262,261],[266,265,264,263,262,261],[266,265,264,263,262,261],[266,265,264,263,262],[266,265,264,263,262,261,260,270,277,276,275,274,247,246,252,251,250,249],[266,265,264,263,262,261,260,270,277,276,275,274,247,246,252,251,250,249],[266,265,264,263,262,261,260,270,277,276,275,274,247,246,252,251,250,249],[266,265,264,263,262,261,260,270,277,276,275,274,247,246,252,251,250,249],[266,265,264,263,262,261,260,270,277],[266,265,264,263,262,261,292,291,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,264,263,262,261,292,291,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,264,263,262,261,292,291,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,264,263,262,261,292,291,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,264,263,262,261,292,582,290,289,288,581],[266,265,264,263,262,261,302,301,300,299,298,297,296,295,294,293],[266,265,264,263,262,261,302,301,300,299,298,297,296,295,294,293],[266,265,264,263,262,261,302,301,300,299,298,297,296,303],[266,265,264,263,262,261,302,301,300,299,298,297,296,303],[266,265,264,263,262,261,302,301,300,299,298,297,296,303],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,264,263,262,261,302,318,317,316,315,314,313,326,325,324,323],[266,265,264,263,262,261,302,318,317,316,315,314,336,335,334,333,584,583],[266,265,264,263,262,261,302,318,317,316,315,314,336,335,334,333,332,331,330,329,328,337],[266,265,264,263,262,261,302,318,317,316,315,314,336,335,334,333,332,331,330,329,328,327],[266,265,264,263,262,261,302,318,317,316,315,314,336,335,334,333,332,331,330,329,328,327],[266,265,264,263,262,261,364,363,444,443,442,441,579,587,586,585],[266,265,264,263,262,261,364,363,444,443,442,465,558,557,562,589,588],[266,265,264,263,262,261],[266,265,264,263,262,261],[266,265,264,263,262,261],[266,265,264,263,262],[266,265,264,263,262],[266,265,264,263,262],[266,265,264,263,262],[266,265,264,263,262],[266,265,264,263,262],[266,265,264,263,262],[266,265,264,263,262],[266,265,264,263,262,261,260,270,269,268,267],[266,265,264,263,262,261,260,270,269,268,267],[266,265,264,263,262,261,260,270,269,268,267],[266,265,264,263,262,261,260,270,269,268,267],[266,265,264,263,262,261,260,270,277],[266,265,264,263,262,261,260,270,277],[266,265,264,263,262,261,260,270,277],[266,265,264,263,262,261,292,291,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,264,263,262,261,292,291,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,264,263,262,261,292,291,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,264,263,262,261,292,291,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,264,263,262,261,292,291,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,264,263,262,261,292,291,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,264,263,262,261,292,291,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,264,263,262,261,292,291,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,264,263,262,261,302,301,300,299,298,297,296,295,294,293],[266,265,264,263,262,261,302,301,300,299,298,297,296,303],[266,265,264,263,262,261,302,301,300,299,298,297,296,303],[266,265,264,263,262,261,302,301,300,299,298,297,296,303],[266,265,264,263,262,261,302,301,300,299,298,297,296,295,294,293],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,319,294,293],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,319,294,293],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,319,294,293],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,591,590],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,592],[266,265,264,263,262,261,302,318,317,316,315,314,313,326,325,324,323],[266,265,264,263,262,261,302,318,317,316,315,314,512,511,510],[266,265,264,263,262,261,302,318,317,316,315,314,512,511,510],[266,265,264,263,262,261,302,318,317,316,315,314,336,335,334,333,332,331,330,329,328,327],[266,265,264,263,262,261,302,318,317,316,315,314,336,335,334,333,332,331,330,329,328,327],[266,265,264,263,262,261,302,318,317,316,315,314,336,335,334,333,332,331,330,329,328,327],[266,265,264,263,262,261,302,318,317,316,315,314,336,335,334,333,332,331,330,329,328,337],[266,265,264,263,262,261,302,318,317,316,315,314,336,335,334,333,332,331,330,329,328,327],[266,265,264,263,262,261,302,318,317,316,315,314,336,335,334,333,332,331,330,329,328,327],[266,265,264,263,262,261,302,318,317,316,315,314,336,335,334,333,332,331,330,329,328,327],[266,265,264,263,262,261,302,318,317,316,315,314,336,335,334,333,332,331,330,329,328,327],[266,265,264,263,262,261,597,596,595,594,593],[266,265,264,263,262,261,364,363,444,443,442,441,579,578,577,576,539,599,598],[266,265,264,263,262,261,364,363,444,443,442,441,573,349,348,604,603,602,601,600,437,447,560],[266,265,264,263,262,261,559,443,442,441,605],[266,265,264,263,262,261,466,443,442,465,464,359,358,357,463,462,608,607,502,503,502,503,606],[266,265,264,263,262,261,466,443,442,465,464,359,358,357,463,462,461,358,357,356,614,459,458,457,613,348,347,612,611,536,610,609],[266,265,264,263,262,261,466,443,442,465,464,359,358,504,502,503,502,503,502,616,502,503,615],[266,265,264,263,262,261],[266,265,264,263,262,261],[266,265,264,263,262],[266,265,264,263,262],[266,265,264,263,262,261,260,270,269,268,267],[266,265,264,263,262,261,260,270,277,276,275,274,247,246,252,251,250,249],[266,265,264,263,262,261,260,270,277,276,275,274,247,246,252,251,250,249],[266,265,264,263,262,261,260,270,277,276,275,274,247,246,252,251,250,249],[266,265,264,263,262,261,260,270,277,276,275,274,247,246,252,251,250,249],[266,265,264,263,262,261,260,270,277],[266,265,264,263,262,261,260,270,277],[266,265,264,263,262,261,292,291,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,264,263,262,261,292,291,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,264,263,262,261,302,301,300,299,298,297,296,303],[266,265,264,263,262,261,302,301,300,299,298,297,296,303],[266,265,264,263,262,261,302,301,300,299,298,297,296,303],[266,265,264,263,262,261,302,301,300,299,298,297,296,303],[266,265,264,263,262,261,302,301,300,299,298,297,296,303],[266,265,264,263,262,261,302,301,300,299,298,297,296,303],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,319,294,293],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,264,263,262,261,302,318,317,316,315,314,313,326,325],[266,265,264,263,262,261,302,318,317,316,315,314,336,335,334,333,584,583],[266,265,264,263,262,261,302,318,317,316,315,314,336,335,334,333,332,331,330,329,328,327],[266,265,264,263,262,261,302,318,317,316,315,314,336,335,334,333,332,331,330,329,328,337],[266,265,264,263,262,261,302,318,317,316,315,314,336,335,334,333,332,331,330,329,328,327],[266,265,264,263,262,261,302,318,317,316,315,314,336,335,334,333,332,331,330,329,328,327],[266,265,264,263,262,261,302,318,317,316,315,314,336,335,334,333,332,331,330,329,328,327],[266,265,264,263,262,261,302,318,317,316,315,314,336,335,334,333,332,331,330,329,328,327],[266,265,264,263,262,261,302,318,317,316,315,314,336,335,334,333,332,331,330,329,328,327],[266,265,264,263,262,261,302,318,317,316,315,314,336,335,334,333,332,331,330,329,328,327],[266,265,264,263,262,261,302,318,317,316,315,314,336,335,334,333,332,331,330,329,328,327],[266,265,264,263,262,261,302,318,317,316,315,314,336,335,334,333,332,331,330,329,328,327],[266,265,264,263,262,261,302,318,317,316,315,314,336,548],[266,265,264,263,262,261,302,318,317,316,315,314,336,548],[266,265,264,263,262,261,575,574,350,440,438,436,619,618,617,550],[266,265,264,263,262,261,364,624,465,558,557,556,555,623,622,439,438,577,621,620],[266,265,264,263,262,261],[266,265,264,263,262,261],[266,265,264,263,262,261],[266,265,264,263,262,261],[266,265,264,263,262,261],[266,265,264,263,262,261],[266,265,264,263,262],[266,265,264,263,262,261,260,270,269,268,628,627,626,625],[266,265,264,263,262,261,260,270,269,268,267],[266,265,264,263,262,261,260,270,269,268,267],[266,265,264,263,262,261,292,291,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,264,263,262,261,292,291,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,264,263,262,261,292,291,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,264,263,262,261,292,291,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,264,263,262,261,292,291,290,289,288,632,631,630,629],[266,265,264,263,262,261,302,301,300,299,298,297,296,303],[266,265,264,263,262,261,302,301,300,299,298,297,296,303],[266,265,264,263,262,261,302,301,300,299,298,297,296,303],[266,265,264,263,262,261,302,301,300,299,298,297,296,295,294,293],[266,265,264,263,262,261,302,301,300,299,298,297,296,303],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,592],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,592],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,508,509],[266,265,264,263,262,261,302,318,317,316,315,314,313,326,325,324,323],[266,265,264,263,262,261,302,318,317,316,315,314,512,511,510],[266,265,264,263,262,261,302,318,317,316,315,314,336,335,334,333,332,331,330,329,328,327],[266,265,264,263,262,261,302,318,317,316,315,314,336,335,334,333,332,331,330,329,328,327],[266,265,264,263,262,261,302,318,317,316,315,314,336,335,334,333,332,331,330,329,328,327],[266,265,264,263,262,261,302,318,317,316,315,314,336,335,334,333,332,331,330,329,328,327],[266,265,264,263,262,261,302,318,317,316,315,314,336,335,334,333,332,331,330,329,328,327],[266,265,264,263,262,261,302,318,317,316,315,314,336,335,334,333,332,331,330,329,328,327],[266,265,264,263,262,261,364,363,444,443,442,465,558,557,562,634,437,633],[266,265,264,263,262,261,466,443,442,465,464,359,358,357,635,607,502,503,502,503,502,616,502,503],[266,265,264,263,262,261,466,443,442,465,464,359,358,357,463,462,461,358,357,356,639,459,458,457,638,358,357,356,637,358,357,356,636],[266,265,264,263,262,261,466,443,442,465,464,359,358,357,463,462,495,358,357,356,643,359,358,357,356,642,358,357,356,641,348,347,530,529,640,610],[266,265,264,263,262,261,466,443,442,465,464,359,358,357,463,462,495,358,357,356,643,359,358,357,356,645,358,357,356,644,348,347,530,529,640,527],[266,265,264,263,262,261],[266,265,264,263,262,261],[266,265,264,263,262,261],[266,265,264,263,262,261],[266,265,264,263,262,261],[266,265,264,263,262],[266,265,264,263,262],[266,265,264,263,262],[266,265,264,263,262,261,260,270,269,268,267],[266,265,264,263,262,261,260,270,269,268,267],[266,265,264,263,262,261,260,270,277,276,275,274,247,246,252,251,250,249],[266,265,264,263,262,261,260,270,277,276,275,274,247,246,252,251,250,249],[266,265,264,263,262,261,260,270,277],[266,265,264,263,262,261,260,270,277],[266,265,264,263,262,261,292,291,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,264,263,262,261,292,291,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,264,263,262,261,292,291,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,264,263,262,261,292,291,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,264,263,262,261,292,291,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,264,263,262,261,292,291,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,264,263,262,261,302,301,300,299,298,297,296,303],[266,265,264,263,262,261,302,301,300,299,298,297,296,303],[266,265,264,263,262,261,302,301,300,299,298,297,296,295,294,293],[266,265,264,263,262,261,302,301,300,299,298,297,296,295,294,293],[266,265,264,263,262,261,302,301,300,299,298,297,296,295,294,293],[266,265,264,263,262,261,302,301,300,299,298,297,296,303],[266,265,264,263,262,261,302,301,300,299,298,297,296,303],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,319,294,293],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,319,294,293],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,506],[266,265,264,263,262,261,302,318,317,316,315,314,313,326,325],[266,265,264,263,262,261,302,318,317,316,315,314,313,326,325],[266,265,264,263,262,261,302,318,317,316,315,314,336,335,334,333,584,646],[266,265,264,263,262,261,302,318,317,316,315,314,336,335,334,333,332,331,330,329,328,327],[266,265,264,263,262,261,302,318,317,316,315,314,336,335,334,333,332,331,330,329,328,327],[266,265,264,263,262,261,302,318,317,316,315,314,336,335,334,333,332,331,330,329,328,327],[266,265,264,263,262,261,302,318,317,316,315,314,336,335,334,333,332,331,330,329,328,327],[266,265,264,263,262,261,302,318,317,316,315,314,336,335,334,333,332,331,330,329,328,327],[266,265,264,263,262,261,302,318,317,316,315,314,336,335,334,333,332,331,330,329,328,327],[266,265,264,263,262,261,302,318,317,316,315,314,336,335,334,333,332,647],[266,265,264,263,262,261,575,574,350,579,587,651,650,649,648],[266,265,264,263,262,261,526,525,654,653,652,348,347,346,520],[266,265,264,263,262,261,526,525,654,653,655,348,347,346,520],[266,265,264,263,262,261,526,525,654,653,657,348,347,656,527],[266,265,264,263,262,261,526,525,524,523,502,503,522],[266,265,264,263,262,261,466,443,442,465,464,359,358,504,502,503,502,503,502,501],[266,265,264,263,262,261,543,542,541,465,558,557,556,555,623,659,658],[266,265,264,263,262,261,543,542,541,465,558,557,556,555,623,660,539,651],[266,265,264,263,262,261],[266,265,264,263,262,261],[266,265,264,263,262,261],[266,265,264,263,262,261],[266,265,264,263,262],[266,265,264,263,262],[266,265,264,263,262],[266,265,264,263,262],[266,265,264,263,262,261,260,270,269,268,267],[266,265,264,263,262,261,260,270,269,268,267],[266,265,264,263,262,261,260,270,269,268,267],[266,265,264,263,262,261,260,270,277],[266,265,264,263,262,261,260,270,277],[266,265,264,263,262,261,292,291,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,264,263,262,261,292,291,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,264,263,262,261,302,301,300,299,298,297,296,303],[266,265,264,263,262,261,302,301,300,299,298,297,296,303],[266,265,264,263,262,261,302,301,300,299,298,297,296,303],[266,265,264,263,262,261,302,301,300,299,298,297,296,295,294,293],[266,265,264,263,262,261,302,301,300,299,298,297,296,303],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,319,294,293],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,319,294,293],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,264,263,262,261,302,318,317,316,315,314,313,312,311,508,509],[266,265,264,263,262,261,302,318,317,316,315,314,313,326,325,324,323],[266,265,264,263,262,261,302,318,317,316,315,314,336,335,334,333,584,583],[266,265,264,263,262,261,302,318,317,316,315,314,336,335,334,333,332,331,330,329,328,327],[266,265,264,263,262,261,302,318,317,316,315,314,336,335,334,333,332,331,330,329,328,327],[266,265,264,263,262,261,302,318,317,316,315,314,336,335,334,333,332,331,330,329,328,327],[266,265,264,263,262,261,351,350,349,348,347,346,345,344,343,342,341,340,281,280,549],[266,265,264,263,262,261,526,525,654,653,655,348,347,346,520],[266,265,264,263,262,261,526,525,524,523,502,503,522],[266,265,264,263,262,261,466,443,442,465,464,359,358,357,635,607,502,503,502,503,606],[266,265,264,263,262,261,466,443,442,465,464,359,358,357,463,462,495,358,357,356,643,359,358,357,356,642,358,357,356,661],[266,265,264,263,262,261,543,542,666,665,664,663,662],[266,265,264,263,262,261,543,542,666,665,668,667],[266,265,264,263,262,261,543,542,669,465,440,438,436],[266,265,264,263,262,261],[266,265,264,263,262,261],[266,265,264,263,262,261],[266,265,264,263,262],[266,265,679,263,678,677,676,675,674,673,447,446,672,353,352,185,671,670],[266,265,679,263,678,677,260,270,269,268,267],[266,265,679,263,678,677,260,270,269,268,267],[266,265,679,263,678,677,260,270,269,268,267],[266,265,679,263,678,677,260,270,269,268,267],[266,265,679,263,678,677,260,270,269,268,267],[266,265,679,263,678,677,260,270,269,268,267],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,251,250,249],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,251,250,249],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,681,680],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,685],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,685],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,685],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,685],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,685],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,685],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,685],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,685],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,685],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,685],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252],[266,265,679,263,678,677,260,270,277],[266,265,679,263,678,677,260,270,277],[266,265,679,263,678,677,292,291,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,679,263,678,677,292,291,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,679,263,678,677,292,291,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,679,263,678,677,292,291,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,679,263,678,677,292,582,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,679,263,678,677,292,582,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,679,263,678,677,302,301,300,299,298,297,296,295,294,293],[266,265,679,263,678,677,302,301,300,299,298,297,296,295,294,293],[266,265,679,263,678,677,302,301,300,299,298,297,296,295,294,293],[266,265,679,263,678,677,302,301,300,299,298,297,296,295,294,293],[266,265,679,263,678,677,302,301,300,299,298,297,296,295,294,293],[266,265,679,263,678,677,302,301,300,299,298,297,296,295,294,293],[266,265,679,263,678,677,302,301,300,299,298,297,296,303],[266,265,679,263,678,677,302,301,300,299,298,297,296,303],[266,265,679,263,678,677,302,301,300,299,298,297,296,295,294,293],[266,265,679,263,678,677,302,301,300,299,298,297,296,303],[266,265,679,263,678,677,302,301,300,299,298,297,296,303],[266,265,679,263,678,677,302,301,300,299,298,297,296,303],[266,265,679,263,678,677,302,301,300,299,298,297,296,303],[266,265,679,263,678,677,302,301,300,299,298,297,296,303],[266,265,679,263,678,677,302,301,300,299,298,297,296,303],[266,265,679,263,678,677,302,301,300,299,298,297,296,303],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,508,509],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,508,509],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,508,509],[266,265,679,263,678,677,302,318,317,316,315,314,313,326,325,324,323],[266,265,679,263,678,677,302,318,317,316,315,314,313,326,325,324,323],[266,265,679,263,678,677,302,318,317,316,315,314,512,511,510],[266,265,679,263,678,677,302,318,317,316,315,314,512,511,510],[266,265,679,263,678,677,302,318,317,316,315,314,512,511,510],[266,265,679,263,678,677,302,318,317,316,315,314,512,511,510],[266,265,679,263,678,677,302,318,317,316,315,314,336,335,334,333,584,583],[266,265,679,263,678,677,302,318,317,316,315,314,336,335,334,333,332,331,330,329,328,327],[266,265,679,263,678,677,302,318,317,316,315,314,336,335,334,333,332,331,330,329,328,327],[266,265,679,263,678,677,687,686,438,447,446,445,354,353,352,5,4,88,87,221],[266,265,679,263,678,677,351,350,349,348,347,689,688,344,343,342,341,340,281,280,279,278],[266,265,679,263,678,677,351,350,349,348,347,346,521,690],[266,265,679,263,678,677,351,350,349,348,347,689,688,344,343,342,341,340,281,280,279,278],[266,265,679,263,678,677,575,574,350,440,438,691],[266,265,679,263,678,677,351,350,349,348,347,689,688],[266,265,679,263,678,677,351,350,349,348,347,346,345,344,343,342,341,340,281,280,279,278],[266,265,679,263,678,677,351,350,349,348,347,346,345,344,343,342,341,340,281,280,279,278],[266,265,679,263,678,677,351,350,349,348,347,346,345,344,343,342,341,340,281,280,279,278],[266,265,679,263,678,677,351,350,349,348,347,689,688,344,343,342,341,340,281,280,279,278],[266,265,679,263,678,677,351,350,349,348,347,689,688],[266,265,679,263,678,677,351,350,349,348,347,346,345,344,343,342,341,340,281,280,279,278],[266,265,679,263,678,677,351,350,349,348,347,346,345,344,343,342,341,340,281,280,279,278],[266,265,679,263,678,677,364,363,373,372,697,696,695,694,693,359,692,607,502,503,502,503,502,616,502,503,502],[266,265,679,263,678,677,364,363,444,443,442,441,440,439,438,437,436,698],[266,265,679,263,678,677,526,525,524,523,502,503,522,699],[266,265,679,263,678,677,526,525,524,523,502,503,522],[266,265,679,263,678,677,526,525,524,523,502,503,522,700],[266,265,679,263,678,677,526,525,524,523,502,503,522,701],[266,265,679,263,678,677,526,525,524,523,502,503,522,702],[266,265,679,263,678,677,526,525,524,523,502,503,522,703],[266,265,679,263,678,677,526,525,524,523,502,503,522],[266,265,679,263,678,677,526,525,524,523,502,503,522,703],[266,265,679,263,678,677,526,525,524,523,502,503,522,703],[266,265,679,263,678,677,526,525,524,523,502,503,522],[266,265,679,263,678,677,526,525,524,523,502,503,522],[266,265,679,263,678,677,526,525,524,523,502,503,522,704],[266,265,679,263,678,677,526,525,524,523,502,503,522,703],[266,265,679,263,678,677,526,525,524,523,502,503,522,502],[266,265,679,263,678,677,526,525,524,523,502,503,522,705],[266,265,679,263,678,677,526,525,524,523,502,503,522],[266,265,679,263,678,677,526,525,524,523,502,503,522,702],[266,265,679,263,678,677,526,525,524,523,502,503,522,703],[266,265,679,263,678,677,526,525,524,523,502,503,522,702],[266,265,679,263,678,677,526,525,524,523,502,503,522,703],[266,265,679,263,678,677,526,525,524,523,502,503,522,502],[266,265,679,263,678,677,526,525,524,523,502,503,522,702],[266,265,679,263,678,677,526,525,524,523,502,503,522,705],[266,265,679,263,678,677,526,525,524,523,502,503,522],[266,265,679,263,678,677,526,525,524,523,502,503,522,703],[266,265,679,263,678,677,526,525,524,523,502,503,522,699],[266,265,679,263,678,677,526,525,524,523,502,503,522,706],[266,265,679,263,678,677,526,525,524,523,502,503,522,702],[266,265,679,263,678,677,526,525,524,523,502,503,522,702],[266,265,679,263,678,677,526,525,524,523,502,503,522],[266,265,679,263,678,677,526,525,524,523,502,503,522,699],[266,265,679,263,678,677,526,525,524,523,502,503,522,703],[266,265,679,263,678,677,526,525,524,523,502,503,522],[266,265,679,263,678,677,526,525,524,523,502,503,522,703],[266,265,679,263,678,677,526,525,524,523,502,503,522],[266,265,679,263,678,677,526,525,524,523,502,503,522,703],[266,265,679,263,678,677,466,443,442,465,464,359,358,357,463,462,461,358,357,356,479,459,458,457,478,348,347,708,707,536,610,609],[266,265,679,263,678,677],[266,265,679,263,678,677],[266,265,679,263,678,677,260,270,269,268,267],[266,265,679,263,678,677,260,270,269,268,267],[266,265,679,263,678,677,260,270,269,268,267],[266,265,679,263,678,677,260,270,269,268,267],[266,265,679,263,678,677,260,270,269,268,267],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,251,250,547],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,251,250,249],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,251,250,249],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,251,250,249],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,681,680],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,681,680],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,681,680],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,681,680],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,681,680],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,681,680],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,681,680],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,681,680],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,681,680],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,685],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,685],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,685],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,685],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,685],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,685],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,685],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,685],[266,265,679,263,678,677,260,270,277],[266,265,679,263,678,677,260,270,277],[266,265,679,263,678,677,292,291,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,679,263,678,677,292,291,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,679,263,678,677,292,291,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,679,263,678,677,292,291,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,679,263,678,677,292,291,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,679,263,678,677,292,291,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,679,263,678,677,292,291,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,679,263,678,677,292,582,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,679,263,678,677,292,582,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,679,263,678,677,292,582,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,679,263,678,677,292,582,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,679,263,678,677,292,582,290,289,288,287,286,285,284,283,282,281,339,710,709],[266,265,679,263,678,677,302,301,300,299,298,297,296,295,294,293],[266,265,679,263,678,677,302,301,300,299,298,297,296,303],[266,265,679,263,678,677,302,301,300,299,298,297,296,303],[266,265,679,263,678,677,302,301,300,299,298,297,296,303],[266,265,679,263,678,677,302,301,300,299,298,297,296,303],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,319,294,293],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,319,294,293],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,319,294,293],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,319,294,293],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,319,294,293],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,679,263,678,677,302,318,317,316,315,314,313,326,325,324,323],[266,265,679,263,678,677,302,318,317,316,315,314,313,326,325,324,323],[266,265,679,263,678,677,302,318,317,316,315,314,313,326,325,324,323],[266,265,679,263,678,677,302,318,317,316,315,314,336,335,334,333,332,331,330,329,328,337],[266,265,679,263,678,677,302,318,317,316,315,314,336,335,334,333,332,331,330,329,328,337],[266,265,679,263,678,677,302,318,317,316,315,314,336,335,334,333,332,331,330,329,328,337],[266,265,679,263,678,677,302,318,317,316,315,314,336,335,334,333,332,331,330,329,328,327],[266,265,679,263,678,677,302,318,317,316,315,314,336,335,334,333,332,331,330,329,328,327],[266,265,679,263,678,677,302,318,317,316,315,314,336,335,334,333,332,331,330,329,328,327],[266,265,679,263,678,677,351,350,349,348,347,689,688,344,343,342,341,340,281,280,279,278],[266,265,679,263,678,677,351,350,349,348,347,689,688,344,343,342,341,340,281,280,279,278],[266,265,679,263,678,677,351,350,349,348,347,346,345,344,343,342,341,340,281,280,279,278],[266,265,679,263,678,677,526,525,654,653,713,358,357,356,712,358,357,711,694],[266,265,679,263,678,677,526,525,654,653,714,348,347,346,690],[266,265,679,263,678,677,526,525,524,523,502,503,522,702],[266,265,679,263,678,677,526,525,524,523,502,503,522,699],[266,265,679,263,678,677,526,525,524,523,502,503,522,704],[266,265,679,263,678,677,526,525,524,523,502,503,522,704],[266,265,679,263,678,677,526,525,524,523,502,503,522,704],[266,265,679,263,678,677,526,525,524,523,502,503,522,699],[266,265,679,263,678,677,526,525,524,523,502,503,522,706],[266,265,679,263,678,677,526,525,524,523,502,503,522],[266,265,679,263,678,677,526,525,524,523,502,503,522,502],[266,265,679,263,678,677,526,525,524,523,502,503,522,502,715],[266,265,679,263,678,677,526,525,524,523,502,503,522,716],[266,265,679,263,678,677,526,525,524,523,502,503,522,703],[266,265,679,263,678,677,526,525,524,523,502,503,522,502],[266,265,679,263,678,677,526,525,524,523,502,503,522,703],[266,265,679,263,678,677,526,525,524,523,502,503,522,701],[266,265,679,263,678,677,526,525,524,523,502,503,522,702],[266,265,679,263,678,677,526,525,524,523,502,503,522,703],[266,265,679,263,678,677,526,525,524,523,502,503,522,699],[266,265,679,263,678,677,526,525,524,523,502,503,522],[266,265,679,263,678,677,526,525,524,523,502,503,522,502],[266,265,679,263,678,677,526,525,524,523,502,503,522],[266,265,679,263,678,677,526,525,524,523,502,503,522],[266,265,679,263,678,677,526,525,524,523,502,503,522,702],[266,265,679,263,678,677,526,525,524,523,502,503,522,702],[266,265,679,263,678,677,526,525,524,523,502,503,522,700],[266,265,679,263,678,677,526,525,524,523,502,503,522,502],[266,265,679,263,678,677,526,525,524,523,502,503,522,706],[266,265,679,263,678,677,526,525,524,523,502,503,522,700],[266,265,679,263,678,677,526,525,524,523,502,503,522],[266,265,679,263,678,677,526,525,524,523,502,503,522,502],[266,265,679,263,678,677,526,525,524,523,502,503,522,703],[266,265,679,263,678,677,526,525,524,523,502,503,522],[266,265,679,263,678,677,526,525,524,523,502,503,522,502],[266,265,679,263,678,677,526,525,717],[266,265,679,263,678,677,526,525,717],[266,265,679,263,678,677,466,443,442,465,464,359,358,357,463,462,461,358,357,356,721,459,720,719,502,503,502,616,502,718],[266,265,679,263,678,677],[266,265,679,263,678,677],[266,265,679,263,678,677],[266,265,679,263,678],[266,265,679,263,678],[266,265,679,263,678,677,260,270,269,268,628,723,722],[266,265,679,263,678,677,260,270,269,268,267],[266,265,679,263,678,677,260,270,269,268,267],[266,265,679,263,678,677,260,270,269,268,267],[266,265,679,263,678,677,260,270,269,268,267],[266,265,679,263,678,677,260,270,269,268,267],[266,265,679,263,678,677,260,270,269,268,267],[266,265,679,263,678,677,260,270,269,268,267],[266,265,679,263,678,677,260,270,269,268,267,724],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,251,250,249],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,681,680],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,681,680],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,685],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,685],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,685],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,685],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,685],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252],[266,265,679,263,678,677,260,270,277],[266,265,679,263,678,677,260,270,277],[266,265,679,263,678,677,292,291,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,679,263,678,677,292,582,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,679,263,678,677,302,301,300,299,298,297,296,295,294,293],[266,265,679,263,678,677,302,301,300,299,298,297,296,303],[266,265,679,263,678,677,302,301,300,299,298,297,296,303],[266,265,679,263,678,677,302,301,300,299,298,297,296,303],[266,265,679,263,678,677,302,301,300,299,298,297,296,303],[266,265,679,263,678,677,302,301,300,299,298,297,296,303],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,679,263,678,677,302,318,317,316,315,314,512,511,510],[266,265,679,263,678,677,302,318,317,316,315,314,336,335,334,333,584,583],[266,265,679,263,678,677,302,318,317,316,315,314,336,335,334,333,332,331,330,329,328,327],[266,265,679,263,678,677,727,484,354,353,352,518,108,726,725],[266,265,679,263,678,677,730,729,349,454,446,728],[266,265,679,263,678,677,351,350,349,348,347,689,688,344,343,342,341,340,281,280,279,278],[266,265,679,263,678,677,351,350,349,348,347,689,688,344,343,342,341,340,281,280,279,278],[266,265,679,263,678,677,351,350,349,348,347,346,345,344,343,342,341,340,281,280,279,278],[266,265,679,263,678,677,351,350,349,348,347,346,345],[266,265,679,263,678,677,351,350,349,348,347,346,521,732,379,353,352,185,671,726,731],[266,265,679,263,678,677,351,350,349,348,347,689,688,344,343,342,341,340,281,280,279,278],[266,265,679,263,678,677,351,350,349,348,347,689,688,344,343,342,341,340,281,280,279,278],[266,265,679,263,678,677,351,350,349,348,347,346,345,344,343,342,341,340,281,280,279,278],[266,265,679,263,678,677,351,350,349,348,347,689,688,344,343,342,341,340,281,280,279,278],[266,265,679,263,678,677,351,350,349,348,347,346,345,344,343,342,341,340,281,280,279,278],[266,265,679,263,678,677,351,350,349,348,347,346,345,344,343,342,341,340,281,280,279,278],[266,265,679,263,678,677,351,350,349,348,347,689,688,344,343,342,341,340,281,280,279,278],[266,265,679,263,678,677,364,363,362,361,738,737,736,357,356,735,348,347,734,733],[266,265,679,263,678,677,364,363,742,741,740,577,576,739],[266,265,679,263,678,677,526,525,524,523,502,503,522,502],[266,265,679,263,678,677,526,525,524,523,502,503,522],[266,265,679,263,678,677,526,525,524,523,502,503,522,716],[266,265,679,263,678,677,526,525,524,523,502,503,522,716],[266,265,679,263,678,677,526,525,524,523,502,503,522,703],[266,265,679,263,678,677,526,525,524,523,502,503,522,701],[266,265,679,263,678,677,526,525,524,523,502,503,522,705],[266,265,679,263,678,677,526,525,524,523,502,503,522,702],[266,265,679,263,678,677,526,525,524,523,502,503,522,502,715],[266,265,679,263,678,677,526,525,524,523,502,503,522,702],[266,265,679,263,678,677,526,525,524,523,502,503,522],[266,265,679,263,678,677,526,525,524,523,502,503,522,702],[266,265,679,263,678,677,526,525,524,523,502,503,522,703],[266,265,679,263,678,677,526,525,524,523,502,503,522,704],[266,265,679,263,678,677,526,525,524,523,502,503,522,705],[266,265,679,263,678,677,526,525,524,523,502,503,522],[266,265,679,263,678,677,526,525,524,523,502,503,522,702],[266,265,679,263,678,677,526,525,524,523,502,503,522,703],[266,265,679,263,678,677,526,525,524,523,502,503,522,502],[266,265,679,263,678,677,526,525,524,523,502,503,522,699],[266,265,679,263,678,677,526,525,524,523,502,503,522,699],[266,265,679,263,678,677,526,525,524,523,502,503,522,502],[266,265,679,263,678,677,526,525,524,523,502,503,522,502],[266,265,679,263,678,677,526,525,524,523,502,503,522,502,743],[266,265,679,263,678,677,526,525,524,523,502,503,522,502],[266,265,679,263,678,677,526,525,524,523,502,503,522,704],[266,265,679,263,678,677,526,525,524,523,502,503,522,704],[266,265,679,263,678,677,526,525,524,523,502,503,522,703],[266,265,679,263,678,677,526,525,524,523,502,503,522,705],[266,265,679,263,678,677,526,525,524,523,502,503,522,702],[266,265,679,263,678,677,526,525,524,523,502,503,522],[266,265,679,263,678,677,559,443,442,465,558,557,562,634,437,744,447,446],[266,265,679,263,678,677,466,443,442,465,464,359,358,357,463,462,461,358,357,356,748,459,458,457,747,358,357,356,746,358,357,356,745,454,560],[266,265,679,263,678,677,466,443,442,465,464,359,358,571,570,569,570,569,568,567,570,569,568,567,568,567,749],[266,265,679,263,678,677],[266,265,679,263,678,677],[266,265,679,263,678,677],[266,265,679,263,678,677],[266,265,679,263,678,677],[266,265,679,263,678,677],[266,265,679,263,678,677],[266,265,679,263,678,677],[266,265,679,263,678],[266,265,679,263,678],[266,265,679,263,678],[266,265,679,263,678],[266,265,679,263,678],[266,265,679,263,678],[266,265,679,263,678],[266,265,679,263,678],[266,265,679,263,678,677,260,270,269,268,267],[266,265,679,263,678,677,260,270,269,268,267],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,251,250,249],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,251,250,547],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,251,250,547],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,251,250,249],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,681,680],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,681,680],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,681,680],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,681,680],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,681,680],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,681,680],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,681,680],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,681,680],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,685],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,685],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,685],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,685],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,685],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,685],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,685],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,685],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,685],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,685],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,685],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,685],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252],[266,265,679,263,678,677,292,291,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,679,263,678,677,292,291,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,679,263,678,677,292,291,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,679,263,678,677,292,291,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,679,263,678,677,292,291,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,679,263,678,677,292,291,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,679,263,678,677,292,291,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,679,263,678,677,292,291,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,679,263,678,677,292,291,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,679,263,678,677,292,582,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,679,263,678,677,292,582,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,679,263,678,677,292,582,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,679,263,678,677,292,582,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,679,263,678,677,292,582,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,679,263,678,677,302,301,300,299,298,297,296,303],[266,265,679,263,678,677,302,301,300,299,298,297,296,303],[266,265,679,263,678,677,302,301,300,299,298,297,296,303],[266,265,679,263,678,677,302,301,300,299,298,297,296,303],[266,265,679,263,678,677,302,301,300,299,298,297,296,303],[266,265,679,263,678,677,302,301,300,299,298,297,296,295,294,293],[266,265,679,263,678,677,302,301,300,299,298,297,296,295,294,293],[266,265,679,263,678,677,302,301,300,299,298,297,296,303],[266,265,679,263,678,677,302,301,300,299,298,297,296,303],[266,265,679,263,678,677,302,301,300,299,298,297,296,303],[266,265,679,263,678,677,302,301,300,299,298,297,296,303],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,319,294,293],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,319,294,293],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,322],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,508,509],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,508,509],[266,265,679,263,678,677,302,318,317,316,315,314,512,511,510],[266,265,679,263,678,677,302,318,317,316,315,314,336,335,334,333,332,331,330,329,328,337],[266,265,679,263,678,677,302,318,317,316,315,314,336,335,334,333,332,331,330,329,328,327],[266,265,679,263,678,677,302,318,317,316,315,314,336,335,334,333,332,331,330,329,328,327],[266,265,679,263,678,677,302,318,317,316,315,314,336,335,334,333,332,331,330,329,328,327],[266,265,679,263,678,677,302,318,317,316,315,314,336,335,334,333,332,331,330,329,328,327],[266,265,679,263,678,677,302,318,317,316,315,314,336,335,334,333,332,331,330,329,328,327],[266,265,679,263,678,677,351,350,349,348,347,689,688,344,343,342,341,340,281,280,279,278],[266,265,679,263,678,677,351,350,349,348,347,346,345,344,343,342,341,340,281,280,279,278],[266,265,679,263,678,677,351,350,349,348,347,346,345,344,343,342,341,340,281,280,279,278],[266,265,679,263,678,677,351,350,349,348,347,689,688,344,343,342,341,340,281,280,279,278],[266,265,679,263,678,677,351,350,349,348,347,689,688,344,343,342,341,340,281,280,279,278],[266,265,679,263,678,677,750,729,349,454,560],[266,265,679,263,678,677,351,350,349,348,347,346,345,344,343,342,341,340,281,280,279,278],[266,265,679,263,678,677,364,363,444,443,442,441,440,439,438,577,576,539,599,751],[266,265,679,263,678,677,364,363,444,443,442,441,579,587,752],[266,265,679,263,678,677,526,525,654,653,753,348,347,656,610],[266,265,679,263,678,677,526,525,524,523,502,503,522,699],[266,265,679,263,678,677,526,525,524,523,502,503,522,701],[266,265,679,263,678,677,526,525,524,523,502,503,522,705],[266,265,679,263,678,677,526,525,524,523,502,503,522,702],[266,265,679,263,678,677,526,525,524,523,502,503,522,703],[266,265,679,263,678,677,526,525,524,523,502,503,522,704],[266,265,679,263,678,677,526,525,524,523,502,503,522,502],[266,265,679,263,678,677,526,525,524,523,502,503,522,699],[266,265,679,263,678,677,526,525,524,523,502,503,522,502],[266,265,679,263,678,677,526,525,524,523,502,503,522,699],[266,265,679,263,678,677,526,525,524,523,502,503,522,700],[266,265,679,263,678,677,526,525,524,523,502,503,522,705],[266,265,679,263,678,677,526,525,524,523,502,503,522,703],[266,265,679,263,678,677,526,525,524,523,502,503,522,701],[266,265,679,263,678,677,526,525,524,523,502,503,522,699],[266,265,679,263,678,677,526,525,524,523,502,503,522],[266,265,679,263,678,677,526,525,524,523,502,503,522,705],[266,265,679,263,678,677,526,525,524,523,502,503,522,703],[266,265,679,263,678,677,526,525,524,523,502,503,522,502],[266,265,679,263,678,677,526,525,524,523,502,503,522,702],[266,265,679,263,678,677,526,525,524,523,502,503,522,699],[266,265,679,263,678,677,526,525,524,523,502,503,522,701],[266,265,679,263,678,677,526,525,524,523,502,503,522,702],[266,265,679,263,678,677,526,525,524,523,502,503,522,705],[266,265,679,263,678,677,526,525,524,523,502,503,522,699],[266,265,679,263,678,677,526,525,717],[266,265,679,263,678,677],[266,265,679,263,678,677],[266,265,679,263,678],[266,265,679,263,678],[266,265,679,263,678,677,260,270,269,268,267],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,251,250,249],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,251,250,249],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,251,250,249],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,681,680],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,681,680],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,681,680],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,681,680],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,685],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,685],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,685],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,685],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,685],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,685],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,685],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,685],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,685],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252],[266,265,679,263,678,677,260,270,277],[266,265,679,263,678,677,260,270,277],[266,265,679,263,678,677,292,291,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,679,263,678,677,292,582,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,679,263,678,677,292,582,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,679,263,678,677,292,582,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,679,263,678,677,292,582,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,679,263,678,677,302,301,300,299,298,297,296,295,294,293],[266,265,679,263,678,677,302,301,300,299,298,297,296,295,294,293],[266,265,679,263,678,677,302,301,300,299,298,297,296,295,294,293],[266,265,679,263,678,677,302,318,317,316,758,757,45,756,755,754],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,319,294,759],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,319,294,293],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,319,294,293],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,679,263,678,677,302,318,317,316,315,314,512,511,510],[266,265,679,263,678,677,302,318,317,316,315,314,336,335,334,333,332,331,330,329,328,327],[266,265,679,263,678,677,302,318,317,316,315,314,336,335,334,333,332,331,330,329,328,327],[266,265,679,263,678,677,302,318,317,316,315,314,336,335,334,333,332,331,330,329,328,327],[266,265,679,263,678,677,302,318,317,316,315,314,336,335,334,333,332,331,330,329,328,327],[266,265,679,263,678,677,302,318,317,316,315,314,336,335,334,333,332,331,330,329,328,327],[266,265,679,263,678,677,302,318,317,316,315,314,336,335,334,333,332,331,330,329,328,327],[266,265,679,263,678,677,351,350,349,348,347,689,688,344,343,342,341,340,281,280,279,278],[266,265,679,263,678,677,575,574,350,573,349,764,578,763,761,762,761,760],[266,265,679,263,678,677,597,767,766,765],[266,265,679,263,678,677,351,350,349,348,347,689,768],[266,265,679,263,678,677,351,350,349,348,347,346,345,344,343,342,341,340,281,280,279,278],[266,265,679,263,678,677,351,350,349,348,347,689,688,344,343,342,341,340,281,280,279,278],[266,265,679,263,678,677,351,350,349,348,347,346,345,344,343,342,341,340,281,280,279,278],[266,265,679,263,678,677,351,350,349,348,347,346,345,344,343,342,341,340,281,280,279,278],[266,265,679,263,678,677,351,350,349,348,347,346,345,344,343,342,772,771,770,769],[266,265,679,263,678,677,364,363,776,359,459,458,457,775,348,347,774,773,179],[266,265,679,263,678,677,526,525,524,523,502,503,522,706],[266,265,679,263,678,677,526,525,524,523,502,503,522,704],[266,265,679,263,678,677,526,525,524,523,502,503,522,502],[266,265,679,263,678,677,526,525,524,523,502,503,522,699],[266,265,679,263,678,677,526,525,524,523,502,503,522,702],[266,265,679,263,678,677,526,525,524,523,502,503,522,703],[266,265,679,263,678,677,526,525,524,523,502,503,522,702],[266,265,679,263,678,677,526,525,524,523,502,503,522,705],[266,265,679,263,678,677,526,525,524,523,502,503,522,702],[266,265,679,263,678,677,526,525,524,523,502,503,522],[266,265,679,263,678,677,526,525,524,523,502,503,522,702],[266,265,679,263,678,677,526,525,524,523,502,503,522],[266,265,679,263,678,677,526,525,524,523,502,503,522],[266,265,679,263,678,677,526,525,524,523,502,503,522,702],[266,265,679,263,678,677,526,525,524,523,502,503,522,502,743],[266,265,679,263,678,677,526,525,524,523,502,503,522,705],[266,265,679,263,678,677,526,525,524,523,502,503,522,699],[266,265,679,263,678,677,526,525,524,523,502,503,522,702],[266,265,679,263,678,677,526,525,524,523,502,503,522],[266,265,679,263,678,677,526,525,524,523,502,503,522],[266,265,679,263,678,677,526,525,524,523,502,503,522,706],[266,265,679,263,678,677,526,525,524,523,502,503,522,702],[266,265,679,263,678,677,526,525,524,523,502,503,522,703],[266,265,679,263,678,677,526,525,524,523,502,503,522,699],[266,265,679,263,678,677,526,525,524,523,502,503,522,703],[266,265,679,263,678,677,526,525,524,523,502,503,522],[266,265,679,263,678,677,526,525,524,523,502,503,522,703],[266,265,679,263,678,677,466,443,442,465,464,359,358,777,502,503,502,503,502,616,502,503,502,616,502],[266,265,679,263,678,677],[266,265,679,263,678,677],[266,265,679,263,678,677,260,270,269,268,267],[266,265,679,263,678,677,260,270,269,268,267],[266,265,679,263,678,677,260,270,269,268,267],[266,265,679,263,678,677,260,270,269,268,267],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,251,250,547],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,251,250,547],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,251,250,547],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,251,250,547],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,251,250,547],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,251,250,249],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,251,250,249],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,251,250,249],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,251,250,249],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,681,680],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,681,680],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,685],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,685],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,685],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,685],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,685],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,685],[266,265,679,263,678,677,260,270,277],[266,265,679,263,678,677,260,270,277],[266,265,679,263,678,677,292,291,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,679,263,678,677,292,291,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,679,263,678,677,292,291,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,679,263,678,677,292,291,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,679,263,678,677,292,582,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,679,263,678,677,292,582,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,679,263,678,677,292,582,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,679,263,678,677,302,301,300,299,298,297,296,295,294,293],[266,265,679,263,678,677,302,301,300,299,298,297,296,303],[266,265,679,263,678,677,302,301,300,299,298,297,296,303],[266,265,679,263,678,677,302,301,300,299,298,297,296,303],[266,265,679,263,678,677,302,301,300,299,298,297,296,303],[266,265,679,263,678,677,302,301,300,299,298,297,296,303],[266,265,679,263,678,677,302,301,300,299,298,297,296,303],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,319,294,293],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,319,294,293],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,506],[266,265,679,263,678,677,302,318,317,316,315,314,313,326,325,324,323],[266,265,679,263,678,677,302,318,317,316,315,314,313,326,325,324,323],[266,265,679,263,678,677,302,318,317,316,315,314,313,326,325],[266,265,679,263,678,677,302,318,317,316,315,314,313,326,325],[266,265,679,263,678,677,302,318,317,316,315,314,512,511,510],[266,265,679,263,678,677,302,318,317,316,315,314,336,335,334,333,332,331,330,329,328,327],[266,265,679,263,678,677,302,318,317,316,315,314,336,335,334,333,332,331,330,329,328,327],[266,265,679,263,678,677,302,318,317,316,315,314,336,335,334,333,332,331,330,329,328,327],[266,265,679,263,678,677,302,318,317,316,315,314,336,335,334,333,332,331,330,329,328,327],[266,265,679,263,678,677,302,318,317,316,315,314,336,335,334,333,332,331,330,329,328,327],[266,265,679,263,678,677,351,350,349,348,347,689,688,344,343,342,341,340,281,280,279,278],[266,265,679,263,678,677,351,350,349,348,347,346,345,344,343,342,341,340,281,280,279,278],[266,265,679,263,678,677,351,350,349,348,347,346,345,344,343,342,341,340,281,280,279,278],[266,265,679,263,678,677,597,767,766],[266,265,679,263,678,677,351,350,349,348,347,346,345,344,343,342,341,340,281,280,279,278],[266,265,679,263,678,677,351,350,349,348,347,346,345,344,343,342,341,340,281,280,279,278],[266,265,679,263,678,677,351,350,349,348,347,346,345,344,343,342,341,340,281,280,279,278],[266,265,679,263,678,677,351,350,349,348,347,346,345,344,343,342,341,340,281,280,279,278],[266,265,679,263,678,677,351,350,349,348,347,689,688],[266,265,679,263,678,677,351,350,349,348,347,689,688,344,343,342,341,340,281,280,279,278],[266,265,679,263,678,677,351,350,349,348,347,346,345,344,343,342,341,340,281,339,778],[266,265,679,263,678,677,351,350,349,348,347,346,345],[266,265,679,263,678,677,364,363,444,443,442,441,579,587,586],[266,265,679,263,678,677,364,363,444,443,442,441,440,439,438,577,621],[266,265,679,263,678,677,364,363,444,443,442,441,440,439,438,437,779,539,651],[266,265,679,263,678,677,526,525,524,523,502,503,522,703],[266,265,679,263,678,677,526,525,524,523,502,503,522,703],[266,265,679,263,678,677,526,525,524,523,502,503,522,701],[266,265,679,263,678,677,526,525,524,523,502,503,522,705],[266,265,679,263,678,677,526,525,524,523,502,503,522,700],[266,265,679,263,678,677,526,525,524,523,502,503,522],[266,265,679,263,678,677,526,525,524,523,502,503,522,704],[266,265,679,263,678,677,526,525,524,523,502,503,522,705],[266,265,679,263,678,677,526,525,524,523,502,503,522,705],[266,265,679,263,678,677,526,525,524,523,502,503,522,606],[266,265,679,263,678,677,526,525,524,523,502,503,522,703],[266,265,679,263,678,677,526,525,524,523,502,503,522,716],[266,265,679,263,678,677,526,525,524,523,502,503,522,502],[266,265,679,263,678,677,526,525,524,523,502,503,522,502],[266,265,679,263,678,677,526,525,524,523,502,503,522,502],[266,265,679,263,678,677,526,525,524,523,502,503,522,502],[266,265,679,263,678,677,526,525,524,523,502,503,522,502],[266,265,679,263,678,677,526,525,524,523,502,503,522],[266,265,679,263,678,677,526,525,524,523,502,503,522,703],[266,265,679,263,678,677,526,525,524,523,502,503,522],[266,265,679,263,678,677,526,525,524,523,502,503,522,702],[266,265,679,263,678,677,526,525,524,523,502,503,522,699],[266,265,679,263,678,677,526,525,524,523,502,503,522,702],[266,265,679,263,678,677,526,525,524,523,502,503,522,606],[266,265,679,263,678,677,526,525,524,523,502,503,522,606],[266,265,679,263,678,677,526,525,524,523,502,503,522,703],[266,265,679,263,678,677,526,525,524,523,502,503,522,704],[266,265,679,263,678,677,526,525,524,523,502,503,522,699],[266,265,679,263,678,677,526,525,524,523,502,503,522],[266,265,679,263,678,677,526,525,524,523,502,503,522,702],[266,265,679,263,678,677,526,525,524,523,502,503,522,502],[266,265,679,263,678,677,526,525,524,523,502,503,522,703],[266,265,679,263,678,677,526,525,524,523,502,503,522,705],[266,265,679,263,678,677,526,525,524,523,502,503,522,703],[266,265,679,263,678,677,526,525,524,523,502,503,522,703],[266,265,679,263,678,677,526,525,524,523,502,503,522,703],[266,265,679,263,678,677,526,525,524,523,502,503,522,703],[266,265,679,263,678,677,526,525,524,523,502,503,522,502,743],[266,265,679,263,678,677,526,525,524,523,502,503,522,502],[266,265,679,263,678,677,526,525,524,523,502,503,522,702],[266,265,679,263,678,677,526,525,524,523,502,503,522,699],[266,265,679,263,678,677,526,525,524,523,502,503,522,703],[266,265,679,263,678,677,526,525,524,523,502,503,522,502],[266,265,679,263,678,677,526,525,524,523,502,503,522,705],[266,265,679,263,678,677,526,525,524,523,502,503,522],[266,265,679,263,678,677,526,525,524,523,502,503,522],[266,265,679,263,678,677,543,542,666,665,664,663,662],[266,265,679,263,678,677],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,251,250,249],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,681,680],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,681,680],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,681,680],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,681,680],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,685],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,685],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,685],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,685],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,685],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,685],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,685],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252],[266,265,679,263,678,677,292,291,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,679,263,678,677,292,291,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,679,263,678,677,292,291,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,679,263,678,677,292,291,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,679,263,678,677,292,582,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,679,263,678,677,292,582,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,679,263,678,677,292,582,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,679,263,678,677,302,301,300,299,298,297,296,295,294,293],[266,265,679,263,678,677,302,301,300,299,298,297,296,303],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,319,294,293],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,508,507],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,508,780],[266,265,679,263,678,677,302,318,317,316,315,314,313,326,325,324,323],[266,265,679,263,678,677,302,318,317,316,315,314,336,335,334,333,584,583],[266,265,679,263,678,677,302,318,317,316,315,314,336,335,334,333,783,782,781],[266,265,679,263,678,677,302,318,317,316,315,314,336,335,334,333,332,331,330,329,328,327],[266,265,679,263,678,677,302,318,317,316,315,314,336,335,334,333,332,331,330,329,328,327],[266,265,679,263,678,677,302,318,317,316,315,314,336,335,334,333,332,331,330,329,328,327],[266,265,679,263,678,677,575,574,350,573,440,540,539,651,650,649,648],[266,265,679,263,678,677,351,350,349,348,347,689,688,344,343,342,341,340,281,280,279,278],[266,265,679,263,678,677,575,574,350,573,579,578,763,761,784,446,728],[266,265,679,263,678,677,351,350,349,348,347,689,688,344,343,342,341,340,281,280,279,278],[266,265,679,263,678,677,351,350,349,348,347,689,688,344,343,342,341,340,281,280,279,278],[266,265,679,263,678,677,351,350,349,348,347,689,688],[266,265,679,263,678,677,351,350,349,348,347,689,688],[266,265,679,263,678,677,364,363,444,443,442,441,579,587,538,552],[266,265,679,263,678,677,364,363,444,443,442,441,440,785,621,620],[266,265,679,263,678,677,364,363,444,443,442,441,579,587,786],[266,265,679,263,678,677,364,363,444,443,442,465,558,557,556,555,623,660,539,651,650,649,648],[266,265,679,263,678,677,526,525,524,523,502,503,522,606],[266,265,679,263,678,677,526,525,524,523,502,503,522],[266,265,679,263,678,677,526,525,524,523,502,503,522,704],[266,265,679,263,678,677,526,525,524,523,502,503,522],[266,265,679,263,678,677,526,525,524,523,502,503,522,703],[266,265,679,263,678,677,526,525,524,523,502,503,522,706],[266,265,679,263,678,677,526,525,524,523,502,503,522,702],[266,265,679,263,678,677,526,525,524,523,502,503,522,705],[266,265,679,263,678,677,526,525,524,523,502,503,522,706],[266,265,679,263,678,677,526,525,524,523,502,503,522,502,715],[266,265,679,263,678,677,526,525,524,523,502,503,522],[266,265,679,263,678,677,526,525,524,523,502,503,522,502],[266,265,679,263,678,677,526,525,524,523,502,503,522],[266,265,679,263,678,677,526,525,524,523,502,503,522,702],[266,265,679,263,678,677,526,525,524,523,502,503,522,702],[266,265,679,263,678,677,526,525,524,523,502,503,522,705],[266,265,679,263,678,677,526,525,524,523,502,503,522,701],[266,265,679,263,678,677,526,525,524,523,502,503,522,699],[266,265,679,263,678,677,526,525,524,523,502,503,522,706],[266,265,679,263,678,677,526,525,524,523,502,503,522,502],[266,265,679,263,678,677,526,525,524,523,502,503,522,702],[266,265,679,263,678,677,526,525,524,523,502,503,522,702],[266,265,679,263,678,677,526,525,524,523,502,503,522],[266,265,679,263,678,677,526,525,524,523,502,503,522,699],[266,265,679,263,678,677,526,525,524,523,502,503,522,702],[266,265,679,263,678,677,526,525,524,523,502,503,522,702],[266,265,679,263,678,677,466,443,442,465,464,359,358,357,463,462,461,358,357,356,472,459,720,719,502,503,716],[266,265,679,263,678,677],[266,265,679,263,678],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,681,680],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,681,680],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,681,680],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,685],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,685],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,685],[266,265,679,263,678,677,260,270,277],[266,265,679,263,678,677,260,270,277],[266,265,679,263,678,677,260,270,277],[266,265,679,263,678,677,260,270,277],[266,265,679,263,678,677,260,270,277],[266,265,679,263,678,677,260,270,277],[266,265,679,263,678,677,260,270,277],[266,265,679,263,678,677,260,270,277],[266,265,679,263,678,677,260,270,277],[266,265,679,263,678,677,292,291,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,679,263,678,677,292,291,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,679,263,678,677,292,291,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,679,263,678,677,292,582,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,679,263,678,677,292,582,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,679,263,678,677,292,582,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,679,263,678,677,302,301,300,299,298,297,296,295,294,293],[266,265,679,263,678,677,302,301,300,299,298,297,296,295,294,293],[266,265,679,263,678,677,302,301,300,299,298,297,296,295,294,293],[266,265,679,263,678,677,302,301,300,299,298,297,296,303],[266,265,679,263,678,677,302,301,300,299,298,297,296,295,294,293],[266,265,679,263,678,677,302,301,300,299,298,297,296,303],[266,265,679,263,678,677,302,301,300,299,298,297,296,303],[266,265,679,263,678,677,302,301,300,299,298,297,296,303],[266,265,679,263,678,677,302,301,300,299,298,297,296,303],[266,265,679,263,678,677,302,301,300,299,298,297,296,303],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,319,294,293],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,679,263,678,677,302,318,317,316,315,314,313,326,325],[266,265,679,263,678,677,302,318,317,316,315,314,336,335,334,333,332,331,330,329,328,327],[266,265,679,263,678,677,302,318,317,316,315,314,336,335,334,333,332,331,330,329,328,327],[266,265,679,263,678,677,302,318,317,316,315,314,336,335,334,333,332,331,330,329,328,327],[266,265,679,263,678,677,302,318,317,316,315,314,336,335,334,333,332,331,330,329,328,327],[266,265,679,263,678,677,302,318,317,316,315,314,336,335,334,333,332,331,330,329,328,327],[266,265,679,263,678,677,302,318,317,316,315,314,336,335,334,333,332,331,330,329,328,327],[266,265,679,263,678,677,302,318,317,316,315,314,336,335,334,333,332,331,330,329,328,327],[266,265,679,263,678,677,302,318,317,316,315,314,336,335,334,333,332,331,330,329,328,327],[266,265,679,263,678,677,302,318,317,316,315,314,336,548],[266,265,679,263,678,677,351,350,349,348,347,689,688,344,343,342,341,340,281,280,279,278],[266,265,679,263,678,677,351,350,349,348,347,689,688,344,343,342,341,340,281,280,279,278],[266,265,679,263,678,677,351,350,349,348,347,689,688,344,343,342,341,340,281,280,279,278],[266,265,679,263,678,677,526,525,524,523,502,503,522,703],[266,265,679,263,678,677,526,525,524,523,502,503,522],[266,265,679,263,678,677,526,525,524,523,502,503,522,703],[266,265,679,263,678,677,526,525,524,523,502,503,522,701],[266,265,679,263,678,677,526,525,524,523,502,503,522,502,743],[266,265,679,263,678,677,526,525,524,523,502,503,522,699],[266,265,679,263,678,677,526,525,524,523,502,503,522,703],[266,265,679,263,678,677,526,525,524,523,502,503,522],[266,265,679,263,678,677,526,525,524,523,502,503,522,704],[266,265,679,263,678,677,526,525,524,523,502,503,522],[266,265,679,263,678,677,526,525,524,523,502,503,522,703],[266,265,679,263,678,677,526,525,524,523,502,503,522,702],[266,265,679,263,678,677,526,525,524,523,502,503,522,700],[266,265,679,263,678,677,526,525,524,523,502,503,522],[266,265,679,263,678,677,526,525,524,523,502,503,522,703],[266,265,679,263,678,677,526,525,524,523,502,503,522,699],[266,265,679,263,678,677,526,525,524,523,502,503,522,502],[266,265,679,263,678,677],[266,265,679,263,678,677],[266,265,679,263,678],[266,265,679,263,678,677,676,790,789,788,761,787,439,438,437,447,560],[266,265,679,263,678,677,260,270,269,268,267],[266,265,679,263,678,677,260,270,269,268,267],[266,265,679,263,678,677,260,270,269,268,267],[266,265,679,263,678,677,260,270,269,268,267],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,251,250,547],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,251,250,249],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,251,250,249],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,251,250,249],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,681,680],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,681,680],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,681,680],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,681,680],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,681,680],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,681,680],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,681,680],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,685],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,685],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,685],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,685],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,685],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,685],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252],[266,265,679,263,678,677,260,270,277],[266,265,679,263,678,677,292,291,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,679,263,678,677,292,291,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,679,263,678,677,292,291,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,679,263,678,677,292,582,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,679,263,678,677,292,582,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,679,263,678,677,292,582,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,679,263,678,677,302,301,300,299,298,297,296,295,294,293],[266,265,679,263,678,677,302,301,300,299,298,297,296,295,294,293],[266,265,679,263,678,677,302,301,300,299,298,297,296,303],[266,265,679,263,678,677,302,301,300,299,298,297,296,303],[266,265,679,263,678,677,302,301,300,299,298,297,296,303],[266,265,679,263,678,677,302,301,300,299,298,297,296,303],[266,265,679,263,678,677,302,301,300,299,298,297,296,303],[266,265,679,263,678,677,302,301,300,299,298,297,296,303],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,319,294,293],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,319,294,293],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,319,294,293],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,319,294,293],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,679,263,678,677,302,318,317,316,315,314,336,335,334,333,584,646],[266,265,679,263,678,677,302,318,317,316,315,314,336,335,334,333,584,583],[266,265,679,263,678,677,302,318,317,316,315,314,336,335,334,333,584,583],[266,265,679,263,678,677,302,318,317,316,315,314,336,335,334,333,584,583],[266,265,679,263,678,677,302,318,317,316,315,314,336,335,334,333,332,331,330,329,328,337],[266,265,679,263,678,677,302,318,317,316,315,314,336,335,334,333,332,331,330,329,328,327],[266,265,679,263,678,677,302,318,317,316,315,314,336,335,334,333,332,331,330,329,328,327],[266,265,679,263,678,677,302,318,317,316,315,314,336,335,334,333,332,331,330,329,328,327],[266,265,679,263,678,677,302,318,317,316,315,314,336,335,334,333,332,331,330,329,328,327],[266,265,679,263,678,677,351,350,349,348,347,689,688,344,343,342,341,340,281,280,279,278],[266,265,679,263,678,677,351,350,349,348,347,689,688,344,343,342,341,340,281,280,279,278],[266,265,679,263,678,677,351,350,349,348,347,689,688,344,343,342,341,340,281,280,279,278],[266,265,679,263,678,677,351,350,349,348,347,689,688,344,343,342,341,340,281,280,279,278],[266,265,679,263,678,677,351,350,349,348,347,346,345,344,343,342,341,340,281,280,279,278],[266,265,679,263,678,677,351,350,349,348,347,346,345,344,343,342,341,340,281,280,279,278],[266,265,679,263,678,677,351,350,349,348,347,346,345],[266,265,679,263,678,677,351,350,349,348,347,346,345,344,343,342,341,340,281,280,279,278],[266,265,679,263,678,677,351,350,349,348,347,346,345,344,343,342,341,340,281,280,279,278],[266,265,679,263,678,677,351,350,349,348,347,346,345,344,343,342,341,340,281,339,791],[266,265,679,263,678,677,526,525,524,523,502,503,522],[266,265,679,263,678,677,526,525,524,523,502,503,522],[266,265,679,263,678,677,526,525,524,523,502,503,522,703],[266,265,679,263,678,677,526,525,524,523,502,503,522,502],[266,265,679,263,678,677,526,525,524,523,502,503,522,701],[266,265,679,263,678,677,526,525,524,523,502,503,522],[266,265,679,263,678,677,526,525,524,523,502,503,522,716],[266,265,679,263,678,677,526,525,524,523,502,503,522,704],[266,265,679,263,678,677,526,525,524,523,502,503,522],[266,265,679,263,678,677,526,525,524,523,502,503,522,606],[266,265,679,263,678,677,526,525,524,523,502,503,522,704],[266,265,679,263,678,677,526,525,524,523,502,503,522,704],[266,265,679,263,678,677,526,525,524,523,502,503,522,703],[266,265,679,263,678,677,526,525,524,523,502,503,522,502],[266,265,679,263,678,677,526,525,524,523,502,503,522,703],[266,265,679,263,678,677,526,525,524,523,502,503,522,606],[266,265,679,263,678,677,526,525,524,523,502,503,522,702],[266,265,679,263,678,677,526,525,524,523,502,503,522],[266,265,679,263,678,677,526,525,524,523,502,503,522,704],[266,265,679,263,678,677,526,525,524,523,502,503,522],[266,265,679,263,678,677,526,525,524,523,502,503,522],[266,265,679,263,678,677,526,525,524,523,502,503,522],[266,265,679,263,678,677,526,525,524,523,502,503,522,502],[266,265,679,263,678,677,526,525,524,523,502,503,522],[266,265,679,263,678,677],[266,265,679,263,678,677],[266,265,679,263,678,677],[266,265,679,263,678,677],[266,265,679,263,678,677],[266,265,679,263,678,677],[266,265,679,263,678,677],[266,265,679,263,678],[266,265,679,263,678,677,260,270,794,595,594,793,792],[266,265,679,263,678,677,260,270,269,268,267],[266,265,679,263,678,677,260,270,269,268,267],[266,265,679,263,678,677,260,270,269,268,267],[266,265,679,263,678,677,260,270,269,268,267],[266,265,679,263,678,677,260,270,269,268,267],[266,265,679,263,678,677,260,270,269,268,267],[266,265,679,263,678,677,260,270,269,268,267],[266,265,679,263,678,677,260,270,269,268,267],[266,265,679,263,678,677,260,270,269,268,267],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,251,250,249],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,251,250,249],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,681,680],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,685],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,685],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,685],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,685],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,685],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252,684,683,682,685],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252],[266,265,679,263,678,677,260,270,277,276,275,274,247,246,252],[266,265,679,263,678,677,292,291,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,679,263,678,677,292,291,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,679,263,678,677,292,291,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,679,263,678,677,292,291,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,679,263,678,677,292,291,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,679,263,678,677,292,582,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,679,263,678,677,292,582,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,679,263,678,677,292,582,290,289,288,287,286,285,284,283,282,281,280,279,278],[266,265,679,263,678,677,302,301,300,299,298,297,296,295,294,293],[266,265,679,263,678,677,302,301,300,299,298,297,296,295,294,293],[266,265,679,263,678,677,302,301,300,299,298,297,296,303],[266,265,679,263,678,677,302,301,300,299,298,297,296,303],[266,265,679,263,678,677,302,301,300,299,298,297,296,303],[266,265,679,263,678,677,302,301,300,299,298,297,296,303],[266,265,679,263,678,677,302,301,300,299,298,297,296,303],[266,265,679,263,678,677,302,301,300,299,298,297,296,295,294,293],[266,265,679,263,678,677,302,301,300,299,298,297,296,303],[266,265,679,263,678,677,302,301,300,299,298,297,296,303],[266,265,679,263,678,677,302,301,300,299,298,297,296,303],[266,265,679,263,678,677,302,301,300,299,298,297,296,303],[266,265,679,263,678,677,302,301,300,299,298,297,296,303],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,319,294,293],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,320,321],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,679,263,678,677,302,318,317,316,315,314,313,312,311,310,309,308,307,306,305,281,304,279,278],[266,265,679,263,678,677,302,318,317,316,315,314,313,326,325,324,323],[266,265,679,263,678,677,302,318,317,316,315,314,313,326,325,324,323],[266,265,679,263,678,677,302,318,317,316,315,314,313,326,325,324,323],[266,265,679,263,678,677,302,318,317,316,315,314,313,326,325,324,323],[266,265,679,263,678,677,302,318,317,316,315,314,313,326,325],[266,265,679,263,678,677,302,318,317,316,315,314,313,326,325],[266,265,679,263,678,677,302,318,317,316,315,314,313,326,325],[266,265,679,263,678,677,302,318,317,316,315,314,336,335,334,333,332,331,330,329,328,337],[266,265,679,263,678,677,302,318,317,316,315,314,336,335,334,333,332,331,330,329,328,337],[266,265,679,263,678,677,302,318,317,316,315,314,336,335,334,333,332,331,330,329,328,327],[266,265,679,263,678,677,796,484,354,353,352,5,27,795],[266,265,679,263,678,677,351,350,349,348,347,689,688,344,343,342,341,340,281,280,279,278],[266,265,679,263,678,677,351,350,349,348,347,689,688,344,343,342,341,340,281,280,279,278],[266,265,679,263,678,677,351,350,349,348,347,689],[266,265,679,263,678,677,351,350,349,348,347,689,688,344,343,342,341,340,281,280,279,278],[266,265,679,263,678,677,351,350,349,348,347,689,688,344,343,342,341,340,281,280,279,278],[266,265,679,263,678,677,351,350,349,348,347,689,688,344,343,342,341,340,281,280,279,278],[266,265,679,263,678,677,351,350,349,348,347,689,688,344,343,342,341,340,281,280,279,278],[266,265,679,263,678,677,351,350,349,348,347,346,345,344,343,342,341,340,281,280,279,278],[266,265,679,263,678,677,526,525,524,523,502,503,522,705],[266,265,679,263,678,677,526,525,524,523,502,503,522,702],[266,265,679,263,678,677,526,525,524,523,502,503,522,699],[266,265,679,263,678,677,526,525,524,523,502,503,522,705],[266,265,679,263,678,677,526,525,524,523,502,503,522,705],[266,265,679,263,678,677,526,525,524,523,502,503,522,702],[266,265,679,263,678,677,526,525,524,523,502,503,522,703],[266,265,679,263,678,677,526,525,524,523,502,503,522,502,715],[266,265,679,263,678,677,526,525,524,523,502,503,522,703],[266,265,679,263,678,677,526,525,524,523,502,503,522,703],[266,265,679,263,678,677,526,525,524,523,502,503,522,702],[266,265,679,263,678,677,526,525,524,523,502,503,522],[266,265,679,263,678,677,526,525,524,523,502,503,522],[266,265,679,263,678,677,526,525,524,523,502,503,522,702],[266,265,679,263,678,677,526,525,524,523,502,503,522,705],[266,265,679,263,678,677,526,525,524,523,502,503,522,705],[266,265,679,263,678,677,526,525,524,523,502,503,522,703],[266,265,679,263,678,677,526,525,524,523,502,503,522,502],[266,265,679,263,678,677,526,525,524,523,502,503,522],[266,265,679,263,678,677,526,525,524,523,502,503,522,606],[266,265,679,263,678,677,526,525,524,523,502,503,522,502,743],[266,265,679,263,678,677,526,525,524,523,502,503,522],[266,265,679,263,678,677,526,525,524,523,502,503,522],[266,265,679,263,678,677,526,525,524,523,502,503,522,702],[266,265,679,263,678,677,526,525,524,523,502,503,522,702],[266,265,679,263,678,677,526,525,524,523,502,503,522,606],[266,265,679,263,678,677,526,525,524,523,502,503,522,699],[266,265,679,263,678,677,466,443,442,465,464,359,358,357,463,462,461,358,357,356,798,797],[266,265,679,263,678,677,466,443,442,465,464,359,358,357,463,462,461,358,357,356,748,459,458,457,747,358,357,356,746,358,357,356,800,799],[266,265,679,263,678,677],[266,265,679,263,678,677],[266,265,679,263,678,677],[266,265,679,263,678,677],[266,265,679,263,678,677],[266,802,801],[266,802,801],[266,802,801],[],[],[],[],[],[],[],[],[],[],[],[],[],[],[]],"weights":[0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01]}],"shared":{"frames":[{"name":"_compile_bytecode","file":"","line":647,"col":null},{"name":"get_code","file":"","line":978,"col":null},{"name":"exec_module","file":"","line":846,"col":null},{"name":"_load_unlocked","file":"","line":680,"col":null},{"name":"_find_and_load_unlocked","file":"","line":986,"col":null},{"name":"_find_and_load","file":"","line":1007,"col":null},{"name":"","file":"/home/fbruzzesi/.local/share/uv/python/cpython-3.9.19-linux-x86_64-gnu/lib/python3.9/pathlib.py","line":4,"col":null},{"name":"_call_with_frames_removed","file":"","line":228,"col":null},{"name":"exec_module","file":"","line":850,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/numpy/compat/py3k.py","line":21,"col":null},{"name":"_handle_fromlist","file":"","line":1058,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/numpy/compat/__init__.py","line":14,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/numpy/core/_type_aliases.py","line":20,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/numpy/core/numerictypes.py","line":105,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/numpy/core/__init__.py","line":72,"col":null},{"name":"_find_and_load_unlocked","file":"","line":972,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/numpy/__config__.py","line":4,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/numpy/__init__.py","line":130,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/t.py","line":1,"col":null},{"name":"_find_and_load_unlocked","file":"","line":984,"col":null},{"name":"","file":"/home/fbruzzesi/.local/share/uv/python/cpython-3.9.19-linux-x86_64-gnu/lib/python3.9/ntpath.py","line":259,"col":null},{"name":"","file":"/home/fbruzzesi/.local/share/uv/python/cpython-3.9.19-linux-x86_64-gnu/lib/python3.9/pathlib.py","line":13,"col":null},{"name":"namedtuple","file":"/home/fbruzzesi/.local/share/uv/python/cpython-3.9.19-linux-x86_64-gnu/lib/python3.9/collections/__init__.py","line":372,"col":null},{"name":"","file":"/home/fbruzzesi/.local/share/uv/python/cpython-3.9.19-linux-x86_64-gnu/lib/python3.9/urllib/parse.py","line":256,"col":null},{"name":"_add_integer_aliases","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/numpy/core/_type_aliases.py","line":133,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/numpy/core/_type_aliases.py","line":142,"col":null},{"name":"release","file":"","line":118,"col":null},{"name":"__exit__","file":"","line":161,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/numpy/core/__init__.py","line":100,"col":null},{"name":"_path_is_mode_type","file":"","line":151,"col":null},{"name":"_path_isfile","file":"","line":156,"col":null},{"name":"find_spec","file":"","line":1555,"col":null},{"name":"_get_spec","file":"","line":1395,"col":null},{"name":"find_spec","file":"","line":1423,"col":null},{"name":"_find_spec","file":"","line":925,"col":null},{"name":"_find_and_load_unlocked","file":"","line":982,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/numpy/core/__init__.py","line":101,"col":null},{"name":"__next","file":"/home/fbruzzesi/.local/share/uv/python/cpython-3.9.19-linux-x86_64-gnu/lib/python3.9/sre_parse.py","line":235,"col":null},{"name":"get","file":"/home/fbruzzesi/.local/share/uv/python/cpython-3.9.19-linux-x86_64-gnu/lib/python3.9/sre_parse.py","line":257,"col":null},{"name":"_parse","file":"/home/fbruzzesi/.local/share/uv/python/cpython-3.9.19-linux-x86_64-gnu/lib/python3.9/sre_parse.py","line":512,"col":null},{"name":"_parse_sub","file":"/home/fbruzzesi/.local/share/uv/python/cpython-3.9.19-linux-x86_64-gnu/lib/python3.9/sre_parse.py","line":444,"col":null},{"name":"_parse","file":"/home/fbruzzesi/.local/share/uv/python/cpython-3.9.19-linux-x86_64-gnu/lib/python3.9/sre_parse.py","line":841,"col":null},{"name":"parse","file":"/home/fbruzzesi/.local/share/uv/python/cpython-3.9.19-linux-x86_64-gnu/lib/python3.9/sre_parse.py","line":955,"col":null},{"name":"compile","file":"/home/fbruzzesi/.local/share/uv/python/cpython-3.9.19-linux-x86_64-gnu/lib/python3.9/sre_compile.py","line":788,"col":null},{"name":"_compile","file":"/home/fbruzzesi/.local/share/uv/python/cpython-3.9.19-linux-x86_64-gnu/lib/python3.9/re.py","line":304,"col":null},{"name":"compile","file":"/home/fbruzzesi/.local/share/uv/python/cpython-3.9.19-linux-x86_64-gnu/lib/python3.9/re.py","line":252,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/numpy/core/_internal.py","line":146,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/numpy/core/__init__.py","line":104,"col":null},{"name":"_path_split","file":"","line":132,"col":null},{"name":"cache_from_source","file":"","line":387,"col":null},{"name":"get_code","file":"","line":930,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/numpy/lib/index_tricks.py","line":12,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/numpy/lib/__init__.py","line":23,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/numpy/__init__.py","line":149,"col":null},{"name":"cache_from_source","file":"","line":388,"col":null},{"name":"_optimize_charset","file":"/home/fbruzzesi/.local/share/uv/python/cpython-3.9.19-linux-x86_64-gnu/lib/python3.9/sre_compile.py","line":421,"col":null},{"name":"_compile","file":"/home/fbruzzesi/.local/share/uv/python/cpython-3.9.19-linux-x86_64-gnu/lib/python3.9/sre_compile.py","line":136,"col":null},{"name":"_compile","file":"/home/fbruzzesi/.local/share/uv/python/cpython-3.9.19-linux-x86_64-gnu/lib/python3.9/sre_compile.py","line":164,"col":null},{"name":"_code","file":"/home/fbruzzesi/.local/share/uv/python/cpython-3.9.19-linux-x86_64-gnu/lib/python3.9/sre_compile.py","line":631,"col":null},{"name":"compile","file":"/home/fbruzzesi/.local/share/uv/python/cpython-3.9.19-linux-x86_64-gnu/lib/python3.9/sre_compile.py","line":792,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/numpy/lib/utils.py","line":751,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/numpy/lib/__init__.py","line":32,"col":null},{"name":"get_data","file":"","line":1040,"col":null},{"name":"get_code","file":"","line":941,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/numpy/lib/__init__.py","line":33,"col":null},{"name":"_path_stat","file":"","line":142,"col":null},{"name":"find_spec","file":"","line":1522,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/numpy/lib/__init__.py","line":34,"col":null},{"name":"","file":"/home/fbruzzesi/.local/share/uv/python/cpython-3.9.19-linux-x86_64-gnu/lib/python3.9/inspect.py","line":39,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/numpy/ma/core.py","line":24,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/numpy/ma/__init__.py","line":42,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/numpy/__init__.py","line":159,"col":null},{"name":"__init__","file":"/home/fbruzzesi/.local/share/uv/python/cpython-3.9.19-linux-x86_64-gnu/lib/python3.9/typing.py","line":740,"col":null},{"name":"Literal","file":"/home/fbruzzesi/.local/share/uv/python/cpython-3.9.19-linux-x86_64-gnu/lib/python3.9/typing.py","line":513,"col":null},{"name":"inner","file":"/home/fbruzzesi/.local/share/uv/python/cpython-3.9.19-linux-x86_64-gnu/lib/python3.9/typing.py","line":274,"col":null},{"name":"__getitem__","file":"/home/fbruzzesi/.local/share/uv/python/cpython-3.9.19-linux-x86_64-gnu/lib/python3.9/typing.py","line":361,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/polars/_typing.py","line":152,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/polars/functions/eager.py","line":11,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/polars/functions/__init__.py","line":28,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/polars/dataframe/frame.py","line":31,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/polars/dataframe/__init__.py","line":1,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/polars/_reexport.py","line":3,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/polars/api.py","line":6,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/polars/__init__.py","line":21,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/t.py","line":3,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/polars/_utils/construction/__init__.py","line":1,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/polars/dataframe/frame.py","line":37,"col":null},{"name":"module_from_spec","file":"","line":571,"col":null},{"name":"_load_unlocked","file":"","line":666,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/polars/series/series.py","line":83,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/polars/series/__init__.py","line":1,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/polars/_reexport.py","line":6,"col":null},{"name":"_expr_lookup","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/polars/series/utils.py","line":84,"col":null},{"name":"expr_dispatch","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/polars/series/utils.py","line":39,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/polars/series/datetime.py","line":29,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/polars/series/series.py","line":101,"col":null},{"name":"find_spec","file":"","line":1527,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/polars/io/database/__init__.py","line":1,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/polars/io/__init__.py","line":6,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/polars/__init__.py","line":154,"col":null},{"name":"__init__","file":"/home/fbruzzesi/.local/share/uv/python/cpython-3.9.19-linux-x86_64-gnu/lib/python3.9/typing.py","line":528,"col":null},{"name":"_type_convert","file":"/home/fbruzzesi/.local/share/uv/python/cpython-3.9.19-linux-x86_64-gnu/lib/python3.9/typing.py","line":133,"col":null},{"name":"_type_check","file":"/home/fbruzzesi/.local/share/uv/python/cpython-3.9.19-linux-x86_64-gnu/lib/python3.9/typing.py","line":155,"col":null},{"name":"","file":"/home/fbruzzesi/.local/share/uv/python/cpython-3.9.19-linux-x86_64-gnu/lib/python3.9/typing.py","line":1956,"col":null},{"name":"__new__","file":"/home/fbruzzesi/.local/share/uv/python/cpython-3.9.19-linux-x86_64-gnu/lib/python3.9/typing.py","line":1955,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/polars/io/database/_arrow_registry.py","line":6,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/polars/io/database/_executor.py","line":18,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/polars/io/database/functions.py","line":9,"col":null},{"name":"_lock_unlock_module","file":"","line":209,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/polars/io/spreadsheet/functions.py","line":10,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/polars/io/spreadsheet/__init__.py","line":1,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/polars/io/__init__.py","line":14,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/polars/__init__.py","line":187,"col":null},{"name":"create_module","file":"","line":1173,"col":null},{"name":"module_from_spec","file":"","line":565,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pyarrow/__init__.py","line":65,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/compat/pyarrow.py","line":8,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/compat/__init__.py","line":27,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/__init__.py","line":26,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/t.py","line":4,"col":null},{"name":"exec_module","file":"","line":1181,"col":null},{"name":"_compile_bytecode","file":"","line":651,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/cloudpickle/__init__.py","line":1,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/cloudpickle/cloudpickle.py","line":63,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/_libs/tslibs/__init__.py","line":39,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/_libs/__init__.py","line":18,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/api.py","line":1,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/__init__.py","line":49,"col":null},{"name":"__exit__","file":"","line":880,"col":null},{"name":"__next","file":"/home/fbruzzesi/.local/share/uv/python/cpython-3.9.19-linux-x86_64-gnu/lib/python3.9/sre_parse.py","line":240,"col":null},{"name":"IntervalDtype","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/dtypes/dtypes.py","line":1200,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/dtypes/dtypes.py","line":1165,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/api.py","line":9,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pyarrow/vendored/docscrape.py","line":13,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pyarrow/compute.py","line":104,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/arrays/arrow/accessors.py","line":23,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/arrays/arrow/__init__.py","line":1,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/arrays/__init__.py","line":1,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/api.py","line":28,"col":null},{"name":"_parse","file":"/home/fbruzzesi/.local/share/uv/python/cpython-3.9.19-linux-x86_64-gnu/lib/python3.9/sre_parse.py","line":500,"col":null},{"name":"NumpyDocString","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pyarrow/vendored/docscrape.py","line":274,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pyarrow/vendored/docscrape.py","line":118,"col":null},{"name":"__init__","file":"/home/fbruzzesi/.local/share/uv/python/cpython-3.9.19-linux-x86_64-gnu/lib/python3.9/inspect.py","line":2791,"col":null},{"name":"_make_signature","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pyarrow/compute.py","line":288,"col":null},{"name":"_wrap_function","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pyarrow/compute.py","line":302,"col":null},{"name":"_make_global_functions","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pyarrow/compute.py","line":333,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pyarrow/compute.py","line":336,"col":null},{"name":"_find_spec","file":"","line":917,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/ops/array_ops.py","line":18,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/ops/__init__.py","line":8,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/arrays/arrow/array.py","line":50,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/arrays/arrow/__init__.py","line":5,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/arrays/datetimes.py","line":17,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/arrays/__init__.py","line":9,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/arrays/datetimes.py","line":62,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/arrays/datetimelike.py","line":54,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/groupby/generic.py","line":68,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/groupby/__init__.py","line":1,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/api.py","line":47,"col":null},{"name":"update_wrapper","file":"/home/fbruzzesi/.local/share/uv/python/cpython-3.9.19-linux-x86_64-gnu/lib/python3.9/functools.py","line":52,"col":null},{"name":"_forbid_nonstring_types","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/strings/accessor.py","line":130,"col":null},{"name":"StringMethods","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/strings/accessor.py","line":2349,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/strings/accessor.py","line":157,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/indexes/base.py","line":190,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/indexes/api.py","line":20,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/indexing.py","line":79,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/generic.py","line":152,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/frame.py","line":149,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/indexes/datetimes.py","line":47,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/indexes/api.py","line":28,"col":null},{"name":"__init__","file":"/home/fbruzzesi/.local/share/uv/python/cpython-3.9.19-linux-x86_64-gnu/lib/python3.9/inspect.py","line":2500,"col":null},{"name":"replace","file":"/home/fbruzzesi/.local/share/uv/python/cpython-3.9.19-linux-x86_64-gnu/lib/python3.9/inspect.py","line":2580,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/util/_decorators.py","line":308,"col":null},{"name":"decorate","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/util/_decorators.py","line":307,"col":null},{"name":"NDFrame","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/generic.py","line":2723,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/generic.py","line":244,"col":null},{"name":"Series","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/series.py","line":381,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/series.py","line":263,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/frame.py","line":188,"col":null},{"name":"sub","file":"/home/fbruzzesi/.local/share/uv/python/cpython-3.9.19-linux-x86_64-gnu/lib/python3.9/re.py","line":210,"col":null},{"name":"dedent","file":"/home/fbruzzesi/.local/share/uv/python/cpython-3.9.19-linux-x86_64-gnu/lib/python3.9/textwrap.py","line":461,"col":null},{"name":"__call__","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/util/_decorators.py","line":488,"col":null},{"name":"Series","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/series.py","line":874,"col":null},{"name":"acquire","file":"","line":97,"col":null},{"name":"__enter__","file":"","line":158,"col":null},{"name":"_find_and_load","file":"","line":1004,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/groupby/ops.py","line":25,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/groupby/generic.py","line":69,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/computation/scope.py","line":121,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/computation/ops.py","line":30,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/computation/engines.py","line":15,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/computation/eval.py","line":15,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/computation/api.py","line":2,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/__init__.py","line":119,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/computation/eval.py","line":16,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/io/api.py","line":22,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/__init__.py","line":142,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/express/_imshow.py","line":2,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/express/__init__.py","line":5,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/t.py","line":7,"col":null},{"name":"namedtuple","file":"/home/fbruzzesi/.local/share/uv/python/cpython-3.9.19-linux-x86_64-gnu/lib/python3.9/collections/__init__.py","line":396,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/express/_core.py","line":125,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/express/_imshow.py","line":3,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/xarray/coding/variables.py","line":13,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/xarray/coding/times.py","line":14,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/xarray/coding/cftimeindex.py","line":54,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/xarray/coding/cftime_offsets.py","line":55,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/xarray/groupers.py","line":17,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/xarray/__init__.py","line":3,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/express/_imshow.py","line":11,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/xarray/core/dataset.py","line":32,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/xarray/core/dataarray.py","line":43,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/xarray/groupers.py","line":19,"col":null},{"name":"_parse","file":"/home/fbruzzesi/.local/share/uv/python/cpython-3.9.19-linux-x86_64-gnu/lib/python3.9/sre_parse.py","line":510,"col":null},{"name":"insert_doc_addendum","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/xarray/core/datatree_ops.py","line":233,"col":null},{"name":"_wrap_then_attach_to_cls","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/xarray/core/datatree_ops.py","line":214,"col":null},{"name":"MappedDatasetMethodsMixin","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/xarray/core/datatree_ops.py","line":276,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/xarray/core/datatree_ops.py","line":270,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/xarray/core/datatree.py","line":30,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/xarray/testing/assertions.py","line":15,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/xarray/testing/__init__.py","line":2,"col":null},{"name":"_init_module_attrs","file":"","line":535,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/xarray/backends/file_manager.py","line":13,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/xarray/backends/__init__.py","line":8,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/xarray/tutorial.py","line":17,"col":null},{"name":"get_data","file":"","line":1039,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/xarray/backends/api.py","line":31,"col":null},{"name":"_split","file":"/home/fbruzzesi/.local/share/uv/python/cpython-3.9.19-linux-x86_64-gnu/lib/python3.9/textwrap.py","line":176,"col":null},{"name":"_split_chunks","file":"/home/fbruzzesi/.local/share/uv/python/cpython-3.9.19-linux-x86_64-gnu/lib/python3.9/textwrap.py","line":338,"col":null},{"name":"wrap","file":"/home/fbruzzesi/.local/share/uv/python/cpython-3.9.19-linux-x86_64-gnu/lib/python3.9/textwrap.py","line":351,"col":null},{"name":"fill","file":"/home/fbruzzesi/.local/share/uv/python/cpython-3.9.19-linux-x86_64-gnu/lib/python3.9/textwrap.py","line":363,"col":null},{"name":"make_docstring","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/express/_doc.py","line":625,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/express/_chart_types.py","line":267,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/express/__init__.py","line":6,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/express/_chart_types.py","line":1837,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/t.py","line":39,"col":null},{"name":"numpy_to_pyseries","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/polars/_utils/construction/series.py","line":454,"col":null},{"name":"__init__","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/polars/series/series.py","line":301,"col":null},{"name":"_expand_dict_values","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/polars/_utils/construction/dataframe.py","line":389,"col":null},{"name":"dict_to_pydf","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/polars/_utils/construction/dataframe.py","line":160,"col":null},{"name":"__init__","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/polars/dataframe/frame.py","line":365,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/t.py","line":44,"col":null},{"name":"_sanitize_str_dtypes","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/construction.py","line":752,"col":null},{"name":"sanitize_array","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/construction.py","line":664,"col":null},{"name":"_homogenize","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/internals/construction.py","line":629,"col":null},{"name":"arrays_to_mgr","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/internals/construction.py","line":119,"col":null},{"name":"dict_to_mgr","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/internals/construction.py","line":503,"col":null},{"name":"__init__","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/frame.py","line":778,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/t.py","line":45,"col":null},{"name":"_stack_arrays","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/internals/managers.py","line":2254,"col":null},{"name":"_form_blocks","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/internals/managers.py","line":2212,"col":null},{"name":"create_block_manager_from_column_arrays","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/internals/managers.py","line":2139,"col":null},{"name":"arrays_to_mgr","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/internals/construction.py","line":152,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/t.py","line":46,"col":null},{"name":"__contains__","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/indexes/base.py","line":5360,"col":null},{"name":"_can_hold_identifiers_and_holds_name","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/indexes/base.py","line":5452,"col":null},{"name":"__getattr__","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/generic.py","line":6296,"col":null},{"name":"_from_native_impl","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/narwhals/translate.py","line":396,"col":null},{"name":"from_native","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/narwhals/stable/v1/__init__.py","line":830,"col":null},{"name":"build_dataframe","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/express/_core.py","line":1448,"col":null},{"name":"make_figure","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/express/_core.py","line":2342,"col":null},{"name":"scatter","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/express/_chart_types.py","line":66,"col":null},{"name":"figure_generation_scatter","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/t.py","line":52,"col":null},{"name":"wrapper","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/t.py","line":20,"col":null},{"name":"test_all_charts","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/t.py","line":82,"col":null},{"name":"run_and_save_results","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/t.py","line":94,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/t.py","line":113,"col":null},{"name":"reset_index","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/series.py","line":1754,"col":null},{"name":"to_unindexed_series","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/express/_core.py","line":1123,"col":null},{"name":"process_args_into_dataframe","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/express/_core.py","line":1294,"col":null},{"name":"build_dataframe","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/express/_core.py","line":1608,"col":null},{"name":"reindex","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/generic.py","line":5592,"col":null},{"name":"reindex","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/series.py","line":5153,"col":null},{"name":"_homogenize","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/internals/construction.py","line":611,"col":null},{"name":"from_dict","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/frame.py","line":1917,"col":null},{"name":"_from_dict_impl","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/narwhals/functions.py","line":384,"col":null},{"name":"from_dict","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/narwhals/stable/v1/__init__.py","line":2428,"col":null},{"name":"process_args_into_dataframe","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/express/_core.py","line":1386,"col":null},{"name":"_isna_string_dtype","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/dtypes/missing.py","line":313,"col":null},{"name":"_isna_array","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/dtypes/missing.py","line":292,"col":null},{"name":"_isna","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/dtypes/missing.py","line":216,"col":null},{"name":"isna","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/dtypes/missing.py","line":178,"col":null},{"name":"notna","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/dtypes/missing.py","line":457,"col":null},{"name":"notna","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/generic.py","line":8821,"col":null},{"name":"notna","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/series.py","line":5788,"col":null},{"name":"_find_valid_index","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/generic.py","line":12786,"col":null},{"name":"first_valid_index","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/generic.py","line":12866,"col":null},{"name":"native_to_narwhals_dtype","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/narwhals/_pandas_like/utils.py","line":287,"col":null},{"name":"dtype","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/narwhals/_pandas_like/series.py","line":178,"col":null},{"name":"dtype","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/narwhals/series.py","line":356,"col":null},{"name":"_is_continuous","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/express/_core.py","line":160,"col":null},{"name":"infer_config","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/express/_core.py","line":2061,"col":null},{"name":"make_figure","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/express/_core.py","line":2351,"col":null},{"name":"_check_object_for_strings","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/algorithms.py","line":297,"col":null},{"name":"_get_hashtable_algo","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/algorithms.py","line":275,"col":null},{"name":"unique_with_mask","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/algorithms.py","line":436,"col":null},{"name":"unique","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/algorithms.py","line":401,"col":null},{"name":"unique","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/base.py","line":1025,"col":null},{"name":"unique","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/series.py","line":2407,"col":null},{"name":"unique","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/narwhals/_pandas_like/series.py","line":472,"col":null},{"name":"unique","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/narwhals/series.py","line":1045,"col":null},{"name":"get_groups_and_orders","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/express/_core.py","line":2295,"col":null},{"name":"make_figure","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/express/_core.py","line":2355,"col":null},{"name":"unique_with_mask","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/algorithms.py","line":440,"col":null},{"name":"_isna","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/dtypes/missing.py","line":207,"col":null},{"name":"factorize","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/algorithms.py","line":789,"col":null},{"name":"_codes_and_uniques","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/groupby/grouper.py","line":835,"col":null},{"name":"codes","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/groupby/grouper.py","line":691,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/groupby/ops.py","line":690,"col":null},{"name":"codes","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/groupby/ops.py","line":690,"col":null},{"name":"_get_compressed_codes","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/groupby/ops.py","line":764,"col":null},{"name":"group_info","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/groupby/ops.py","line":745,"col":null},{"name":"_get_splitter","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/groupby/ops.py","line":629,"col":null},{"name":"get_iterator","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/groupby/ops.py","line":618,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/narwhals/_pandas_like/group_by.py","line":108,"col":null},{"name":"__iter__","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/narwhals/_pandas_like/group_by.py","line":108,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/narwhals/group_by.py","line":115,"col":null},{"name":"__iter__","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/narwhals/group_by.py","line":115,"col":null},{"name":"get_groups_and_orders","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/express/_core.py","line":2311,"col":null},{"name":"factorize_array","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/algorithms.py","line":592,"col":null},{"name":"factorize","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/algorithms.py","line":795,"col":null},{"name":"factorize_array","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/algorithms.py","line":595,"col":null},{"name":"get_group_index","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/sorting.py","line":198,"col":null},{"name":"get_group_index_sorter","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/sorting.py","line":677,"col":null},{"name":"_sort_idx","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/groupby/ops.py","line":944,"col":null},{"name":"_sorted_ids","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/groupby/ops.py","line":950,"col":null},{"name":"_get_splitter","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/groupby/ops.py","line":634,"col":null},{"name":"_take_nd_ndarray","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/array_algos/take.py","line":162,"col":null},{"name":"take_nd","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/array_algos/take.py","line":117,"col":null},{"name":"take_nd","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/internals/blocks.py","line":1307,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/internals/managers.py","line":688,"col":null},{"name":"reindex_indexer","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/internals/managers.py","line":687,"col":null},{"name":"take","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/internals/managers.py","line":894,"col":null},{"name":"take","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/generic.py","line":4133,"col":null},{"name":"_sorted_data","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/groupby/ops.py","line":1164,"col":null},{"name":"__iter__","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/groupby/ops.py","line":1150,"col":null},{"name":"get_iterator","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/groupby/ops.py","line":620,"col":null},{"name":"_take_nd_ndarray","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/array_algos/take.py","line":157,"col":null},{"name":"__init__","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/series.py","line":410,"col":null},{"name":"_isna","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/dtypes/missing.py","line":218,"col":null},{"name":"isna","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/generic.py","line":8754,"col":null},{"name":"isna","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/series.py","line":5775,"col":null},{"name":"to_numpy","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/narwhals/_pandas_like/series.py","line":508,"col":null},{"name":"__array__","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/narwhals/_pandas_like/series.py","line":501,"col":null},{"name":"__array__","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/narwhals/series.py","line":58,"col":null},{"name":"copy_to_readonly_numpy_array","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/_plotly_utils/basevalidators.py","line":129,"col":null},{"name":"validate_coerce","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/_plotly_utils/basevalidators.py","line":408,"col":null},{"name":"_set_prop","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":5199,"col":null},{"name":"__setitem__","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":4860,"col":null},{"name":"_perform_update","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":3926,"col":null},{"name":"update","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":5125,"col":null},{"name":"make_figure","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/express/_core.py","line":2531,"col":null},{"name":"_gcd_import","file":"","line":1030,"col":null},{"name":"import_module","file":"/home/fbruzzesi/.local/share/uv/python/cpython-3.9.19-linux-x86_64-gnu/lib/python3.9/importlib/__init__.py","line":127,"col":null},{"name":"__getattr__","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/_plotly_utils/importers.py","line":36,"col":null},{"name":"data_class","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/_plotly_utils/basevalidators.py","line":2482,"col":null},{"name":"validate_coerce","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/_plotly_utils/basevalidators.py","line":2512,"col":null},{"name":"_set_compound_prop","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":5263,"col":null},{"name":"__setitem__","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":4852,"col":null},{"name":"__setitem__","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":5898,"col":null},{"name":"_init_subplot_xy","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/_subplots.py","line":989,"col":null},{"name":"_init_subplot","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/_subplots.py","line":1124,"col":null},{"name":"make_subplots","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/_subplots.py","line":752,"col":null},{"name":"init_figure","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/express/_core.py","line":2727,"col":null},{"name":"make_figure","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/express/_core.py","line":2591,"col":null},{"name":"","file":"","line":129,"col":null},{"name":"_path_split","file":"","line":129,"col":null},{"name":"_get_cached","file":"","line":494,"col":null},{"name":"cached","file":"","line":391,"col":null},{"name":"_init_module_attrs","file":"","line":550,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/offline/__init__.py","line":6,"col":null},{"name":"__init__","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":578,"col":null},{"name":"__init__","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/graph_objs/_figure.py","line":641,"col":null},{"name":"make_subplots","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/_subplots.py","line":880,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/stack_data/__init__.py","line":1,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/IPython/core/ultratb.py","line":104,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/IPython/core/crashhandler.py","line":27,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/IPython/core/application.py","line":26,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/IPython/__init__.py","line":53,"col":null},{"name":"get_module","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/_plotly_utils/optional_imports.py","line":28,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/tools.py","line":64,"col":null},{"name":"__getattr__","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/_plotly_utils/importers.py","line":29,"col":null},{"name":"_handle_fromlist","file":"","line":1055,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/offline/offline.py","line":11,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/IPython/core/display.py","line":717,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/IPython/display.py","line":16,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/IPython/core/page.py","line":28,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/IPython/core/oinspect.py","line":37,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/IPython/core/magic.py","line":20,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/IPython/terminal/embed.py","line":14,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/IPython/__init__.py","line":54,"col":null},{"name":"__new__","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/traitlets/traitlets.py","line":963,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/IPython/core/alias.py","line":192,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/IPython/core/interactiveshell.py","line":85,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/IPython/terminal/embed.py","line":15,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/prompt_toolkit/key_binding/defaults.py","line":19,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/prompt_toolkit/application/application.py","line":56,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/prompt_toolkit/application/__init__.py","line":3,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/prompt_toolkit/__init__.py","line":26,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/IPython/terminal/interactiveshell.py","line":31,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/IPython/terminal/embed.py","line":16,"col":null},{"name":"__new__","file":"/home/fbruzzesi/.local/share/uv/python/cpython-3.9.19-linux-x86_64-gnu/lib/python3.9/abc.py","line":107,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/prompt_toolkit/shortcuts/progress_bar/formatters.py","line":251,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/prompt_toolkit/shortcuts/progress_bar/base.py","line":57,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/prompt_toolkit/shortcuts/progress_bar/__init__.py","line":3,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/prompt_toolkit/shortcuts/__init__.py","line":12,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/prompt_toolkit/__init__.py","line":28,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/jedi/inference/gradual/typing.py","line":21,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/jedi/inference/gradual/stub_value.py","line":5,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/jedi/inference/gradual/typeshed.py","line":12,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/jedi/inference/imports.py","line":29,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/jedi/inference/__init__.py","line":70,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/jedi/api/classes.py","line":24,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/jedi/api/__init__.py","line":21,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/jedi/__init__.py","line":32,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/IPython/core/completer.py","line":250,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/IPython/terminal/debugger.py","line":6,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/IPython/terminal/interactiveshell.py","line":48,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/jedi/inference/gradual/stub_value.py","line":79,"col":null},{"name":"_path_is_mode_type","file":"","line":148,"col":null},{"name":"find_spec","file":"","line":1541,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/jedi/api/keywords.py","line":8,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/jedi/api/classes.py","line":31,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/jedi/plugins/stdlib.py","line":353,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/jedi/plugins/registry.py","line":5,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/jedi/__init__.py","line":41,"col":null},{"name":"_get_spec","file":"","line":1511,"col":null},{"name":"find_spec","file":"","line":1556,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/jedi/plugins/registry.py","line":6,"col":null},{"name":"getmembers","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/traitlets/traitlets.py","line":245,"col":null},{"name":"setup_class","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/traitlets/traitlets.py","line":985,"col":null},{"name":"setup_class","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/traitlets/traitlets.py","line":1002,"col":null},{"name":"__init__","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/traitlets/traitlets.py","line":970,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/IPython/terminal/interactiveshell.py","line":188,"col":null},{"name":"source_to_code","file":"","line":913,"col":null},{"name":"get_code","file":"","line":983,"col":null},{"name":"__getitem__","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":4714,"col":null},{"name":"__getitem__","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":5869,"col":null},{"name":"__contains__","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":4791,"col":null},{"name":"__contains__","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":5876,"col":null},{"name":"_perform_update","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":3872,"col":null},{"name":"update","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":5122,"col":null},{"name":"update_layout","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":1392,"col":null},{"name":"update_layout","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/graph_objs/_figure.py","line":787,"col":null},{"name":"make_subplots","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/_subplots.py","line":881,"col":null},{"name":"get_validator","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/validator_cache.py","line":28,"col":null},{"name":"_get_validator","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":4330,"col":null},{"name":"__getitem__","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":4709,"col":null},{"name":"_resolve_name","file":"","line":889,"col":null},{"name":"_gcd_import","file":"","line":1029,"col":null},{"name":"__getattr__","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/graph_objects/__init__.py","line":303,"col":null},{"name":"get_validator","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/validator_cache.py","line":20,"col":null},{"name":"_verbose_message","file":"","line":236,"col":null},{"name":"find_spec","file":"","line":1553,"col":null},{"name":"__setitem__","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":4848,"col":null},{"name":"__init__","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/graph_objs/carpet/_baxis.py","line":2313,"col":null},{"name":"__init__","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/graph_objs/_carpet.py","line":1906,"col":null},{"name":"validate_coerce","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/_plotly_utils/basevalidators.py","line":2604,"col":null},{"name":"_set_array_prop","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":5337,"col":null},{"name":"__setitem__","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":4856,"col":null},{"name":"__init__","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/graph_objs/layout/template/_data.py","line":1611,"col":null},{"name":"__init__","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/graph_objs/layout/_template.py","line":327,"col":null},{"name":"validate_coerce","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/_plotly_utils/basevalidators.py","line":2516,"col":null},{"name":"validate_coerce","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/_plotly_utils/basevalidators.py","line":2805,"col":null},{"name":"_perform_update","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":3896,"col":null},{"name":"update","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":5123,"col":null},{"name":"make_figure","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/express/_core.py","line":2614,"col":null},{"name":"__init__","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/validators/carpet/baxis/_minorgridcolor.py","line":8,"col":null},{"name":"__init__","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/graph_objs/carpet/_baxis.py","line":2325,"col":null},{"name":"_path_join","file":"","line":123,"col":null},{"name":"cache_from_source","file":"","line":429,"col":null},{"name":"__init__","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/graph_objs/_choropleth.py","line":2172,"col":null},{"name":"__init__","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/graph_objs/layout/template/_data.py","line":1623,"col":null},{"name":"_classify_pyc","file":"","line":577,"col":null},{"name":"get_code","file":"","line":950,"col":null},{"name":"__init__","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/graph_objs/_histogram2dcontour.py","line":3057,"col":null},{"name":"__init__","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/graph_objs/layout/template/_data.py","line":1659,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/validators/histogram2d/_colorscale.py","line":4,"col":null},{"name":"__init__","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/graph_objs/_histogram2d.py","line":2914,"col":null},{"name":"__init__","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/graph_objs/layout/template/_data.py","line":1663,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/graph_objs/scattercarpet/marker/_colorbar.py","line":5,"col":null},{"name":"__init__","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/graph_objs/scattercarpet/_marker.py","line":1622,"col":null},{"name":"__init__","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/graph_objs/_scattercarpet.py","line":2364,"col":null},{"name":"__init__","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/graph_objs/layout/template/_data.py","line":1715,"col":null},{"name":"__getattr__","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/graph_objs/__init__.py","line":303,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/graph_objs/layout/template/data/_scattergeo.py","line":1,"col":null},{"name":"data_class","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/_plotly_utils/basevalidators.py","line":2588,"col":null},{"name":"validate_coerce","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/_plotly_utils/basevalidators.py","line":2601,"col":null},{"name":"__init__","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/graph_objs/layout/template/_data.py","line":1719,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/graph_objs/scattergl/marker/_colorbar.py","line":5,"col":null},{"name":"__init__","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/graph_objs/scattergl/_marker.py","line":1445,"col":null},{"name":"__init__","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/graph_objs/_scattergl.py","line":2883,"col":null},{"name":"__init__","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/graph_objs/layout/template/_data.py","line":1723,"col":null},{"name":"__init__","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/graph_objs/layout/_ternary.py","line":1036,"col":null},{"name":"__init__","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/graph_objs/_layout.py","line":7128,"col":null},{"name":"__init__","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/graph_objs/layout/_template.py","line":331,"col":null},{"name":"_find_and_load_unlocked","file":"","line":969,"col":null},{"name":"__init__","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/graph_objs/layout/_ternary.py","line":1038,"col":null},{"name":"__init__","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/graph_objs/layout/ternary/_caxis.py","line":1751,"col":null},{"name":"__init__","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/graph_objs/layout/_xaxis.py","line":4465,"col":null},{"name":"__init__","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/graph_objs/_layout.py","line":7188,"col":null},{"name":"_deepcopy_list","file":"/home/fbruzzesi/.local/share/uv/python/cpython-3.9.19-linux-x86_64-gnu/lib/python3.9/copy.py","line":204,"col":null},{"name":"deepcopy","file":"/home/fbruzzesi/.local/share/uv/python/cpython-3.9.19-linux-x86_64-gnu/lib/python3.9/copy.py","line":146,"col":null},{"name":"_deepcopy_dict","file":"/home/fbruzzesi/.local/share/uv/python/cpython-3.9.19-linux-x86_64-gnu/lib/python3.9/copy.py","line":230,"col":null},{"name":"_set_compound_prop","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":5274,"col":null},{"name":"unique_with_mask","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/algorithms.py","line":438,"col":null},{"name":"get_group_index","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/sorting.py","line":193,"col":null},{"name":"compress_group_index","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/sorting.py","line":695,"col":null},{"name":"_get_compressed_codes","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/groupby/ops.py","line":765,"col":null},{"name":"compress_group_index","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/sorting.py","line":710,"col":null},{"name":"get_flattened_list","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/sorting.py","line":605,"col":null},{"name":"group_keys_seq","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/groupby/ops.py","line":648,"col":null},{"name":"get_iterator","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/groupby/ops.py","line":619,"col":null},{"name":"attrs","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/generic.py","line":399,"col":null},{"name":"__finalize__","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/generic.py","line":6255,"col":null},{"name":"_chop","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/groupby/ops.py","line":1188,"col":null},{"name":"__iter__","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/groupby/ops.py","line":1160,"col":null},{"name":"_get_module_lock","file":"","line":177,"col":null},{"name":"_find_and_load","file":"","line":1014,"col":null},{"name":"make_figure","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/express/_core.py","line":2422,"col":null},{"name":"copy_to_readonly_numpy_array","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/_plotly_utils/basevalidators.py","line":149,"col":null},{"name":"copy_to_readonly_numpy_array","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/_plotly_utils/basevalidators.py","line":128,"col":null},{"name":"deepcopy","file":"/home/fbruzzesi/.local/share/uv/python/cpython-3.9.19-linux-x86_64-gnu/lib/python3.9/copy.py","line":153,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":2250,"col":null},{"name":"add_traces","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":2250,"col":null},{"name":"add_traces","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/graph_objs/_figure.py","line":992,"col":null},{"name":"make_figure","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/express/_core.py","line":2611,"col":null},{"name":"fullmatch","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/_plotly_utils/basevalidators.py","line":18,"col":null},{"name":"perform_validate_coerce","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/_plotly_utils/basevalidators.py","line":1476,"col":null},{"name":"vc_scalar","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/_plotly_utils/basevalidators.py","line":1439,"col":null},{"name":"validate_coerce","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/_plotly_utils/basevalidators.py","line":1409,"col":null},{"name":"__init__","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/graph_objs/_layout.py","line":7044,"col":null},{"name":"perform_validate_coerce","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/_plotly_utils/basevalidators.py","line":1470,"col":null},{"name":"__init__","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/graph_objs/layout/polar/_angularaxis.py","line":2094,"col":null},{"name":"__init__","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/graph_objs/layout/_polar.py","line":1086,"col":null},{"name":"__init__","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/graph_objs/_layout.py","line":7056,"col":null},{"name":"perform_validate_coerce","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/_plotly_utils/basevalidators.py","line":1473,"col":null},{"name":"split_multichar","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/_plotly_utils/utils.py","line":408,"col":null},{"name":"_str_to_dict_path_full","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":70,"col":null},{"name":"_str_to_dict_path","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":1847,"col":null},{"name":"__contains__","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":4776,"col":null},{"name":"configure_cartesian_axes","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/express/_core.py","line":680,"col":null},{"name":"configure_axes","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/express/_core.py","line":573,"col":null},{"name":"make_figure","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/express/_core.py","line":2651,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/narwhals/utils.py","line":132,"col":null},{"name":"parse_version","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/narwhals/utils.py","line":132,"col":null},{"name":"_from_native_impl","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/narwhals/translate.py","line":477,"col":null},{"name":"_stack_arrays","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/internals/managers.py","line":2252,"col":null},{"name":"__iter__","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/groupby/ops.py","line":1157,"col":null},{"name":"_isna_array","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/dtypes/missing.py","line":300,"col":null},{"name":"__setattr__","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":4914,"col":null},{"name":"__init__","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/graph_objs/layout/_xaxis.py","line":4497,"col":null},{"name":"split_multichar","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/_plotly_utils/utils.py","line":409,"col":null},{"name":"_set_in","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":1881,"col":null},{"name":"_perform_plotly_relayout","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":2656,"col":null},{"name":"_perform_plotly_update","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":2964,"col":null},{"name":"plotly_update","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":2916,"col":null},{"name":"batch_update","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":3063,"col":null},{"name":"__exit__","file":"/home/fbruzzesi/.local/share/uv/python/cpython-3.9.19-linux-x86_64-gnu/lib/python3.9/contextlib.py","line":126,"col":null},{"name":"make_figure","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/express/_core.py","line":2612,"col":null},{"name":"_get_validator","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":4328,"col":null},{"name":"_dispatch_layout_change_callbacks","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":2808,"col":null},{"name":"plotly_update","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":2941,"col":null},{"name":"_set_compound_prop","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":5262,"col":null},{"name":"__init__","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/graph_objs/layout/_hoverlabel.py","line":435,"col":null},{"name":"__init__","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/graph_objs/_layout.py","line":6976,"col":null},{"name":"_vals_equal","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":5647,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":5655,"col":null},{"name":"_vals_equal","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":5655,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":5662,"col":null},{"name":"_vals_equal","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":5662,"col":null},{"name":"_set_compound_prop","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":5290,"col":null},{"name":"get_group_index","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/sorting.py","line":196,"col":null},{"name":"_perform_update","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":3905,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/express/_core.py","line":244,"col":null},{"name":"make_figure","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/express/_core.py","line":2433,"col":null},{"name":"_strip_subplot_suffix_of_1","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":5826,"col":null},{"name":"__getitem__","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":5868,"col":null},{"name":"_check_path_in_prop_tree","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":185,"col":null},{"name":"_perform_update","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":3880,"col":null},{"name":"__getitem__","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":4711,"col":null},{"name":"native_to_narwhals_dtype","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/narwhals/_pandas_like/utils.py","line":222,"col":null},{"name":"infer_config","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/express/_core.py","line":2116,"col":null},{"name":"maybe_convert_indices","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/indexers/utils.py","line":280,"col":null},{"name":"take","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/internals/managers.py","line":891,"col":null},{"name":"_make_underscore_key","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":86,"col":null},{"name":"_str_to_dict_path_full","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":113,"col":null},{"name":"_check_path_in_prop_tree","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":179,"col":null},{"name":"_build_dispatch_plan","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":2780,"col":null},{"name":"_dispatch_layout_change_callbacks","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":2803,"col":null},{"name":"_int64_cut_off","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/sorting.py","line":160,"col":null},{"name":"get_group_index","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/sorting.py","line":182,"col":null},{"name":"get_group_index","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/sorting.py","line":186,"col":null},{"name":"__getitem__","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/frame.py","line":4070,"col":null},{"name":"get_column","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/narwhals/_pandas_like/dataframe.py","line":107,"col":null},{"name":"get_column","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/narwhals/dataframe.py","line":705,"col":null},{"name":"make_trace_kwargs","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/express/_core.py","line":525,"col":null},{"name":"make_figure","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/express/_core.py","line":2528,"col":null},{"name":"_make_hyphen_key","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":81,"col":null},{"name":"_str_to_dict_path_full","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":88,"col":null},{"name":"yaxis","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/graph_objs/_layout.py","line":5578,"col":null},{"name":"_prop_set_child","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":5428,"col":null},{"name":"_relayout_child","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":5462,"col":null},{"name":"_send_prop_set","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":5683,"col":null},{"name":"_set_prop","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":5233,"col":null},{"name":"_perform_update","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":3861,"col":null},{"name":"deepcopy","file":"/home/fbruzzesi/.local/share/uv/python/cpython-3.9.19-linux-x86_64-gnu/lib/python3.9/copy.py","line":134,"col":null},{"name":"to_plotly_json","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":5594,"col":null},{"name":"__init__","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/graph_objs/layout/_template.py","line":306,"col":null},{"name":"match","file":"/home/fbruzzesi/.local/share/uv/python/cpython-3.9.19-linux-x86_64-gnu/lib/python3.9/re.py","line":191,"col":null},{"name":"fullmatch","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/_plotly_utils/basevalidators.py","line":22,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/_plotly_utils/basevalidators.py","line":1666,"col":null},{"name":"validate_coerce","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/_plotly_utils/basevalidators.py","line":1657,"col":null},{"name":"__init__","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/graph_objs/_contour.py","line":3207,"col":null},{"name":"__init__","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/graph_objs/layout/template/_data.py","line":1635,"col":null},{"name":"deepcopy","file":"/home/fbruzzesi/.local/share/uv/python/cpython-3.9.19-linux-x86_64-gnu/lib/python3.9/copy.py","line":176,"col":null},{"name":"_deepcopy_list","file":"/home/fbruzzesi/.local/share/uv/python/cpython-3.9.19-linux-x86_64-gnu/lib/python3.9/copy.py","line":205,"col":null},{"name":"__init__","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":4308,"col":null},{"name":"__init__","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":5933,"col":null},{"name":"__init__","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/graph_objs/scattergl/_marker.py","line":1377,"col":null},{"name":"_subplot_re_match","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/graph_objs/_layout.py","line":63,"col":null},{"name":"_strip_subplot_suffix_of_1","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":5836,"col":null},{"name":"_is_key_path_compatible","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":2683,"col":null},{"name":"_perform_plotly_relayout","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":2645,"col":null},{"name":"init_figure","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/express/_core.py","line":2748,"col":null},{"name":"is_hashable","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/dtypes/inference.py","line":365,"col":null},{"name":"maybe_extract_name","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/indexes/base.py","line":7698,"col":null},{"name":"__init__","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/series.py","line":487,"col":null},{"name":"reset_index","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/series.py","line":1753,"col":null},{"name":"is_bool_indexer","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/common.py","line":125,"col":null},{"name":"_getitem_axis","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/indexing.py","line":1412,"col":null},{"name":"__getitem__","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/indexing.py","line":1191,"col":null},{"name":"native_to_narwhals_dtype","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/narwhals/_pandas_like/utils.py","line":288,"col":null},{"name":"__getitem__","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":4712,"col":null},{"name":"_dispatch_layout_change_callbacks","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":2809,"col":null},{"name":"validate_coerce","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/_plotly_utils/basevalidators.py","line":2799,"col":null},{"name":"__init__","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/graph_objs/scatter3d/marker/_colorbar.py","line":2216,"col":null},{"name":"__init__","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/graph_objs/scatter3d/_marker.py","line":1263,"col":null},{"name":"__init__","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/graph_objs/_scatter3d.py","line":2697,"col":null},{"name":"__init__","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/graph_objs/layout/template/_data.py","line":1711,"col":null},{"name":"perform_validate_coerce","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/_plotly_utils/basevalidators.py","line":1481,"col":null},{"name":"__init__","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/graph_objs/layout/scene/_xaxis.py","line":2600,"col":null},{"name":"__init__","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/graph_objs/layout/_scene.py","line":1773,"col":null},{"name":"__init__","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/graph_objs/_layout.py","line":7068,"col":null},{"name":"__init__","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/graph_objs/layout/scene/_yaxis.py","line":2584,"col":null},{"name":"__init__","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/graph_objs/layout/_scene.py","line":1777,"col":null},{"name":"maybe_convert_indices","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/indexers/utils.py","line":274,"col":null},{"name":"reindex_indexer","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/internals/managers.py","line":704,"col":null},{"name":"chomp_empty_strings","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/_plotly_utils/utils.py","line":512,"col":null},{"name":"chomp_empty_strings","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/_plotly_utils/utils.py","line":506,"col":null},{"name":"_split_and_chomp","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":99,"col":null},{"name":"_str_to_dict_path_full","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":106,"col":null},{"name":"__init__","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/graph_objs/_scattergl.py","line":2967,"col":null},{"name":"validate_coerce","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/_plotly_utils/basevalidators.py","line":2721,"col":null},{"name":"add_traces","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":2191,"col":null},{"name":"__init__","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/graph_objs/_scattergl.py","line":3003,"col":null},{"name":"validate_coerce","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/_plotly_utils/basevalidators.py","line":1807,"col":null},{"name":"__init__","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/graph_objs/_scattergl.py","line":3011,"col":null},{"name":"__getitem__","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":741,"col":null},{"name":"layout","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":2545,"col":null},{"name":"_is_key_path_compatible","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":2673,"col":null},{"name":"__init__","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/graph_objs/layout/scene/_xaxis.py","line":2643,"col":null},{"name":"key","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/_plotly_utils/utils.py","line":374,"col":null},{"name":"_natural_sort_strings","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/_plotly_utils/utils.py","line":380,"col":null},{"name":"_select_layout_subplots_by_prefix","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":1440,"col":null},{"name":"select_yaxes","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/graph_objs/_figure.py","line":23186,"col":null},{"name":"configure_cartesian_axes","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/express/_core.py","line":672,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":1442,"col":null},{"name":"_select_layout_subplots_by_prefix","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":1442,"col":null},{"name":"configure_cartesian_axes","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/express/_core.py","line":673,"col":null},{"name":"_get_module_lock","file":"","line":185,"col":null},{"name":"__enter__","file":"","line":157,"col":null},{"name":"get_validator","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/validator_cache.py","line":29,"col":null},{"name":"shape","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/graph_objs/bar/marker/_pattern.py","line":252,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/express/_core.py","line":1016,"col":null},{"name":"apply_default_cascade","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/express/_core.py","line":1015,"col":null},{"name":"make_figure","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/express/_core.py","line":2340,"col":null},{"name":"bar","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/express/_chart_types.py","line":373,"col":null},{"name":"figure_generation_bar","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/t.py","line":65,"col":null},{"name":"test_all_charts","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/t.py","line":83,"col":null},{"name":"vstack","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/numpy/core/shape_base.py","line":289,"col":null},{"name":"_merge_blocks","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/internals/managers.py","line":2294,"col":null},{"name":"_consolidate","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/internals/managers.py","line":2269,"col":null},{"name":"_consolidate_inplace","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/internals/managers.py","line":1788,"col":null},{"name":"create_block_manager_from_column_arrays","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/internals/managers.py","line":2144,"col":null},{"name":"_merge_blocks","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/internals/managers.py","line":2301,"col":null},{"name":"_subplot_type_for_trace_type","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/_subplots.py","line":1073,"col":null},{"name":"make_figure","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/express/_core.py","line":2377,"col":null},{"name":"is_homogeneous_array","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/_plotly_utils/basevalidators.py","line":201,"col":null},{"name":"validate_coerce","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/_plotly_utils/basevalidators.py","line":407,"col":null},{"name":"copy_to_readonly_numpy_array","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/_plotly_utils/basevalidators.py","line":152,"col":null},{"name":"__getitem__","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":4719,"col":null},{"name":"__setitem__","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":4867,"col":null},{"name":"template","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/graph_objs/_layout.py","line":3828,"col":null},{"name":"__setattr__","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":4916,"col":null},{"name":"__setattr__","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":5912,"col":null},{"name":"_initialize_layout_template","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":2532,"col":null},{"name":"__init__","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":630,"col":null},{"name":"__init__","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/graph_objs/layout/_geo.py","line":1412,"col":null},{"name":"deepcopy","file":"/home/fbruzzesi/.local/share/uv/python/cpython-3.9.19-linux-x86_64-gnu/lib/python3.9/copy.py","line":142,"col":null},{"name":"deepcopy","file":"/home/fbruzzesi/.local/share/uv/python/cpython-3.9.19-linux-x86_64-gnu/lib/python3.9/copy.py","line":178,"col":null},{"name":"deepcopy","file":"/home/fbruzzesi/.local/share/uv/python/cpython-3.9.19-linux-x86_64-gnu/lib/python3.9/copy.py","line":139,"col":null},{"name":"deepcopy","file":"/home/fbruzzesi/.local/share/uv/python/cpython-3.9.19-linux-x86_64-gnu/lib/python3.9/copy.py","line":144,"col":null},{"name":"deepcopy","file":"/home/fbruzzesi/.local/share/uv/python/cpython-3.9.19-linux-x86_64-gnu/lib/python3.9/copy.py","line":138,"col":null},{"name":"deepcopy","file":"/home/fbruzzesi/.local/share/uv/python/cpython-3.9.19-linux-x86_64-gnu/lib/python3.9/copy.py","line":175,"col":null},{"name":"deepcopy","file":"/home/fbruzzesi/.local/share/uv/python/cpython-3.9.19-linux-x86_64-gnu/lib/python3.9/copy.py","line":137,"col":null},{"name":"deepcopy","file":"/home/fbruzzesi/.local/share/uv/python/cpython-3.9.19-linux-x86_64-gnu/lib/python3.9/copy.py","line":145,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/_plotly_utils/basevalidators.py","line":1675,"col":null},{"name":"validate_coerce","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/_plotly_utils/basevalidators.py","line":1674,"col":null},{"name":"__init__","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/generic.py","line":282,"col":null},{"name":"__init__","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/series.py","line":592,"col":null},{"name":"validate_coerce","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/_plotly_utils/basevalidators.py","line":2523,"col":null},{"name":"__init__","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/graph_objs/bar/_marker.py","line":1215,"col":null},{"name":"__init__","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/graph_objs/_bar.py","line":3313,"col":null},{"name":"__init__","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/graph_objs/_bar.py","line":3425,"col":null},{"name":"_deepcopy_atomic","file":"/home/fbruzzesi/.local/share/uv/python/cpython-3.9.19-linux-x86_64-gnu/lib/python3.9/copy.py","line":182,"col":null},{"name":"deepcopy","file":"/home/fbruzzesi/.local/share/uv/python/cpython-3.9.19-linux-x86_64-gnu/lib/python3.9/copy.py","line":128,"col":null},{"name":"add_traces","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":2255,"col":null},{"name":"_deepcopy_list","file":"/home/fbruzzesi/.local/share/uv/python/cpython-3.9.19-linux-x86_64-gnu/lib/python3.9/copy.py","line":201,"col":null},{"name":"","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":5348,"col":null},{"name":"_set_array_prop","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":5348,"col":null},{"name":"__init__","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/graph_objs/layout/template/_data.py","line":1655,"col":null},{"name":"allows_duplicate_labels","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/flags.py","line":90,"col":null},{"name":"__finalize__","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/generic.py","line":6262,"col":null},{"name":"dtype","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/series.py","line":718,"col":null},{"name":"__init__","file":"","line":63,"col":null},{"name":"_get_module_lock","file":"","line":183,"col":null},{"name":"make_figure","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/express/_core.py","line":2407,"col":null},{"name":"get_validator","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/validator_cache.py","line":12,"col":null},{"name":"update","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":5126,"col":null},{"name":"make_figure","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/express/_core.py","line":2423,"col":null},{"name":"__init__","file":"","line":61,"col":null},{"name":"copy_to_readonly_numpy_array","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/_plotly_utils/basevalidators.py","line":73,"col":null},{"name":"validate_coerce","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/_plotly_utils/basevalidators.py","line":806,"col":null},{"name":"validate_coerce","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/_plotly_utils/basevalidators.py","line":2240,"col":null},{"name":"__init__","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/graph_objs/layout/_yaxis.py","line":4128,"col":null},{"name":"_set_subplotid_prop","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":5805,"col":null},{"name":"__setitem__","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":5901,"col":null},{"name":"_init_subplot_xy","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/_subplots.py","line":990,"col":null},{"name":"_str_to_dict_path","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":1837,"col":null},{"name":"update_axis_matches","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/_subplots.py","line":916,"col":null},{"name":"_configure_shared_axes","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/_subplots.py","line":963,"col":null},{"name":"make_subplots","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/_subplots.py","line":757,"col":null},{"name":"_deepcopy_atomic","file":"/home/fbruzzesi/.local/share/uv/python/cpython-3.9.19-linux-x86_64-gnu/lib/python3.9/copy.py","line":183,"col":null},{"name":"__getitem__","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":4750,"col":null},{"name":"__init__","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/graph_objs/barpolar/marker/_pattern.py","line":560,"col":null},{"name":"__init__","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/graph_objs/barpolar/_marker.py","line":1176,"col":null},{"name":"__init__","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/graph_objs/_barpolar.py","line":1924,"col":null},{"name":"__init__","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/graph_objs/layout/template/_data.py","line":1595,"col":null},{"name":"_vals_equal","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":5665,"col":null},{"name":"make_figure","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/express/_core.py","line":2418,"col":null},{"name":"_make_hyphen_key","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":83,"col":null},{"name":"_str_to_dict_path_full","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":114,"col":null},{"name":"__init__","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/graph_objs/_bar.py","line":3469,"col":null},{"name":"value","file":"/home/fbruzzesi/.local/share/uv/python/cpython-3.9.19-linux-x86_64-gnu/lib/python3.9/enum.py","line":795,"col":null},{"name":"__get__","file":"/home/fbruzzesi/.local/share/uv/python/cpython-3.9.19-linux-x86_64-gnu/lib/python3.9/types.py","line":178,"col":null},{"name":"_compile","file":"/home/fbruzzesi/.local/share/uv/python/cpython-3.9.19-linux-x86_64-gnu/lib/python3.9/re.py","line":292,"col":null},{"name":"filterwarnings","file":"/home/fbruzzesi/.local/share/uv/python/cpython-3.9.19-linux-x86_64-gnu/lib/python3.9/warnings.py","line":155,"col":null},{"name":"__iter__","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/narwhals/_pandas_like/group_by.py","line":102,"col":null},{"name":"_check_object_for_strings","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/algorithms.py","line":292,"col":null},{"name":"_get_child_props","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":4450,"col":null},{"name":"_props","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":4429,"col":null},{"name":"_get_child_props","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":4458,"col":null},{"name":"__getitem__","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":4734,"col":null},{"name":"__setitem__","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":4891,"col":null},{"name":"get_label","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/express/_core.py","line":144,"col":null},{"name":"get_decorated_label","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/express/_core.py","line":164,"col":null},{"name":"make_trace_kwargs","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/express/_core.py","line":288,"col":null},{"name":"is_homogeneous_array","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/_plotly_utils/basevalidators.py","line":203,"col":null},{"name":"_any","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/numpy/core/_methods.py","line":58,"col":null},{"name":"nanany","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/nanops.py","line":520,"col":null},{"name":"_reduce","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/series.py","line":6457,"col":null},{"name":"any","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/series.py","line":6471,"col":null},{"name":"perform_replacemenet","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/_plotly_utils/basevalidators.py","line":518,"col":null},{"name":"validate_coerce","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/_plotly_utils/basevalidators.py","line":617,"col":null},{"name":"__init__","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/graph_objs/layout/_annotation.py","line":2100,"col":null},{"name":"make_subplots","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/_subplots.py","line":838,"col":null},{"name":"_set_compound_prop","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":5269,"col":null},{"name":"__init__","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/series.py","line":574,"col":null},{"name":"__getitem__","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":4690,"col":null},{"name":"compress_group_index","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/sorting.py","line":705,"col":null},{"name":"_amax","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/numpy/core/_methods.py","line":41,"col":null},{"name":"take","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/indexes/range.py","line":1168,"col":null},{"name":"take","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/internals/managers.py","line":893,"col":null},{"name":"_get_child_props","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":4455,"col":null},{"name":"__contains__","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":5875,"col":null},{"name":"_str_to_dict_path_full","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":68,"col":null},{"name":"_get_child_props","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":4449,"col":null},{"name":"__getitem__","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":4735,"col":null},{"name":"sequential","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/graph_objs/layout/_colorscale.py","line":98,"col":null},{"name":"apply_default_cascade","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/express/_core.py","line":974,"col":null},{"name":"__init__","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/series.py","line":521,"col":null},{"name":"_get_item_cache","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/frame.py","line":4628,"col":null},{"name":"__getitem__","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/pandas/core/frame.py","line":4078,"col":null},{"name":"process_args_into_dataframe","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/express/_core.py","line":1295,"col":null},{"name":"release","file":"","line":123,"col":null},{"name":"make_figure","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/express/_core.py","line":2406,"col":null},{"name":"__init__","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/basedatatypes.py","line":5678,"col":null},{"name":"__init__","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/graph_objs/layout/template/_data.py","line":1563,"col":null},{"name":"copy","file":"/home/fbruzzesi/.local/share/uv/python/cpython-3.9.19-linux-x86_64-gnu/lib/python3.9/copy.py","line":76,"col":null},{"name":"__init__","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/plotly/graph_objs/barpolar/marker/_pattern.py","line":509,"col":null},{"name":"write_csv","file":"/home/fbruzzesi/open-source/plotly.py/.py39/lib/python3.9/site-packages/polars/dataframe/frame.py","line":2896,"col":null},{"name":"run_and_save_results","file":"/home/fbruzzesi/open-source/plotly.py/packages/python/plotly/t.py","line":101,"col":null}]},"activeProfileIndex":null,"exporter":"py-spy@0.3.14","name":"py-spy profile"} From d3a28c0650a84e7e3e961cd732776726302a61dd Mon Sep 17 00:00:00 2001 From: FBruzzesi Date: Sun, 27 Oct 2024 09:50:34 +0100 Subject: [PATCH 064/106] feedback adjustments --- packages/python/plotly/plotly/express/_core.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/packages/python/plotly/plotly/express/_core.py b/packages/python/plotly/plotly/express/_core.py index 30906c5745..d980e5969b 100644 --- a/packages/python/plotly/plotly/express/_core.py +++ b/packages/python/plotly/plotly/express/_core.py @@ -10,7 +10,6 @@ import math import narwhals.stable.v1 as nw -from narwhals.dependencies import is_into_series from narwhals.utils import generate_unique_token from plotly._subplots import ( @@ -276,9 +275,7 @@ def make_trace_kwargs(args, trace_spec, trace_data, mapping_labels, sizeref): df: nw.DataFrame = args["data_frame"] if "line_close" in args and args["line_close"]: - trace_data = nw.maybe_reset_index( - nw.concat([trace_data, trace_data.head(1)], how="vertical") - ) + trace_data = nw.concat([trace_data, trace_data.head(1)], how="vertical") trace_patch = trace_spec.trace_patch.copy() or {} fit_results = None @@ -1050,7 +1047,7 @@ def _get_reserved_col_names(args): continue elif isinstance(arg, str): # no need to add ints since kw arg are not ints reserved_names.add(arg) - elif is_into_series(arg): + elif nw.dependencies.is_into_series(arg): arg_series = nw.from_native(arg, series_only=True) arg_name = arg_series.name if arg_name and arg_name in df.columns: From e6e9994108e5898bd2fc3bbdf4e2742077cef239 Mon Sep 17 00:00:00 2001 From: Marco Gorelli <33491632+MarcoGorelli@users.noreply.github.com> Date: Fri, 25 Oct 2024 17:04:28 +0100 Subject: [PATCH 065/106] use drop_null_keys, some pandas fastpaths --- .../plotly/_plotly_utils/basevalidators.py | 16 +++++++++++++++- packages/python/plotly/plotly/express/_core.py | 17 ++++++++--------- .../python/plotly/plotly/express/_imshow.py | 1 - .../plotly/figure_factory/_hexbin_mapbox.py | 2 +- 4 files changed, 24 insertions(+), 12 deletions(-) diff --git a/packages/python/plotly/_plotly_utils/basevalidators.py b/packages/python/plotly/_plotly_utils/basevalidators.py index 21731afad4..251141fe47 100644 --- a/packages/python/plotly/_plotly_utils/basevalidators.py +++ b/packages/python/plotly/_plotly_utils/basevalidators.py @@ -8,6 +8,7 @@ import re import sys import warnings +import narwhals.stable.v1 as nw from _plotly_utils.optional_imports import get_module @@ -93,8 +94,19 @@ def copy_to_readonly_numpy_array(v, kind=None, force_numeric=False): "O": "object", } - # Handle pandas Series and Index objects + if isinstance(v, nw.Series): + if nw.dependencies.is_pandas_like_series(v_native := v.to_native()): + v = v_native + else: + v = v.to_numpy() + elif isinstance(v, nw.DataFrame): + if nw.dependencies.is_pandas_like_dataframe(v_native := v.to_native()): + v = v_native + else: + v = v.to_numpy() + if pd and isinstance(v, (pd.Series, pd.Index)): + # Handle pandas Series and Index objects if v.dtype.kind in numeric_kinds: # Get the numeric numpy array so we use fast path below v = v.values @@ -189,10 +201,12 @@ def is_homogeneous_array(v): """ np = get_module("numpy", should_load=False) pd = get_module("pandas", should_load=False) + import narwhals as nw if ( np and isinstance(v, np.ndarray) or (pd and isinstance(v, (pd.Series, pd.Index))) + or (isinstance(v, nw.Series)) ): return True if is_numpy_convertable(v): diff --git a/packages/python/plotly/plotly/express/_core.py b/packages/python/plotly/plotly/express/_core.py index d980e5969b..773c406b10 100644 --- a/packages/python/plotly/plotly/express/_core.py +++ b/packages/python/plotly/plotly/express/_core.py @@ -156,6 +156,8 @@ def invert_label(args, column): def _is_continuous(df: nw.DataFrame, col_name: str) -> bool: + if nw.dependencies.is_pandas_like_dataframe(df_native := df.to_native()): + return df_native[col_name].dtype.kind in 'ifc' return df.get_column(col_name).dtype.is_numeric() @@ -1114,15 +1116,12 @@ def to_unindexed_series(x, name=None, native_namespace=None): itx index reset if pandas-like). Stripping the index from existing pd.Series is required to get things to match up right in the new DataFrame we're building. """ - x_native = nw.to_native(x, strict=False) - if nw.dependencies.is_pandas_like_series(x_native): - return nw.from_native( - x_native.__class__(x_native, name=name).reset_index(drop=True), - series_only=True, - ) x = nw.from_native(x, series_only=True, strict=False) if isinstance(x, nw.Series): - return x.rename(name) + if name == x.name: + # Avoid potentially creating a copy in pre-copy-on-write pandas + return nw.maybe_reset_index(x) + return nw.maybe_reset_index(x).rename(name) elif native_namespace is not None: return nw.new_series(name=name, values=x, native_namespace=native_namespace) else: @@ -1907,7 +1906,7 @@ def post_agg(dframe: nw.LazyFrame, continuous_aggs, discrete_aggs) -> nw.LazyFra for i, level in enumerate(path): dfg = ( - df.group_by(path[i:]) + df.group_by(path[i:], drop_null_keys=True) .agg(**agg_f) .pipe(post_agg, continuous_aggs, discrete_aggs) ) @@ -2307,7 +2306,7 @@ def get_groups_and_orders(args, grouper): groups = {tuple(single_group_name): df} else: required_grouper = list(orders.keys()) - grouped = dict(df.group_by(required_grouper).__iter__()) + grouped = dict(df.group_by(required_grouper, drop_null_keys=True).__iter__()) sorted_group_names = list(grouped.keys()) for i, col in reversed(list(enumerate(required_grouper))): diff --git a/packages/python/plotly/plotly/express/_imshow.py b/packages/python/plotly/plotly/express/_imshow.py index d60a47b5f4..225bdb4515 100644 --- a/packages/python/plotly/plotly/express/_imshow.py +++ b/packages/python/plotly/plotly/express/_imshow.py @@ -326,7 +326,6 @@ def imshow( if binary_string: raise ValueError("Binary strings cannot be used with pandas arrays") is_dataframe = True - img = img.to_numpy() else: is_dataframe = False diff --git a/packages/python/plotly/plotly/figure_factory/_hexbin_mapbox.py b/packages/python/plotly/plotly/figure_factory/_hexbin_mapbox.py index f62513b49b..0bda32a973 100644 --- a/packages/python/plotly/plotly/figure_factory/_hexbin_mapbox.py +++ b/packages/python/plotly/plotly/figure_factory/_hexbin_mapbox.py @@ -407,7 +407,7 @@ def create_hexbin_mapbox( center = dict(lat=lat_range.mean(), lon=lon_range.mean()) if args["animation_frame"] is not None: - groups = dict(args["data_frame"].group_by(args["animation_frame"]).__iter__()) + groups = dict(args["data_frame"].group_by(args["animation_frame"], drop_null_keys=True).__iter__()) else: groups = {(0,): args["data_frame"]} From 64b8c700e60198b2f526b576a4334c0732729f31 Mon Sep 17 00:00:00 2001 From: Marco Gorelli <33491632+MarcoGorelli@users.noreply.github.com> Date: Sun, 27 Oct 2024 15:56:05 +0000 Subject: [PATCH 066/106] bump narwhals version --- packages/python/plotly/_plotly_utils/basevalidators.py | 1 - packages/python/plotly/optional-requirements.txt | 2 +- packages/python/plotly/plotly/express/_core.py | 6 +++--- packages/python/plotly/requirements.txt | 2 +- packages/python/plotly/setup.py | 2 +- .../plotly/test_requirements/requirements_310_optional.txt | 2 +- .../plotly/test_requirements/requirements_311_optional.txt | 2 +- .../requirements_312_no_numpy_optional.txt | 2 +- .../plotly/test_requirements/requirements_312_optional.txt | 2 +- .../plotly/test_requirements/requirements_38_optional.txt | 2 +- .../plotly/test_requirements/requirements_39_optional.txt | 2 +- .../test_requirements/requirements_39_pandas_2_optional.txt | 2 +- 12 files changed, 13 insertions(+), 14 deletions(-) diff --git a/packages/python/plotly/_plotly_utils/basevalidators.py b/packages/python/plotly/_plotly_utils/basevalidators.py index 251141fe47..47cf39d4d8 100644 --- a/packages/python/plotly/_plotly_utils/basevalidators.py +++ b/packages/python/plotly/_plotly_utils/basevalidators.py @@ -201,7 +201,6 @@ def is_homogeneous_array(v): """ np = get_module("numpy", should_load=False) pd = get_module("pandas", should_load=False) - import narwhals as nw if ( np and isinstance(v, np.ndarray) diff --git a/packages/python/plotly/optional-requirements.txt b/packages/python/plotly/optional-requirements.txt index e686089801..3fde1914f7 100644 --- a/packages/python/plotly/optional-requirements.txt +++ b/packages/python/plotly/optional-requirements.txt @@ -39,7 +39,7 @@ ipython ## pandas deps for some matplotlib functionality ## pandas -narwhals>=1.10.0 +narwhals>=1.11.0 ## scipy deps for some FigureFactory functions ## scipy diff --git a/packages/python/plotly/plotly/express/_core.py b/packages/python/plotly/plotly/express/_core.py index 773c406b10..b97c899da9 100644 --- a/packages/python/plotly/plotly/express/_core.py +++ b/packages/python/plotly/plotly/express/_core.py @@ -157,6 +157,9 @@ def invert_label(args, column): def _is_continuous(df: nw.DataFrame, col_name: str) -> bool: if nw.dependencies.is_pandas_like_dataframe(df_native := df.to_native()): + # fastpath for pandas: Narwhals' Series.dtype has a bit of overhead, as it + # tries to distinguish between true "object" columns, and "string" columns + # disguised as "object". But here, we deal with neither. return df_native[col_name].dtype.kind in 'ifc' return df.get_column(col_name).dtype.is_numeric() @@ -1118,9 +1121,6 @@ def to_unindexed_series(x, name=None, native_namespace=None): """ x = nw.from_native(x, series_only=True, strict=False) if isinstance(x, nw.Series): - if name == x.name: - # Avoid potentially creating a copy in pre-copy-on-write pandas - return nw.maybe_reset_index(x) return nw.maybe_reset_index(x).rename(name) elif native_namespace is not None: return nw.new_series(name=name, values=x, native_namespace=native_namespace) diff --git a/packages/python/plotly/requirements.txt b/packages/python/plotly/requirements.txt index ad7c54e4d2..67233cdabf 100644 --- a/packages/python/plotly/requirements.txt +++ b/packages/python/plotly/requirements.txt @@ -6,4 +6,4 @@ ################################################### ## dataframe agnostic layer ## -narwhals>=1.10.0 +narwhals>=1.11.0 diff --git a/packages/python/plotly/setup.py b/packages/python/plotly/setup.py index 2433740d47..b9ed1b218f 100644 --- a/packages/python/plotly/setup.py +++ b/packages/python/plotly/setup.py @@ -603,7 +603,7 @@ def run(self): data_files=[ ("etc/jupyter/nbconfig/notebook.d", ["jupyterlab-plotly.json"]), ], - install_requires=["narwhals>=1.10.0", "packaging"], + install_requires=["narwhals>=1.11.0", "packaging"], zip_safe=False, cmdclass=dict( build_py=js_prerelease(versioneer_cmds["build_py"]), diff --git a/packages/python/plotly/test_requirements/requirements_310_optional.txt b/packages/python/plotly/test_requirements/requirements_310_optional.txt index 437e32b371..bf09485b81 100644 --- a/packages/python/plotly/test_requirements/requirements_310_optional.txt +++ b/packages/python/plotly/test_requirements/requirements_310_optional.txt @@ -21,4 +21,4 @@ kaleido orjson==3.8.12 polars[timezone] pyarrow -narwhals>=1.10.0 +narwhals>=1.11.0 diff --git a/packages/python/plotly/test_requirements/requirements_311_optional.txt b/packages/python/plotly/test_requirements/requirements_311_optional.txt index 2bd7075bc6..9658b85672 100644 --- a/packages/python/plotly/test_requirements/requirements_311_optional.txt +++ b/packages/python/plotly/test_requirements/requirements_311_optional.txt @@ -21,4 +21,4 @@ kaleido orjson==3.8.12 polars[timezone] pyarrow -narwhals>=1.10.0 +narwhals>=1.11.0 diff --git a/packages/python/plotly/test_requirements/requirements_312_no_numpy_optional.txt b/packages/python/plotly/test_requirements/requirements_312_no_numpy_optional.txt index d5dc1f68c2..19f259a2ab 100644 --- a/packages/python/plotly/test_requirements/requirements_312_no_numpy_optional.txt +++ b/packages/python/plotly/test_requirements/requirements_312_no_numpy_optional.txt @@ -20,4 +20,4 @@ kaleido orjson==3.9.10 polars[timezone] pyarrow -narwhals>=1.10.0 +narwhals>=1.11.0 diff --git a/packages/python/plotly/test_requirements/requirements_312_optional.txt b/packages/python/plotly/test_requirements/requirements_312_optional.txt index 96ec9f5867..4e32e070dd 100644 --- a/packages/python/plotly/test_requirements/requirements_312_optional.txt +++ b/packages/python/plotly/test_requirements/requirements_312_optional.txt @@ -21,4 +21,4 @@ kaleido orjson==3.9.10 polars[timezone] pyarrow -narwhals>=1.10.0 +narwhals>=1.11.0 diff --git a/packages/python/plotly/test_requirements/requirements_38_optional.txt b/packages/python/plotly/test_requirements/requirements_38_optional.txt index 8f3ea84958..2fe906275f 100644 --- a/packages/python/plotly/test_requirements/requirements_38_optional.txt +++ b/packages/python/plotly/test_requirements/requirements_38_optional.txt @@ -21,4 +21,4 @@ psutil==5.7.0 kaleido polars[timezone] pyarrow -narwhals>=1.10.0 +narwhals>=1.11.0 diff --git a/packages/python/plotly/test_requirements/requirements_39_optional.txt b/packages/python/plotly/test_requirements/requirements_39_optional.txt index ffb998e17e..7e93b6b15a 100644 --- a/packages/python/plotly/test_requirements/requirements_39_optional.txt +++ b/packages/python/plotly/test_requirements/requirements_39_optional.txt @@ -22,4 +22,4 @@ kaleido orjson==3.8.12 polars[timezone] pyarrow -narwhals>=1.10.0 +narwhals>=1.11.0 diff --git a/packages/python/plotly/test_requirements/requirements_39_pandas_2_optional.txt b/packages/python/plotly/test_requirements/requirements_39_pandas_2_optional.txt index 523d4bbdd9..0e07405cc4 100644 --- a/packages/python/plotly/test_requirements/requirements_39_pandas_2_optional.txt +++ b/packages/python/plotly/test_requirements/requirements_39_pandas_2_optional.txt @@ -22,4 +22,4 @@ vaex pydantic<=1.10.11 # for vaex, see https://github.com/vaexio/vaex/issues/2384 polars[timezone] pyarrow -narwhals>=1.10.0 +narwhals>=1.11.0 From 755aea87208ec6c575c078d4da07c508cd064036 Mon Sep 17 00:00:00 2001 From: FBruzzesi Date: Sun, 27 Oct 2024 18:58:02 +0100 Subject: [PATCH 067/106] format and pyspark path --- packages/python/plotly/plotly/express/_core.py | 12 +++++++++++- .../plotly/plotly/figure_factory/_hexbin_mapbox.py | 6 +++++- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/packages/python/plotly/plotly/express/_core.py b/packages/python/plotly/plotly/express/_core.py index b97c899da9..209c787a72 100644 --- a/packages/python/plotly/plotly/express/_core.py +++ b/packages/python/plotly/plotly/express/_core.py @@ -160,7 +160,7 @@ def _is_continuous(df: nw.DataFrame, col_name: str) -> bool: # fastpath for pandas: Narwhals' Series.dtype has a bit of overhead, as it # tries to distinguish between true "object" columns, and "string" columns # disguised as "object". But here, we deal with neither. - return df_native[col_name].dtype.kind in 'ifc' + return df_native[col_name].dtype.kind in "ifc" return df.get_column(col_name).dtype.is_numeric() @@ -1454,6 +1454,7 @@ def build_dataframe(args, constructor): is_pd_like = True elif hasattr(args["data_frame"], "__dataframe__"): + # data_frame supports interchange protocol args["data_frame"] = nw.from_native( nw.from_native( args["data_frame"], eager_or_interchange_only=True @@ -1463,6 +1464,15 @@ def build_dataframe(args, constructor): columns = args["data_frame"].columns is_pd_like = True + elif hasattr(args["data_frame"], "toPandas"): + # data_frame is PySpark: it does not support interchange and it is not + # integrated in narwhals just yet + args["data_frame"] = nw.from_native( + args["data_frame"].toPandas(), eager_only=True + ) + columns = args["data_frame"].columns + is_pd_like = True + else: try: import pandas as pd diff --git a/packages/python/plotly/plotly/figure_factory/_hexbin_mapbox.py b/packages/python/plotly/plotly/figure_factory/_hexbin_mapbox.py index 0bda32a973..c76352248b 100644 --- a/packages/python/plotly/plotly/figure_factory/_hexbin_mapbox.py +++ b/packages/python/plotly/plotly/figure_factory/_hexbin_mapbox.py @@ -407,7 +407,11 @@ def create_hexbin_mapbox( center = dict(lat=lat_range.mean(), lon=lon_range.mean()) if args["animation_frame"] is not None: - groups = dict(args["data_frame"].group_by(args["animation_frame"], drop_null_keys=True).__iter__()) + groups = dict( + args["data_frame"] + .group_by(args["animation_frame"], drop_null_keys=True) + .__iter__() + ) else: groups = {(0,): args["data_frame"]} From 6f180217a01c222404a59e2818a738b45aca5e45 Mon Sep 17 00:00:00 2001 From: FBruzzesi Date: Sun, 27 Oct 2024 20:12:35 +0100 Subject: [PATCH 068/106] add narwhals to requirements core --- .../python/plotly/test_requirements/requirements_310_core.txt | 1 + .../python/plotly/test_requirements/requirements_311_core.txt | 1 + .../python/plotly/test_requirements/requirements_312_core.txt | 1 + .../python/plotly/test_requirements/requirements_38_core.txt | 1 + .../python/plotly/test_requirements/requirements_39_core.txt | 1 + 5 files changed, 5 insertions(+) diff --git a/packages/python/plotly/test_requirements/requirements_310_core.txt b/packages/python/plotly/test_requirements/requirements_310_core.txt index c3af689b05..0f1eac4b31 100644 --- a/packages/python/plotly/test_requirements/requirements_310_core.txt +++ b/packages/python/plotly/test_requirements/requirements_310_core.txt @@ -1,2 +1,3 @@ requests==2.25.1 pytest==7.4.4 +narwhals>=1.11.0 diff --git a/packages/python/plotly/test_requirements/requirements_311_core.txt b/packages/python/plotly/test_requirements/requirements_311_core.txt index c3af689b05..0f1eac4b31 100644 --- a/packages/python/plotly/test_requirements/requirements_311_core.txt +++ b/packages/python/plotly/test_requirements/requirements_311_core.txt @@ -1,2 +1,3 @@ requests==2.25.1 pytest==7.4.4 +narwhals>=1.11.0 diff --git a/packages/python/plotly/test_requirements/requirements_312_core.txt b/packages/python/plotly/test_requirements/requirements_312_core.txt index c3af689b05..0f1eac4b31 100644 --- a/packages/python/plotly/test_requirements/requirements_312_core.txt +++ b/packages/python/plotly/test_requirements/requirements_312_core.txt @@ -1,2 +1,3 @@ requests==2.25.1 pytest==7.4.4 +narwhals>=1.11.0 diff --git a/packages/python/plotly/test_requirements/requirements_38_core.txt b/packages/python/plotly/test_requirements/requirements_38_core.txt index 659fe1a370..51dd7389ea 100644 --- a/packages/python/plotly/test_requirements/requirements_38_core.txt +++ b/packages/python/plotly/test_requirements/requirements_38_core.txt @@ -1,2 +1,3 @@ requests==2.25.1 pytest==8.1.1 +narwhals>=1.11.0 diff --git a/packages/python/plotly/test_requirements/requirements_39_core.txt b/packages/python/plotly/test_requirements/requirements_39_core.txt index f4605b806c..35045b8561 100644 --- a/packages/python/plotly/test_requirements/requirements_39_core.txt +++ b/packages/python/plotly/test_requirements/requirements_39_core.txt @@ -1,2 +1,3 @@ requests==2.25.1 pytest==6.2.3 +narwhals>=1.11.0 From 4d62e7353d4cfbf4576b5c00215f3868f9b5139b Mon Sep 17 00:00:00 2001 From: Francesco Bruzzesi <42817048+FBruzzesi@users.noreply.github.com> Date: Mon, 28 Oct 2024 13:58:12 +0100 Subject: [PATCH 069/106] Update packages/python/plotly/plotly/express/_core.py Co-authored-by: Marco Edward Gorelli --- packages/python/plotly/plotly/express/_core.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/python/plotly/plotly/express/_core.py b/packages/python/plotly/plotly/express/_core.py index 209c787a72..5f98d22d2e 100644 --- a/packages/python/plotly/plotly/express/_core.py +++ b/packages/python/plotly/plotly/express/_core.py @@ -1116,7 +1116,7 @@ def _escape_col_name(columns, col_name, extra): def to_unindexed_series(x, name=None, native_namespace=None): """Assuming x is list-like or even an existing Series, returns a new Series (with - itx index reset if pandas-like). Stripping the index from existing pd.Series is + its index reset if pandas-like). Stripping the index from existing pd.Series is required to get things to match up right in the new DataFrame we're building. """ x = nw.from_native(x, series_only=True, strict=False) From a770fd852de403921acac1c623c22b6020859615 Mon Sep 17 00:00:00 2001 From: Marco Gorelli <33491632+MarcoGorelli@users.noreply.github.com> Date: Tue, 29 Oct 2024 10:13:53 +0000 Subject: [PATCH 070/106] refactor checking for df --- .../python/plotly/plotly/express/_core.py | 45 ++++++------------- 1 file changed, 14 insertions(+), 31 deletions(-) diff --git a/packages/python/plotly/plotly/express/_core.py b/packages/python/plotly/plotly/express/_core.py index 5f98d22d2e..cce5b61d70 100644 --- a/packages/python/plotly/plotly/express/_core.py +++ b/packages/python/plotly/plotly/express/_core.py @@ -1423,46 +1423,25 @@ def build_dataframe(args, constructor): is_pd_like = False if df_provided: - if nw.dependencies.is_polars_dataframe( - args["data_frame"] - ) or nw.dependencies.is_pyarrow_table(args["data_frame"]): - args["data_frame"] = nw.from_native(args["data_frame"], eager_only=True) - columns = args["data_frame"].columns - - elif nw.dependencies.is_polars_series( - args["data_frame"] - ) or nw.dependencies.is_pyarrow_chunked_array(args["data_frame"]): - args["data_frame"] = nw.from_native( - args["data_frame"], - series_only=True, - ).to_frame() - columns = args["data_frame"].columns - - elif nw.dependencies.is_pandas_like_dataframe(args["data_frame"]): + if nw.dependencies.is_pandas_like_dataframe(args["data_frame"]): columns = args["data_frame"].columns # This can be multi index - args["data_frame"] = nw.from_native(args["data_frame"]) + args["data_frame"] = nw.from_native(args['data_frame'], eager_only=True) is_pd_like = True elif nw.dependencies.is_pandas_like_series(args["data_frame"]): - args["data_frame"] = nw.from_native( - args["data_frame"], - series_only=True, - ).to_frame() + args["data_frame"] = nw.from_native(args['data_frame'], series_only=True).to_frame() columns = args["data_frame"].columns is_pd_like = True - elif hasattr(args["data_frame"], "__dataframe__"): - # data_frame supports interchange protocol - args["data_frame"] = nw.from_native( - nw.from_native( - args["data_frame"], eager_or_interchange_only=True - ).to_pandas(), # Converts to pandas - eager_only=True, - ) - columns = args["data_frame"].columns - is_pd_like = True + elif isinstance(data_frame := nw.from_native(args['data_frame'], eager_or_interchange_only=True, strict=False), nw.DataFrame): + args["data_frame"] = data_frame + columns = args['data_frame'].schema.names() + + elif isinstance(series := nw.from_native(args['data_frame'], series_only=True, strict=False), nw.Series): + args["data_frame"] = series.to_frame() + columns = args['data_frame'].columns elif hasattr(args["data_frame"], "toPandas"): # data_frame is PySpark: it does not support interchange and it is not @@ -1575,6 +1554,10 @@ def build_dataframe(args, constructor): value_name = _escape_col_name(columns, "value", []) var_name = _escape_col_name(columns, var_name, []) + if isinstance(args['data_frame'], nw.DataFrame) and nw.get_level(args['data_frame']) == 'interchange': + # Interchange to PyArrow if necessary + args['data_frame'] = nw.from_native(data_frame.to_arrow(), eager_only=True) + missing_bar_dim = None if ( constructor in [go.Scatter, go.Bar, go.Funnel] + hist2d_types From 7d6f7d65c227e245d932a45cfe0532a28e28e26b Mon Sep 17 00:00:00 2001 From: Marco Gorelli <33491632+MarcoGorelli@users.noreply.github.com> Date: Tue, 29 Oct 2024 13:50:51 +0000 Subject: [PATCH 071/106] pushdown only for interchange libraries, sort out test --- .../python/plotly/plotly/express/_core.py | 30 ++++++++++++---- .../test_optional/test_px/test_px_input.py | 36 +++++++++++-------- 2 files changed, 44 insertions(+), 22 deletions(-) diff --git a/packages/python/plotly/plotly/express/_core.py b/packages/python/plotly/plotly/express/_core.py index cce5b61d70..adcf153966 100644 --- a/packages/python/plotly/plotly/express/_core.py +++ b/packages/python/plotly/plotly/express/_core.py @@ -1421,6 +1421,7 @@ def build_dataframe(args, constructor): # Cast data_frame argument to DataFrame (it could be a numpy array, dict etc.) df_provided = args["data_frame"] is not None is_pd_like = False + needs_interchanging = False if df_provided: if nw.dependencies.is_pandas_like_dataframe(args["data_frame"]): @@ -1437,7 +1438,8 @@ def build_dataframe(args, constructor): elif isinstance(data_frame := nw.from_native(args['data_frame'], eager_or_interchange_only=True, strict=False), nw.DataFrame): args["data_frame"] = data_frame - columns = args['data_frame'].schema.names() + needs_interchanging = nw.get_level(data_frame) == 'interchange' + columns = args['data_frame'].columns elif isinstance(series := nw.from_native(args['data_frame'], series_only=True, strict=False), nw.Series): args["data_frame"] = series.to_frame() @@ -1479,9 +1481,7 @@ def build_dataframe(args, constructor): df_input: nw.DataFrame | None = args["data_frame"] index = nw.maybe_get_index(df_input) if df_provided else None - # This is safe since at this point `_compliant_frame` is one of the "full" level - # support dataframe(s) - native_namespace = nw.get_native_namespace(df_input) if df_provided else None + native_namespace = nw.get_native_namespace(df_input) if df_provided and not needs_interchanging else None # now we handle special cases like wide-mode or x-xor-y specification # by rearranging args to tee things up for process_args_into_dataframe to work @@ -1554,9 +1554,25 @@ def build_dataframe(args, constructor): value_name = _escape_col_name(columns, "value", []) var_name = _escape_col_name(columns, var_name, []) - if isinstance(args['data_frame'], nw.DataFrame) and nw.get_level(args['data_frame']) == 'interchange': - # Interchange to PyArrow if necessary - args['data_frame'] = nw.from_native(data_frame.to_arrow(), eager_only=True) + if isinstance(args['data_frame'], nw.DataFrame) and needs_interchanging: + # Interchange to PyArrow + if wide_mode: + args["data_frame"] = nw.from_native(args['data_frame'].to_arrow(), eager_only=True) + else: + # Save precious resources by only interchanging columns that are + # actually going to be plotted. This is tricky to do in the general case, + # because Plotly allows calls like `px.line(df, x='x', y=['y1', df['y1']])`, + # but interchange-only objects (e.g. DuckDB) don't typically have a concept + # of self-standing Series. It's more important to perform project pushdown + # here seeing as we're materialising to an (eager) PyArrow table. + necessary_columns = { + i for i in args.values() if isinstance(i, str) and i in columns + } + for field in args: + if args[field] is not None and field in array_attrables: + necessary_columns.update(i for i in args[field] if i in columns) + columns = list(necessary_columns) + args["data_frame"] = nw.from_native(args['data_frame'].select(columns).to_arrow(), eager_only=True) missing_bar_dim = None if ( diff --git a/packages/python/plotly/plotly/tests/test_optional/test_px/test_px_input.py b/packages/python/plotly/plotly/tests/test_optional/test_px/test_px_input.py index 3dbf3c254d..1f386ca4b2 100644 --- a/packages/python/plotly/plotly/tests/test_optional/test_px/test_px_input.py +++ b/packages/python/plotly/plotly/tests/test_optional/test_px/test_px_input.py @@ -1,4 +1,5 @@ import plotly.express as px +import pyarrow as pa import plotly.graph_objects as go import narwhals.stable.v1 as nw import numpy as np @@ -290,35 +291,40 @@ def test_build_df_with_index(): def test_build_df_using_interchange_protocol_mock(): class InterchangeDataFrame: - def __init__(self, columns): - self._columns = columns + def __init__(self, df): + self._df = df + + def __dataframe__(self): + return self def column_names(self): - return self._columns - - interchange_dataframe = InterchangeDataFrame( - ["petal_width", "sepal_length", "sepal_width"] - ) + return list(self._df._data.keys()) + + def select_columns_by_name(self, columns): + return InterchangeDataFrame(CustomDataFrame({key: value for key, value in self._df._data.items() if key in columns})) class CustomDataFrame: - def __dataframe__(self): - return interchange_dataframe + def __init__(self, data): + self._data = data + + def __dataframe__(self, allow_copy: bool = True): + return InterchangeDataFrame(self) - input_dataframe = CustomDataFrame() + input_dataframe = CustomDataFrame({'a': [1,2,3], 'b': [4,5,6]}) - iris_pandas = px.data.iris() + input_dataframe_pa = pa.table({'a': [1,2,3], 'b': [4,5,6]}) - args = dict(data_frame=input_dataframe, x="petal_width", y="sepal_length") + args = dict(data_frame=input_dataframe, x="a", y="b") with mock.patch( - "narwhals._interchange.dataframe.InterchangeFrame.to_pandas", - return_value=iris_pandas, + "narwhals._interchange.dataframe.InterchangeFrame.to_arrow", + return_value=input_dataframe_pa, ) as mock_from_dataframe: out = build_dataframe(args, go.Scatter) mock_from_dataframe.assert_called_once() assert_frame_equal( - iris_pandas.reset_index()[out["data_frame"].columns], + input_dataframe_pa.select(out["data_frame"].columns).to_pandas(), out["data_frame"].to_pandas(), ) From b8c10ec703b090f667d2cacee95fdbcf83c7a632 Mon Sep 17 00:00:00 2001 From: Marco Edward Gorelli Date: Tue, 29 Oct 2024 14:09:06 +0000 Subject: [PATCH 072/106] Update packages/python/plotly/plotly/express/_core.py Co-authored-by: Francesco Bruzzesi <42817048+FBruzzesi@users.noreply.github.com> --- packages/python/plotly/plotly/express/_core.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/python/plotly/plotly/express/_core.py b/packages/python/plotly/plotly/express/_core.py index adcf153966..c94d05e0fb 100644 --- a/packages/python/plotly/plotly/express/_core.py +++ b/packages/python/plotly/plotly/express/_core.py @@ -1573,7 +1573,8 @@ def build_dataframe(args, constructor): necessary_columns.update(i for i in args[field] if i in columns) columns = list(necessary_columns) args["data_frame"] = nw.from_native(args['data_frame'].select(columns).to_arrow(), eager_only=True) - + import pyarrow as pa + native_namespace = pa missing_bar_dim = None if ( constructor in [go.Scatter, go.Bar, go.Funnel] + hist2d_types From 490b64a6a0dd85af0fc9e1eb1b98ca5b863f651e Mon Sep 17 00:00:00 2001 From: Marco Gorelli <33491632+MarcoGorelli@users.noreply.github.com> Date: Tue, 29 Oct 2024 14:12:46 +0000 Subject: [PATCH 073/106] fixup --- packages/python/plotly/plotly/express/_core.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/python/plotly/plotly/express/_core.py b/packages/python/plotly/plotly/express/_core.py index adcf153966..43f52d3511 100644 --- a/packages/python/plotly/plotly/express/_core.py +++ b/packages/python/plotly/plotly/express/_core.py @@ -1479,8 +1479,7 @@ def build_dataframe(args, constructor): columns = None # no data_frame df_input: nw.DataFrame | None = args["data_frame"] - index = nw.maybe_get_index(df_input) if df_provided else None - + index = nw.maybe_get_index(df_input) if df_provided and not needs_interchanging else None native_namespace = nw.get_native_namespace(df_input) if df_provided and not needs_interchanging else None # now we handle special cases like wide-mode or x-xor-y specification From 8753acbc355b9c73a7d8cff1ca6b71ea2252d5a7 Mon Sep 17 00:00:00 2001 From: Marco Gorelli <33491632+MarcoGorelli@users.noreply.github.com> Date: Tue, 29 Oct 2024 14:15:21 +0000 Subject: [PATCH 074/106] lint --- .../python/plotly/plotly/express/_core.py | 49 ++++++++++++++----- .../test_optional/test_px/test_px_input.py | 18 +++++-- 2 files changed, 50 insertions(+), 17 deletions(-) diff --git a/packages/python/plotly/plotly/express/_core.py b/packages/python/plotly/plotly/express/_core.py index e020f102de..1f5e297510 100644 --- a/packages/python/plotly/plotly/express/_core.py +++ b/packages/python/plotly/plotly/express/_core.py @@ -1427,23 +1427,35 @@ def build_dataframe(args, constructor): if nw.dependencies.is_pandas_like_dataframe(args["data_frame"]): columns = args["data_frame"].columns # This can be multi index - args["data_frame"] = nw.from_native(args['data_frame'], eager_only=True) + args["data_frame"] = nw.from_native(args["data_frame"], eager_only=True) is_pd_like = True elif nw.dependencies.is_pandas_like_series(args["data_frame"]): - args["data_frame"] = nw.from_native(args['data_frame'], series_only=True).to_frame() + args["data_frame"] = nw.from_native( + args["data_frame"], series_only=True + ).to_frame() columns = args["data_frame"].columns is_pd_like = True - elif isinstance(data_frame := nw.from_native(args['data_frame'], eager_or_interchange_only=True, strict=False), nw.DataFrame): + elif isinstance( + data_frame := nw.from_native( + args["data_frame"], eager_or_interchange_only=True, strict=False + ), + nw.DataFrame, + ): args["data_frame"] = data_frame - needs_interchanging = nw.get_level(data_frame) == 'interchange' - columns = args['data_frame'].columns + needs_interchanging = nw.get_level(data_frame) == "interchange" + columns = args["data_frame"].columns - elif isinstance(series := nw.from_native(args['data_frame'], series_only=True, strict=False), nw.Series): + elif isinstance( + series := nw.from_native( + args["data_frame"], series_only=True, strict=False + ), + nw.Series, + ): args["data_frame"] = series.to_frame() - columns = args['data_frame'].columns + columns = args["data_frame"].columns elif hasattr(args["data_frame"], "toPandas"): # data_frame is PySpark: it does not support interchange and it is not @@ -1479,8 +1491,16 @@ def build_dataframe(args, constructor): columns = None # no data_frame df_input: nw.DataFrame | None = args["data_frame"] - index = nw.maybe_get_index(df_input) if df_provided and not needs_interchanging else None - native_namespace = nw.get_native_namespace(df_input) if df_provided and not needs_interchanging else None + index = ( + nw.maybe_get_index(df_input) + if df_provided and not needs_interchanging + else None + ) + native_namespace = ( + nw.get_native_namespace(df_input) + if df_provided and not needs_interchanging + else None + ) # now we handle special cases like wide-mode or x-xor-y specification # by rearranging args to tee things up for process_args_into_dataframe to work @@ -1553,10 +1573,12 @@ def build_dataframe(args, constructor): value_name = _escape_col_name(columns, "value", []) var_name = _escape_col_name(columns, var_name, []) - if isinstance(args['data_frame'], nw.DataFrame) and needs_interchanging: + if isinstance(args["data_frame"], nw.DataFrame) and needs_interchanging: # Interchange to PyArrow if wide_mode: - args["data_frame"] = nw.from_native(args['data_frame'].to_arrow(), eager_only=True) + args["data_frame"] = nw.from_native( + args["data_frame"].to_arrow(), eager_only=True + ) else: # Save precious resources by only interchanging columns that are # actually going to be plotted. This is tricky to do in the general case, @@ -1571,8 +1593,11 @@ def build_dataframe(args, constructor): if args[field] is not None and field in array_attrables: necessary_columns.update(i for i in args[field] if i in columns) columns = list(necessary_columns) - args["data_frame"] = nw.from_native(args['data_frame'].select(columns).to_arrow(), eager_only=True) + args["data_frame"] = nw.from_native( + args["data_frame"].select(columns).to_arrow(), eager_only=True + ) import pyarrow as pa + native_namespace = pa missing_bar_dim = None if ( diff --git a/packages/python/plotly/plotly/tests/test_optional/test_px/test_px_input.py b/packages/python/plotly/plotly/tests/test_optional/test_px/test_px_input.py index 1f386ca4b2..6ee0b8ae9c 100644 --- a/packages/python/plotly/plotly/tests/test_optional/test_px/test_px_input.py +++ b/packages/python/plotly/plotly/tests/test_optional/test_px/test_px_input.py @@ -293,15 +293,23 @@ def test_build_df_using_interchange_protocol_mock(): class InterchangeDataFrame: def __init__(self, df): self._df = df - + def __dataframe__(self): return self def column_names(self): return list(self._df._data.keys()) - + def select_columns_by_name(self, columns): - return InterchangeDataFrame(CustomDataFrame({key: value for key, value in self._df._data.items() if key in columns})) + return InterchangeDataFrame( + CustomDataFrame( + { + key: value + for key, value in self._df._data.items() + if key in columns + } + ) + ) class CustomDataFrame: def __init__(self, data): @@ -310,9 +318,9 @@ def __init__(self, data): def __dataframe__(self, allow_copy: bool = True): return InterchangeDataFrame(self) - input_dataframe = CustomDataFrame({'a': [1,2,3], 'b': [4,5,6]}) + input_dataframe = CustomDataFrame({"a": [1, 2, 3], "b": [4, 5, 6]}) - input_dataframe_pa = pa.table({'a': [1,2,3], 'b': [4,5,6]}) + input_dataframe_pa = pa.table({"a": [1, 2, 3], "b": [4, 5, 6]}) args = dict(data_frame=input_dataframe, x="a", y="b") with mock.patch( From 1429e6f40ed36edb9515535707f34db66997a2dd Mon Sep 17 00:00:00 2001 From: Marco Gorelli <33491632+MarcoGorelli@users.noreply.github.com> Date: Tue, 29 Oct 2024 14:53:11 +0000 Subject: [PATCH 075/106] bump narwhals version --- packages/python/plotly/optional-requirements.txt | 2 +- packages/python/plotly/requirements.txt | 2 +- packages/python/plotly/setup.py | 2 +- .../python/plotly/test_requirements/requirements_310_core.txt | 2 +- .../plotly/test_requirements/requirements_310_optional.txt | 2 +- .../python/plotly/test_requirements/requirements_311_core.txt | 2 +- .../plotly/test_requirements/requirements_311_optional.txt | 2 +- .../python/plotly/test_requirements/requirements_312_core.txt | 2 +- .../test_requirements/requirements_312_no_numpy_optional.txt | 2 +- .../plotly/test_requirements/requirements_312_optional.txt | 2 +- .../python/plotly/test_requirements/requirements_38_core.txt | 2 +- .../plotly/test_requirements/requirements_38_optional.txt | 2 +- .../python/plotly/test_requirements/requirements_39_core.txt | 2 +- .../plotly/test_requirements/requirements_39_optional.txt | 2 +- .../test_requirements/requirements_39_pandas_2_optional.txt | 2 +- 15 files changed, 15 insertions(+), 15 deletions(-) diff --git a/packages/python/plotly/optional-requirements.txt b/packages/python/plotly/optional-requirements.txt index 3fde1914f7..465188f802 100644 --- a/packages/python/plotly/optional-requirements.txt +++ b/packages/python/plotly/optional-requirements.txt @@ -39,7 +39,7 @@ ipython ## pandas deps for some matplotlib functionality ## pandas -narwhals>=1.11.0 +narwhals>=1.12.0 ## scipy deps for some FigureFactory functions ## scipy diff --git a/packages/python/plotly/requirements.txt b/packages/python/plotly/requirements.txt index 67233cdabf..9b4b1093ce 100644 --- a/packages/python/plotly/requirements.txt +++ b/packages/python/plotly/requirements.txt @@ -6,4 +6,4 @@ ################################################### ## dataframe agnostic layer ## -narwhals>=1.11.0 +narwhals>=1.12.0 diff --git a/packages/python/plotly/setup.py b/packages/python/plotly/setup.py index b9ed1b218f..3d0aa1273d 100644 --- a/packages/python/plotly/setup.py +++ b/packages/python/plotly/setup.py @@ -603,7 +603,7 @@ def run(self): data_files=[ ("etc/jupyter/nbconfig/notebook.d", ["jupyterlab-plotly.json"]), ], - install_requires=["narwhals>=1.11.0", "packaging"], + install_requires=["narwhals>=1.12.0", "packaging"], zip_safe=False, cmdclass=dict( build_py=js_prerelease(versioneer_cmds["build_py"]), diff --git a/packages/python/plotly/test_requirements/requirements_310_core.txt b/packages/python/plotly/test_requirements/requirements_310_core.txt index 0f1eac4b31..c5ea5ec903 100644 --- a/packages/python/plotly/test_requirements/requirements_310_core.txt +++ b/packages/python/plotly/test_requirements/requirements_310_core.txt @@ -1,3 +1,3 @@ requests==2.25.1 pytest==7.4.4 -narwhals>=1.11.0 +narwhals>=1.12.0 diff --git a/packages/python/plotly/test_requirements/requirements_310_optional.txt b/packages/python/plotly/test_requirements/requirements_310_optional.txt index bf09485b81..41c0c2a51a 100644 --- a/packages/python/plotly/test_requirements/requirements_310_optional.txt +++ b/packages/python/plotly/test_requirements/requirements_310_optional.txt @@ -21,4 +21,4 @@ kaleido orjson==3.8.12 polars[timezone] pyarrow -narwhals>=1.11.0 +narwhals>=1.12.0 diff --git a/packages/python/plotly/test_requirements/requirements_311_core.txt b/packages/python/plotly/test_requirements/requirements_311_core.txt index 0f1eac4b31..c5ea5ec903 100644 --- a/packages/python/plotly/test_requirements/requirements_311_core.txt +++ b/packages/python/plotly/test_requirements/requirements_311_core.txt @@ -1,3 +1,3 @@ requests==2.25.1 pytest==7.4.4 -narwhals>=1.11.0 +narwhals>=1.12.0 diff --git a/packages/python/plotly/test_requirements/requirements_311_optional.txt b/packages/python/plotly/test_requirements/requirements_311_optional.txt index 9658b85672..77cceb72eb 100644 --- a/packages/python/plotly/test_requirements/requirements_311_optional.txt +++ b/packages/python/plotly/test_requirements/requirements_311_optional.txt @@ -21,4 +21,4 @@ kaleido orjson==3.8.12 polars[timezone] pyarrow -narwhals>=1.11.0 +narwhals>=1.12.0 diff --git a/packages/python/plotly/test_requirements/requirements_312_core.txt b/packages/python/plotly/test_requirements/requirements_312_core.txt index 0f1eac4b31..c5ea5ec903 100644 --- a/packages/python/plotly/test_requirements/requirements_312_core.txt +++ b/packages/python/plotly/test_requirements/requirements_312_core.txt @@ -1,3 +1,3 @@ requests==2.25.1 pytest==7.4.4 -narwhals>=1.11.0 +narwhals>=1.12.0 diff --git a/packages/python/plotly/test_requirements/requirements_312_no_numpy_optional.txt b/packages/python/plotly/test_requirements/requirements_312_no_numpy_optional.txt index 19f259a2ab..864cc0ac95 100644 --- a/packages/python/plotly/test_requirements/requirements_312_no_numpy_optional.txt +++ b/packages/python/plotly/test_requirements/requirements_312_no_numpy_optional.txt @@ -20,4 +20,4 @@ kaleido orjson==3.9.10 polars[timezone] pyarrow -narwhals>=1.11.0 +narwhals>=1.12.0 diff --git a/packages/python/plotly/test_requirements/requirements_312_optional.txt b/packages/python/plotly/test_requirements/requirements_312_optional.txt index 4e32e070dd..b1806e5232 100644 --- a/packages/python/plotly/test_requirements/requirements_312_optional.txt +++ b/packages/python/plotly/test_requirements/requirements_312_optional.txt @@ -21,4 +21,4 @@ kaleido orjson==3.9.10 polars[timezone] pyarrow -narwhals>=1.11.0 +narwhals>=1.12.0 diff --git a/packages/python/plotly/test_requirements/requirements_38_core.txt b/packages/python/plotly/test_requirements/requirements_38_core.txt index 51dd7389ea..c84e860854 100644 --- a/packages/python/plotly/test_requirements/requirements_38_core.txt +++ b/packages/python/plotly/test_requirements/requirements_38_core.txt @@ -1,3 +1,3 @@ requests==2.25.1 pytest==8.1.1 -narwhals>=1.11.0 +narwhals>=1.12.0 diff --git a/packages/python/plotly/test_requirements/requirements_38_optional.txt b/packages/python/plotly/test_requirements/requirements_38_optional.txt index 2fe906275f..95160ee63d 100644 --- a/packages/python/plotly/test_requirements/requirements_38_optional.txt +++ b/packages/python/plotly/test_requirements/requirements_38_optional.txt @@ -21,4 +21,4 @@ psutil==5.7.0 kaleido polars[timezone] pyarrow -narwhals>=1.11.0 +narwhals>=1.12.0 diff --git a/packages/python/plotly/test_requirements/requirements_39_core.txt b/packages/python/plotly/test_requirements/requirements_39_core.txt index 35045b8561..b15f894884 100644 --- a/packages/python/plotly/test_requirements/requirements_39_core.txt +++ b/packages/python/plotly/test_requirements/requirements_39_core.txt @@ -1,3 +1,3 @@ requests==2.25.1 pytest==6.2.3 -narwhals>=1.11.0 +narwhals>=1.12.0 diff --git a/packages/python/plotly/test_requirements/requirements_39_optional.txt b/packages/python/plotly/test_requirements/requirements_39_optional.txt index 7e93b6b15a..fd887bb94f 100644 --- a/packages/python/plotly/test_requirements/requirements_39_optional.txt +++ b/packages/python/plotly/test_requirements/requirements_39_optional.txt @@ -22,4 +22,4 @@ kaleido orjson==3.8.12 polars[timezone] pyarrow -narwhals>=1.11.0 +narwhals>=1.12.0 diff --git a/packages/python/plotly/test_requirements/requirements_39_pandas_2_optional.txt b/packages/python/plotly/test_requirements/requirements_39_pandas_2_optional.txt index 0e07405cc4..4ee93e25b5 100644 --- a/packages/python/plotly/test_requirements/requirements_39_pandas_2_optional.txt +++ b/packages/python/plotly/test_requirements/requirements_39_pandas_2_optional.txt @@ -22,4 +22,4 @@ vaex pydantic<=1.10.11 # for vaex, see https://github.com/vaexio/vaex/issues/2384 polars[timezone] pyarrow -narwhals>=1.11.0 +narwhals>=1.12.0 From 192e0a89267653f28f275f8eaa47544f662e6c73 Mon Sep 17 00:00:00 2001 From: FBruzzesi Date: Tue, 29 Oct 2024 16:05:47 +0100 Subject: [PATCH 076/106] use token in process_dataframe_hierarchy --- .../python/plotly/plotly/express/_core.py | 28 +++++++++++-------- .../test_px/test_px_functions.py | 4 +-- .../test_optional/test_px/test_px_hover.py | 3 ++ .../test_optional/test_px/test_px_input.py | 10 +++++-- 4 files changed, 29 insertions(+), 16 deletions(-) diff --git a/packages/python/plotly/plotly/express/_core.py b/packages/python/plotly/plotly/express/_core.py index 1f5e297510..f98a7a31f4 100644 --- a/packages/python/plotly/plotly/express/_core.py +++ b/packages/python/plotly/plotly/express/_core.py @@ -1868,6 +1868,10 @@ def process_dataframe_hierarchy(args): discrete_aggs = [] continuous_aggs = [] + n_unique_token = nw.generate_temporary_column_name( + n_bytes=16, columns=[*path, count_colname] + ) + if args["color"]: if discrete_color: @@ -1888,10 +1892,10 @@ def process_dataframe_hierarchy(args): # ``` # However we cannot do that just yet, therefore a workaround is provided agg_f[args["color"]] = nw.col(args["color"]).max() - agg_f[f'{args["color"]}__plotly_n_unique__'] = ( + agg_f[f'{args["color"]}_{n_unique_token}__'] = ( nw.col(args["color"]) .n_unique() - .alias(f'{args["color"]}__plotly_n_unique__') + .alias(f'{args["color"]}_{n_unique_token}__') ) else: # This first needs to be multiplied by `count_colname` @@ -1909,8 +1913,8 @@ def process_dataframe_hierarchy(args): # Similar trick as above discrete_aggs.append(col) agg_f[col] = nw.col(col).max() - agg_f[f"{col}__plotly_n_unique__"] = ( - nw.col(col).n_unique().alias(f"{col}__plotly_n_unique__") + agg_f[f"{col}_{n_unique_token}__"] = ( + nw.col(col).n_unique().alias(f"{col}_{n_unique_token}__") ) # Avoid collisions with reserved names - columns in the path have been copied already cols = list(set(cols) - set(["labels", "parent", "id"])) @@ -1930,12 +1934,12 @@ def post_agg(dframe: nw.LazyFrame, continuous_aggs, discrete_aggs) -> nw.LazyFra return dframe.with_columns( **{c: nw.col(c) / nw.col(count_colname) for c in continuous_aggs}, **{ - c: nw.when(nw.col(f"{c}__plotly_n_unique__") == 1) + c: nw.when(nw.col(f"{c}_{n_unique_token}__") == 1) .then(nw.col(c)) .otherwise(nw.lit("(?)")) for c in discrete_aggs }, - ).drop([f"{c}__plotly_n_unique__" for c in discrete_aggs]) + ).drop([f"{c}_{n_unique_token}__" for c in discrete_aggs]) for i, level in enumerate(path): @@ -1953,11 +1957,13 @@ def post_agg(dframe: nw.LazyFrame, continuous_aggs, discrete_aggs) -> nw.LazyFra id=nw.col(level).cast(nw.String()), ) if i < len(path) - 1: - token = generate_unique_token(n_bytes=8, columns=df_tree.columns) + _concat_str_token = nw.generate_temporary_column_name( + n_bytes=8, columns=[*cols, "labels", "parent", "id"] + ) df_tree = ( df_tree.with_columns( **{ - token: nw.concat_str( + _concat_str_token: nw.concat_str( [ nw.col(path[j]).cast(nw.String()) for j in range(len(path) - 1, i, -1) @@ -1969,14 +1975,14 @@ def post_agg(dframe: nw.LazyFrame, continuous_aggs, discrete_aggs) -> nw.LazyFra .with_columns( **{ "parent": nw.concat_str( - [nw.col(token), nw.col("parent")], separator="/" + [nw.col(_concat_str_token), nw.col("parent")], separator="/" ), "id": nw.concat_str( - [nw.col(token), nw.col("id")], separator="/" + [nw.col(_concat_str_token), nw.col("id")], separator="/" ), } ) - .drop(token) + .drop(_concat_str_token) ) # strip "/" if at the end of the string, equivalent to `.str.rstrip` diff --git a/packages/python/plotly/plotly/tests/test_optional/test_px/test_px_functions.py b/packages/python/plotly/plotly/tests/test_optional/test_px/test_px_functions.py index f92298c88a..65ef0b502c 100644 --- a/packages/python/plotly/plotly/tests/test_optional/test_px/test_px_functions.py +++ b/packages/python/plotly/plotly/tests/test_optional/test_px/test_px_functions.py @@ -542,9 +542,7 @@ def check_label(label, fig): check_label("density of max of tip", fig) -def test_timeline(request, constructor): - if "pyarrow_table" in str(constructor) or "polars_eager" in str(constructor): - request.applymarker(pytest.mark.xfail) +def test_timeline(constructor): df = constructor( { diff --git a/packages/python/plotly/plotly/tests/test_optional/test_px/test_px_hover.py b/packages/python/plotly/plotly/tests/test_optional/test_px/test_px_hover.py index fca0c3e0ef..2fae55f126 100644 --- a/packages/python/plotly/plotly/tests/test_optional/test_px/test_px_hover.py +++ b/packages/python/plotly/plotly/tests/test_optional/test_px/test_px_hover.py @@ -191,10 +191,13 @@ def test_sunburst_hoverdict_color(constructor): def test_date_in_hover(request, constructor): if "pyarrow_table" in str(constructor) or "polars_eager" in str(constructor): + # fig.data[0].customdata[0][0] is a numpy.datetime64 for non pandas + # input, and it does not keep the timezone when converting to py scalar request.applymarker(pytest.mark.xfail) df = nw.from_native( constructor({"date": ["2015-04-04 19:31:30+01:00"], "value": [3]}) ).with_columns(date=nw.col("date").str.to_datetime(format="%Y-%m-%d %H:%M:%S%z")) fig = px.scatter(df.to_native(), x="value", y="value", hover_data=["date"]) + assert fig.data[0].customdata[0][0] == df.item(row=0, column="date") diff --git a/packages/python/plotly/plotly/tests/test_optional/test_px/test_px_input.py b/packages/python/plotly/plotly/tests/test_optional/test_px/test_px_input.py index 6ee0b8ae9c..74237d470e 100644 --- a/packages/python/plotly/plotly/tests/test_optional/test_px/test_px_input.py +++ b/packages/python/plotly/plotly/tests/test_optional/test_px/test_px_input.py @@ -53,13 +53,13 @@ def test_with_index(): def test_series(request, constructor): if "pyarrow_table" in str(constructor): + # By converting to native, we lose the name for pyarrow chunked_array + # and the assertions fail request.applymarker(pytest.mark.xfail) data = px.data.tips().to_dict(orient="list") tips = nw.from_native(constructor(data)) before_tip = (tips.get_column("total_bill") - tips.get_column("tip")).to_native() - # By converting to native, we lose the name for pyarrow chunked_array and the last - # assertion fails day = tips.get_column("day").to_native() tips = tips.to_native() @@ -74,6 +74,8 @@ def test_series(request, constructor): def test_several_dataframes(request, constructor): if "pyarrow_table" in str(constructor): + # By converting to native, we lose the name for pyarrow chunked_array + # and the assertions fail request.applymarker(pytest.mark.xfail) df = nw.from_native(constructor(dict(x=[0, 1], y=[1, 10], z=[0.1, 0.8]))) @@ -153,6 +155,8 @@ def test_several_dataframes(request, constructor): def test_name_heuristics(request, constructor): if "pyarrow_table" in str(constructor): + # By converting to native, we lose the name for pyarrow chunked_array + # and the assertions fail request.applymarker(pytest.mark.xfail) df = nw.from_native(constructor(dict(x=[0, 1], y=[3, 4], z=[0.1, 0.2]))) @@ -482,6 +486,8 @@ def test_pass_df_columns(constructor): def test_size_column(request, constructor): if "pyarrow_table" in str(constructor): + # By converting to native, we lose the name for pyarrow chunked_array + # and the assertions fail request.applymarker(pytest.mark.xfail) data = px.data.tips().to_dict(orient="list") tips = nw.from_native(constructor(data)) From de6761c60b7a62884c63f6c50ba6363c24986747 Mon Sep 17 00:00:00 2001 From: FBruzzesi Date: Tue, 29 Oct 2024 20:48:11 +0100 Subject: [PATCH 077/106] Range(label=...) for px.funnel --- packages/python/plotly/plotly/express/_core.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/python/plotly/plotly/express/_core.py b/packages/python/plotly/plotly/express/_core.py index f98a7a31f4..63214b43ef 100644 --- a/packages/python/plotly/plotly/express/_core.py +++ b/packages/python/plotly/plotly/express/_core.py @@ -1607,7 +1607,13 @@ def build_dataframe(args, constructor): if not wide_mode and (no_x != no_y): for ax in ["x", "y"]: if args.get(ax) is None: - args[ax] = index if index is not None else Range() + args[ax] = ( + index + if index is not None + else Range( + label=_escape_col_name(columns, ax, [var_name, value_name]) + ) + ) if constructor == go.Bar: missing_bar_dim = ax else: From bcfef68e029207d7ca95666af803ccf8c517968d Mon Sep 17 00:00:00 2001 From: FBruzzesi Date: Wed, 30 Oct 2024 09:48:29 +0100 Subject: [PATCH 078/106] improve error message and in-line comments --- .../python/plotly/plotly/express/_core.py | 48 +++++++++++++++---- 1 file changed, 38 insertions(+), 10 deletions(-) diff --git a/packages/python/plotly/plotly/express/_core.py b/packages/python/plotly/plotly/express/_core.py index 63214b43ef..e84336d9e3 100644 --- a/packages/python/plotly/plotly/express/_core.py +++ b/packages/python/plotly/plotly/express/_core.py @@ -1420,16 +1420,30 @@ def build_dataframe(args, constructor): # Cast data_frame argument to DataFrame (it could be a numpy array, dict etc.) df_provided = args["data_frame"] is not None + + # Flag that indicates if the resulting data_frame after parsing is pandas-like + # (in terms of resulting Narwhals DataFrame). + # True if pandas, modin.pandas or cudf DataFrame/Series instance, or converted from + # PySpark to pandas. is_pd_like = False + + # Flag that indicates if data_frame requires to be converted to arrow via the + # dataframe interchange protocol. + # True if Ibis, DuckDB, Vaex or implementes __dataframe__ needs_interchanging = False + + # If data_frame is provided, we parse it into a narwhals DataFrame, while accounting + # for compatibility with pandas specific paths (e.g. Index/MultiIndex case). if df_provided: + # data_frame is pandas-like DataFrame (pandas, modin.pandas, cudf) if nw.dependencies.is_pandas_like_dataframe(args["data_frame"]): columns = args["data_frame"].columns # This can be multi index args["data_frame"] = nw.from_native(args["data_frame"], eager_only=True) is_pd_like = True + # data_frame is pandas-like Series (pandas, modin.pandas, cudf) elif nw.dependencies.is_pandas_like_series(args["data_frame"]): args["data_frame"] = nw.from_native( @@ -1438,6 +1452,9 @@ def build_dataframe(args, constructor): columns = args["data_frame"].columns is_pd_like = True + # data_frame is any other DataFrame object natively supported via Narwhals. + # With strict=False, the original object will be returned if unable to convert + # to a Narwhals DataFrame, making this condition False. elif isinstance( data_frame := nw.from_native( args["data_frame"], eager_or_interchange_only=True, strict=False @@ -1448,6 +1465,9 @@ def build_dataframe(args, constructor): needs_interchanging = nw.get_level(data_frame) == "interchange" columns = args["data_frame"].columns + # data_frame is any other Series object natively supported via Narwhals. + # With strict=False, the original object will be returned if unable to convert + # to a Narwhals DataFrame, making this condition False. elif isinstance( series := nw.from_native( args["data_frame"], series_only=True, strict=False @@ -1457,15 +1477,19 @@ def build_dataframe(args, constructor): args["data_frame"] = series.to_frame() columns = args["data_frame"].columns + # data_frame is PySpark: it does not support interchange protocol and it is not + # integrated in Narwhals. We use its native method to convert it to pandas. elif hasattr(args["data_frame"], "toPandas"): - # data_frame is PySpark: it does not support interchange and it is not - # integrated in narwhals just yet + # data_frame is PySpark: args["data_frame"] = nw.from_native( args["data_frame"].toPandas(), eager_only=True ) columns = args["data_frame"].columns is_pd_like = True + # data_frame is some other object type (e.g. dict, list, ...) + # We try to import pandas, and then try to instantiate a pandas dataframe from + # this such object else: try: import pandas as pd @@ -1477,18 +1501,24 @@ def build_dataframe(args, constructor): columns = args["data_frame"].columns is_pd_like = True except Exception: - msg = f"Unsupported type: {type(args['data_frame'])}" + msg = ( + f"Unable to convert data_frame of type {type(args['data_frame'])} " + "to pandas DataFrame. Please provide a supported dataframe type " + "or a type that can be passed to pd.DataFrame." + ) + raise NotImplementedError(msg) except ImportError: msg = ( - f"data_frame of type {type(args['data_frame'])} requires Pandas " - "to be installed. Convert it to supported dataframe type or " - "install Pandas." + f"Attempting to convert data_frame of type {type(args['data_frame'])} " + "to pandas DataFrame, but Pandas is not installed. " + "Convert it to supported dataframe type or install pandas." ) raise NotImplementedError(msg) + # data_frame is not provided else: - columns = None # no data_frame + columns = None df_input: nw.DataFrame | None = args["data_frame"] index = ( @@ -1573,7 +1603,7 @@ def build_dataframe(args, constructor): value_name = _escape_col_name(columns, "value", []) var_name = _escape_col_name(columns, var_name, []) - if isinstance(args["data_frame"], nw.DataFrame) and needs_interchanging: + if needs_interchanging: # Interchange to PyArrow if wide_mode: args["data_frame"] = nw.from_native( @@ -2035,8 +2065,6 @@ def process_dataframe_timeline(args): raise ValueError("Both x_start and x_end are required") try: - # TODO(FBruzzesi): We still cannot infer datetime format for pyarrow - # Related issue: https://github.com/narwhals-dev/narwhals/issues/1151 df: nw.DataFrame = args["data_frame"] df = df.with_columns( **{ From 519cc680fe5c694682c0b41ce7672a4e31574f1e Mon Sep 17 00:00:00 2001 From: FBruzzesi Date: Wed, 30 Oct 2024 10:06:49 +0100 Subject: [PATCH 079/106] better comments --- .../python/plotly/plotly/express/_core.py | 48 ++++++++++++------- .../express/trendline_functions/__init__.py | 5 +- 2 files changed, 35 insertions(+), 18 deletions(-) diff --git a/packages/python/plotly/plotly/express/_core.py b/packages/python/plotly/plotly/express/_core.py index e84336d9e3..b4c21919fd 100644 --- a/packages/python/plotly/plotly/express/_core.py +++ b/packages/python/plotly/plotly/express/_core.py @@ -1480,7 +1480,6 @@ def build_dataframe(args, constructor): # data_frame is PySpark: it does not support interchange protocol and it is not # integrated in Narwhals. We use its native method to convert it to pandas. elif hasattr(args["data_frame"], "toPandas"): - # data_frame is PySpark: args["data_frame"] = nw.from_native( args["data_frame"].toPandas(), eager_only=True ) @@ -1603,8 +1602,10 @@ def build_dataframe(args, constructor): value_name = _escape_col_name(columns, "value", []) var_name = _escape_col_name(columns, var_name, []) + # If the data_frame has interchange-only support levelin Narwhals, then we need to + # convert it to a full support level backend. + # Hence we convert requires Interchange to PyArrow. if needs_interchanging: - # Interchange to PyArrow if wide_mode: args["data_frame"] = nw.from_native( args["data_frame"].to_arrow(), eager_only=True @@ -1908,25 +1909,38 @@ def process_dataframe_hierarchy(args): n_bytes=16, columns=[*path, count_colname] ) + # In theory, for discrete columns aggregation, we should have a way to do + # `.agg(nw.col(x).unique())` in group_by and successively unpack/parse it as: + # ``` + # (nw.when(nw.col(x).list.len()==1) + # .then(nw.col(x).list.first()) + # .otherwise(nw.lit("(?)")) + # ) + # ``` + # which replicates the original pandas only codebase: + # ``` + # def discrete_agg(x): + # uniques = x.unique() + # return uniques[0] if len(uniques) == 1 else "(?)" + # + # df.groupby(path[i:]).agg(...) + # ``` + # However this is not possible, therefore the following workaround is provided. + # We make two aggregations for the same column: + # - take the max value + # - take the number of unique values + # Finally, after the group by statement, it is unpacked via: + # ``` + # (nw.when(nw.col(col_n_unique) == 1) + # .then(nw.col(col_max_value)) # which is the unique value + # .otherwise(nw.lit("(?)")) + # ) + # ``` + if args["color"]: if discrete_color: discrete_aggs.append(args["color"]) - # Hack: In theory, we should have a way to do `.agg(nw.col(x).unique())` and - # successively unpack/parse it as: - # ``` - # (nw.when(nw.col(x).list.len()==1) - # .then(nw.col(x).list.first()) - # .otherwise(nw.lit("(?)")) - # ) - # ``` - # which replicates: - # ``` - # def discrete_agg(x): - # uniques = x.unique() - # return uniques[0] if len(uniques) == 1 else "(?)" - # ``` - # However we cannot do that just yet, therefore a workaround is provided agg_f[args["color"]] = nw.col(args["color"]).max() agg_f[f'{args["color"]}_{n_unique_token}__'] = ( nw.col(args["color"]) diff --git a/packages/python/plotly/plotly/express/trendline_functions/__init__.py b/packages/python/plotly/plotly/express/trendline_functions/__init__.py index ad9131176e..18ff219979 100644 --- a/packages/python/plotly/plotly/express/trendline_functions/__init__.py +++ b/packages/python/plotly/plotly/express/trendline_functions/__init__.py @@ -124,7 +124,10 @@ def _pandas(mode, trendline_options, x_raw, y, non_missing): series = pd.Series(np.copy(y), index=x_raw.to_pandas()) - # TODO: If narwhals were to support rolling, ewm and expanding then we could go around these + # TODO: Narwhals Series/DataFrame do not support rolling, ewm nor expanding, therefore + # it fallbacks to pandas Series independently of the original type. + # Plotly issue: https://github.com/plotly/plotly.py/issues/4834 + # Narwhals issue: https://github.com/narwhals-dev/narwhals/issues/1254 agg = getattr(series, mode) # e.g. series.rolling agg_obj = agg(**trendline_options) # e.g. series.rolling(**opts) function = getattr(agg_obj, function_name) # e.g. series.rolling(**opts).mean From e5520a79794ffd895b96d3d8de6671ac2aa125ff Mon Sep 17 00:00:00 2001 From: FBruzzesi Date: Thu, 31 Oct 2024 16:49:16 +0100 Subject: [PATCH 080/106] rm unused import and fix typo --- packages/python/plotly/plotly/express/_core.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/python/plotly/plotly/express/_core.py b/packages/python/plotly/plotly/express/_core.py index b4c21919fd..767b651cf0 100644 --- a/packages/python/plotly/plotly/express/_core.py +++ b/packages/python/plotly/plotly/express/_core.py @@ -10,7 +10,6 @@ import math import narwhals.stable.v1 as nw -from narwhals.utils import generate_unique_token from plotly._subplots import ( make_subplots, @@ -1429,7 +1428,7 @@ def build_dataframe(args, constructor): # Flag that indicates if data_frame requires to be converted to arrow via the # dataframe interchange protocol. - # True if Ibis, DuckDB, Vaex or implementes __dataframe__ + # True if Ibis, DuckDB, Vaex or implements __dataframe__ needs_interchanging = False # If data_frame is provided, we parse it into a narwhals DataFrame, while accounting From 51e2b23b0e617c151e742f0d1f63952ba5fc5cbf Mon Sep 17 00:00:00 2001 From: FBruzzesi Date: Thu, 31 Oct 2024 20:04:54 +0100 Subject: [PATCH 081/106] make sure column + token is unique, replace **{} with .alias() --- .../python/plotly/plotly/express/_core.py | 189 +++++++++--------- 1 file changed, 98 insertions(+), 91 deletions(-) diff --git a/packages/python/plotly/plotly/express/_core.py b/packages/python/plotly/plotly/express/_core.py index d08b6afa21..208ca00011 100644 --- a/packages/python/plotly/plotly/express/_core.py +++ b/packages/python/plotly/plotly/express/_core.py @@ -163,6 +163,50 @@ def _is_continuous(df: nw.DataFrame, col_name: str) -> bool: return df.get_column(col_name).dtype.is_numeric() +def _to_unix_epoch_seconds(s: nw.Series) -> nw.Series: + dtype = s.dtype + if dtype == nw.Date: + return s.dt.timestamp("ms") / 1_000 + if dtype == nw.Datetime: + if dtype.time_unit in ("s", "ms"): + return s.dt.timestamp("ms") / 1_000 + elif dtype.time_unit == "us": + return s.dt.timestamp("us") / 1_000_000 + elif dtype.time_unit == "ns": + return s.dt.timestamp("ns") / 1_000_000_000 + else: + msg = "Unexpected dtype, please report a bug" + raise ValueError(msg) + else: + msg = f"Expected Date or Datetime, got {dtype}" + raise TypeError(msg) + + +def _generate_temporary_column_name(n_bytes: int, columns: list[str]) -> str: + """Wraps of Narwhals generate_temporary_column_name to generate a token + which is guaranteed to not be in columns, nor in [col + token for col in columns] + """ + counter = 0 + while True: + # This is guaranteed to not be in columns by Narwhals + token = nw.generate_temporary_column_name(n_bytes, columns=columns) + + # Now check that it is not in the [col + token for col in columns] list + if token not in {f"{c}{token}" for c in columns}: + return token + + counter += 1 + if counter > 100: + msg = ( + "Internal Error: Plotly was not able to generate a column name with " + f"{n_bytes=} and not in {columns}.\n" + "Please report this to " + "https://github.com/plotly/plotly.py/issues/new and we will try to " + "replicate and fix it." + ) + raise AssertionError(msg) + + def get_decorated_label(args, column, role): original_label = label = get_label(args, column) if "histfunc" in args and ( @@ -443,7 +487,7 @@ def make_trace_kwargs(args, trace_spec, trace_data, mapping_labels, sizeref): # dict.fromkeys(customdata_cols) allows to deduplicate column # names, yet maintaining the original order. trace_patch["customdata"] = trace_data.select( - [nw.col(c) for c in dict.fromkeys(customdata_cols)] + *[nw.col(c) for c in dict.fromkeys(customdata_cols)] ) elif attr_name == "color": if trace_spec.constructor in [ @@ -1693,7 +1737,7 @@ def build_dataframe(args, constructor): other_dim = "x" if missing_bar_dim == "y" else "y" if not _is_continuous(df_output, args[other_dim]): args[missing_bar_dim] = count_name - df_output = df_output.with_columns(**{count_name: nw.lit(1)}) + df_output = df_output.with_columns(nw.lit(1).alias(count_name)) else: # on the other hand, if the non-missing dimension is continuous, then we # can use this information to override the normal auto-orientation code @@ -1760,7 +1804,7 @@ def build_dataframe(args, constructor): else: args["x" if orient_v else "y"] = value_name args["y" if orient_v else "x"] = count_name - df_output = df_output.with_columns(**{count_name: nw.lit(1)}) + df_output = df_output.with_columns(nw.lit(1).alias(count_name)) args["color"] = args["color"] or var_name elif constructor in [go.Violin, go.Box]: args["x" if orient_v else "y"] = wide_cross_name or var_name @@ -1773,12 +1817,12 @@ def build_dataframe(args, constructor): args["histfunc"] = None args["orientation"] = "h" args["x"] = count_name - df_output = df_output.with_columns(**{count_name: nw.lit(1)}) + df_output = df_output.with_columns(nw.lit(1).alias(count_name)) else: args["histfunc"] = None args["orientation"] = "v" args["y"] = count_name - df_output = df_output.with_columns(**{count_name: nw.lit(1)}) + df_output = df_output.with_columns(nw.lit(1).alias(count_name)) if no_color: args["color"] = None @@ -1789,10 +1833,10 @@ def build_dataframe(args, constructor): def _check_dataframe_all_leaves(df: nw.DataFrame) -> None: cols = df.columns df_sorted = df.sort(by=cols, descending=False, nulls_last=True) - null_mask = df_sorted.select(*[nw.col(c).is_null() for c in cols]) - df_sorted = df_sorted.with_columns(nw.col(*cols).cast(nw.String())) + null_mask = df_sorted.select(nw.all().is_null()) + df_sorted = df_sorted.select(nw.all().cast(nw.String())) null_indices_mask = null_mask.select( - null_mask=nw.any_horizontal(nw.col(cols)) + null_mask=nw.any_horizontal(nw.all()) ).get_column("null_mask") for row_idx, row in zip( @@ -1854,26 +1898,15 @@ def process_dataframe_hierarchy(args): new_path = [col_name + "_path_copy" for col_name in path] df = df.with_columns( - **{ - new_col_name: nw.col(col_name) - for new_col_name, col_name in zip(new_path, path) - } + nw.col(col_name).alias(new_col_name) + for new_col_name, col_name in zip(new_path, path) ) path = new_path # ------------ Define aggregation functions -------------------------------- agg_f = {} if args["values"]: try: - if isinstance(args["values"], Sequence) and not isinstance( - args["values"], str - ): - df = df.with_columns( - **{c: nw.col(c).cast(nw.Float64()) for c in args["values"]} - ) - else: - df = df.with_columns( - **{args["values"]: nw.col(args["values"]).cast(nw.Float64())} - ) + df = df.with_columns(nw.col(args["values"]).cast(nw.Float64())) except Exception: # pandas, Polars and pyarrow exception types are different raise ValueError( @@ -1883,7 +1916,7 @@ def process_dataframe_hierarchy(args): if args["color"] and args["color"] == args["values"]: new_value_col_name = args["values"] + "_sum" - df = df.with_columns(**{new_value_col_name: nw.col(args["values"])}) + df = df.with_columns(nw.col(args["values"]).alias(new_value_col_name)) args["values"] = new_value_col_name count_colname = args["values"] else: @@ -1894,7 +1927,7 @@ def process_dataframe_hierarchy(args): "count" if "count" not in columns else "".join([str(el) for el in columns]) ) # we can modify df because it's a copy of the px argument - df = df.with_columns(**{count_colname: nw.lit(1)}) + df = df.with_columns(nw.lit(1).alias(count_colname)) args["values"] = count_colname # Since count_colname is always in agg_f, it can be used later to normalize color @@ -1904,8 +1937,8 @@ def process_dataframe_hierarchy(args): discrete_aggs = [] continuous_aggs = [] - n_unique_token = nw.generate_temporary_column_name( - n_bytes=16, columns=[*path, count_colname] + n_unique_token = _generate_temporary_column_name( + n_bytes=16, columns=df.collect_schema().names() ) # In theory, for discrete columns aggregation, we should have a way to do @@ -1941,10 +1974,10 @@ def process_dataframe_hierarchy(args): discrete_aggs.append(args["color"]) agg_f[args["color"]] = nw.col(args["color"]).max() - agg_f[f'{args["color"]}_{n_unique_token}__'] = ( + agg_f[f'{args["color"]}{n_unique_token}'] = ( nw.col(args["color"]) .n_unique() - .alias(f'{args["color"]}_{n_unique_token}__') + .alias(f'{args["color"]}{n_unique_token}') ) else: # This first needs to be multiplied by `count_colname` @@ -1954,16 +1987,15 @@ def process_dataframe_hierarchy(args): # Other columns (for color, hover_data, custom_data etc.) cols = list(set(df.collect_schema().names()).difference(path)) - df = df.with_columns( - **{c: nw.col(c).cast(nw.String()) for c in cols if c not in agg_f} - ) + df = df.with_columns(nw.col(c).cast(nw.String()) for c in cols if c not in agg_f) + for col in cols: # for hover_data, custom_data etc. if col not in agg_f: # Similar trick as above discrete_aggs.append(col) agg_f[col] = nw.col(col).max() - agg_f[f"{col}_{n_unique_token}__"] = ( - nw.col(col).n_unique().alias(f"{col}_{n_unique_token}__") + agg_f[f"{col}{n_unique_token}"] = ( + nw.col(col).n_unique().alias(f"{col}{n_unique_token}") ) # Avoid collisions with reserved names - columns in the path have been copied already cols = list(set(cols) - set(["labels", "parent", "id"])) @@ -1972,7 +2004,7 @@ def process_dataframe_hierarchy(args): if args["color"] and not discrete_color: df = df.with_columns( - **{args["color"]: nw.col(args["color"]) * nw.col(count_colname)} + (nw.col(args["color"]) * nw.col(count_colname)).alias(args["color"]) ) def post_agg(dframe: nw.LazyFrame, continuous_aggs, discrete_aggs) -> nw.LazyFrame: @@ -1981,14 +2013,14 @@ def post_agg(dframe: nw.LazyFrame, continuous_aggs, discrete_aggs) -> nw.LazyFra - discrete_aggs is either [args["color"], ] or [] """ return dframe.with_columns( - **{c: nw.col(c) / nw.col(count_colname) for c in continuous_aggs}, + **{col: nw.col(col) / nw.col(count_colname) for col in continuous_aggs}, **{ - c: nw.when(nw.col(f"{c}_{n_unique_token}__") == 1) - .then(nw.col(c)) + col: nw.when(nw.col(f"{col}{n_unique_token}") == 1) + .then(nw.col(col)) .otherwise(nw.lit("(?)")) - for c in discrete_aggs + for col in discrete_aggs }, - ).drop([f"{c}_{n_unique_token}__" for c in discrete_aggs]) + ).drop([f"{col}{n_unique_token}" for col in discrete_aggs]) for i, level in enumerate(path): @@ -2006,30 +2038,26 @@ def post_agg(dframe: nw.LazyFrame, continuous_aggs, discrete_aggs) -> nw.LazyFra id=nw.col(level).cast(nw.String()), ) if i < len(path) - 1: - _concat_str_token = nw.generate_temporary_column_name( - n_bytes=8, columns=[*cols, "labels", "parent", "id"] + _concat_str_token = _generate_temporary_column_name( + n_bytes=16, columns=[*cols, "labels", "parent", "id"] ) df_tree = ( df_tree.with_columns( - **{ - _concat_str_token: nw.concat_str( - [ - nw.col(path[j]).cast(nw.String()) - for j in range(len(path) - 1, i, -1) - ], - separator="/", - ) - } + nw.concat_str( + [ + nw.col(path[j]).cast(nw.String()) + for j in range(len(path) - 1, i, -1) + ], + separator="/", + ).alias(_concat_str_token) ) .with_columns( - **{ - "parent": nw.concat_str( - [nw.col(_concat_str_token), nw.col("parent")], separator="/" - ), - "id": nw.concat_str( - [nw.col(_concat_str_token), nw.col("id")], separator="/" - ), - } + parent=nw.concat_str( + [nw.col(_concat_str_token), nw.col("parent")], separator="/" + ), + id=nw.concat_str( + [nw.col(_concat_str_token), nw.col("id")], separator="/" + ), ) .drop(_concat_str_token) ) @@ -2049,7 +2077,7 @@ def post_agg(dframe: nw.LazyFrame, continuous_aggs, discrete_aggs) -> nw.LazyFra while sort_col_name in df_all_trees.columns: sort_col_name += "0" df_all_trees = df_all_trees.with_columns( - **{sort_col_name: nw.col(args["color"]).cast(nw.String())} + nw.col(args["color"]).cast(nw.String()).alias(sort_col_name) ).sort(by=sort_col_name, nulls_last=True) # Now modify arguments @@ -2080,10 +2108,8 @@ def process_dataframe_timeline(args): try: df: nw.DataFrame = args["data_frame"] df = df.with_columns( - **{ - args["x_start"]: nw.col(args["x_start"]).str.to_datetime(), - args["x_end"]: nw.col(args["x_end"]).str.to_datetime(), - } + nw.col(args["x_start"]).str.to_datetime().alias(args["x_start"]), + nw.col(args["x_end"]).str.to_datetime().alias(args["x_end"]), ) except Exception: raise TypeError( @@ -2092,11 +2118,9 @@ def process_dataframe_timeline(args): # note that we are not adding any columns to the data frame here, so no risk of overwrite args["data_frame"] = df.with_columns( - **{ - args["x_end"]: ( - nw.col(args["x_end"]) - nw.col(args["x_start"]) - ).dt.total_milliseconds() - } + (nw.col(args["x_end"]) - nw.col(args["x_start"])) + .dt.total_milliseconds() + .alias(args["x_end"]) ) args["x"] = args["x_end"] args["base"] = args["x_start"] @@ -2594,20 +2618,22 @@ def make_figure(args, constructor, trace_patch=None, layout_patch=None): group_sum = group.get_column( var ).sum() # compute here before next line mutates - group = group.with_columns(**{var: nw.col(var).cum_sum()}) + group = group.with_columns(nw.col(var).cum_sum().alias(var)) if not ascending: group = group.sort(by=base, descending=False, nulls_last=True) if args.get("ecdfmode", "standard") == "complementary": group = group.with_columns( - **{var: (nw.col(var) - nw.lit(group_sum)) * (-1)} + ((nw.col(var) - nw.lit(group_sum)) * (-1)).alias(var) ) if args["ecdfnorm"] == "probability": - group = group.with_columns(**{var: nw.col(var) / nw.lit(group_sum)}) + group = group.with_columns( + (nw.col(var) / nw.lit(group_sum)).alias(var) + ) elif args["ecdfnorm"] == "percent": group = group.with_columns( - **{var: nw.col(var) / nw.lit(group_sum) * nw.lit(100.0)} + (nw.col(var) / nw.lit(group_sum) * nw.lit(100.0)).alias(var) ) patch, fit_results = make_trace_kwargs( @@ -2835,22 +2861,3 @@ def _spacing_error_translator(e, direction, facet_arg): annot.update(font=None) return fig - - -def _to_unix_epoch_seconds(s: nw.Series) -> nw.Series: - dtype = s.dtype - if dtype == nw.Date: - return s.dt.timestamp("ms") / 1_000 - if dtype == nw.Datetime: - if dtype.time_unit in ("s", "ms"): - return s.dt.timestamp("ms") / 1_000 - elif dtype.time_unit == "us": - return s.dt.timestamp("us") / 1_000_000 - elif dtype.time_unit == "ns": - return s.dt.timestamp("ns") / 1_000_000_000 - else: - msg = "Unexpected dtype, please report a bug" - raise ValueError(msg) - else: - msg = f"Expected Date or Datetime, got {dtype}" - raise TypeError(msg) From 7ef9f28b5cae97cde2b4276bea93c7e37c7efb3d Mon Sep 17 00:00:00 2001 From: FBruzzesi Date: Thu, 31 Oct 2024 20:47:30 +0100 Subject: [PATCH 082/106] WIP --- .../python/plotly/plotly/data/__init__.py | 151 +++++++++++------- packages/python/plotly/requirements.txt | 3 + packages/python/plotly/setup.py | 2 +- 3 files changed, 97 insertions(+), 59 deletions(-) diff --git a/packages/python/plotly/plotly/data/__init__.py b/packages/python/plotly/plotly/data/__init__.py index 7669c4588c..b850ddd48c 100644 --- a/packages/python/plotly/plotly/data/__init__.py +++ b/packages/python/plotly/plotly/data/__init__.py @@ -1,9 +1,9 @@ """ Built-in datasets for demonstration, educational and test purposes. """ +import narwhals.stable.v1 as nw - -def gapminder(datetimes=False, centroids=False, year=None, pretty_names=False): +def gapminder(datetimes=False, centroids=False, year=None, pretty_names=False, return_type="pandas"): """ Each row represents a country on a given year. @@ -17,16 +17,16 @@ def gapminder(datetimes=False, centroids=False, year=None, pretty_names=False): If `centroids` is True, two new columns are added: ['centroid_lat', 'centroid_lon'] If `year` is an integer, the dataset will be filtered for that year """ - df = _get_dataset("gapminder") + df = nw.from_native(_get_dataset("gapminder", return_type=return_type), eager_only=True) if year: - df = df[df["year"] == year] + df = df.filter(nw.col("year") == year) if datetimes: - df["year"] = (df["year"].astype(str) + "-01-01").astype("datetime64[ns]") + df = df.with_columns(nw.concat_str([nw.col("year").cast(nw.String()), nw.lit("-01-01")]).cast(nw.Datetime(time_unit="ns"))) if not centroids: - df = df.drop(["centroid_lat", "centroid_lon"], axis=1) + df = df.drop("centroid_lat", "centroid_lon") if pretty_names: - df.rename( - mapper=dict( + df = df.rename( + dict( country="Country", continent="Continent", year="Year", @@ -37,14 +37,12 @@ def gapminder(datetimes=False, centroids=False, year=None, pretty_names=False): iso_num="ISO Numeric Country Code", centroid_lat="Centroid Latitude", centroid_lon="Centroid Longitude", - ), - axis="columns", - inplace=True, + ) ) - return df + return df.to_native() -def tips(pretty_names=False): +def tips(pretty_names=False, return_type="pandas"): """ Each row represents a restaurant bill. @@ -54,10 +52,10 @@ def tips(pretty_names=False): A `pandas.DataFrame` with 244 rows and the following columns: `['total_bill', 'tip', 'sex', 'smoker', 'day', 'time', 'size']`.""" - df = _get_dataset("tips") + df = nw.from_native(_get_dataset("tips", return_type=return_type), eager_only=True) if pretty_names: - df.rename( - mapper=dict( + df = df.rename( + dict( total_bill="Total Bill", tip="Tip", sex="Payer Gender", @@ -65,14 +63,12 @@ def tips(pretty_names=False): day="Day of Week", time="Meal", size="Party Size", - ), - axis="columns", - inplace=True, + ) ) - return df + return df.to_native() -def iris(): +def iris(return_type="pandas"): """ Each row represents a flower. @@ -81,20 +77,20 @@ def iris(): Returns: A `pandas.DataFrame` with 150 rows and the following columns: `['sepal_length', 'sepal_width', 'petal_length', 'petal_width', 'species', 'species_id']`.""" - return _get_dataset("iris") + return _get_dataset("iris", return_type=return_type) -def wind(): +def wind(return_type="pandas"): """ Each row represents a level of wind intensity in a cardinal direction, and its frequency. Returns: A `pandas.DataFrame` with 128 rows and the following columns: `['direction', 'strength', 'frequency']`.""" - return _get_dataset("wind") + return _get_dataset("wind", return_type=return_type) -def election(): +def election(return_type="pandas"): """ Each row represents voting results for an electoral district in the 2013 Montreal mayoral election. @@ -102,7 +98,7 @@ def election(): Returns: A `pandas.DataFrame` with 58 rows and the following columns: `['district', 'Coderre', 'Bergeron', 'Joly', 'total', 'winner', 'result', 'district_id']`.""" - return _get_dataset("election") + return _get_dataset("election", return_type=return_type) def election_geojson(): @@ -128,7 +124,7 @@ def election_geojson(): return result -def carshare(): +def carshare(return_type="pandas"): """ Each row represents the availability of car-sharing services near the centroid of a zone in Montreal over a month-long period. @@ -136,10 +132,10 @@ def carshare(): Returns: A `pandas.DataFrame` with 249 rows and the following columns: `['centroid_lat', 'centroid_lon', 'car_hours', 'peak_hour']`.""" - return _get_dataset("carshare") + return _get_dataset("carshare", return_type=return_type) -def stocks(indexed=False, datetimes=False): +def stocks(indexed=False, datetimes=False, return_type="pandas"): """ Each row in this wide dataset represents closing prices from 6 tech stocks in 2018/2019. @@ -149,16 +145,23 @@ def stocks(indexed=False, datetimes=False): If `indexed` is True, the 'date' column is used as the index and the column index If `datetimes` is True, the 'date' column will be a datetime column is named 'company'""" - df = _get_dataset("stocks") + if indexed and return_type != "pandas": + msg = "Cannot set index for backend different from pandas" + raise NotImplementedError(msg) + + df = nw.from_native(_get_dataset("stocks", return_type=return_type), eager_only=True) if datetimes: - df["date"] = df["date"].astype("datetime64[ns]") - if indexed: - df = df.set_index("date") + df = df.with_columns(nw.col("date").cast(nw.Datetime(time_unit="ns"))) + + if indexed: # then it must be pandas + df = df.to_native().set_index("date") df.columns.name = "company" - return df + return df + return df.to_native() -def experiment(indexed=False): + +def experiment(indexed=False, return_type="pandas"): """ Each row in this wide dataset represents the results of 100 simulated participants on three hypothetical experiments, along with their gender and control/treatment group. @@ -168,13 +171,20 @@ def experiment(indexed=False): A `pandas.DataFrame` with 100 rows and the following columns: `['experiment_1', 'experiment_2', 'experiment_3', 'gender', 'group']`. If `indexed` is True, the data frame index is named "participant" """ - df = _get_dataset("experiment") - if indexed: + + if indexed and return_type != "pandas": + msg = "Cannot set index for backend different from pandas" + raise NotImplementedError(msg) + + df = nw.from_native(_get_dataset("experiment", return_type=return_type), eager_only=True) + if indexed: # then it must be pandas + df = df.to_native() df.index.name = "participant" - return df + return df + return df.to_native() -def medals_wide(indexed=False): +def medals_wide(indexed=False, return_type="pandas"): """ This dataset represents the medal table for Olympic Short Track Speed Skating for the top three nations as of 2020. @@ -184,14 +194,20 @@ def medals_wide(indexed=False): `['nation', 'gold', 'silver', 'bronze']`. If `indexed` is True, the 'nation' column is used as the index and the column index is named 'medal'""" - df = _get_dataset("medals") - if indexed: - df = df.set_index("nation") + + if indexed and return_type != "pandas": + msg = "Cannot set index for backend different from pandas" + raise NotImplementedError(msg) + + df = nw.from_native(_get_dataset("medals", return_type=return_type), eager_only=True) + if indexed: # then it must be pandas + df = df.to_native().set_index("nation") df.columns.name = "medal" - return df + return df + return df.to_native() -def medals_long(indexed=False): +def medals_long(indexed=False, return_type="pandas"): """ This dataset represents the medal table for Olympic Short Track Speed Skating for the top three nations as of 2020. @@ -200,23 +216,42 @@ def medals_long(indexed=False): A `pandas.DataFrame` with 9 rows and the following columns: `['nation', 'medal', 'count']`. If `indexed` is True, the 'nation' column is used as the index.""" - df = _get_dataset("medals").melt( - id_vars=["nation"], value_name="count", var_name="medal" - ) + + if indexed and return_type != "pandas": + msg = "Cannot set index for backend different from pandas" + raise NotImplementedError(msg) + + df = ( + nw.from_native(_get_dataset("medals", return_type=return_type), eager_only=True) + .unpivot( + index=["nation"], + value_name="count", + variable_name="medal", + )) if indexed: - df = df.set_index("nation") - return df + df = nw.maybe_set_index(df, "nation") + return df.to_native() -def _get_dataset(d): - import pandas +def _get_dataset(d, return_type): import os + from importlib import import_module - return pandas.read_csv( - os.path.join( - os.path.dirname(os.path.dirname(__file__)), - "package_data", - "datasets", - d + ".csv.gz", - ) + AVAILABLE_BACKENDS = {"pandas", "polars", "pyarrow"} + + filepath = os.path.join( + os.path.dirname(os.path.dirname(__file__)), + "package_data", + "datasets", + d + ".csv.gz", ) + if return_type not in AVAILABLE_BACKENDS: + msg = f"Unsupported return_type. Found {return_type}, expected one of {AVAILABLE_BACKENDS}" + raise NotImplementedError(msg) + + try: + backend = import_module(return_type) + return backend.read_csv(filepath) + except ModuleNotFoundError: + msg = f"return_type={return_type}, but {return_type} is not installed" + raise ModuleNotFoundError(msg) diff --git a/packages/python/plotly/requirements.txt b/packages/python/plotly/requirements.txt index 463fe1bbfb..9b4b1093ce 100644 --- a/packages/python/plotly/requirements.txt +++ b/packages/python/plotly/requirements.txt @@ -4,3 +4,6 @@ ### $ pip install -r requirements.txt ### ### ### ################################################### + +## dataframe agnostic layer ## +narwhals>=1.12.0 diff --git a/packages/python/plotly/setup.py b/packages/python/plotly/setup.py index 2c49f298cd..3d0aa1273d 100644 --- a/packages/python/plotly/setup.py +++ b/packages/python/plotly/setup.py @@ -603,7 +603,7 @@ def run(self): data_files=[ ("etc/jupyter/nbconfig/notebook.d", ["jupyterlab-plotly.json"]), ], - install_requires=["packaging"], + install_requires=["narwhals>=1.12.0", "packaging"], zip_safe=False, cmdclass=dict( build_py=js_prerelease(versioneer_cmds["build_py"]), From e9a367de4231f8218bfb081c1c88cefe038ef53d Mon Sep 17 00:00:00 2001 From: FBruzzesi Date: Thu, 31 Oct 2024 20:47:57 +0100 Subject: [PATCH 083/106] WIP --- .../python/plotly/plotly/data/__init__.py | 55 +++++++++++++------ 1 file changed, 37 insertions(+), 18 deletions(-) diff --git a/packages/python/plotly/plotly/data/__init__.py b/packages/python/plotly/plotly/data/__init__.py index b850ddd48c..f9c8baa268 100644 --- a/packages/python/plotly/plotly/data/__init__.py +++ b/packages/python/plotly/plotly/data/__init__.py @@ -3,7 +3,14 @@ """ import narwhals.stable.v1 as nw -def gapminder(datetimes=False, centroids=False, year=None, pretty_names=False, return_type="pandas"): + +def gapminder( + datetimes=False, + centroids=False, + year=None, + pretty_names=False, + return_type="pandas", +): """ Each row represents a country on a given year. @@ -17,11 +24,17 @@ def gapminder(datetimes=False, centroids=False, year=None, pretty_names=False, r If `centroids` is True, two new columns are added: ['centroid_lat', 'centroid_lon'] If `year` is an integer, the dataset will be filtered for that year """ - df = nw.from_native(_get_dataset("gapminder", return_type=return_type), eager_only=True) + df = nw.from_native( + _get_dataset("gapminder", return_type=return_type), eager_only=True + ) if year: df = df.filter(nw.col("year") == year) if datetimes: - df = df.with_columns(nw.concat_str([nw.col("year").cast(nw.String()), nw.lit("-01-01")]).cast(nw.Datetime(time_unit="ns"))) + df = df.with_columns( + nw.concat_str([nw.col("year").cast(nw.String()), nw.lit("-01-01")]).cast( + nw.Datetime(time_unit="ns") + ) + ) if not centroids: df = df.drop("centroid_lat", "centroid_lon") if pretty_names: @@ -149,10 +162,12 @@ def stocks(indexed=False, datetimes=False, return_type="pandas"): msg = "Cannot set index for backend different from pandas" raise NotImplementedError(msg) - df = nw.from_native(_get_dataset("stocks", return_type=return_type), eager_only=True) + df = nw.from_native( + _get_dataset("stocks", return_type=return_type), eager_only=True + ) if datetimes: df = df.with_columns(nw.col("date").cast(nw.Datetime(time_unit="ns"))) - + if indexed: # then it must be pandas df = df.to_native().set_index("date") df.columns.name = "company" @@ -171,12 +186,14 @@ def experiment(indexed=False, return_type="pandas"): A `pandas.DataFrame` with 100 rows and the following columns: `['experiment_1', 'experiment_2', 'experiment_3', 'gender', 'group']`. If `indexed` is True, the data frame index is named "participant" """ - + if indexed and return_type != "pandas": msg = "Cannot set index for backend different from pandas" raise NotImplementedError(msg) - df = nw.from_native(_get_dataset("experiment", return_type=return_type), eager_only=True) + df = nw.from_native( + _get_dataset("experiment", return_type=return_type), eager_only=True + ) if indexed: # then it must be pandas df = df.to_native() df.index.name = "participant" @@ -194,12 +211,14 @@ def medals_wide(indexed=False, return_type="pandas"): `['nation', 'gold', 'silver', 'bronze']`. If `indexed` is True, the 'nation' column is used as the index and the column index is named 'medal'""" - + if indexed and return_type != "pandas": msg = "Cannot set index for backend different from pandas" raise NotImplementedError(msg) - df = nw.from_native(_get_dataset("medals", return_type=return_type), eager_only=True) + df = nw.from_native( + _get_dataset("medals", return_type=return_type), eager_only=True + ) if indexed: # then it must be pandas df = df.to_native().set_index("nation") df.columns.name = "medal" @@ -216,18 +235,18 @@ def medals_long(indexed=False, return_type="pandas"): A `pandas.DataFrame` with 9 rows and the following columns: `['nation', 'medal', 'count']`. If `indexed` is True, the 'nation' column is used as the index.""" - + if indexed and return_type != "pandas": msg = "Cannot set index for backend different from pandas" raise NotImplementedError(msg) - - df = ( - nw.from_native(_get_dataset("medals", return_type=return_type), eager_only=True) - .unpivot( - index=["nation"], - value_name="count", - variable_name="medal", - )) + + df = nw.from_native( + _get_dataset("medals", return_type=return_type), eager_only=True + ).unpivot( + index=["nation"], + value_name="count", + variable_name="medal", + ) if indexed: df = nw.maybe_set_index(df, "nation") return df.to_native() From 27b29968a92cdf808feb5a3e90edf5c0069f9dd0 Mon Sep 17 00:00:00 2001 From: FBruzzesi Date: Fri, 1 Nov 2024 09:49:35 +0100 Subject: [PATCH 084/106] use nw.get_native_namespace --- packages/python/plotly/plotly/express/_core.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/python/plotly/plotly/express/_core.py b/packages/python/plotly/plotly/express/_core.py index 208ca00011..f8ce87ceaf 100644 --- a/packages/python/plotly/plotly/express/_core.py +++ b/packages/python/plotly/plotly/express/_core.py @@ -1855,7 +1855,7 @@ def _check_dataframe_all_leaves(df: nw.DataFrame) -> None: name="fill_value", values=[""] * len(df_sorted), dtype=nw.String(), - native_namespace=df_sorted.__native_namespace__(), + native_namespace=nw.get_native_namespace(df_sorted), ) df_sorted = df_sorted.with_columns( **{ From 77353663f5f9c07f9a6148660c0411d5a16fa9c4 Mon Sep 17 00:00:00 2001 From: FBruzzesi Date: Fri, 1 Nov 2024 09:54:16 +0100 Subject: [PATCH 085/106] add narwhals in various requirements --- packages/python/plotly/optional-requirements.txt | 1 + .../python/plotly/test_requirements/requirements_310_core.txt | 1 + .../plotly/test_requirements/requirements_310_optional.txt | 1 + .../python/plotly/test_requirements/requirements_311_core.txt | 1 + .../plotly/test_requirements/requirements_311_optional.txt | 1 + .../python/plotly/test_requirements/requirements_312_core.txt | 1 + .../test_requirements/requirements_312_no_numpy_optional.txt | 3 ++- .../plotly/test_requirements/requirements_312_optional.txt | 3 ++- .../python/plotly/test_requirements/requirements_38_core.txt | 1 + .../plotly/test_requirements/requirements_38_optional.txt | 1 + .../python/plotly/test_requirements/requirements_39_core.txt | 1 + .../plotly/test_requirements/requirements_39_optional.txt | 1 + .../test_requirements/requirements_39_pandas_2_optional.txt | 1 + 13 files changed, 15 insertions(+), 2 deletions(-) diff --git a/packages/python/plotly/optional-requirements.txt b/packages/python/plotly/optional-requirements.txt index ba3c054912..465188f802 100644 --- a/packages/python/plotly/optional-requirements.txt +++ b/packages/python/plotly/optional-requirements.txt @@ -39,6 +39,7 @@ ipython ## pandas deps for some matplotlib functionality ## pandas +narwhals>=1.12.0 ## scipy deps for some FigureFactory functions ## scipy diff --git a/packages/python/plotly/test_requirements/requirements_310_core.txt b/packages/python/plotly/test_requirements/requirements_310_core.txt index c3af689b05..c5ea5ec903 100644 --- a/packages/python/plotly/test_requirements/requirements_310_core.txt +++ b/packages/python/plotly/test_requirements/requirements_310_core.txt @@ -1,2 +1,3 @@ requests==2.25.1 pytest==7.4.4 +narwhals>=1.12.0 diff --git a/packages/python/plotly/test_requirements/requirements_310_optional.txt b/packages/python/plotly/test_requirements/requirements_310_optional.txt index 5494291abf..dd4c6c078e 100644 --- a/packages/python/plotly/test_requirements/requirements_310_optional.txt +++ b/packages/python/plotly/test_requirements/requirements_310_optional.txt @@ -19,3 +19,4 @@ scikit-image==0.22.0 psutil==5.7.0 kaleido orjson==3.8.12 +narwhals>=1.12.0 diff --git a/packages/python/plotly/test_requirements/requirements_311_core.txt b/packages/python/plotly/test_requirements/requirements_311_core.txt index c3af689b05..c5ea5ec903 100644 --- a/packages/python/plotly/test_requirements/requirements_311_core.txt +++ b/packages/python/plotly/test_requirements/requirements_311_core.txt @@ -1,2 +1,3 @@ requests==2.25.1 pytest==7.4.4 +narwhals>=1.12.0 diff --git a/packages/python/plotly/test_requirements/requirements_311_optional.txt b/packages/python/plotly/test_requirements/requirements_311_optional.txt index 64392dacd6..8f4391cfb4 100644 --- a/packages/python/plotly/test_requirements/requirements_311_optional.txt +++ b/packages/python/plotly/test_requirements/requirements_311_optional.txt @@ -19,3 +19,4 @@ scikit-image==0.22.0 psutil==5.7.0 kaleido orjson==3.8.12 +narwhals>=1.12.0 diff --git a/packages/python/plotly/test_requirements/requirements_312_core.txt b/packages/python/plotly/test_requirements/requirements_312_core.txt index c3af689b05..c5ea5ec903 100644 --- a/packages/python/plotly/test_requirements/requirements_312_core.txt +++ b/packages/python/plotly/test_requirements/requirements_312_core.txt @@ -1,2 +1,3 @@ requests==2.25.1 pytest==7.4.4 +narwhals>=1.12.0 diff --git a/packages/python/plotly/test_requirements/requirements_312_no_numpy_optional.txt b/packages/python/plotly/test_requirements/requirements_312_no_numpy_optional.txt index 9de2a98492..e869652865 100644 --- a/packages/python/plotly/test_requirements/requirements_312_no_numpy_optional.txt +++ b/packages/python/plotly/test_requirements/requirements_312_no_numpy_optional.txt @@ -18,4 +18,5 @@ scikit-image==0.22.0 psutil==5.9.7 kaleido orjson==3.9.10 -jupyter-console==6.4.4 \ No newline at end of file +jupyter-console==6.4.4 +narwhals>=1.12.0 diff --git a/packages/python/plotly/test_requirements/requirements_312_optional.txt b/packages/python/plotly/test_requirements/requirements_312_optional.txt index 8bde6e8b09..b47966a799 100644 --- a/packages/python/plotly/test_requirements/requirements_312_optional.txt +++ b/packages/python/plotly/test_requirements/requirements_312_optional.txt @@ -19,4 +19,5 @@ scikit-image==0.22.0 psutil==5.9.7 kaleido orjson==3.9.10 -jupyter-console==6.4.4 \ No newline at end of file +jupyter-console==6.4.4 +narwhals>=1.12.0 diff --git a/packages/python/plotly/test_requirements/requirements_38_core.txt b/packages/python/plotly/test_requirements/requirements_38_core.txt index 659fe1a370..c84e860854 100644 --- a/packages/python/plotly/test_requirements/requirements_38_core.txt +++ b/packages/python/plotly/test_requirements/requirements_38_core.txt @@ -1,2 +1,3 @@ requests==2.25.1 pytest==8.1.1 +narwhals>=1.12.0 diff --git a/packages/python/plotly/test_requirements/requirements_38_optional.txt b/packages/python/plotly/test_requirements/requirements_38_optional.txt index 0381d36d30..aee7724e7e 100644 --- a/packages/python/plotly/test_requirements/requirements_38_optional.txt +++ b/packages/python/plotly/test_requirements/requirements_38_optional.txt @@ -19,3 +19,4 @@ matplotlib==3.7.3 scikit-image==0.18.1 psutil==5.7.0 kaleido +narwhals>=1.12.0 diff --git a/packages/python/plotly/test_requirements/requirements_39_core.txt b/packages/python/plotly/test_requirements/requirements_39_core.txt index f4605b806c..b15f894884 100644 --- a/packages/python/plotly/test_requirements/requirements_39_core.txt +++ b/packages/python/plotly/test_requirements/requirements_39_core.txt @@ -1,2 +1,3 @@ requests==2.25.1 pytest==6.2.3 +narwhals>=1.12.0 diff --git a/packages/python/plotly/test_requirements/requirements_39_optional.txt b/packages/python/plotly/test_requirements/requirements_39_optional.txt index 91e3af383e..87c6546c9c 100644 --- a/packages/python/plotly/test_requirements/requirements_39_optional.txt +++ b/packages/python/plotly/test_requirements/requirements_39_optional.txt @@ -20,3 +20,4 @@ scikit-image==0.18.1 psutil==5.7.0 kaleido orjson==3.8.12 +narwhals>=1.12.0 diff --git a/packages/python/plotly/test_requirements/requirements_39_pandas_2_optional.txt b/packages/python/plotly/test_requirements/requirements_39_pandas_2_optional.txt index affdb9ddfc..ffba0f2793 100644 --- a/packages/python/plotly/test_requirements/requirements_39_pandas_2_optional.txt +++ b/packages/python/plotly/test_requirements/requirements_39_pandas_2_optional.txt @@ -21,3 +21,4 @@ kaleido vaex pydantic<=1.10.11 # for vaex, see https://github.com/vaexio/vaex/issues/2384 polars +narwhals>=1.12.0 From b6516b4b8900450552a65b189dbeb0eafe7bd396 Mon Sep 17 00:00:00 2001 From: FBruzzesi Date: Fri, 1 Nov 2024 10:19:34 +0100 Subject: [PATCH 086/106] docstrings --- .../python/plotly/plotly/data/__init__.py | 197 +++++++++++++++--- 1 file changed, 165 insertions(+), 32 deletions(-) diff --git a/packages/python/plotly/plotly/data/__init__.py b/packages/python/plotly/plotly/data/__init__.py index f9c8baa268..ff2a04ecd6 100644 --- a/packages/python/plotly/plotly/data/__init__.py +++ b/packages/python/plotly/plotly/data/__init__.py @@ -16,10 +16,30 @@ def gapminder( https://www.gapminder.org/data/ - Returns: - A `pandas.DataFrame` with 1704 rows and the following columns: + Parameters + ---------- + datetimes: bool + Whether or not 'year' column will converted to datetime type + + centroids: bool + If True, ['centroid_lat', 'centroid_lon'] columns are added + + year: int | None + If provided, the dataset will be filtered for that year + + pretty_names: bool + If True, prettifies the column names + + return_type: {'pandas', 'polars', 'pyarrow'} + Type of the resulting dataframe + + Returns + ------- + Dataframe of `return_type` type + Dataframe with 1704 rows and the following columns: `['country', 'continent', 'year', 'lifeExp', 'pop', 'gdpPercap', 'iso_alpha', 'iso_num']`. + If `datetimes` is True, the 'year' column will be a datetime column If `centroids` is True, two new columns are added: ['centroid_lat', 'centroid_lon'] If `year` is an integer, the dataset will be filtered for that year @@ -61,9 +81,20 @@ def tips(pretty_names=False, return_type="pandas"): https://vincentarelbundock.github.io/Rdatasets/doc/reshape2/tips.html - Returns: - A `pandas.DataFrame` with 244 rows and the following columns: - `['total_bill', 'tip', 'sex', 'smoker', 'day', 'time', 'size']`.""" + Parameters + ---------- + pretty_names: bool + If True, prettifies the column names + + return_type: {'pandas', 'polars', 'pyarrow'} + Type of the resulting dataframe + + Returns + ------- + Dataframe of `return_type` type + Dataframe with 244 rows and the following columns: + `['total_bill', 'tip', 'sex', 'smoker', 'day', 'time', 'size']`. + """ df = nw.from_native(_get_dataset("tips", return_type=return_type), eager_only=True) if pretty_names: @@ -87,9 +118,17 @@ def iris(return_type="pandas"): https://en.wikipedia.org/wiki/Iris_flower_data_set - Returns: - A `pandas.DataFrame` with 150 rows and the following columns: - `['sepal_length', 'sepal_width', 'petal_length', 'petal_width', 'species', 'species_id']`.""" + Parameters + ---------- + return_type: {'pandas', 'polars', 'pyarrow'} + Type of the resulting dataframe + + Returns + ------- + Dataframe of `return_type` type + Dataframe with 150 rows and the following columns: + `['sepal_length', 'sepal_width', 'petal_length', 'petal_width', 'species', 'species_id']`. + """ return _get_dataset("iris", return_type=return_type) @@ -97,9 +136,17 @@ def wind(return_type="pandas"): """ Each row represents a level of wind intensity in a cardinal direction, and its frequency. - Returns: - A `pandas.DataFrame` with 128 rows and the following columns: - `['direction', 'strength', 'frequency']`.""" + Parameters + ---------- + return_type: {'pandas', 'polars', 'pyarrow'} + Type of the resulting dataframe + + Returns + ------- + Dataframe of `return_type` type + Dataframe with 128 rows and the following columns: + `['direction', 'strength', 'frequency']`. + """ return _get_dataset("wind", return_type=return_type) @@ -108,9 +155,17 @@ def election(return_type="pandas"): Each row represents voting results for an electoral district in the 2013 Montreal mayoral election. - Returns: - A `pandas.DataFrame` with 58 rows and the following columns: - `['district', 'Coderre', 'Bergeron', 'Joly', 'total', 'winner', 'result', 'district_id']`.""" + Parameters + ---------- + return_type: {'pandas', 'polars', 'pyarrow'} + Type of the resulting dataframe + + Returns + ------- + Dataframe of `return_type` type + Dataframe with 58 rows and the following columns: + `['district', 'Coderre', 'Bergeron', 'Joly', 'total', 'winner', 'result', 'district_id']`. + """ return _get_dataset("election", return_type=return_type) @@ -118,10 +173,12 @@ def election_geojson(): """ Each feature represents an electoral district in the 2013 Montreal mayoral election. - Returns: + Returns + ------- A GeoJSON-formatted `dict` with 58 polygon or multi-polygon features whose `id` is an electoral district numerical ID and whose `district` property is the ID and - district name.""" + district name. + """ import gzip import json import os @@ -142,9 +199,17 @@ def carshare(return_type="pandas"): Each row represents the availability of car-sharing services near the centroid of a zone in Montreal over a month-long period. - Returns: - A `pandas.DataFrame` with 249 rows and the following columns: - `['centroid_lat', 'centroid_lon', 'car_hours', 'peak_hour']`.""" + Parameters + ---------- + return_type: {'pandas', 'polars', 'pyarrow'} + Type of the resulting dataframe + + Returns + ------- + Dataframe of `return_type` type + Dataframe` with 249 rows and the following columns: + `['centroid_lat', 'centroid_lon', 'car_hours', 'peak_hour']`. + """ return _get_dataset("carshare", return_type=return_type) @@ -152,12 +217,27 @@ def stocks(indexed=False, datetimes=False, return_type="pandas"): """ Each row in this wide dataset represents closing prices from 6 tech stocks in 2018/2019. - Returns: - A `pandas.DataFrame` with 100 rows and the following columns: + Parameters + ---------- + indexed: bool + Whether or not the 'date' column is used as the index and the column index + is named 'company'. Applicable only if `return_type='pandas'` + + datetimes: bool + Whether or not the 'date' column will be of datetime type + + return_type: {'pandas', 'polars', 'pyarrow'} + Type of the resulting dataframe + + Returns + ------- + Dataframe of `return_type` type + Dataframe with 100 rows and the following columns: `['date', 'GOOG', 'AAPL', 'AMZN', 'FB', 'NFLX', 'MSFT']`. If `indexed` is True, the 'date' column is used as the index and the column index + is named 'company' If `datetimes` is True, the 'date' column will be a datetime column - is named 'company'""" + """ if indexed and return_type != "pandas": msg = "Cannot set index for backend different from pandas" raise NotImplementedError(msg) @@ -181,11 +261,22 @@ def experiment(indexed=False, return_type="pandas"): Each row in this wide dataset represents the results of 100 simulated participants on three hypothetical experiments, along with their gender and control/treatment group. + Parameters + ---------- + indexed: bool + If True, then the index is named "participant". + Applicable only if `return_type='pandas'` + + return_type: {'pandas', 'polars', 'pyarrow'} + Type of the resulting dataframe - Returns: - A `pandas.DataFrame` with 100 rows and the following columns: + Returns + ------- + Dataframe of `return_type` type + Dataframe with 100 rows and the following columns: `['experiment_1', 'experiment_2', 'experiment_3', 'gender', 'group']`. - If `indexed` is True, the data frame index is named "participant" """ + If `indexed` is True, the data frame index is named "participant" + """ if indexed and return_type != "pandas": msg = "Cannot set index for backend different from pandas" @@ -206,11 +297,23 @@ def medals_wide(indexed=False, return_type="pandas"): This dataset represents the medal table for Olympic Short Track Speed Skating for the top three nations as of 2020. - Returns: - A `pandas.DataFrame` with 3 rows and the following columns: + Parameters + ---------- + indexed: bool + Whether or not the 'nation' column is used as the index and the column index + is named 'medal'. Applicable only if `return_type='pandas'` + + return_type: {'pandas', 'polars', 'pyarrow'} + Type of the resulting dataframe + + Returns + ------- + Dataframe of `return_type` type + Dataframe with 3 rows and the following columns: `['nation', 'gold', 'silver', 'bronze']`. If `indexed` is True, the 'nation' column is used as the index and the column index - is named 'medal'""" + is named 'medal' + """ if indexed and return_type != "pandas": msg = "Cannot set index for backend different from pandas" @@ -231,10 +334,21 @@ def medals_long(indexed=False, return_type="pandas"): This dataset represents the medal table for Olympic Short Track Speed Skating for the top three nations as of 2020. - Returns: - A `pandas.DataFrame` with 9 rows and the following columns: - `['nation', 'medal', 'count']`. - If `indexed` is True, the 'nation' column is used as the index.""" + Parameters + ---------- + indexed: bool + Whether or not the 'nation' column is used as the index. + Applicable only if `return_type='pandas'` + + return_type: {'pandas', 'polars', 'pyarrow'} + Type of the resulting dataframe + + Returns + ------- + Dataframe of `return_type` type + Dataframe with 9 rows and the following columns: `['nation', 'medal', 'count']`. + If `indexed` is True, the 'nation' column is used as the index. + """ if indexed and return_type != "pandas": msg = "Cannot set index for backend different from pandas" @@ -253,6 +367,25 @@ def medals_long(indexed=False, return_type="pandas"): def _get_dataset(d, return_type): + """ + Loads the dataset using the specified backend. + + Notice that the available backends are 'pandas', 'polars', 'pyarrow' and they all + have a `read_csv` function. Therefore we can dynamically load the library via + `importlib.import_module` and then call `backend.read_csv(filepath)`. + + Parameters + ---------- + d: str + Name of the dataset to load. + + return_type: {'pandas', 'polars', 'pyarrow'} + Type of the resulting dataframe + + Returns + ------- + Dataframe of `return_type` type + """ import os from importlib import import_module From 6f1389f7c59cb2b11f23ae40e107532c222bdeb5 Mon Sep 17 00:00:00 2001 From: FBruzzesi Date: Fri, 1 Nov 2024 10:29:20 +0100 Subject: [PATCH 087/106] rm type hints, change post_agg to use alias --- packages/python/plotly/plotly/express/_core.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/packages/python/plotly/plotly/express/_core.py b/packages/python/plotly/plotly/express/_core.py index f8ce87ceaf..42e42b4848 100644 --- a/packages/python/plotly/plotly/express/_core.py +++ b/packages/python/plotly/plotly/express/_core.py @@ -1,7 +1,6 @@ import plotly.graph_objs as go import plotly.io as pio from collections import namedtuple, OrderedDict -from collections.abc import Sequence from ._special_inputs import IdentityMap, Constant, Range from .trendline_functions import ols, lowess, rolling, expanding, ewm @@ -182,7 +181,7 @@ def _to_unix_epoch_seconds(s: nw.Series) -> nw.Series: raise TypeError(msg) -def _generate_temporary_column_name(n_bytes: int, columns: list[str]) -> str: +def _generate_temporary_column_name(n_bytes, columns) -> str: """Wraps of Narwhals generate_temporary_column_name to generate a token which is guaranteed to not be in columns, nor in [col + token for col in columns] """ @@ -2013,13 +2012,16 @@ def post_agg(dframe: nw.LazyFrame, continuous_aggs, discrete_aggs) -> nw.LazyFra - discrete_aggs is either [args["color"], ] or [] """ return dframe.with_columns( - **{col: nw.col(col) / nw.col(count_colname) for col in continuous_aggs}, - **{ - col: nw.when(nw.col(f"{col}{n_unique_token}") == 1) - .then(nw.col(col)) - .otherwise(nw.lit("(?)")) + *[nw.col(col) / nw.col(count_colname) for col in continuous_aggs], + *[ + ( + nw.when(nw.col(f"{col}{n_unique_token}") == 1) + .then(nw.col(col)) + .otherwise(nw.lit("(?)")) + .alias(col) + ) for col in discrete_aggs - }, + ], ).drop([f"{col}{n_unique_token}" for col in discrete_aggs]) for i, level in enumerate(path): From db22268552da2d515d8f01b7e37d83f57189ff52 Mon Sep 17 00:00:00 2001 From: FBruzzesi Date: Fri, 1 Nov 2024 16:08:47 +0100 Subject: [PATCH 088/106] feedback adjustments --- packages/python/plotly/plotly/data/__init__.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/packages/python/plotly/plotly/data/__init__.py b/packages/python/plotly/plotly/data/__init__.py index ff2a04ecd6..feb2586055 100644 --- a/packages/python/plotly/plotly/data/__init__.py +++ b/packages/python/plotly/plotly/data/__init__.py @@ -51,9 +51,11 @@ def gapminder( df = df.filter(nw.col("year") == year) if datetimes: df = df.with_columns( - nw.concat_str([nw.col("year").cast(nw.String()), nw.lit("-01-01")]).cast( - nw.Datetime(time_unit="ns") - ) + # Concatenate the year value with the literal "-01-01" so that it can be + # casted to datetime from "%Y-%m-%d" format + nw.concat_str( + [nw.col("year").cast(nw.String()), nw.lit("-01-01")] + ).str.to_datetime(format="%Y-%m-%d") ) if not centroids: df = df.drop("centroid_lat", "centroid_lon") @@ -403,7 +405,12 @@ def _get_dataset(d, return_type): try: backend = import_module(return_type) - return backend.read_csv(filepath) except ModuleNotFoundError: msg = f"return_type={return_type}, but {return_type} is not installed" raise ModuleNotFoundError(msg) + + try: + return backend.read_csv(filepath) + except Exception as e: + msg = f"Unable to read '{d}' dataset due to: {e}" + raise Exception(msg).with_traceback(e.__traceback__) From b514c01adf59f92696fcc9d0c4e2fe7f7b033adc Mon Sep 17 00:00:00 2001 From: FBruzzesi Date: Fri, 1 Nov 2024 17:01:15 +0100 Subject: [PATCH 089/106] move imports out, fix pyarrow --- .../python/plotly/plotly/data/__init__.py | 21 +++++++++++-------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/packages/python/plotly/plotly/data/__init__.py b/packages/python/plotly/plotly/data/__init__.py index feb2586055..60ce0c41ce 100644 --- a/packages/python/plotly/plotly/data/__init__.py +++ b/packages/python/plotly/plotly/data/__init__.py @@ -1,8 +1,13 @@ """ Built-in datasets for demonstration, educational and test purposes. """ +import os +from importlib import import_module + import narwhals.stable.v1 as nw +AVAILABLE_BACKENDS = {"pandas", "polars", "pyarrow"} + def gapminder( datetimes=False, @@ -372,9 +377,10 @@ def _get_dataset(d, return_type): """ Loads the dataset using the specified backend. - Notice that the available backends are 'pandas', 'polars', 'pyarrow' and they all - have a `read_csv` function. Therefore we can dynamically load the library via - `importlib.import_module` and then call `backend.read_csv(filepath)`. + Notice that the available backends are 'pandas', 'polars', 'pyarrow' and they all have + a `read_csv` function (pyarrow has it via pyarrow.csv). Therefore we can dynamically + load the library using `importlib.import_module` and then call + `backend.read_csv(filepath)`. Parameters ---------- @@ -388,23 +394,20 @@ def _get_dataset(d, return_type): ------- Dataframe of `return_type` type """ - import os - from importlib import import_module - - AVAILABLE_BACKENDS = {"pandas", "polars", "pyarrow"} - filepath = os.path.join( os.path.dirname(os.path.dirname(__file__)), "package_data", "datasets", d + ".csv.gz", ) + if return_type not in AVAILABLE_BACKENDS: msg = f"Unsupported return_type. Found {return_type}, expected one of {AVAILABLE_BACKENDS}" raise NotImplementedError(msg) try: - backend = import_module(return_type) + module_to_load = "pyarrow.csv" if return_type == "pyarrow" else return_type + backend = import_module(module_to_load) except ModuleNotFoundError: msg = f"return_type={return_type}, but {return_type} is not installed" raise ModuleNotFoundError(msg) From ce8fb9a12fc12ba54b5e9d35e30aecebf2d70404 Mon Sep 17 00:00:00 2001 From: FBruzzesi Date: Fri, 1 Nov 2024 17:52:51 +0100 Subject: [PATCH 090/106] rm unused narwhals wrapper --- .../tests/test_optional/test_px/test_px.py | 26 +++++++++---------- .../test_px/test_px_functions.py | 4 +-- .../test_optional/test_px/test_px_wide.py | 2 +- 3 files changed, 15 insertions(+), 17 deletions(-) diff --git a/packages/python/plotly/plotly/tests/test_optional/test_px/test_px.py b/packages/python/plotly/plotly/tests/test_optional/test_px/test_px.py index f46c3f9f6e..922605e941 100644 --- a/packages/python/plotly/plotly/tests/test_optional/test_px/test_px.py +++ b/packages/python/plotly/plotly/tests/test_optional/test_px/test_px.py @@ -102,10 +102,10 @@ def test_labels(constructor): ) def test_line_mode(constructor, extra_kwargs, expected_mode): data = px.data.gapminder().to_dict(orient="list") - gapminder = nw.from_native(constructor(data)) + gapminder = constructor(data) fig = px.line( - gapminder.to_native(), + gapminder, x="year", y="pop", color="country", @@ -119,7 +119,7 @@ def test_px_templates(constructor): import plotly.graph_objects as go data = px.data.tips().to_dict(orient="list") - tips = nw.from_native(constructor(data)) + tips = constructor(data) # use the normal defaults fig = px.scatter() @@ -146,7 +146,7 @@ def test_px_templates(constructor): # read colorway from the template fig = px.scatter( - tips.to_native(), + tips, x="total_bill", y="tip", color="sex", @@ -156,23 +156,21 @@ def test_px_templates(constructor): assert fig.data[1].marker.color == "blue" # default colorway fallback - fig = px.scatter( - tips.to_native(), x="total_bill", y="tip", color="sex", template=dict() - ) + fig = px.scatter(tips, x="total_bill", y="tip", color="sex", template=dict()) assert fig.data[0].marker.color == px.colors.qualitative.D3[0] assert fig.data[1].marker.color == px.colors.qualitative.D3[1] # pio default template colorway fallback pio.templates.default = "seaborn" px.defaults.template = None - fig = px.scatter(tips.to_native(), x="total_bill", y="tip", color="sex") + fig = px.scatter(tips, x="total_bill", y="tip", color="sex") assert fig.data[0].marker.color == pio.templates["seaborn"].layout.colorway[0] assert fig.data[1].marker.color == pio.templates["seaborn"].layout.colorway[1] # pio default template colorway fallback pio.templates.default = "seaborn" px.defaults.template = "ggplot2" - fig = px.scatter(tips.to_native(), x="total_bill", y="tip", color="sex") + fig = px.scatter(tips, x="total_bill", y="tip", color="sex") assert fig.data[0].marker.color == pio.templates["ggplot2"].layout.colorway[0] assert fig.data[1].marker.color == pio.templates["ggplot2"].layout.colorway[1] @@ -190,7 +188,7 @@ def test_px_templates(constructor): pio.templates.default = "none" px.defaults.template = None fig = px.scatter( - tips.to_native(), + tips, x="total_bill", y="tip", marginal_x="histogram", @@ -202,7 +200,7 @@ def test_px_templates(constructor): assert fig.layout.yaxis3.showgrid fig = px.scatter( - tips.to_native(), + tips, x="total_bill", y="tip", marginal_x="histogram", @@ -215,7 +213,7 @@ def test_px_templates(constructor): assert fig.layout.yaxis3.showgrid is None fig = px.scatter( - tips.to_native(), + tips, x="total_bill", y="tip", marginal_x="histogram", @@ -306,9 +304,9 @@ def test_permissive_defaults(): def test_marginal_ranges(constructor): data = px.data.tips().to_dict(orient="list") - df = nw.from_native(constructor(data)) + df = constructor(data) fig = px.scatter( - df.to_native(), + df, x="total_bill", y="tip", marginal_x="histogram", diff --git a/packages/python/plotly/plotly/tests/test_optional/test_px/test_px_functions.py b/packages/python/plotly/plotly/tests/test_optional/test_px/test_px_functions.py index 65ef0b502c..61cdecb1f2 100644 --- a/packages/python/plotly/plotly/tests/test_optional/test_px/test_px_functions.py +++ b/packages/python/plotly/plotly/tests/test_optional/test_px/test_px_functions.py @@ -154,7 +154,7 @@ def test_sunburst_treemap_with_path(constructor): # Error when values cannot be converted to numerical data type df = nw.from_native(df) - native_namespace = df.__native_namespace__() + native_namespace = nw.get_native_namespace(df) df = df.with_columns( values=nw.new_series( "values", @@ -259,7 +259,7 @@ def test_sunburst_treemap_with_path_color(constructor): name="hover", values=hover, dtype=nw.String(), - native_namespace=df.__native_namespace__(), + native_namespace=nw.get_native_namespace(df), ) ) fig = px.sunburst(df.to_native(), path=path, color="calls", hover_data=["hover"]) diff --git a/packages/python/plotly/plotly/tests/test_optional/test_px/test_px_wide.py b/packages/python/plotly/plotly/tests/test_optional/test_px/test_px_wide.py index db4221dfc1..88e1fd0278 100644 --- a/packages/python/plotly/plotly/tests/test_optional/test_px/test_px_wide.py +++ b/packages/python/plotly/plotly/tests/test_optional/test_px/test_px_wide.py @@ -11,7 +11,7 @@ def test_is_col_list(constructor): df_input = nw.from_native(constructor(dict(a=[1, 2], b=[1, 2]))) - native_namespace = df_input.__native_namespace__() + native_namespace = nw.get_native_namespace(df_input) columns = df_input.columns df_input = df_input.to_native() is_pd_like = nw.dependencies.is_pandas_like_dataframe(df_input) From e47827ef4fe877ff700df993ebb8854bae77f819 Mon Sep 17 00:00:00 2001 From: FBruzzesi Date: Fri, 1 Nov 2024 17:53:08 +0100 Subject: [PATCH 091/106] comment about stable api --- packages/python/plotly/plotly/express/_core.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/python/plotly/plotly/express/_core.py b/packages/python/plotly/plotly/express/_core.py index 42e42b4848..3356240a03 100644 --- a/packages/python/plotly/plotly/express/_core.py +++ b/packages/python/plotly/plotly/express/_core.py @@ -8,14 +8,19 @@ from plotly.colors import qualitative, sequential import math -import narwhals.stable.v1 as nw - from plotly._subplots import ( make_subplots, _set_trace_grid_reference, _subplot_type_for_trace_type, ) +import narwhals.stable.v1 as nw + +# The reason to use narwhals.stable.v1 is to have a stable and perfectly +# backwards-compatible API, hence the confidence to not pin the Narwhals version exactly, +# allowing for multiple major libraries to have Narwhals as a dependency without +# forbidding users to install them all together due to dependency conflicts. + NO_COLOR = "px_no_color_constant" From 9a9283a4db93857d32743564a0d1ff99d62f1580 Mon Sep 17 00:00:00 2001 From: FBruzzesi Date: Fri, 1 Nov 2024 19:44:20 +0100 Subject: [PATCH 092/106] update changelog --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d6d9008571..882645cf99 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,8 +8,9 @@ This project adheres to [Semantic Versioning](http://semver.org/). ### Updated -- Updated plotly.py to use base64 encoding of arrays in plotly JSON to improve performance. +- Updated plotly.py to use base64 encoding of arrays in plotly JSON to improve performance. - Add `subtitle` attribute to all Plotly Express traces +- Allow to load plotly data directly via pandas, polars and pyarrow, without depending directly on any [#4843](https://github.com/plotly/plotly.py/pull/4843) ## [5.24.1] - 2024-09-12 From 2630a5a16e3fd02dbf285e74e2e827b2087d2f5d Mon Sep 17 00:00:00 2001 From: Marco Gorelli <33491632+MarcoGorelli@users.noreply.github.com> Date: Fri, 1 Nov 2024 21:38:26 +0000 Subject: [PATCH 093/106] fixup time zone handling --- .../plotly/_plotly_utils/basevalidators.py | 47 +++++-------------- .../python/plotly/optional-requirements.txt | 2 +- .../python/plotly/plotly/express/_core.py | 14 ++++-- .../test_optional/test_px/test_px_hover.py | 10 ++-- .../test_optional/test_utils/test_utils.py | 6 +-- packages/python/plotly/requirements.txt | 2 +- packages/python/plotly/setup.py | 2 +- .../requirements_310_core.txt | 2 +- .../requirements_310_optional.txt | 2 +- .../requirements_311_core.txt | 2 +- .../requirements_311_optional.txt | 2 +- .../requirements_312_core.txt | 2 +- .../requirements_312_no_numpy_optional.txt | 2 +- .../requirements_312_optional.txt | 2 +- .../requirements_38_core.txt | 2 +- .../requirements_38_optional.txt | 2 +- .../requirements_39_core.txt | 2 +- .../requirements_39_optional.txt | 2 +- .../requirements_39_pandas_2_optional.txt | 2 +- 19 files changed, 45 insertions(+), 62 deletions(-) diff --git a/packages/python/plotly/_plotly_utils/basevalidators.py b/packages/python/plotly/_plotly_utils/basevalidators.py index 47cf39d4d8..8b32137759 100644 --- a/packages/python/plotly/_plotly_utils/basevalidators.py +++ b/packages/python/plotly/_plotly_utils/basevalidators.py @@ -95,44 +95,21 @@ def copy_to_readonly_numpy_array(v, kind=None, force_numeric=False): } if isinstance(v, nw.Series): - if nw.dependencies.is_pandas_like_series(v_native := v.to_native()): - v = v_native + if v.dtype == nw.Datetime and v.dtype.time_zone is not None: + # Remove time zone so that local time is displayed + v = v.dt.replace_time_zone(None).to_numpy() else: v = v.to_numpy() elif isinstance(v, nw.DataFrame): - if nw.dependencies.is_pandas_like_dataframe(v_native := v.to_native()): - v = v_native - else: - v = v.to_numpy() - - if pd and isinstance(v, (pd.Series, pd.Index)): - # Handle pandas Series and Index objects - if v.dtype.kind in numeric_kinds: - # Get the numeric numpy array so we use fast path below - v = v.values - elif v.dtype.kind == "M": - # Convert datetime Series/Index to numpy array of datetimes - if isinstance(v, pd.Series): - with warnings.catch_warnings(): - warnings.simplefilter("ignore", FutureWarning) - # Series.dt.to_pydatetime will return Index[object] - # https://github.com/pandas-dev/pandas/pull/52459 - v = np.array(v.dt.to_pydatetime()) - else: - # DatetimeIndex - v = v.to_pydatetime() - elif pd and isinstance(v, pd.DataFrame) and len(set(v.dtypes)) == 1: - dtype = v.dtypes.tolist()[0] - if dtype.kind in numeric_kinds: - v = v.values - elif dtype.kind == "M": - with warnings.catch_warnings(): - warnings.simplefilter("ignore", FutureWarning) - # Series.dt.to_pydatetime will return Index[object] - # https://github.com/pandas-dev/pandas/pull/52459 - v = [ - np.array(row.dt.to_pydatetime()).tolist() for i, row in v.iterrows() - ] + schema = v.schema + overrides = {} + for key, val in schema.items(): + if val == nw.Datetime and val.time_zone is not None: + # Remove time zone so that local time is displayed + overrides[key] = nw.col(key).dt.replace_time_zone(None) + if overrides: + v = v.with_columns(**overrides) + v = v.to_numpy() if not isinstance(v, np.ndarray): # v has its own logic on how to convert itself into a numpy array diff --git a/packages/python/plotly/optional-requirements.txt b/packages/python/plotly/optional-requirements.txt index 465188f802..9d3206a1b8 100644 --- a/packages/python/plotly/optional-requirements.txt +++ b/packages/python/plotly/optional-requirements.txt @@ -39,7 +39,7 @@ ipython ## pandas deps for some matplotlib functionality ## pandas -narwhals>=1.12.0 +narwhals>=1.13.1 ## scipy deps for some FigureFactory functions ## scipy diff --git a/packages/python/plotly/plotly/express/_core.py b/packages/python/plotly/plotly/express/_core.py index 3356240a03..de710891f3 100644 --- a/packages/python/plotly/plotly/express/_core.py +++ b/packages/python/plotly/plotly/express/_core.py @@ -413,12 +413,20 @@ def make_trace_kwargs(args, trace_spec, trace_data, mapping_labels, sizeref): # i.e. we can't do resampling, because then the X values might not line up! non_missing = ~(x.is_null() | y.is_null()) trace_patch["x"] = ( - sorted_trace_data.filter(non_missing) - .get_column(args["x"]) - .to_numpy() + sorted_trace_data.filter(non_missing).get_column(args["x"]) # FIXME: Converting to numpy is needed to pass `test_trendline_on_timeseries` # test, but I wonder if it is the right way to do it in the first place. ) + if ( + trace_patch["x"].dtype == nw.Datetime + and trace_patch["x"].dtype.time_zone is not None + ): + # Remove time zone so that local time is displayed + trace_patch["x"] = ( + trace_patch["x"].dt.replace_time_zone(None).to_numpy() + ) + else: + trace_patch["x"] = trace_patch["x"].to_numpy() trendline_function = trendline_functions[attr_value] y_out, hover_header, fit_results = trendline_function( diff --git a/packages/python/plotly/plotly/tests/test_optional/test_px/test_px_hover.py b/packages/python/plotly/plotly/tests/test_optional/test_px/test_px_hover.py index 2fae55f126..7920e94725 100644 --- a/packages/python/plotly/plotly/tests/test_optional/test_px/test_px_hover.py +++ b/packages/python/plotly/plotly/tests/test_optional/test_px/test_px_hover.py @@ -190,14 +190,12 @@ def test_sunburst_hoverdict_color(constructor): def test_date_in_hover(request, constructor): - if "pyarrow_table" in str(constructor) or "polars_eager" in str(constructor): - # fig.data[0].customdata[0][0] is a numpy.datetime64 for non pandas - # input, and it does not keep the timezone when converting to py scalar - request.applymarker(pytest.mark.xfail) - df = nw.from_native( constructor({"date": ["2015-04-04 19:31:30+01:00"], "value": [3]}) ).with_columns(date=nw.col("date").str.to_datetime(format="%Y-%m-%d %H:%M:%S%z")) fig = px.scatter(df.to_native(), x="value", y="value", hover_data=["date"]) - assert fig.data[0].customdata[0][0] == df.item(row=0, column="date") + # Check that what gets displayed is the local datetime + assert nw.to_py_scalar(fig.data[0].customdata[0][0]) == nw.to_py_scalar( + df.item(row=0, column="date") + ).replace(tzinfo=None) diff --git a/packages/python/plotly/plotly/tests/test_optional/test_utils/test_utils.py b/packages/python/plotly/plotly/tests/test_optional/test_utils/test_utils.py index 8a13605ec5..e167f23d1c 100644 --- a/packages/python/plotly/plotly/tests/test_optional/test_utils/test_utils.py +++ b/packages/python/plotly/plotly/tests/test_optional/test_utils/test_utils.py @@ -270,7 +270,7 @@ def test_encode_customdata_datetime_series(self): ) self.assertTrue( fig_json.startswith( - '{"data":[{"customdata":["2010-01-01T00:00:00","2010-01-02T00:00:00"]' + '{"data":[{"customdata":["2010-01-01T00:00:00.000000000","2010-01-02T00:00:00.000000000"]' ) ) @@ -292,8 +292,8 @@ def test_encode_customdata_datetime_homogeneous_dataframe(self): self.assertTrue( fig_json.startswith( '{"data":[{"customdata":' - '[["2010-01-01T00:00:00","2011-01-01T00:00:00"],' - '["2010-01-02T00:00:00","2011-01-02T00:00:00"]' + '[["2010-01-01T00:00:00.000000000","2011-01-01T00:00:00.000000000"],' + '["2010-01-02T00:00:00.000000000","2011-01-02T00:00:00.000000000"]' ) ) diff --git a/packages/python/plotly/requirements.txt b/packages/python/plotly/requirements.txt index 9b4b1093ce..3dcbb4003c 100644 --- a/packages/python/plotly/requirements.txt +++ b/packages/python/plotly/requirements.txt @@ -6,4 +6,4 @@ ################################################### ## dataframe agnostic layer ## -narwhals>=1.12.0 +narwhals>=1.13.1 diff --git a/packages/python/plotly/setup.py b/packages/python/plotly/setup.py index 3d0aa1273d..906e62e585 100644 --- a/packages/python/plotly/setup.py +++ b/packages/python/plotly/setup.py @@ -603,7 +603,7 @@ def run(self): data_files=[ ("etc/jupyter/nbconfig/notebook.d", ["jupyterlab-plotly.json"]), ], - install_requires=["narwhals>=1.12.0", "packaging"], + install_requires=["narwhals>=1.13.1", "packaging"], zip_safe=False, cmdclass=dict( build_py=js_prerelease(versioneer_cmds["build_py"]), diff --git a/packages/python/plotly/test_requirements/requirements_310_core.txt b/packages/python/plotly/test_requirements/requirements_310_core.txt index c5ea5ec903..2ee8e7b437 100644 --- a/packages/python/plotly/test_requirements/requirements_310_core.txt +++ b/packages/python/plotly/test_requirements/requirements_310_core.txt @@ -1,3 +1,3 @@ requests==2.25.1 pytest==7.4.4 -narwhals>=1.12.0 +narwhals>=1.13.1 diff --git a/packages/python/plotly/test_requirements/requirements_310_optional.txt b/packages/python/plotly/test_requirements/requirements_310_optional.txt index 41c0c2a51a..6620d59cd5 100644 --- a/packages/python/plotly/test_requirements/requirements_310_optional.txt +++ b/packages/python/plotly/test_requirements/requirements_310_optional.txt @@ -21,4 +21,4 @@ kaleido orjson==3.8.12 polars[timezone] pyarrow -narwhals>=1.12.0 +narwhals>=1.13.1 diff --git a/packages/python/plotly/test_requirements/requirements_311_core.txt b/packages/python/plotly/test_requirements/requirements_311_core.txt index c5ea5ec903..2ee8e7b437 100644 --- a/packages/python/plotly/test_requirements/requirements_311_core.txt +++ b/packages/python/plotly/test_requirements/requirements_311_core.txt @@ -1,3 +1,3 @@ requests==2.25.1 pytest==7.4.4 -narwhals>=1.12.0 +narwhals>=1.13.1 diff --git a/packages/python/plotly/test_requirements/requirements_311_optional.txt b/packages/python/plotly/test_requirements/requirements_311_optional.txt index 77cceb72eb..17874ffd4b 100644 --- a/packages/python/plotly/test_requirements/requirements_311_optional.txt +++ b/packages/python/plotly/test_requirements/requirements_311_optional.txt @@ -21,4 +21,4 @@ kaleido orjson==3.8.12 polars[timezone] pyarrow -narwhals>=1.12.0 +narwhals>=1.13.1 diff --git a/packages/python/plotly/test_requirements/requirements_312_core.txt b/packages/python/plotly/test_requirements/requirements_312_core.txt index c5ea5ec903..2ee8e7b437 100644 --- a/packages/python/plotly/test_requirements/requirements_312_core.txt +++ b/packages/python/plotly/test_requirements/requirements_312_core.txt @@ -1,3 +1,3 @@ requests==2.25.1 pytest==7.4.4 -narwhals>=1.12.0 +narwhals>=1.13.1 diff --git a/packages/python/plotly/test_requirements/requirements_312_no_numpy_optional.txt b/packages/python/plotly/test_requirements/requirements_312_no_numpy_optional.txt index b2a0f33029..6f135270f5 100644 --- a/packages/python/plotly/test_requirements/requirements_312_no_numpy_optional.txt +++ b/packages/python/plotly/test_requirements/requirements_312_no_numpy_optional.txt @@ -20,5 +20,5 @@ kaleido orjson==3.9.10 polars[timezone] pyarrow -narwhals>=1.12.0 +narwhals>=1.13.1 jupyter-console==6.4.4 diff --git a/packages/python/plotly/test_requirements/requirements_312_optional.txt b/packages/python/plotly/test_requirements/requirements_312_optional.txt index 2cd04bf100..09ed18e186 100644 --- a/packages/python/plotly/test_requirements/requirements_312_optional.txt +++ b/packages/python/plotly/test_requirements/requirements_312_optional.txt @@ -21,5 +21,5 @@ kaleido orjson==3.9.10 polars[timezone] pyarrow -narwhals>=1.12.0 +narwhals>=1.13.1 jupyter-console==6.4.4 diff --git a/packages/python/plotly/test_requirements/requirements_38_core.txt b/packages/python/plotly/test_requirements/requirements_38_core.txt index c84e860854..0de7c8e883 100644 --- a/packages/python/plotly/test_requirements/requirements_38_core.txt +++ b/packages/python/plotly/test_requirements/requirements_38_core.txt @@ -1,3 +1,3 @@ requests==2.25.1 pytest==8.1.1 -narwhals>=1.12.0 +narwhals>=1.13.1 diff --git a/packages/python/plotly/test_requirements/requirements_38_optional.txt b/packages/python/plotly/test_requirements/requirements_38_optional.txt index 95160ee63d..54cd9235ab 100644 --- a/packages/python/plotly/test_requirements/requirements_38_optional.txt +++ b/packages/python/plotly/test_requirements/requirements_38_optional.txt @@ -21,4 +21,4 @@ psutil==5.7.0 kaleido polars[timezone] pyarrow -narwhals>=1.12.0 +narwhals>=1.13.1 diff --git a/packages/python/plotly/test_requirements/requirements_39_core.txt b/packages/python/plotly/test_requirements/requirements_39_core.txt index b15f894884..ea1fac55d0 100644 --- a/packages/python/plotly/test_requirements/requirements_39_core.txt +++ b/packages/python/plotly/test_requirements/requirements_39_core.txt @@ -1,3 +1,3 @@ requests==2.25.1 pytest==6.2.3 -narwhals>=1.12.0 +narwhals>=1.13.1 diff --git a/packages/python/plotly/test_requirements/requirements_39_optional.txt b/packages/python/plotly/test_requirements/requirements_39_optional.txt index fd887bb94f..53d60da03f 100644 --- a/packages/python/plotly/test_requirements/requirements_39_optional.txt +++ b/packages/python/plotly/test_requirements/requirements_39_optional.txt @@ -22,4 +22,4 @@ kaleido orjson==3.8.12 polars[timezone] pyarrow -narwhals>=1.12.0 +narwhals>=1.13.1 diff --git a/packages/python/plotly/test_requirements/requirements_39_pandas_2_optional.txt b/packages/python/plotly/test_requirements/requirements_39_pandas_2_optional.txt index 4ee93e25b5..caeb553ba8 100644 --- a/packages/python/plotly/test_requirements/requirements_39_pandas_2_optional.txt +++ b/packages/python/plotly/test_requirements/requirements_39_pandas_2_optional.txt @@ -22,4 +22,4 @@ vaex pydantic<=1.10.11 # for vaex, see https://github.com/vaexio/vaex/issues/2384 polars[timezone] pyarrow -narwhals>=1.12.0 +narwhals>=1.13.1 From fef6dbe319c67a70564cded28a3831d0cf293b5a Mon Sep 17 00:00:00 2001 From: FBruzzesi Date: Sun, 3 Nov 2024 22:38:51 +0100 Subject: [PATCH 094/106] modin and cudf --- .../python/plotly/plotly/data/__init__.py | 48 +++++++++++-------- 1 file changed, 27 insertions(+), 21 deletions(-) diff --git a/packages/python/plotly/plotly/data/__init__.py b/packages/python/plotly/plotly/data/__init__.py index 60ce0c41ce..0dfb3abac6 100644 --- a/packages/python/plotly/plotly/data/__init__.py +++ b/packages/python/plotly/plotly/data/__init__.py @@ -6,7 +6,8 @@ import narwhals.stable.v1 as nw -AVAILABLE_BACKENDS = {"pandas", "polars", "pyarrow"} +AVAILABLE_BACKENDS = {"pandas", "polars", "pyarrow", "modin", "cudf"} +BACKENDS_WITH_INDEX_SUPPORT = {"pandas", "modin", "cudf"} def gapminder( @@ -35,7 +36,7 @@ def gapminder( pretty_names: bool If True, prettifies the column names - return_type: {'pandas', 'polars', 'pyarrow'} + return_type: {'pandas', 'polars', 'pyarrow', 'modin', 'cudf'} Type of the resulting dataframe Returns @@ -93,7 +94,7 @@ def tips(pretty_names=False, return_type="pandas"): pretty_names: bool If True, prettifies the column names - return_type: {'pandas', 'polars', 'pyarrow'} + return_type: {'pandas', 'polars', 'pyarrow', 'modin', 'cudf'} Type of the resulting dataframe Returns @@ -127,7 +128,7 @@ def iris(return_type="pandas"): Parameters ---------- - return_type: {'pandas', 'polars', 'pyarrow'} + return_type: {'pandas', 'polars', 'pyarrow', 'modin', 'cudf'} Type of the resulting dataframe Returns @@ -145,7 +146,7 @@ def wind(return_type="pandas"): Parameters ---------- - return_type: {'pandas', 'polars', 'pyarrow'} + return_type: {'pandas', 'polars', 'pyarrow', 'modin', 'cudf'} Type of the resulting dataframe Returns @@ -164,7 +165,7 @@ def election(return_type="pandas"): Parameters ---------- - return_type: {'pandas', 'polars', 'pyarrow'} + return_type: {'pandas', 'polars', 'pyarrow', 'modin', 'cudf'} Type of the resulting dataframe Returns @@ -208,7 +209,7 @@ def carshare(return_type="pandas"): Parameters ---------- - return_type: {'pandas', 'polars', 'pyarrow'} + return_type: {'pandas', 'polars', 'pyarrow', 'modin', 'cudf'} Type of the resulting dataframe Returns @@ -233,7 +234,7 @@ def stocks(indexed=False, datetimes=False, return_type="pandas"): datetimes: bool Whether or not the 'date' column will be of datetime type - return_type: {'pandas', 'polars', 'pyarrow'} + return_type: {'pandas', 'polars', 'pyarrow', 'modin', 'cudf'} Type of the resulting dataframe Returns @@ -245,8 +246,8 @@ def stocks(indexed=False, datetimes=False, return_type="pandas"): is named 'company' If `datetimes` is True, the 'date' column will be a datetime column """ - if indexed and return_type != "pandas": - msg = "Cannot set index for backend different from pandas" + if indexed and return_type not in BACKENDS_WITH_INDEX_SUPPORT: + msg = f"Backend '{return_type}' does not support setting index" raise NotImplementedError(msg) df = nw.from_native( @@ -274,7 +275,7 @@ def experiment(indexed=False, return_type="pandas"): If True, then the index is named "participant". Applicable only if `return_type='pandas'` - return_type: {'pandas', 'polars', 'pyarrow'} + return_type: {'pandas', 'polars', 'pyarrow', 'modin', 'cudf'} Type of the resulting dataframe Returns @@ -285,8 +286,8 @@ def experiment(indexed=False, return_type="pandas"): If `indexed` is True, the data frame index is named "participant" """ - if indexed and return_type != "pandas": - msg = "Cannot set index for backend different from pandas" + if indexed and return_type not in BACKENDS_WITH_INDEX_SUPPORT: + msg = f"Backend '{return_type}' does not support setting index" raise NotImplementedError(msg) df = nw.from_native( @@ -310,7 +311,7 @@ def medals_wide(indexed=False, return_type="pandas"): Whether or not the 'nation' column is used as the index and the column index is named 'medal'. Applicable only if `return_type='pandas'` - return_type: {'pandas', 'polars', 'pyarrow'} + return_type: {'pandas', 'polars', 'pyarrow', 'modin', 'cudf'} Type of the resulting dataframe Returns @@ -322,8 +323,8 @@ def medals_wide(indexed=False, return_type="pandas"): is named 'medal' """ - if indexed and return_type != "pandas": - msg = "Cannot set index for backend different from pandas" + if indexed and return_type not in BACKENDS_WITH_INDEX_SUPPORT: + msg = f"Backend '{return_type}' does not support setting index" raise NotImplementedError(msg) df = nw.from_native( @@ -347,7 +348,7 @@ def medals_long(indexed=False, return_type="pandas"): Whether or not the 'nation' column is used as the index. Applicable only if `return_type='pandas'` - return_type: {'pandas', 'polars', 'pyarrow'} + return_type: {'pandas', 'polars', 'pyarrow', 'modin', 'cudf'} Type of the resulting dataframe Returns @@ -357,8 +358,8 @@ def medals_long(indexed=False, return_type="pandas"): If `indexed` is True, the 'nation' column is used as the index. """ - if indexed and return_type != "pandas": - msg = "Cannot set index for backend different from pandas" + if indexed and return_type not in BACKENDS_WITH_INDEX_SUPPORT: + msg = f"Backend '{return_type}' does not support setting index" raise NotImplementedError(msg) df = nw.from_native( @@ -387,7 +388,7 @@ def _get_dataset(d, return_type): d: str Name of the dataset to load. - return_type: {'pandas', 'polars', 'pyarrow'} + return_type: {'pandas', 'polars', 'pyarrow', 'modin', 'cudf'} Type of the resulting dataframe Returns @@ -406,7 +407,12 @@ def _get_dataset(d, return_type): raise NotImplementedError(msg) try: - module_to_load = "pyarrow.csv" if return_type == "pyarrow" else return_type + if return_type == "pyarrow": + module_to_load = "pyarrow.csv" + elif return_type == "modin": + module_to_load = "modin.pandas" + else: + module_to_load = return_type backend = import_module(module_to_load) except ModuleNotFoundError: msg = f"return_type={return_type}, but {return_type} is not installed" From 48c7f62bde3438fb1571fb788e61ea03e4ffae9c Mon Sep 17 00:00:00 2001 From: FBruzzesi Date: Mon, 4 Nov 2024 15:06:49 +0100 Subject: [PATCH 095/106] defensive from_native call --- .../python/plotly/_plotly_utils/basevalidators.py | 6 ++++-- packages/python/plotly/plotly/express/_core.py | 12 ++++++------ packages/python/plotly/plotly/express/_imshow.py | 2 +- 3 files changed, 11 insertions(+), 9 deletions(-) diff --git a/packages/python/plotly/_plotly_utils/basevalidators.py b/packages/python/plotly/_plotly_utils/basevalidators.py index 8b32137759..3bb591e288 100644 --- a/packages/python/plotly/_plotly_utils/basevalidators.py +++ b/packages/python/plotly/_plotly_utils/basevalidators.py @@ -73,8 +73,6 @@ def copy_to_readonly_numpy_array(v, kind=None, force_numeric=False): """ np = get_module("numpy") - # Don't force pandas to be loaded, we only want to know if it's already loaded - pd = get_module("pandas", should_load=False) assert np is not None # ### Process kind ### @@ -94,6 +92,10 @@ def copy_to_readonly_numpy_array(v, kind=None, force_numeric=False): "O": "object", } + # With `pass_through=True``, the original object will be returned if unable to convert + # to a Narwhals DataFrame or Series. + v = nw.from_native(v, allow_series=True, pass_through=True) + if isinstance(v, nw.Series): if v.dtype == nw.Datetime and v.dtype.time_zone is not None: # Remove time zone so that local time is displayed diff --git a/packages/python/plotly/plotly/express/_core.py b/packages/python/plotly/plotly/express/_core.py index de710891f3..b4b19f2778 100644 --- a/packages/python/plotly/plotly/express/_core.py +++ b/packages/python/plotly/plotly/express/_core.py @@ -1174,7 +1174,7 @@ def to_unindexed_series(x, name=None, native_namespace=None): its index reset if pandas-like). Stripping the index from existing pd.Series is required to get things to match up right in the new DataFrame we're building. """ - x = nw.from_native(x, series_only=True, strict=False) + x = nw.from_native(x, series_only=True, pass_through=True) if isinstance(x, nw.Series): return nw.maybe_reset_index(x).rename(name) elif native_namespace is not None: @@ -1380,7 +1380,7 @@ def process_args_into_dataframe( ) df_output[str(col_name)] = to_unindexed_series( - x=nw.from_native(argument, series_only=True, strict=False), + x=nw.from_native(argument, series_only=True, pass_through=True), name=str(col_name), native_namespace=native_namespace, ) @@ -1508,11 +1508,11 @@ def build_dataframe(args, constructor): is_pd_like = True # data_frame is any other DataFrame object natively supported via Narwhals. - # With strict=False, the original object will be returned if unable to convert + # With pass_through=True, the original object will be returned if unable to convert # to a Narwhals DataFrame, making this condition False. elif isinstance( data_frame := nw.from_native( - args["data_frame"], eager_or_interchange_only=True, strict=False + args["data_frame"], eager_or_interchange_only=True, pass_through=True ), nw.DataFrame, ): @@ -1521,11 +1521,11 @@ def build_dataframe(args, constructor): columns = args["data_frame"].columns # data_frame is any other Series object natively supported via Narwhals. - # With strict=False, the original object will be returned if unable to convert + # With pass_through=True, the original object will be returned if unable to convert # to a Narwhals DataFrame, making this condition False. elif isinstance( series := nw.from_native( - args["data_frame"], series_only=True, strict=False + args["data_frame"], series_only=True, pass_through=True ), nw.Series, ): diff --git a/packages/python/plotly/plotly/express/_imshow.py b/packages/python/plotly/plotly/express/_imshow.py index 225bdb4515..ce6ddb8428 100644 --- a/packages/python/plotly/plotly/express/_imshow.py +++ b/packages/python/plotly/plotly/express/_imshow.py @@ -321,7 +321,7 @@ def imshow( aspect = "equal" # --- Set the value of binary_string (forbidden for pandas) - img = nw.from_native(img, strict=False) + img = nw.from_native(img, pass_through=True) if isinstance(img, nw.DataFrame): if binary_string: raise ValueError("Binary strings cannot be used with pandas arrays") From 18cc11cace2ec16e15899c9000c7ea23afb6e6b9 Mon Sep 17 00:00:00 2001 From: FBruzzesi Date: Mon, 4 Nov 2024 15:17:52 +0100 Subject: [PATCH 096/106] typo --- packages/python/plotly/plotly/express/_core.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/python/plotly/plotly/express/_core.py b/packages/python/plotly/plotly/express/_core.py index b4b19f2778..53f6183252 100644 --- a/packages/python/plotly/plotly/express/_core.py +++ b/packages/python/plotly/plotly/express/_core.py @@ -1522,7 +1522,7 @@ def build_dataframe(args, constructor): # data_frame is any other Series object natively supported via Narwhals. # With pass_through=True, the original object will be returned if unable to convert - # to a Narwhals DataFrame, making this condition False. + # to a Narwhals Series, making this condition False. elif isinstance( series := nw.from_native( args["data_frame"], series_only=True, pass_through=True From c320c467cbc2e30cc539626b16684e1551b717eb Mon Sep 17 00:00:00 2001 From: FBruzzesi Date: Mon, 4 Nov 2024 16:21:20 +0100 Subject: [PATCH 097/106] move from object to datetime dtype in _plotly_utils/test/validators --- .../validators/test_pandas_series_input.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/packages/python/plotly/_plotly_utils/tests/validators/test_pandas_series_input.py b/packages/python/plotly/_plotly_utils/tests/validators/test_pandas_series_input.py index ef8818181d..8bb50d1808 100644 --- a/packages/python/plotly/_plotly_utils/tests/validators/test_pandas_series_input.py +++ b/packages/python/plotly/_plotly_utils/tests/validators/test_pandas_series_input.py @@ -73,12 +73,13 @@ def color_categorical_pandas(request, pandas_type): def dates_array(request): return np.array( [ - datetime(year=2013, month=10, day=10), - datetime(year=2013, month=11, day=10), - datetime(year=2013, month=12, day=10), - datetime(year=2014, month=1, day=10), - datetime(year=2014, month=2, day=10), - ] + "2013-10-10", + "2013-11-10", + "2013-12-10", + "2014-01-10", + "2014-02-10", + ], + dtype="datetime64[ns]", ) @@ -183,7 +184,7 @@ def test_data_array_validator_dates_series( assert isinstance(res, np.ndarray) # Check dtype - assert res.dtype == "object" + assert res.dtype == " Date: Mon, 4 Nov 2024 22:32:36 +0000 Subject: [PATCH 098/106] simplify ecdfnorm --- packages/python/plotly/plotly/express/_core.py | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/packages/python/plotly/plotly/express/_core.py b/packages/python/plotly/plotly/express/_core.py index 53f6183252..60e2552f38 100644 --- a/packages/python/plotly/plotly/express/_core.py +++ b/packages/python/plotly/plotly/express/_core.py @@ -2638,18 +2638,12 @@ def make_figure(args, constructor, trace_patch=None, layout_patch=None): group = group.sort(by=base, descending=False, nulls_last=True) if args.get("ecdfmode", "standard") == "complementary": - group = group.with_columns( - ((nw.col(var) - nw.lit(group_sum)) * (-1)).alias(var) - ) + group = group.with_columns((group_sum - nw.col(var)).alias(var)) if args["ecdfnorm"] == "probability": - group = group.with_columns( - (nw.col(var) / nw.lit(group_sum)).alias(var) - ) + group = group.with_columns(nw.col(var) / group_sum) elif args["ecdfnorm"] == "percent": - group = group.with_columns( - (nw.col(var) / nw.lit(group_sum) * nw.lit(100.0)).alias(var) - ) + group = group.with_columns((nw.col(var) / group_sum) * 100.0) patch, fit_results = make_trace_kwargs( args, trace_spec, group, mapping_labels.copy(), sizeref From f102998c88302b1697b3eb57a02a7e14997b5339 Mon Sep 17 00:00:00 2001 From: FBruzzesi Date: Tue, 5 Nov 2024 15:28:07 +0100 Subject: [PATCH 099/106] rm to_py_scalar call in for loop -> fix Pie performances --- packages/python/plotly/plotly/express/_core.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/python/plotly/plotly/express/_core.py b/packages/python/plotly/plotly/express/_core.py index 53f6183252..0e9e0cb721 100644 --- a/packages/python/plotly/plotly/express/_core.py +++ b/packages/python/plotly/plotly/express/_core.py @@ -532,8 +532,7 @@ def make_trace_kwargs(args, trace_spec, trace_data, mapping_labels, sizeref): mapping = args["color_discrete_map"].copy() else: mapping = {} - for cat in trace_data.get_column(attr_value): - cat = nw.to_py_scalar(cat) + for cat in trace_data.get_column(attr_value).to_list(): if mapping.get(cat) is None: mapping[cat] = args["color_discrete_sequence"][ len(mapping) % len(args["color_discrete_sequence"]) From 7d611fb6483b81eb2ab20085fe753b76860a22fe Mon Sep 17 00:00:00 2001 From: FBruzzesi Date: Tue, 5 Nov 2024 17:59:34 +0100 Subject: [PATCH 100/106] use return_type directly when building datasets --- CHANGELOG.md | 2 +- .../tests/test_optional/test_px/conftest.py | 5 ++ .../test_optional/test_px/test_facets.py | 10 ++-- .../test_optional/test_px/test_marginals.py | 10 ++-- .../tests/test_optional/test_px/test_px.py | 49 ++++++++----------- .../test_px/test_px_functions.py | 36 ++++++-------- .../test_optional/test_px/test_px_hover.py | 27 +++++----- .../test_optional/test_px/test_px_input.py | 48 ++++++++---------- .../test_optional/test_px/test_trendline.py | 25 +++++----- 9 files changed, 94 insertions(+), 118 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 882645cf99..832e124ae8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,7 @@ This project adheres to [Semantic Versioning](http://semver.org/). - Updated plotly.py to use base64 encoding of arrays in plotly JSON to improve performance. - Add `subtitle` attribute to all Plotly Express traces -- Allow to load plotly data directly via pandas, polars and pyarrow, without depending directly on any [#4843](https://github.com/plotly/plotly.py/pull/4843) +- Make plotly-express dataframe agnostic via Narwhals [#4790](https://github.com/plotly/plotly.py/pull/4790) ## [5.24.1] - 2024-09-12 diff --git a/packages/python/plotly/plotly/tests/test_optional/test_px/conftest.py b/packages/python/plotly/plotly/tests/test_optional/test_px/conftest.py index 0a25142046..b207fb29a8 100644 --- a/packages/python/plotly/plotly/tests/test_optional/test_px/conftest.py +++ b/packages/python/plotly/plotly/tests/test_optional/test_px/conftest.py @@ -41,3 +41,8 @@ def pyarrow_table_constructor(obj) -> IntoDataFrame: @pytest.fixture(params=constructors) def constructor(request: pytest.FixtureRequest): return request.param # type: ignore[no-any-return] + + +@pytest.fixture(params=["pandas", "pyarrow", "polars"]) +def backend(request: pytest.FixtureRequest) -> str: + return request.param # type: ignore[no-any-return] diff --git a/packages/python/plotly/plotly/tests/test_optional/test_px/test_facets.py b/packages/python/plotly/plotly/tests/test_optional/test_px/test_facets.py index 3b467b10f5..593b214ed1 100644 --- a/packages/python/plotly/plotly/tests/test_optional/test_px/test_facets.py +++ b/packages/python/plotly/plotly/tests/test_optional/test_px/test_facets.py @@ -4,9 +4,8 @@ import random -def test_facets(constructor): - data = px.data.tips().to_dict(orient="list") - df = constructor(data) +def test_facets(backend): + df = px.data.tips(return_type=backend) fig = px.scatter(df, x="total_bill", y="tip") assert "xaxis2" not in fig.layout @@ -47,9 +46,8 @@ def test_facets(constructor): assert fig.layout.yaxis4.domain[0] - fig.layout.yaxis.domain[1] == approx(0.08) -def test_facets_with_marginals(constructor): - data = px.data.tips().to_dict(orient="list") - df = constructor(data) +def test_facets_with_marginals(backend): + df = px.data.tips(return_type=backend) fig = px.histogram(df, x="total_bill", facet_col="sex", marginal="rug") assert len(fig.data) == 4 diff --git a/packages/python/plotly/plotly/tests/test_optional/test_px/test_marginals.py b/packages/python/plotly/plotly/tests/test_optional/test_px/test_marginals.py index a02bad50b8..9a7ec64d12 100644 --- a/packages/python/plotly/plotly/tests/test_optional/test_px/test_marginals.py +++ b/packages/python/plotly/plotly/tests/test_optional/test_px/test_marginals.py @@ -5,9 +5,8 @@ @pytest.mark.parametrize("px_fn", [px.scatter, px.density_heatmap, px.density_contour]) @pytest.mark.parametrize("marginal_x", [None, "histogram", "box", "violin"]) @pytest.mark.parametrize("marginal_y", [None, "rug"]) -def test_xy_marginals(constructor, px_fn, marginal_x, marginal_y): - data = px.data.tips().to_dict(orient="list") - df = constructor(data) +def test_xy_marginals(backend, px_fn, marginal_x, marginal_y): + df = px.data.tips(return_type=backend) fig = px_fn( df, x="total_bill", y="tip", marginal_x=marginal_x, marginal_y=marginal_y @@ -18,9 +17,8 @@ def test_xy_marginals(constructor, px_fn, marginal_x, marginal_y): @pytest.mark.parametrize("px_fn", [px.histogram, px.ecdf]) @pytest.mark.parametrize("marginal", [None, "rug", "histogram", "box", "violin"]) @pytest.mark.parametrize("orientation", ["h", "v"]) -def test_single_marginals(constructor, px_fn, marginal, orientation): - data = px.data.tips().to_dict(orient="list") - df = constructor(data) +def test_single_marginals(backend, px_fn, marginal, orientation): + df = px.data.tips(return_type=backend) fig = px_fn( df, x="total_bill", y="total_bill", marginal=marginal, orientation=orientation diff --git a/packages/python/plotly/plotly/tests/test_optional/test_px/test_px.py b/packages/python/plotly/plotly/tests/test_optional/test_px/test_px.py index 922605e941..8d091df3ae 100644 --- a/packages/python/plotly/plotly/tests/test_optional/test_px/test_px.py +++ b/packages/python/plotly/plotly/tests/test_optional/test_px/test_px.py @@ -6,9 +6,8 @@ from itertools import permutations -def test_scatter(constructor): - data = px.data.iris().to_dict(orient="list") - iris = nw.from_native(constructor(data)) +def test_scatter(backend): + iris = nw.from_native(px.data.iris(return_type=backend)) fig = px.scatter(iris.to_native(), x="sepal_width", y="sepal_length") assert fig.data[0].type == "scatter" assert np.all(fig.data[0].x == iris.get_column("sepal_width").to_numpy()) @@ -17,9 +16,8 @@ def test_scatter(constructor): assert fig.data[0].mode == "markers" -def test_custom_data_scatter(constructor): - data = px.data.iris().to_dict(orient="list") - iris = nw.from_native(constructor(data)) +def test_custom_data_scatter(backend): + iris = nw.from_native(px.data.iris(return_type=backend)) # No hover, no custom data fig = px.scatter( iris.to_native(), x="sepal_width", y="sepal_length", color="species" @@ -67,9 +65,8 @@ def test_custom_data_scatter(constructor): ) -def test_labels(constructor): - data = px.data.tips().to_dict(orient="list") - tips = nw.from_native(constructor(data)) +def test_labels(backend): + tips = nw.from_native(px.data.tips(return_type=backend)) fig = px.scatter( tips.to_native(), x="total_bill", @@ -100,10 +97,8 @@ def test_labels(constructor): ({"text": "continent"}, "lines+markers+text"), ], ) -def test_line_mode(constructor, extra_kwargs, expected_mode): - data = px.data.gapminder().to_dict(orient="list") - gapminder = constructor(data) - +def test_line_mode(backend, extra_kwargs, expected_mode): + gapminder = px.data.gapminder(return_type=backend) fig = px.line( gapminder, x="year", @@ -114,12 +109,11 @@ def test_line_mode(constructor, extra_kwargs, expected_mode): assert fig.data[0].mode == expected_mode -def test_px_templates(constructor): +def test_px_templates(backend): try: import plotly.graph_objects as go - data = px.data.tips().to_dict(orient="list") - tips = constructor(data) + tips = px.data.tips(return_type=backend) # use the normal defaults fig = px.scatter() @@ -245,12 +239,11 @@ def test_px_defaults(): pio.templates.default = "plotly" -def assert_orderings(constructor, days_order, days_check, times_order, times_check): +def assert_orderings(backend, days_order, days_check, times_order, times_check): symbol_sequence = ["circle", "diamond", "square", "cross", "circle", "diamond"] color_sequence = ["red", "blue", "red", "blue", "red", "blue", "red", "blue"] - data = px.data.tips().to_dict(orient="list") - tips = nw.from_native(constructor(data)) + tips = nw.from_native(px.data.tips(return_type=backend)) fig = px.scatter( tips.to_native(), @@ -284,16 +277,16 @@ def assert_orderings(constructor, days_order, days_check, times_order, times_che @pytest.mark.parametrize("days", permutations(["Sun", "Sat", "Fri", "x"])) @pytest.mark.parametrize("times", permutations(["Lunch", "x"])) -def test_orthogonal_and_missing_orderings(constructor, days, times): +def test_orthogonal_and_missing_orderings(backend, days, times): assert_orderings( - constructor, days, list(days) + ["Thur"], times, list(times) + ["Dinner"] + backend, days, list(days) + ["Thur"], times, list(times) + ["Dinner"] ) @pytest.mark.parametrize("days", permutations(["Sun", "Sat", "Fri", "Thur"])) @pytest.mark.parametrize("times", permutations(["Lunch", "Dinner"])) -def test_orthogonal_orderings(constructor, days, times): - assert_orderings(constructor, days, days, times, times) +def test_orthogonal_orderings(backend, days, times): + assert_orderings(backend, days, days, times, times) def test_permissive_defaults(): @@ -302,9 +295,8 @@ def test_permissive_defaults(): px.defaults.should_not_work = "test" -def test_marginal_ranges(constructor): - data = px.data.tips().to_dict(orient="list") - df = constructor(data) +def test_marginal_ranges(backend): + df = px.data.tips(return_type=backend) fig = px.scatter( df, x="total_bill", @@ -318,9 +310,8 @@ def test_marginal_ranges(constructor): assert fig.layout.yaxis3.range is None -def test_render_mode(constructor): - data = px.data.gapminder().to_dict(orient="list") - df = nw.from_native(constructor(data)) +def test_render_mode(backend): + df = nw.from_native(px.data.gapminder(return_type=backend)) df2007 = df.filter(nw.col("year") == 2007) fig = px.scatter(df2007.to_native(), x="gdpPercap", y="lifeExp", trendline="ols") diff --git a/packages/python/plotly/plotly/tests/test_optional/test_px/test_px_functions.py b/packages/python/plotly/plotly/tests/test_optional/test_px/test_px_functions.py index 61cdecb1f2..2f165db078 100644 --- a/packages/python/plotly/plotly/tests/test_optional/test_px/test_px_functions.py +++ b/packages/python/plotly/plotly/tests/test_optional/test_px/test_px_functions.py @@ -187,31 +187,28 @@ def test_sunburst_treemap_with_path(constructor): assert fig.data[0].values[-1] == 8 -def test_sunburst_treemap_with_path_and_hover(constructor): - data = px.data.tips().to_dict(orient="list") - df = constructor(data) +def test_sunburst_treemap_with_path_and_hover(backend): + df = px.data.tips(return_type=backend) fig = px.sunburst( df, path=["sex", "day", "time", "smoker"], color="smoker", hover_data=["smoker"] ) assert "smoker" in fig.data[0].hovertemplate - data = px.data.gapminder().query("year == 2007").to_dict(orient="list") - df = constructor(data) - + df = nw.from_native(px.data.gapminder(year=2007, return_type=backend)) fig = px.sunburst( - df, path=["continent", "country"], color="lifeExp", hover_data=df.columns + df.to_native(), + path=["continent", "country"], + color="lifeExp", + hover_data=df.columns, ) assert fig.layout.coloraxis.colorbar.title.text == "lifeExp" - data = px.data.tips().to_dict(orient="list") - df = constructor(data) - + df = px.data.tips(return_type=backend) fig = px.sunburst(df, path=["sex", "day", "time", "smoker"], hover_name="smoker") assert "smoker" not in fig.data[0].hovertemplate # represented as '%{hovertext}' assert "%{hovertext}" in fig.data[0].hovertemplate # represented as '%{hovertext}' - data = px.data.tips().to_dict(orient="list") - df = constructor(data) + df = px.data.tips(return_type=backend) fig = px.sunburst(df, path=["sex", "day", "time", "smoker"], custom_data=["smoker"]) assert fig.data[0].customdata[0][0] in ["Yes", "No"] assert "smoker" not in fig.data[0].hovertemplate @@ -414,9 +411,8 @@ def test_funnel(): assert len(fig.data) == 2 -def test_parcats_dimensions_max(constructor): - data = px.data.tips().to_dict(orient="list") - df = constructor(data) +def test_parcats_dimensions_max(backend): + df = px.data.tips(return_type=backend) # default behaviour fig = px.parallel_categories(df) @@ -449,13 +445,12 @@ def test_parcats_dimensions_max(constructor): @pytest.mark.parametrize("histfunc,y", [(None, None), ("count", "tip")]) -def test_histfunc_hoverlabels_univariate(constructor, histfunc, y): +def test_histfunc_hoverlabels_univariate(backend, histfunc, y): def check_label(label, fig): assert fig.layout.yaxis.title.text == label assert label + "=" in fig.data[0].hovertemplate - data = px.data.tips().to_dict(orient="list") - df = constructor(data) + df = px.data.tips(return_type=backend) # base case, just "count" (note count(tip) is same as count()) fig = px.histogram(df, x="total_bill", y=y, histfunc=histfunc) @@ -481,13 +476,12 @@ def check_label(label, fig): check_label("%s (normalized as %s)" % (histnorm, barnorm), fig) -def test_histfunc_hoverlabels_bivariate(constructor): +def test_histfunc_hoverlabels_bivariate(backend): def check_label(label, fig): assert fig.layout.yaxis.title.text == label assert label + "=" in fig.data[0].hovertemplate - data = px.data.tips().to_dict(orient="list") - df = constructor(data) + df = px.data.tips(return_type=backend) # with y, should be same as forcing histfunc to sum fig = px.histogram(df, x="total_bill", y="tip") diff --git a/packages/python/plotly/plotly/tests/test_optional/test_px/test_px_hover.py b/packages/python/plotly/plotly/tests/test_optional/test_px/test_px_hover.py index 7920e94725..35a91a5c34 100644 --- a/packages/python/plotly/plotly/tests/test_optional/test_px/test_px_hover.py +++ b/packages/python/plotly/plotly/tests/test_optional/test_px/test_px_hover.py @@ -6,9 +6,8 @@ from collections import OrderedDict # an OrderedDict is needed for Python 2 -def test_skip_hover(constructor): - data = px.data.iris().to_dict(orient="list") - df = constructor(data) +def test_skip_hover(backend): + df = px.data.iris(return_type=backend) fig = px.scatter( df, x="petal_length", @@ -19,9 +18,8 @@ def test_skip_hover(constructor): assert fig.data[0].hovertemplate == "species_id=%{marker.size}" -def test_hover_data_string_column(constructor): - data = px.data.tips().to_dict(orient="list") - df = constructor(data) +def test_hover_data_string_column(backend): + df = px.data.tips(return_type=backend) fig = px.scatter( df, x="tip", @@ -31,9 +29,8 @@ def test_hover_data_string_column(constructor): assert "sex" in fig.data[0].hovertemplate -def test_composite_hover(constructor): - data = px.data.tips().to_dict(orient="list") - df = constructor(data) +def test_composite_hover(backend): + df = px.data.tips(return_type=backend) hover_dict = OrderedDict( {"day": False, "time": False, "sex": True, "total_bill": ":.1f"} ) @@ -91,9 +88,8 @@ def test_newdatain_hover_data(): ) -def test_formatted_hover_and_labels(constructor): - data = px.data.tips().to_dict(orient="list") - df = constructor(data) +def test_formatted_hover_and_labels(backend): + df = px.data.tips(return_type=backend) fig = px.scatter( df, x="tip", @@ -176,9 +172,8 @@ def test_fail_wrong_column(): ) -def test_sunburst_hoverdict_color(constructor): - data = px.data.gapminder().query("year == 2007").to_dict(orient="list") - df = constructor(data) +def test_sunburst_hoverdict_color(backend): + df = px.data.gapminder(year=2007, return_type=backend) fig = px.sunburst( df, path=["continent", "country"], @@ -189,7 +184,7 @@ def test_sunburst_hoverdict_color(constructor): assert "color" in fig.data[0].hovertemplate -def test_date_in_hover(request, constructor): +def test_date_in_hover(constructor): df = nw.from_native( constructor({"date": ["2015-04-04 19:31:30+01:00"], "value": [3]}) ).with_columns(date=nw.col("date").str.to_datetime(format="%Y-%m-%d %H:%M:%S%z")) diff --git a/packages/python/plotly/plotly/tests/test_optional/test_px/test_px_input.py b/packages/python/plotly/plotly/tests/test_optional/test_px/test_px_input.py index 74237d470e..81605b59a2 100644 --- a/packages/python/plotly/plotly/tests/test_optional/test_px/test_px_input.py +++ b/packages/python/plotly/plotly/tests/test_optional/test_px/test_px_input.py @@ -51,14 +51,13 @@ def test_with_index(): assert fig.data[0]["hovertemplate"] == "item=%{x}
total_bill=%{y}" -def test_series(request, constructor): - if "pyarrow_table" in str(constructor): +def test_series(request, backend): + if backend == "pyarrow": # By converting to native, we lose the name for pyarrow chunked_array # and the assertions fail request.applymarker(pytest.mark.xfail) - data = px.data.tips().to_dict(orient="list") - tips = nw.from_native(constructor(data)) + tips = nw.from_native(px.data.tips(return_type=backend)) before_tip = (tips.get_column("total_bill") - tips.get_column("tip")).to_native() day = tips.get_column("day").to_native() tips = tips.to_native() @@ -171,9 +170,8 @@ def test_name_heuristics(request, constructor): assert fig.data[0].hovertemplate == "y=%{marker.size}
x=%{y}" -def test_repeated_name(constructor): - data = px.data.iris().to_dict(orient="list") - iris = constructor(data) +def test_repeated_name(backend): + iris = px.data.iris(return_type=backend) fig = px.scatter( iris, x="sepal_width", @@ -184,9 +182,8 @@ def test_repeated_name(constructor): assert fig.data[0].customdata.shape[1] == 4 -def test_arrayattrable_numpy(constructor): - data = px.data.tips().to_dict(orient="list") - tips = constructor(data) +def test_arrayattrable_numpy(backend): + tips = px.data.tips(return_type=backend) fig = px.scatter( tips, x="total_bill", y="tip", hover_data=[np.random.random(tips.shape[0])] ) @@ -234,9 +231,8 @@ def test_wrong_dimensions_mixed_case(constructor): assert "All arguments should have the same length." in str(err_msg.value) -def test_wrong_dimensions(constructor): - data = px.data.tips().to_dict(orient="list") - df = constructor(data) +def test_wrong_dimensions(backend): + df = px.data.tips(return_type=backend) with pytest.raises(ValueError) as err_msg: px.scatter(df, x="tip", y=[1, 2, 3]) assert "All arguments should have the same length." in str(err_msg.value) @@ -427,9 +423,8 @@ def test_non_matching_index(): assert_frame_equal(expected, out["data_frame"].to_pandas()) -def test_splom_case(constructor): - data = px.data.iris().to_dict(orient="list") - iris = constructor(data) +def test_splom_case(backend): + iris = px.data.iris(return_type=backend) fig = px.scatter_matrix(iris) assert len(fig.data[0].dimensions) == len(iris.columns) dic = {"a": [1, 2, 3], "b": [4, 5, 6], "c": [7, 8, 9]} @@ -457,9 +452,8 @@ def test_data_frame_from_dict(): assert np.all(fig.data[0].x == [0, 1]) -def test_arguments_not_modified(constructor): - data = px.data.iris().to_dict(orient="list") - iris = nw.from_native(constructor(data)) +def test_arguments_not_modified(backend): + iris = nw.from_native(px.data.iris(return_type=backend)) petal_length = iris.get_column("petal_length").to_native() hover_data = [iris.get_column("sepal_length").to_native()] px.scatter(iris.to_native(), x=petal_length, y="petal_width", hover_data=hover_data) @@ -467,11 +461,10 @@ def test_arguments_not_modified(constructor): assert iris.get_column("sepal_length").to_native().equals(hover_data[0]) -def test_pass_df_columns(constructor): - data = px.data.tips().to_dict(orient="list") - tips = nw.from_native(constructor(data)) +def test_pass_df_columns(backend): + tips = nw.from_native(px.data.tips(return_type=backend)) fig = px.histogram( - tips.to_native(), + tips, x="total_bill", y="tip", color="sex", @@ -480,17 +473,16 @@ def test_pass_df_columns(constructor): ) # the "- 2" is because we re-use x and y in the hovertemplate where possible assert fig.data[1].hovertemplate.count("customdata") == len(tips.columns) - 2 - tips_copy = nw.from_native(constructor(data)) + tips_copy = nw.from_native(px.data.tips(return_type=backend)) assert tips_copy.columns == tips.columns -def test_size_column(request, constructor): - if "pyarrow_table" in str(constructor): +def test_size_column(request, backend): + if backend == "pyarrow": # By converting to native, we lose the name for pyarrow chunked_array # and the assertions fail request.applymarker(pytest.mark.xfail) - data = px.data.tips().to_dict(orient="list") - tips = nw.from_native(constructor(data)) + tips = nw.from_native(px.data.tips(return_type=backend)) fig = px.scatter( tips.to_native(), x=tips.get_column("size").to_native(), diff --git a/packages/python/plotly/plotly/tests/test_optional/test_px/test_trendline.py b/packages/python/plotly/plotly/tests/test_optional/test_px/test_trendline.py index 1ec4263cd9..89dcd0f6bd 100644 --- a/packages/python/plotly/plotly/tests/test_optional/test_px/test_trendline.py +++ b/packages/python/plotly/plotly/tests/test_optional/test_px/test_trendline.py @@ -16,9 +16,10 @@ ("ewm", dict(alpha=0.5)), ], ) -def test_trendline_results_passthrough(constructor, mode, options): - data = px.data.gapminder().query("continent == 'Oceania'").to_dict(orient="list") - df = nw.from_native(constructor(data)) +def test_trendline_results_passthrough(backend, mode, options): + df = nw.from_native(px.data.gapminder(return_type=backend)).filter( + nw.col("continent") == "Oceania" + ) fig = px.scatter( df.to_native(), x="year", @@ -111,9 +112,11 @@ def test_trendline_enough_values(mode, options): ("ewm", dict(alpha=0.5)), ], ) -def test_trendline_nan_values(constructor, mode, options): - data = px.data.gapminder().query("continent == 'Oceania'").to_dict(orient="list") - df = nw.from_native(constructor(data)) +def test_trendline_nan_values(backend, mode, options): + df = nw.from_native(px.data.gapminder(return_type=backend)).filter( + nw.col("continent") == "Oceania" + ) + start_date = 1970 df = df.with_columns(pop=nw.when(nw.col("year") >= start_date).then(nw.col("pop"))) @@ -219,16 +222,16 @@ def test_trendline_on_timeseries(constructor, mode, options): assert str(fig.data[0].x[0]) == str(fig.data[1].x[0]) -def test_overall_trendline(constructor): - df = nw.from_native(constructor(px.data.tips().to_dict(orient="list"))) - fig1 = px.scatter(df.to_native(), x="total_bill", y="tip", trendline="ols") +def test_overall_trendline(backend): + df = px.data.tips(return_type=backend) + fig1 = px.scatter(df, x="total_bill", y="tip", trendline="ols") assert len(fig1.data) == 2 assert "trendline" in fig1.data[1].hovertemplate results1 = px.get_trendline_results(fig1) params1 = results1["px_fit_results"].iloc[0].params fig2 = px.scatter( - df.to_native(), + df, x="total_bill", y="tip", color="sex", @@ -243,7 +246,7 @@ def test_overall_trendline(constructor): assert np.all(np.array_equal(params1, params2)) fig3 = px.scatter( - df.to_native(), + df, x="total_bill", y="tip", facet_row="sex", From a22a7beac8e0120dc2a650aa57b7674bc956ddac Mon Sep 17 00:00:00 2001 From: FBruzzesi Date: Wed, 6 Nov 2024 14:17:50 +0100 Subject: [PATCH 101/106] stocks date to string and test_trendline_on_timeseries fix --- packages/python/plotly/plotly/data/__init__.py | 5 +++-- .../plotly/tests/test_optional/test_px/test_trendline.py | 5 +++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/python/plotly/plotly/data/__init__.py b/packages/python/plotly/plotly/data/__init__.py index d2569e19ab..ef6bcdd2f0 100644 --- a/packages/python/plotly/plotly/data/__init__.py +++ b/packages/python/plotly/plotly/data/__init__.py @@ -252,9 +252,10 @@ def stocks(indexed=False, datetimes=False, return_type="pandas"): df = nw.from_native( _get_dataset("stocks", return_type=return_type), eager_only=True - ) + ).with_columns(nw.col("date").cast(nw.String())) + if datetimes: - df = df.with_columns(nw.col("date").cast(nw.Datetime(time_unit="ns"))) + df = df.with_columns(nw.col("date").str.to_datetime()) if indexed: # then it must be pandas df = df.to_native().set_index("date") diff --git a/packages/python/plotly/plotly/tests/test_optional/test_px/test_trendline.py b/packages/python/plotly/plotly/tests/test_optional/test_px/test_trendline.py index 89dcd0f6bd..a1205568d1 100644 --- a/packages/python/plotly/plotly/tests/test_optional/test_px/test_trendline.py +++ b/packages/python/plotly/plotly/tests/test_optional/test_px/test_trendline.py @@ -189,8 +189,9 @@ def test_ols_trendline_slopes(): ("ewm", dict(alpha=0.5)), ], ) -def test_trendline_on_timeseries(constructor, mode, options): - df = nw.from_native(constructor(px.data.stocks().to_dict(orient="list"))) +def test_trendline_on_timeseries(backend, mode, options): + + df = nw.from_native(px.data.stocks(return_type=backend)) pd_err_msg = r"Could not convert value of 'x' \('date'\) into a numeric type." pl_err_msg = "conversion from `str` to `f64` failed in column 'date'" From fc74b2e2ed1aa070e14c9c6fce2711579e733407 Mon Sep 17 00:00:00 2001 From: FBruzzesi Date: Fri, 8 Nov 2024 12:27:30 +0100 Subject: [PATCH 102/106] do not repeat new_series unnecessarely --- .../python/plotly/plotly/express/_core.py | 27 ++++++++++--------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/packages/python/plotly/plotly/express/_core.py b/packages/python/plotly/plotly/express/_core.py index 39db25b23f..978b75b99d 100644 --- a/packages/python/plotly/plotly/express/_core.py +++ b/packages/python/plotly/plotly/express/_core.py @@ -1383,10 +1383,8 @@ def process_args_into_dataframe( % (field, len_arg, str(list(df_output.keys())), length) ) - # With `pass_through=True`, the original object will be returned if unable to convert - # to a Narwhals Series. df_output[str(col_name)] = to_unindexed_series( - x=nw.from_native(argument, series_only=True, pass_through=True), + x=argument, name=str(col_name), native_namespace=native_namespace, ) @@ -1418,14 +1416,15 @@ def process_args_into_dataframe( msg = "Pandas installation is required if no dataframe is provided." raise NotImplementedError(msg) - df_output.update( - { - col_name: nw.new_series( - name=col_name, values=range(length), native_namespace=native_namespace - ) - for col_name in ranges - } - ) + if ranges: + range_series = nw.new_series( + name="__placeholder__", + values=range(length), + native_namespace=native_namespace, + ) + df_output.update( + {col_name: range_series.alias(col_name) for col_name in ranges} + ) df_output.update( { @@ -2159,7 +2158,7 @@ def process_dataframe_pie(args, trace_patch): df: nw.DataFrame = args["data_frame"] trace_patch["sort"] = False trace_patch["direction"] = "clockwise" - uniques = df.get_column(names).unique().to_list() + uniques = df.get_column(names).unique(maintain_order=True).to_list() order = [x for x in OrderedDict.fromkeys(list(order_in) + uniques) if x in uniques] # Original implementation: args["data_frame"] = df.set_index(names).loc[order].reset_index() @@ -2422,7 +2421,9 @@ def get_groups_and_orders(args, grouper): single_group_name.append("") else: if col not in unique_cache: - unique_cache[col] = df.get_column(col).unique().to_list() + unique_cache[col] = ( + df.get_column(col).unique(maintain_order=True).to_list() + ) uniques = unique_cache[col] if len(uniques) == 1: single_group_name.append(uniques[0]) From 499e2facd5db337ca9fa9b95ac3951bc5cbe0d31 Mon Sep 17 00:00:00 2001 From: FBruzzesi Date: Fri, 8 Nov 2024 16:28:21 +0100 Subject: [PATCH 103/106] bump version, use numpy for range --- .../python/plotly/optional-requirements.txt | 2 +- packages/python/plotly/plotly/express/_core.py | 18 +++++++++++++----- packages/python/plotly/requirements.txt | 2 +- packages/python/plotly/setup.py | 2 +- .../requirements_310_core.txt | 2 +- .../requirements_310_optional.txt | 2 +- .../requirements_311_core.txt | 2 +- .../requirements_311_optional.txt | 2 +- .../test_requirements/requirements_38_core.txt | 2 +- .../requirements_38_optional.txt | 2 +- .../test_requirements/requirements_39_core.txt | 2 +- .../requirements_39_optional.txt | 2 +- .../requirements_39_pandas_2_optional.txt | 2 +- 13 files changed, 25 insertions(+), 17 deletions(-) diff --git a/packages/python/plotly/optional-requirements.txt b/packages/python/plotly/optional-requirements.txt index 7213214b6c..4cd4051edc 100644 --- a/packages/python/plotly/optional-requirements.txt +++ b/packages/python/plotly/optional-requirements.txt @@ -39,7 +39,7 @@ ipython ## pandas deps for some matplotlib functionality ## pandas -narwhals>=1.13.2 +narwhals>=1.13.3 ## scipy deps for some FigureFactory functions ## scipy diff --git a/packages/python/plotly/plotly/express/_core.py b/packages/python/plotly/plotly/express/_core.py index 978b75b99d..b3bcd096d3 100644 --- a/packages/python/plotly/plotly/express/_core.py +++ b/packages/python/plotly/plotly/express/_core.py @@ -1417,9 +1417,11 @@ def process_args_into_dataframe( raise NotImplementedError(msg) if ranges: + import numpy as np + range_series = nw.new_series( name="__placeholder__", - values=range(length), + values=np.arange(length), native_namespace=native_namespace, ) df_output.update( @@ -2161,10 +2163,16 @@ def process_dataframe_pie(args, trace_patch): uniques = df.get_column(names).unique(maintain_order=True).to_list() order = [x for x in OrderedDict.fromkeys(list(order_in) + uniques) if x in uniques] - # Original implementation: args["data_frame"] = df.set_index(names).loc[order].reset_index() - # However we do not have a way to custom sort a dataframe in narwhals. - args["data_frame"] = nw.concat( - [df.filter(nw.col(names) == value) for value in order], how="vertical" + # Sort args['data_frame'] by column 'b' according to order `order`. + token = nw.generate_temporary_column_name(8, df.columns) + args["data_frame"] = ( + df.with_columns( + nw.col("b") + .replace_strict(order, range(len(order)), return_dtype=nw.UInt32) + .alias(token) + ) + .sort(token) + .drop(token) ) return args, trace_patch diff --git a/packages/python/plotly/requirements.txt b/packages/python/plotly/requirements.txt index e1e1e8c2c4..ddd5d2bf77 100644 --- a/packages/python/plotly/requirements.txt +++ b/packages/python/plotly/requirements.txt @@ -6,4 +6,4 @@ ################################################### ## dataframe agnostic layer ## -narwhals>=1.13.2 +narwhals>=1.13.3 diff --git a/packages/python/plotly/setup.py b/packages/python/plotly/setup.py index 07dbf24990..081e082a20 100644 --- a/packages/python/plotly/setup.py +++ b/packages/python/plotly/setup.py @@ -603,7 +603,7 @@ def run(self): data_files=[ ("etc/jupyter/nbconfig/notebook.d", ["jupyterlab-plotly.json"]), ], - install_requires=["narwhals>=1.13.2", "packaging"], + install_requires=["narwhals>=1.13.3", "packaging"], zip_safe=False, cmdclass=dict( build_py=js_prerelease(versioneer_cmds["build_py"]), diff --git a/packages/python/plotly/test_requirements/requirements_310_core.txt b/packages/python/plotly/test_requirements/requirements_310_core.txt index ade22cb378..771df9cc87 100644 --- a/packages/python/plotly/test_requirements/requirements_310_core.txt +++ b/packages/python/plotly/test_requirements/requirements_310_core.txt @@ -1,3 +1,3 @@ requests==2.25.1 pytest==7.4.4 -narwhals>=1.13.2 +narwhals>=1.13.3 diff --git a/packages/python/plotly/test_requirements/requirements_310_optional.txt b/packages/python/plotly/test_requirements/requirements_310_optional.txt index b100253787..6c9322739d 100644 --- a/packages/python/plotly/test_requirements/requirements_310_optional.txt +++ b/packages/python/plotly/test_requirements/requirements_310_optional.txt @@ -21,4 +21,4 @@ kaleido orjson==3.8.12 polars[timezone] pyarrow -narwhals>=1.13.2 +narwhals>=1.13.3 diff --git a/packages/python/plotly/test_requirements/requirements_311_core.txt b/packages/python/plotly/test_requirements/requirements_311_core.txt index 2f67b3de68..938e282b0f 100644 --- a/packages/python/plotly/test_requirements/requirements_311_core.txt +++ b/packages/python/plotly/test_requirements/requirements_311_core.txt @@ -1,3 +1,3 @@ requests==2.25.1 pytest==7.4.4 -narwhals>=1.13.2 \ No newline at end of file +narwhals>=1.13.3 \ No newline at end of file diff --git a/packages/python/plotly/test_requirements/requirements_311_optional.txt b/packages/python/plotly/test_requirements/requirements_311_optional.txt index c0aab90b6b..fe17acad91 100644 --- a/packages/python/plotly/test_requirements/requirements_311_optional.txt +++ b/packages/python/plotly/test_requirements/requirements_311_optional.txt @@ -21,4 +21,4 @@ kaleido orjson==3.8.12 polars[timezone] pyarrow -narwhals>=1.13.2 +narwhals>=1.13.3 diff --git a/packages/python/plotly/test_requirements/requirements_38_core.txt b/packages/python/plotly/test_requirements/requirements_38_core.txt index 47488db756..2983296113 100644 --- a/packages/python/plotly/test_requirements/requirements_38_core.txt +++ b/packages/python/plotly/test_requirements/requirements_38_core.txt @@ -1,3 +1,3 @@ requests==2.25.1 pytest==8.1.1 -narwhals>=1.13.2 +narwhals>=1.13.3 diff --git a/packages/python/plotly/test_requirements/requirements_38_optional.txt b/packages/python/plotly/test_requirements/requirements_38_optional.txt index 2b89bf3da6..2f0fe7003e 100644 --- a/packages/python/plotly/test_requirements/requirements_38_optional.txt +++ b/packages/python/plotly/test_requirements/requirements_38_optional.txt @@ -21,4 +21,4 @@ psutil==5.7.0 kaleido polars[timezone] pyarrow -narwhals>=1.13.2 +narwhals>=1.13.3 diff --git a/packages/python/plotly/test_requirements/requirements_39_core.txt b/packages/python/plotly/test_requirements/requirements_39_core.txt index 45f8b20c79..b6055de809 100644 --- a/packages/python/plotly/test_requirements/requirements_39_core.txt +++ b/packages/python/plotly/test_requirements/requirements_39_core.txt @@ -1,3 +1,3 @@ requests==2.25.1 pytest==6.2.3 -narwhals>=1.13.2 +narwhals>=1.13.3 diff --git a/packages/python/plotly/test_requirements/requirements_39_optional.txt b/packages/python/plotly/test_requirements/requirements_39_optional.txt index a7ca3200b0..79813b4772 100644 --- a/packages/python/plotly/test_requirements/requirements_39_optional.txt +++ b/packages/python/plotly/test_requirements/requirements_39_optional.txt @@ -22,4 +22,4 @@ kaleido orjson==3.8.12 polars[timezone] pyarrow -narwhals>=1.13.2 +narwhals>=1.13.3 diff --git a/packages/python/plotly/test_requirements/requirements_39_pandas_2_optional.txt b/packages/python/plotly/test_requirements/requirements_39_pandas_2_optional.txt index 5f68572d00..b1d9180e4d 100644 --- a/packages/python/plotly/test_requirements/requirements_39_pandas_2_optional.txt +++ b/packages/python/plotly/test_requirements/requirements_39_pandas_2_optional.txt @@ -22,4 +22,4 @@ vaex pydantic<=1.10.11 # for vaex, see https://github.com/vaexio/vaex/issues/2384 polars[timezone] pyarrow -narwhals>=1.13.2 +narwhals>=1.13.3 From d2e1008332b9c48f9e98caa1239f14b2d481178d Mon Sep 17 00:00:00 2001 From: FBruzzesi Date: Fri, 8 Nov 2024 16:44:38 +0100 Subject: [PATCH 104/106] trigger ci now that new version is published From 742b2ecbf16968da3e461668c3f1d149402dc6a8 Mon Sep 17 00:00:00 2001 From: FBruzzesi Date: Fri, 8 Nov 2024 17:04:35 +0100 Subject: [PATCH 105/106] add narwhals to np2_optional.txt --- .../test_requirements/requirements_312_np2_optional.txt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/python/plotly/test_requirements/requirements_312_np2_optional.txt b/packages/python/plotly/test_requirements/requirements_312_np2_optional.txt index 7938433a5b..8252a5ab98 100644 --- a/packages/python/plotly/test_requirements/requirements_312_np2_optional.txt +++ b/packages/python/plotly/test_requirements/requirements_312_np2_optional.txt @@ -18,4 +18,7 @@ matplotlib==3.9.2 scikit-image==0.24.0 psutil==5.9.7 kaleido -orjson==3.9.10 \ No newline at end of file +orjson==3.9.10 +polars[timezone] +pyarrow +narwhals>=1.13.1 From 269dea642c5cd5e734ceb3033b7711963dc755ce Mon Sep 17 00:00:00 2001 From: FBruzzesi Date: Fri, 8 Nov 2024 17:05:48 +0100 Subject: [PATCH 106/106] version --- .../python/plotly/test_requirements/requirements_312_core.txt | 2 +- .../test_requirements/requirements_312_no_numpy_optional.txt | 2 +- .../plotly/test_requirements/requirements_312_np2_optional.txt | 2 +- .../plotly/test_requirements/requirements_312_optional.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/python/plotly/test_requirements/requirements_312_core.txt b/packages/python/plotly/test_requirements/requirements_312_core.txt index 2ee8e7b437..771df9cc87 100644 --- a/packages/python/plotly/test_requirements/requirements_312_core.txt +++ b/packages/python/plotly/test_requirements/requirements_312_core.txt @@ -1,3 +1,3 @@ requests==2.25.1 pytest==7.4.4 -narwhals>=1.13.1 +narwhals>=1.13.3 diff --git a/packages/python/plotly/test_requirements/requirements_312_no_numpy_optional.txt b/packages/python/plotly/test_requirements/requirements_312_no_numpy_optional.txt index da1020e5c3..85fdfa06e4 100644 --- a/packages/python/plotly/test_requirements/requirements_312_no_numpy_optional.txt +++ b/packages/python/plotly/test_requirements/requirements_312_no_numpy_optional.txt @@ -20,5 +20,5 @@ kaleido orjson==3.9.10 polars[timezone] pyarrow -narwhals>=1.13.1 +narwhals>=1.13.3 jupyter-console==6.4.4 diff --git a/packages/python/plotly/test_requirements/requirements_312_np2_optional.txt b/packages/python/plotly/test_requirements/requirements_312_np2_optional.txt index 8252a5ab98..fa64782aba 100644 --- a/packages/python/plotly/test_requirements/requirements_312_np2_optional.txt +++ b/packages/python/plotly/test_requirements/requirements_312_np2_optional.txt @@ -21,4 +21,4 @@ kaleido orjson==3.9.10 polars[timezone] pyarrow -narwhals>=1.13.1 +narwhals>=1.13.3 diff --git a/packages/python/plotly/test_requirements/requirements_312_optional.txt b/packages/python/plotly/test_requirements/requirements_312_optional.txt index 9e4726d62a..8b9725ef3a 100644 --- a/packages/python/plotly/test_requirements/requirements_312_optional.txt +++ b/packages/python/plotly/test_requirements/requirements_312_optional.txt @@ -21,5 +21,5 @@ kaleido orjson==3.9.10 polars[timezone] pyarrow -narwhals>=1.13.1 +narwhals>=1.13.3 jupyter-console==6.4.4