mirror of
https://github.com/Ladebeze66/ft_linear_regression.git
synced 2026-11-09 13:47:18 +01:00
first
This commit is contained in:
21
ftline/lib/python3.12/site-packages/seaborn/__init__.py
Normal file
21
ftline/lib/python3.12/site-packages/seaborn/__init__.py
Normal file
@@ -0,0 +1,21 @@
|
||||
# Import seaborn objects
|
||||
from .rcmod import * # noqa: F401,F403
|
||||
from .utils import * # noqa: F401,F403
|
||||
from .palettes import * # noqa: F401,F403
|
||||
from .relational import * # noqa: F401,F403
|
||||
from .regression import * # noqa: F401,F403
|
||||
from .categorical import * # noqa: F401,F403
|
||||
from .distributions import * # noqa: F401,F403
|
||||
from .matrix import * # noqa: F401,F403
|
||||
from .miscplot import * # noqa: F401,F403
|
||||
from .axisgrid import * # noqa: F401,F403
|
||||
from .widgets import * # noqa: F401,F403
|
||||
from .colors import xkcd_rgb, crayons # noqa: F401
|
||||
from . import cm # noqa: F401
|
||||
|
||||
# Capture the original matplotlib rcParams
|
||||
import matplotlib as mpl
|
||||
_orig_rc_params = mpl.rcParams.copy()
|
||||
|
||||
# Define the seaborn version
|
||||
__version__ = "0.13.2"
|
||||
1777
ftline/lib/python3.12/site-packages/seaborn/_base.py
Normal file
1777
ftline/lib/python3.12/site-packages/seaborn/_base.py
Normal file
File diff suppressed because it is too large
Load Diff
123
ftline/lib/python3.12/site-packages/seaborn/_compat.py
Normal file
123
ftline/lib/python3.12/site-packages/seaborn/_compat.py
Normal file
@@ -0,0 +1,123 @@
|
||||
from __future__ import annotations
|
||||
from typing import Literal
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
import matplotlib as mpl
|
||||
from matplotlib.figure import Figure
|
||||
from seaborn.utils import _version_predates
|
||||
|
||||
|
||||
def norm_from_scale(scale, norm):
|
||||
"""Produce a Normalize object given a Scale and min/max domain limits."""
|
||||
# This is an internal maplotlib function that simplifies things to access
|
||||
# It is likely to become part of the matplotlib API at some point:
|
||||
# https://github.com/matplotlib/matplotlib/issues/20329
|
||||
if isinstance(norm, mpl.colors.Normalize):
|
||||
return norm
|
||||
|
||||
if scale is None:
|
||||
return None
|
||||
|
||||
if norm is None:
|
||||
vmin = vmax = None
|
||||
else:
|
||||
vmin, vmax = norm # TODO more helpful error if this fails?
|
||||
|
||||
class ScaledNorm(mpl.colors.Normalize):
|
||||
|
||||
def __call__(self, value, clip=None):
|
||||
# From github.com/matplotlib/matplotlib/blob/v3.4.2/lib/matplotlib/colors.py
|
||||
# See github.com/matplotlib/matplotlib/tree/v3.4.2/LICENSE
|
||||
value, is_scalar = self.process_value(value)
|
||||
self.autoscale_None(value)
|
||||
if self.vmin > self.vmax:
|
||||
raise ValueError("vmin must be less or equal to vmax")
|
||||
if self.vmin == self.vmax:
|
||||
return np.full_like(value, 0)
|
||||
if clip is None:
|
||||
clip = self.clip
|
||||
if clip:
|
||||
value = np.clip(value, self.vmin, self.vmax)
|
||||
# ***** Seaborn changes start ****
|
||||
t_value = self.transform(value).reshape(np.shape(value))
|
||||
t_vmin, t_vmax = self.transform([self.vmin, self.vmax])
|
||||
# ***** Seaborn changes end *****
|
||||
if not np.isfinite([t_vmin, t_vmax]).all():
|
||||
raise ValueError("Invalid vmin or vmax")
|
||||
t_value -= t_vmin
|
||||
t_value /= (t_vmax - t_vmin)
|
||||
t_value = np.ma.masked_invalid(t_value, copy=False)
|
||||
return t_value[0] if is_scalar else t_value
|
||||
|
||||
new_norm = ScaledNorm(vmin, vmax)
|
||||
new_norm.transform = scale.get_transform().transform
|
||||
|
||||
return new_norm
|
||||
|
||||
|
||||
def get_colormap(name):
|
||||
"""Handle changes to matplotlib colormap interface in 3.6."""
|
||||
try:
|
||||
return mpl.colormaps[name]
|
||||
except AttributeError:
|
||||
return mpl.cm.get_cmap(name)
|
||||
|
||||
|
||||
def register_colormap(name, cmap):
|
||||
"""Handle changes to matplotlib colormap interface in 3.6."""
|
||||
try:
|
||||
if name not in mpl.colormaps:
|
||||
mpl.colormaps.register(cmap, name=name)
|
||||
except AttributeError:
|
||||
mpl.cm.register_cmap(name, cmap)
|
||||
|
||||
|
||||
def set_layout_engine(
|
||||
fig: Figure,
|
||||
engine: Literal["constrained", "compressed", "tight", "none"],
|
||||
) -> None:
|
||||
"""Handle changes to auto layout engine interface in 3.6"""
|
||||
if hasattr(fig, "set_layout_engine"):
|
||||
fig.set_layout_engine(engine)
|
||||
else:
|
||||
# _version_predates(mpl, 3.6)
|
||||
if engine == "tight":
|
||||
fig.set_tight_layout(True) # type: ignore # predates typing
|
||||
elif engine == "constrained":
|
||||
fig.set_constrained_layout(True) # type: ignore
|
||||
elif engine == "none":
|
||||
fig.set_tight_layout(False) # type: ignore
|
||||
fig.set_constrained_layout(False) # type: ignore
|
||||
|
||||
|
||||
def get_layout_engine(fig: Figure) -> mpl.layout_engine.LayoutEngine | None:
|
||||
"""Handle changes to auto layout engine interface in 3.6"""
|
||||
if hasattr(fig, "get_layout_engine"):
|
||||
return fig.get_layout_engine()
|
||||
else:
|
||||
# _version_predates(mpl, 3.6)
|
||||
return None
|
||||
|
||||
|
||||
def share_axis(ax0, ax1, which):
|
||||
"""Handle changes to post-hoc axis sharing."""
|
||||
if _version_predates(mpl, "3.5"):
|
||||
group = getattr(ax0, f"get_shared_{which}_axes")()
|
||||
group.join(ax1, ax0)
|
||||
else:
|
||||
getattr(ax1, f"share{which}")(ax0)
|
||||
|
||||
|
||||
def get_legend_handles(legend):
|
||||
"""Handle legendHandles attribute rename."""
|
||||
if _version_predates(mpl, "3.7"):
|
||||
return legend.legendHandles
|
||||
else:
|
||||
return legend.legend_handles
|
||||
|
||||
|
||||
def groupby_apply_include_groups(val):
|
||||
if _version_predates(pd, "2.2.0"):
|
||||
return {}
|
||||
return {"include_groups": val}
|
||||
319
ftline/lib/python3.12/site-packages/seaborn/_core/data.py
Normal file
319
ftline/lib/python3.12/site-packages/seaborn/_core/data.py
Normal file
@@ -0,0 +1,319 @@
|
||||
"""
|
||||
Components for parsing variable assignments and internally representing plot data.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Mapping, Sized
|
||||
from typing import cast
|
||||
import warnings
|
||||
|
||||
import pandas as pd
|
||||
from pandas import DataFrame
|
||||
|
||||
from seaborn._core.typing import DataSource, VariableSpec, ColumnName
|
||||
from seaborn.utils import _version_predates
|
||||
|
||||
|
||||
class PlotData:
|
||||
"""
|
||||
Data table with plot variable schema and mapping to original names.
|
||||
|
||||
Contains logic for parsing variable specification arguments and updating
|
||||
the table with layer-specific data and/or mappings.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
data
|
||||
Input data where variable names map to vector values.
|
||||
variables
|
||||
Keys are names of plot variables (x, y, ...) each value is one of:
|
||||
|
||||
- name of a column (or index level, or dictionary entry) in `data`
|
||||
- vector in any format that can construct a :class:`pandas.DataFrame`
|
||||
|
||||
Attributes
|
||||
----------
|
||||
frame
|
||||
Data table with column names having defined plot variables.
|
||||
names
|
||||
Dictionary mapping plot variable names to names in source data structure(s).
|
||||
ids
|
||||
Dictionary mapping plot variable names to unique data source identifiers.
|
||||
|
||||
"""
|
||||
frame: DataFrame
|
||||
frames: dict[tuple, DataFrame]
|
||||
names: dict[str, str | None]
|
||||
ids: dict[str, str | int]
|
||||
source_data: DataSource
|
||||
source_vars: dict[str, VariableSpec]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
data: DataSource,
|
||||
variables: dict[str, VariableSpec],
|
||||
):
|
||||
|
||||
data = handle_data_source(data)
|
||||
frame, names, ids = self._assign_variables(data, variables)
|
||||
|
||||
self.frame = frame
|
||||
self.names = names
|
||||
self.ids = ids
|
||||
|
||||
# The reason we possibly have a dictionary of frames is to support the
|
||||
# Plot.pair operation, post scaling, where each x/y variable needs its
|
||||
# own frame. This feels pretty clumsy and there are a bunch of places in
|
||||
# the client code with awkard if frame / elif frames constructions.
|
||||
# It would be great to have a cleaner abstraction here.
|
||||
self.frames = {}
|
||||
|
||||
self.source_data = data
|
||||
self.source_vars = variables
|
||||
|
||||
def __contains__(self, key: str) -> bool:
|
||||
"""Boolean check on whether a variable is defined in this dataset."""
|
||||
if self.frame is None:
|
||||
return any(key in df for df in self.frames.values())
|
||||
return key in self.frame
|
||||
|
||||
def join(
|
||||
self,
|
||||
data: DataSource,
|
||||
variables: dict[str, VariableSpec] | None,
|
||||
) -> PlotData:
|
||||
"""Add, replace, or drop variables and return as a new dataset."""
|
||||
# Inherit the original source of the upstream data by default
|
||||
if data is None:
|
||||
data = self.source_data
|
||||
|
||||
# TODO allow `data` to be a function (that is called on the source data?)
|
||||
|
||||
if not variables:
|
||||
variables = self.source_vars
|
||||
|
||||
# Passing var=None implies that we do not want that variable in this layer
|
||||
disinherit = [k for k, v in variables.items() if v is None]
|
||||
|
||||
# Create a new dataset with just the info passed here
|
||||
new = PlotData(data, variables)
|
||||
|
||||
# -- Update the inherited DataSource with this new information
|
||||
|
||||
drop_cols = [k for k in self.frame if k in new.frame or k in disinherit]
|
||||
parts = [self.frame.drop(columns=drop_cols), new.frame]
|
||||
|
||||
# Because we are combining distinct columns, this is perhaps more
|
||||
# naturally thought of as a "merge"/"join". But using concat because
|
||||
# some simple testing suggests that it is marginally faster.
|
||||
frame = pd.concat(parts, axis=1, sort=False, copy=False)
|
||||
|
||||
names = {k: v for k, v in self.names.items() if k not in disinherit}
|
||||
names.update(new.names)
|
||||
|
||||
ids = {k: v for k, v in self.ids.items() if k not in disinherit}
|
||||
ids.update(new.ids)
|
||||
|
||||
new.frame = frame
|
||||
new.names = names
|
||||
new.ids = ids
|
||||
|
||||
# Multiple chained operations should always inherit from the original object
|
||||
new.source_data = self.source_data
|
||||
new.source_vars = self.source_vars
|
||||
|
||||
return new
|
||||
|
||||
def _assign_variables(
|
||||
self,
|
||||
data: DataFrame | Mapping | None,
|
||||
variables: dict[str, VariableSpec],
|
||||
) -> tuple[DataFrame, dict[str, str | None], dict[str, str | int]]:
|
||||
"""
|
||||
Assign values for plot variables given long-form data and/or vector inputs.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
data
|
||||
Input data where variable names map to vector values.
|
||||
variables
|
||||
Keys are names of plot variables (x, y, ...) each value is one of:
|
||||
|
||||
- name of a column (or index level, or dictionary entry) in `data`
|
||||
- vector in any format that can construct a :class:`pandas.DataFrame`
|
||||
|
||||
Returns
|
||||
-------
|
||||
frame
|
||||
Table mapping seaborn variables (x, y, color, ...) to data vectors.
|
||||
names
|
||||
Keys are defined seaborn variables; values are names inferred from
|
||||
the inputs (or None when no name can be determined).
|
||||
ids
|
||||
Like the `names` dict, but `None` values are replaced by the `id()`
|
||||
of the data object that defined the variable.
|
||||
|
||||
Raises
|
||||
------
|
||||
TypeError
|
||||
When data source is not a DataFrame or Mapping.
|
||||
ValueError
|
||||
When variables are strings that don't appear in `data`, or when they are
|
||||
non-indexed vector datatypes that have a different length from `data`.
|
||||
|
||||
"""
|
||||
source_data: Mapping | DataFrame
|
||||
frame: DataFrame
|
||||
names: dict[str, str | None]
|
||||
ids: dict[str, str | int]
|
||||
|
||||
plot_data = {}
|
||||
names = {}
|
||||
ids = {}
|
||||
|
||||
given_data = data is not None
|
||||
if data is None:
|
||||
# Data is optional; all variables can be defined as vectors
|
||||
# But simplify downstream code by always having a usable source data object
|
||||
source_data = {}
|
||||
else:
|
||||
source_data = data
|
||||
|
||||
# Variables can also be extracted from the index of a DataFrame
|
||||
if isinstance(source_data, pd.DataFrame):
|
||||
index = source_data.index.to_frame().to_dict("series")
|
||||
else:
|
||||
index = {}
|
||||
|
||||
for key, val in variables.items():
|
||||
|
||||
# Simply ignore variables with no specification
|
||||
if val is None:
|
||||
continue
|
||||
|
||||
# Try to treat the argument as a key for the data collection.
|
||||
# But be flexible about what can be used as a key.
|
||||
# Usually it will be a string, but allow other hashables when
|
||||
# taking from the main data object. Allow only strings to reference
|
||||
# fields in the index, because otherwise there is too much ambiguity.
|
||||
|
||||
# TODO this will be rendered unnecessary by the following pandas fix:
|
||||
# https://github.com/pandas-dev/pandas/pull/41283
|
||||
try:
|
||||
hash(val)
|
||||
val_is_hashable = True
|
||||
except TypeError:
|
||||
val_is_hashable = False
|
||||
|
||||
val_as_data_key = (
|
||||
# See https://github.com/pandas-dev/pandas/pull/41283
|
||||
# (isinstance(val, abc.Hashable) and val in source_data)
|
||||
(val_is_hashable and val in source_data)
|
||||
or (isinstance(val, str) and val in index)
|
||||
)
|
||||
|
||||
if val_as_data_key:
|
||||
val = cast(ColumnName, val)
|
||||
if val in source_data:
|
||||
plot_data[key] = source_data[val]
|
||||
elif val in index:
|
||||
plot_data[key] = index[val]
|
||||
names[key] = ids[key] = str(val)
|
||||
|
||||
elif isinstance(val, str):
|
||||
|
||||
# This looks like a column name but, lookup failed.
|
||||
|
||||
err = f"Could not interpret value `{val}` for `{key}`. "
|
||||
if not given_data:
|
||||
err += "Value is a string, but `data` was not passed."
|
||||
else:
|
||||
err += "An entry with this name does not appear in `data`."
|
||||
raise ValueError(err)
|
||||
|
||||
else:
|
||||
|
||||
# Otherwise, assume the value somehow represents data
|
||||
|
||||
# Ignore empty data structures
|
||||
if isinstance(val, Sized) and len(val) == 0:
|
||||
continue
|
||||
|
||||
# If vector has no index, it must match length of data table
|
||||
if isinstance(data, pd.DataFrame) and not isinstance(val, pd.Series):
|
||||
if isinstance(val, Sized) and len(data) != len(val):
|
||||
val_cls = val.__class__.__name__
|
||||
err = (
|
||||
f"Length of {val_cls} vectors must match length of `data`"
|
||||
f" when both are used, but `data` has length {len(data)}"
|
||||
f" and the vector passed to `{key}` has length {len(val)}."
|
||||
)
|
||||
raise ValueError(err)
|
||||
|
||||
plot_data[key] = val
|
||||
|
||||
# Try to infer the original name using pandas-like metadata
|
||||
if hasattr(val, "name"):
|
||||
names[key] = ids[key] = str(val.name) # type: ignore # mypy/1424
|
||||
else:
|
||||
names[key] = None
|
||||
ids[key] = id(val)
|
||||
|
||||
# Construct a tidy plot DataFrame. This will convert a number of
|
||||
# types automatically, aligning on index in case of pandas objects
|
||||
# TODO Note: this fails when variable specs *only* have scalars!
|
||||
frame = pd.DataFrame(plot_data)
|
||||
|
||||
return frame, names, ids
|
||||
|
||||
|
||||
def handle_data_source(data: object) -> pd.DataFrame | Mapping | None:
|
||||
"""Convert the data source object to a common union representation."""
|
||||
if isinstance(data, pd.DataFrame) or hasattr(data, "__dataframe__"):
|
||||
# Check for pd.DataFrame inheritance could be removed once
|
||||
# minimal pandas version supports dataframe interchange (1.5.0).
|
||||
data = convert_dataframe_to_pandas(data)
|
||||
elif data is not None and not isinstance(data, Mapping):
|
||||
err = f"Data source must be a DataFrame or Mapping, not {type(data)!r}."
|
||||
raise TypeError(err)
|
||||
|
||||
return data
|
||||
|
||||
|
||||
def convert_dataframe_to_pandas(data: object) -> pd.DataFrame:
|
||||
"""Use the DataFrame exchange protocol, or fail gracefully."""
|
||||
if isinstance(data, pd.DataFrame):
|
||||
return data
|
||||
|
||||
if not hasattr(pd.api, "interchange"):
|
||||
msg = (
|
||||
"Support for non-pandas DataFrame objects requires a version of pandas "
|
||||
"that implements the DataFrame interchange protocol. Please upgrade "
|
||||
"your pandas version or coerce your data to pandas before passing "
|
||||
"it to seaborn."
|
||||
)
|
||||
raise TypeError(msg)
|
||||
|
||||
if _version_predates(pd, "2.0.2"):
|
||||
msg = (
|
||||
"DataFrame interchange with pandas<2.0.2 has some known issues. "
|
||||
f"You are using pandas {pd.__version__}. "
|
||||
"Continuing, but it is recommended to carefully inspect the results and to "
|
||||
"consider upgrading."
|
||||
)
|
||||
warnings.warn(msg, stacklevel=2)
|
||||
|
||||
try:
|
||||
# This is going to convert all columns in the input dataframe, even though
|
||||
# we may only need one or two of them. It would be more efficient to select
|
||||
# the columns that are going to be used in the plot prior to interchange.
|
||||
# Solving that in general is a hard problem, especially with the objects
|
||||
# interface where variables passed in Plot() may only be referenced later
|
||||
# in Plot.add(). But noting here in case this seems to be a bottleneck.
|
||||
return pd.api.interchange.from_dataframe(data)
|
||||
except Exception as err:
|
||||
msg = (
|
||||
"Encountered an exception when converting data source "
|
||||
"to a pandas DataFrame. See traceback above for details."
|
||||
)
|
||||
raise RuntimeError(msg) from err
|
||||
@@ -0,0 +1,32 @@
|
||||
"""
|
||||
Custom exceptions for the seaborn.objects interface.
|
||||
|
||||
This is very lightweight, but it's a separate module to avoid circular imports.
|
||||
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
class PlotSpecError(RuntimeError):
|
||||
"""
|
||||
Error class raised from seaborn.objects.Plot for compile-time failures.
|
||||
|
||||
In the declarative Plot interface, exceptions may not be triggered immediately
|
||||
by bad user input (and validation at input time may not be possible). This class
|
||||
is used to signal that indirect dependency. It should be raised in an exception
|
||||
chain when compile-time operations fail with an error message providing useful
|
||||
context (e.g., scaling errors could specify the variable that failed.)
|
||||
|
||||
"""
|
||||
@classmethod
|
||||
def _during(cls, step: str, var: str = "") -> PlotSpecError:
|
||||
"""
|
||||
Initialize the class to report the failure of a specific operation.
|
||||
"""
|
||||
message = []
|
||||
if var:
|
||||
message.append(f"{step} failed for the `{var}` variable.")
|
||||
else:
|
||||
message.append(f"{step} failed.")
|
||||
message.append("See the traceback above for more information.")
|
||||
return cls(" ".join(message))
|
||||
129
ftline/lib/python3.12/site-packages/seaborn/_core/groupby.py
Normal file
129
ftline/lib/python3.12/site-packages/seaborn/_core/groupby.py
Normal file
@@ -0,0 +1,129 @@
|
||||
"""Simplified split-apply-combine paradigm on dataframes for internal use."""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import cast, Iterable
|
||||
|
||||
import pandas as pd
|
||||
|
||||
from seaborn._core.rules import categorical_order
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
if TYPE_CHECKING:
|
||||
from typing import Callable
|
||||
from pandas import DataFrame, MultiIndex, Index
|
||||
|
||||
|
||||
class GroupBy:
|
||||
"""
|
||||
Interface for Pandas GroupBy operations allowing specified group order.
|
||||
|
||||
Writing our own class to do this has a few advantages:
|
||||
- It constrains the interface between Plot and Stat/Move objects
|
||||
- It allows control over the row order of the GroupBy result, which is
|
||||
important when using in the context of some Move operations (dodge, stack, ...)
|
||||
- It simplifies some complexities regarding the return type and Index contents
|
||||
one encounters with Pandas, especially for DataFrame -> DataFrame applies
|
||||
- It increases future flexibility regarding alternate DataFrame libraries
|
||||
|
||||
"""
|
||||
def __init__(self, order: list[str] | dict[str, list | None]):
|
||||
"""
|
||||
Initialize the GroupBy from grouping variables and optional level orders.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
order
|
||||
List of variable names or dict mapping names to desired level orders.
|
||||
Level order values can be None to use default ordering rules. The
|
||||
variables can include names that are not expected to appear in the
|
||||
data; these will be dropped before the groups are defined.
|
||||
|
||||
"""
|
||||
if not order:
|
||||
raise ValueError("GroupBy requires at least one grouping variable")
|
||||
|
||||
if isinstance(order, list):
|
||||
order = {k: None for k in order}
|
||||
self.order = order
|
||||
|
||||
def _get_groups(
|
||||
self, data: DataFrame
|
||||
) -> tuple[str | list[str], Index | MultiIndex]:
|
||||
"""Return index with Cartesian product of ordered grouping variable levels."""
|
||||
levels = {}
|
||||
for var, order in self.order.items():
|
||||
if var in data:
|
||||
if order is None:
|
||||
order = categorical_order(data[var])
|
||||
levels[var] = order
|
||||
|
||||
grouper: str | list[str]
|
||||
groups: Index | MultiIndex
|
||||
if not levels:
|
||||
grouper = []
|
||||
groups = pd.Index([])
|
||||
elif len(levels) > 1:
|
||||
grouper = list(levels)
|
||||
groups = pd.MultiIndex.from_product(levels.values(), names=grouper)
|
||||
else:
|
||||
grouper, = list(levels)
|
||||
groups = pd.Index(levels[grouper], name=grouper)
|
||||
return grouper, groups
|
||||
|
||||
def _reorder_columns(self, res, data):
|
||||
"""Reorder result columns to match original order with new columns appended."""
|
||||
cols = [c for c in data if c in res]
|
||||
cols += [c for c in res if c not in data]
|
||||
return res.reindex(columns=pd.Index(cols))
|
||||
|
||||
def agg(self, data: DataFrame, *args, **kwargs) -> DataFrame:
|
||||
"""
|
||||
Reduce each group to a single row in the output.
|
||||
|
||||
The output will have a row for each unique combination of the grouping
|
||||
variable levels with null values for the aggregated variable(s) where
|
||||
those combinations do not appear in the dataset.
|
||||
|
||||
"""
|
||||
grouper, groups = self._get_groups(data)
|
||||
|
||||
if not grouper:
|
||||
# We will need to see whether there are valid usecases that end up here
|
||||
raise ValueError("No grouping variables are present in dataframe")
|
||||
|
||||
res = (
|
||||
data
|
||||
.groupby(grouper, sort=False, observed=False)
|
||||
.agg(*args, **kwargs)
|
||||
.reindex(groups)
|
||||
.reset_index()
|
||||
.pipe(self._reorder_columns, data)
|
||||
)
|
||||
|
||||
return res
|
||||
|
||||
def apply(
|
||||
self, data: DataFrame, func: Callable[..., DataFrame],
|
||||
*args, **kwargs,
|
||||
) -> DataFrame:
|
||||
"""Apply a DataFrame -> DataFrame mapping to each group."""
|
||||
grouper, groups = self._get_groups(data)
|
||||
|
||||
if not grouper:
|
||||
return self._reorder_columns(func(data, *args, **kwargs), data)
|
||||
|
||||
parts = {}
|
||||
for key, part_df in data.groupby(grouper, sort=False, observed=False):
|
||||
parts[key] = func(part_df, *args, **kwargs)
|
||||
stack = []
|
||||
for key in groups:
|
||||
if key in parts:
|
||||
if isinstance(grouper, list):
|
||||
# Implies that we had a MultiIndex so key is iterable
|
||||
group_ids = dict(zip(grouper, cast(Iterable, key)))
|
||||
else:
|
||||
group_ids = {grouper: key}
|
||||
stack.append(parts[key].assign(**group_ids))
|
||||
|
||||
res = pd.concat(stack, ignore_index=True)
|
||||
return self._reorder_columns(res, data)
|
||||
274
ftline/lib/python3.12/site-packages/seaborn/_core/moves.py
Normal file
274
ftline/lib/python3.12/site-packages/seaborn/_core/moves.py
Normal file
@@ -0,0 +1,274 @@
|
||||
from __future__ import annotations
|
||||
from dataclasses import dataclass
|
||||
from typing import ClassVar, Callable, Optional, Union, cast
|
||||
|
||||
import numpy as np
|
||||
from pandas import DataFrame
|
||||
|
||||
from seaborn._core.groupby import GroupBy
|
||||
from seaborn._core.scales import Scale
|
||||
from seaborn._core.typing import Default
|
||||
|
||||
default = Default()
|
||||
|
||||
|
||||
@dataclass
|
||||
class Move:
|
||||
"""Base class for objects that apply simple positional transforms."""
|
||||
|
||||
group_by_orient: ClassVar[bool] = True
|
||||
|
||||
def __call__(
|
||||
self, data: DataFrame, groupby: GroupBy, orient: str, scales: dict[str, Scale],
|
||||
) -> DataFrame:
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
@dataclass
|
||||
class Jitter(Move):
|
||||
"""
|
||||
Random displacement along one or both axes to reduce overplotting.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
width : float
|
||||
Magnitude of jitter, relative to mark width, along the orientation axis.
|
||||
If not provided, the default value will be 0 when `x` or `y` are set, otherwise
|
||||
there will be a small amount of jitter applied by default.
|
||||
x : float
|
||||
Magnitude of jitter, in data units, along the x axis.
|
||||
y : float
|
||||
Magnitude of jitter, in data units, along the y axis.
|
||||
|
||||
Examples
|
||||
--------
|
||||
.. include:: ../docstrings/objects.Jitter.rst
|
||||
|
||||
"""
|
||||
width: float | Default = default
|
||||
x: float = 0
|
||||
y: float = 0
|
||||
seed: int | None = None
|
||||
|
||||
def __call__(
|
||||
self, data: DataFrame, groupby: GroupBy, orient: str, scales: dict[str, Scale],
|
||||
) -> DataFrame:
|
||||
|
||||
data = data.copy()
|
||||
rng = np.random.default_rng(self.seed)
|
||||
|
||||
def jitter(data, col, scale):
|
||||
noise = rng.uniform(-.5, +.5, len(data))
|
||||
offsets = noise * scale
|
||||
return data[col] + offsets
|
||||
|
||||
if self.width is default:
|
||||
width = 0.0 if self.x or self.y else 0.2
|
||||
else:
|
||||
width = cast(float, self.width)
|
||||
|
||||
if self.width:
|
||||
data[orient] = jitter(data, orient, width * data["width"])
|
||||
if self.x:
|
||||
data["x"] = jitter(data, "x", self.x)
|
||||
if self.y:
|
||||
data["y"] = jitter(data, "y", self.y)
|
||||
|
||||
return data
|
||||
|
||||
|
||||
@dataclass
|
||||
class Dodge(Move):
|
||||
"""
|
||||
Displacement and narrowing of overlapping marks along orientation axis.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
empty : {'keep', 'drop', 'fill'}
|
||||
gap : float
|
||||
Size of gap between dodged marks.
|
||||
by : list of variable names
|
||||
Variables to apply the movement to, otherwise use all.
|
||||
|
||||
Examples
|
||||
--------
|
||||
.. include:: ../docstrings/objects.Dodge.rst
|
||||
|
||||
"""
|
||||
empty: str = "keep" # Options: keep, drop, fill
|
||||
gap: float = 0
|
||||
|
||||
# TODO accept just a str here?
|
||||
# TODO should this always be present?
|
||||
# TODO should the default be an "all" singleton?
|
||||
by: Optional[list[str]] = None
|
||||
|
||||
def __call__(
|
||||
self, data: DataFrame, groupby: GroupBy, orient: str, scales: dict[str, Scale],
|
||||
) -> DataFrame:
|
||||
|
||||
grouping_vars = [v for v in groupby.order if v in data]
|
||||
groups = groupby.agg(data, {"width": "max"})
|
||||
if self.empty == "fill":
|
||||
groups = groups.dropna()
|
||||
|
||||
def groupby_pos(s):
|
||||
grouper = [groups[v] for v in [orient, "col", "row"] if v in data]
|
||||
return s.groupby(grouper, sort=False, observed=True)
|
||||
|
||||
def scale_widths(w):
|
||||
# TODO what value to fill missing widths??? Hard problem...
|
||||
# TODO short circuit this if outer widths has no variance?
|
||||
empty = 0 if self.empty == "fill" else w.mean()
|
||||
filled = w.fillna(empty)
|
||||
scale = filled.max()
|
||||
norm = filled.sum()
|
||||
if self.empty == "keep":
|
||||
w = filled
|
||||
return w / norm * scale
|
||||
|
||||
def widths_to_offsets(w):
|
||||
return w.shift(1).fillna(0).cumsum() + (w - w.sum()) / 2
|
||||
|
||||
new_widths = groupby_pos(groups["width"]).transform(scale_widths)
|
||||
offsets = groupby_pos(new_widths).transform(widths_to_offsets)
|
||||
|
||||
if self.gap:
|
||||
new_widths *= 1 - self.gap
|
||||
|
||||
groups["_dodged"] = groups[orient] + offsets
|
||||
groups["width"] = new_widths
|
||||
|
||||
out = (
|
||||
data
|
||||
.drop("width", axis=1)
|
||||
.merge(groups, on=grouping_vars, how="left")
|
||||
.drop(orient, axis=1)
|
||||
.rename(columns={"_dodged": orient})
|
||||
)
|
||||
|
||||
return out
|
||||
|
||||
|
||||
@dataclass
|
||||
class Stack(Move):
|
||||
"""
|
||||
Displacement of overlapping bar or area marks along the value axis.
|
||||
|
||||
Examples
|
||||
--------
|
||||
.. include:: ../docstrings/objects.Stack.rst
|
||||
|
||||
"""
|
||||
# TODO center? (or should this be a different move, eg. Stream())
|
||||
|
||||
def _stack(self, df, orient):
|
||||
|
||||
# TODO should stack do something with ymin/ymax style marks?
|
||||
# Should there be an upstream conversion to baseline/height parameterization?
|
||||
|
||||
if df["baseline"].nunique() > 1:
|
||||
err = "Stack move cannot be used when baselines are already heterogeneous"
|
||||
raise RuntimeError(err)
|
||||
|
||||
other = {"x": "y", "y": "x"}[orient]
|
||||
stacked_lengths = (df[other] - df["baseline"]).dropna().cumsum()
|
||||
offsets = stacked_lengths.shift(1).fillna(0)
|
||||
|
||||
df[other] = stacked_lengths
|
||||
df["baseline"] = df["baseline"] + offsets
|
||||
|
||||
return df
|
||||
|
||||
def __call__(
|
||||
self, data: DataFrame, groupby: GroupBy, orient: str, scales: dict[str, Scale],
|
||||
) -> DataFrame:
|
||||
|
||||
# TODO where to ensure that other semantic variables are sorted properly?
|
||||
# TODO why are we not using the passed in groupby here?
|
||||
groupers = ["col", "row", orient]
|
||||
return GroupBy(groupers).apply(data, self._stack, orient)
|
||||
|
||||
|
||||
@dataclass
|
||||
class Shift(Move):
|
||||
"""
|
||||
Displacement of all marks with the same magnitude / direction.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
x, y : float
|
||||
Magnitude of shift, in data units, along each axis.
|
||||
|
||||
Examples
|
||||
--------
|
||||
.. include:: ../docstrings/objects.Shift.rst
|
||||
|
||||
"""
|
||||
x: float = 0
|
||||
y: float = 0
|
||||
|
||||
def __call__(
|
||||
self, data: DataFrame, groupby: GroupBy, orient: str, scales: dict[str, Scale],
|
||||
) -> DataFrame:
|
||||
|
||||
data = data.copy(deep=False)
|
||||
data["x"] = data["x"] + self.x
|
||||
data["y"] = data["y"] + self.y
|
||||
return data
|
||||
|
||||
|
||||
@dataclass
|
||||
class Norm(Move):
|
||||
"""
|
||||
Divisive scaling on the value axis after aggregating within groups.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
func : str or callable
|
||||
Function called on each group to define the comparison value.
|
||||
where : str
|
||||
Query string defining the subset used to define the comparison values.
|
||||
by : list of variables
|
||||
Variables used to define aggregation groups.
|
||||
percent : bool
|
||||
If True, multiply the result by 100.
|
||||
|
||||
Examples
|
||||
--------
|
||||
.. include:: ../docstrings/objects.Norm.rst
|
||||
|
||||
"""
|
||||
|
||||
func: Union[Callable, str] = "max"
|
||||
where: Optional[str] = None
|
||||
by: Optional[list[str]] = None
|
||||
percent: bool = False
|
||||
|
||||
group_by_orient: ClassVar[bool] = False
|
||||
|
||||
def _norm(self, df, var):
|
||||
|
||||
if self.where is None:
|
||||
denom_data = df[var]
|
||||
else:
|
||||
denom_data = df.query(self.where)[var]
|
||||
df[var] = df[var] / denom_data.agg(self.func)
|
||||
|
||||
if self.percent:
|
||||
df[var] = df[var] * 100
|
||||
|
||||
return df
|
||||
|
||||
def __call__(
|
||||
self, data: DataFrame, groupby: GroupBy, orient: str, scales: dict[str, Scale],
|
||||
) -> DataFrame:
|
||||
|
||||
other = {"x": "y", "y": "x"}[orient]
|
||||
return groupby.apply(data, self._norm, other)
|
||||
|
||||
|
||||
# TODO
|
||||
# @dataclass
|
||||
# class Ridge(Move):
|
||||
# ...
|
||||
1830
ftline/lib/python3.12/site-packages/seaborn/_core/plot.py
Normal file
1830
ftline/lib/python3.12/site-packages/seaborn/_core/plot.py
Normal file
File diff suppressed because it is too large
Load Diff
834
ftline/lib/python3.12/site-packages/seaborn/_core/properties.py
Normal file
834
ftline/lib/python3.12/site-packages/seaborn/_core/properties.py
Normal file
@@ -0,0 +1,834 @@
|
||||
from __future__ import annotations
|
||||
import itertools
|
||||
import warnings
|
||||
|
||||
import numpy as np
|
||||
from numpy.typing import ArrayLike
|
||||
from pandas import Series
|
||||
import matplotlib as mpl
|
||||
from matplotlib.colors import to_rgb, to_rgba, to_rgba_array
|
||||
from matplotlib.markers import MarkerStyle
|
||||
from matplotlib.path import Path
|
||||
|
||||
from seaborn._core.scales import Scale, Boolean, Continuous, Nominal, Temporal
|
||||
from seaborn._core.rules import categorical_order, variable_type
|
||||
from seaborn.palettes import QUAL_PALETTES, color_palette, blend_palette
|
||||
from seaborn.utils import get_color_cycle
|
||||
|
||||
from typing import Any, Callable, Tuple, List, Union, Optional
|
||||
|
||||
RGBTuple = Tuple[float, float, float]
|
||||
RGBATuple = Tuple[float, float, float, float]
|
||||
ColorSpec = Union[RGBTuple, RGBATuple, str]
|
||||
|
||||
DashPattern = Tuple[float, ...]
|
||||
DashPatternWithOffset = Tuple[float, Optional[DashPattern]]
|
||||
|
||||
MarkerPattern = Union[
|
||||
float,
|
||||
str,
|
||||
Tuple[int, int, float],
|
||||
List[Tuple[float, float]],
|
||||
Path,
|
||||
MarkerStyle,
|
||||
]
|
||||
|
||||
Mapping = Callable[[ArrayLike], ArrayLike]
|
||||
|
||||
|
||||
# =================================================================================== #
|
||||
# Base classes
|
||||
# =================================================================================== #
|
||||
|
||||
|
||||
class Property:
|
||||
"""Base class for visual properties that can be set directly or be data scaling."""
|
||||
|
||||
# When True, scales for this property will populate the legend by default
|
||||
legend = False
|
||||
|
||||
# When True, scales for this property normalize data to [0, 1] before mapping
|
||||
normed = False
|
||||
|
||||
def __init__(self, variable: str | None = None):
|
||||
"""Initialize the property with the name of the corresponding plot variable."""
|
||||
if not variable:
|
||||
variable = self.__class__.__name__.lower()
|
||||
self.variable = variable
|
||||
|
||||
def default_scale(self, data: Series) -> Scale:
|
||||
"""Given data, initialize appropriate scale class."""
|
||||
|
||||
var_type = variable_type(data, boolean_type="boolean", strict_boolean=True)
|
||||
if var_type == "numeric":
|
||||
return Continuous()
|
||||
elif var_type == "datetime":
|
||||
return Temporal()
|
||||
elif var_type == "boolean":
|
||||
return Boolean()
|
||||
else:
|
||||
return Nominal()
|
||||
|
||||
def infer_scale(self, arg: Any, data: Series) -> Scale:
|
||||
"""Given data and a scaling argument, initialize appropriate scale class."""
|
||||
# TODO put these somewhere external for validation
|
||||
# TODO putting this here won't pick it up if subclasses define infer_scale
|
||||
# (e.g. color). How best to handle that? One option is to call super after
|
||||
# handling property-specific possibilities (e.g. for color check that the
|
||||
# arg is not a valid palette name) but that could get tricky.
|
||||
trans_args = ["log", "symlog", "logit", "pow", "sqrt"]
|
||||
if isinstance(arg, str):
|
||||
if any(arg.startswith(k) for k in trans_args):
|
||||
# TODO validate numeric type? That should happen centrally somewhere
|
||||
return Continuous(trans=arg)
|
||||
else:
|
||||
msg = f"Unknown magic arg for {self.variable} scale: '{arg}'."
|
||||
raise ValueError(msg)
|
||||
else:
|
||||
arg_type = type(arg).__name__
|
||||
msg = f"Magic arg for {self.variable} scale must be str, not {arg_type}."
|
||||
raise TypeError(msg)
|
||||
|
||||
def get_mapping(self, scale: Scale, data: Series) -> Mapping:
|
||||
"""Return a function that maps from data domain to property range."""
|
||||
def identity(x):
|
||||
return x
|
||||
return identity
|
||||
|
||||
def standardize(self, val: Any) -> Any:
|
||||
"""Coerce flexible property value to standardized representation."""
|
||||
return val
|
||||
|
||||
def _check_dict_entries(self, levels: list, values: dict) -> None:
|
||||
"""Input check when values are provided as a dictionary."""
|
||||
missing = set(levels) - set(values)
|
||||
if missing:
|
||||
formatted = ", ".join(map(repr, sorted(missing, key=str)))
|
||||
err = f"No entry in {self.variable} dictionary for {formatted}"
|
||||
raise ValueError(err)
|
||||
|
||||
def _check_list_length(self, levels: list, values: list) -> list:
|
||||
"""Input check when values are provided as a list."""
|
||||
message = ""
|
||||
if len(levels) > len(values):
|
||||
message = " ".join([
|
||||
f"\nThe {self.variable} list has fewer values ({len(values)})",
|
||||
f"than needed ({len(levels)}) and will cycle, which may",
|
||||
"produce an uninterpretable plot."
|
||||
])
|
||||
values = [x for _, x in zip(levels, itertools.cycle(values))]
|
||||
|
||||
elif len(values) > len(levels):
|
||||
message = " ".join([
|
||||
f"The {self.variable} list has more values ({len(values)})",
|
||||
f"than needed ({len(levels)}), which may not be intended.",
|
||||
])
|
||||
values = values[:len(levels)]
|
||||
|
||||
# TODO look into custom PlotSpecWarning with better formatting
|
||||
if message:
|
||||
warnings.warn(message, UserWarning)
|
||||
|
||||
return values
|
||||
|
||||
|
||||
# =================================================================================== #
|
||||
# Properties relating to spatial position of marks on the plotting axes
|
||||
# =================================================================================== #
|
||||
|
||||
|
||||
class Coordinate(Property):
|
||||
"""The position of visual marks with respect to the axes of the plot."""
|
||||
legend = False
|
||||
normed = False
|
||||
|
||||
|
||||
# =================================================================================== #
|
||||
# Properties with numeric values where scale range can be defined as an interval
|
||||
# =================================================================================== #
|
||||
|
||||
|
||||
class IntervalProperty(Property):
|
||||
"""A numeric property where scale range can be defined as an interval."""
|
||||
legend = True
|
||||
normed = True
|
||||
|
||||
_default_range: tuple[float, float] = (0, 1)
|
||||
|
||||
@property
|
||||
def default_range(self) -> tuple[float, float]:
|
||||
"""Min and max values used by default for semantic mapping."""
|
||||
return self._default_range
|
||||
|
||||
def _forward(self, values: ArrayLike) -> ArrayLike:
|
||||
"""Transform applied to native values before linear mapping into interval."""
|
||||
return values
|
||||
|
||||
def _inverse(self, values: ArrayLike) -> ArrayLike:
|
||||
"""Transform applied to results of mapping that returns to native values."""
|
||||
return values
|
||||
|
||||
def infer_scale(self, arg: Any, data: Series) -> Scale:
|
||||
"""Given data and a scaling argument, initialize appropriate scale class."""
|
||||
|
||||
# TODO infer continuous based on log/sqrt etc?
|
||||
|
||||
var_type = variable_type(data, boolean_type="boolean", strict_boolean=True)
|
||||
|
||||
if var_type == "boolean":
|
||||
return Boolean(arg)
|
||||
elif isinstance(arg, (list, dict)):
|
||||
return Nominal(arg)
|
||||
elif var_type == "categorical":
|
||||
return Nominal(arg)
|
||||
elif var_type == "datetime":
|
||||
return Temporal(arg)
|
||||
# TODO other variable types
|
||||
else:
|
||||
return Continuous(arg)
|
||||
|
||||
def get_mapping(self, scale: Scale, data: Series) -> Mapping:
|
||||
"""Return a function that maps from data domain to property range."""
|
||||
if isinstance(scale, Nominal):
|
||||
return self._get_nominal_mapping(scale, data)
|
||||
elif isinstance(scale, Boolean):
|
||||
return self._get_boolean_mapping(scale, data)
|
||||
|
||||
if scale.values is None:
|
||||
vmin, vmax = self._forward(self.default_range)
|
||||
elif isinstance(scale.values, tuple) and len(scale.values) == 2:
|
||||
vmin, vmax = self._forward(scale.values)
|
||||
else:
|
||||
if isinstance(scale.values, tuple):
|
||||
actual = f"{len(scale.values)}-tuple"
|
||||
else:
|
||||
actual = str(type(scale.values))
|
||||
scale_class = scale.__class__.__name__
|
||||
err = " ".join([
|
||||
f"Values for {self.variable} variables with {scale_class} scale",
|
||||
f"must be 2-tuple; not {actual}.",
|
||||
])
|
||||
raise TypeError(err)
|
||||
|
||||
def mapping(x):
|
||||
return self._inverse(np.multiply(x, vmax - vmin) + vmin)
|
||||
|
||||
return mapping
|
||||
|
||||
def _get_nominal_mapping(self, scale: Nominal, data: Series) -> Mapping:
|
||||
"""Identify evenly-spaced values using interval or explicit mapping."""
|
||||
levels = categorical_order(data, scale.order)
|
||||
values = self._get_values(scale, levels)
|
||||
|
||||
def mapping(x):
|
||||
ixs = np.asarray(x, np.intp)
|
||||
out = np.full(len(x), np.nan)
|
||||
use = np.isfinite(x)
|
||||
out[use] = np.take(values, ixs[use])
|
||||
return out
|
||||
|
||||
return mapping
|
||||
|
||||
def _get_boolean_mapping(self, scale: Boolean, data: Series) -> Mapping:
|
||||
"""Identify evenly-spaced values using interval or explicit mapping."""
|
||||
values = self._get_values(scale, [True, False])
|
||||
|
||||
def mapping(x):
|
||||
out = np.full(len(x), np.nan)
|
||||
use = np.isfinite(x)
|
||||
out[use] = np.where(x[use], *values)
|
||||
return out
|
||||
|
||||
return mapping
|
||||
|
||||
def _get_values(self, scale: Scale, levels: list) -> list:
|
||||
"""Validate scale.values and identify a value for each level."""
|
||||
if isinstance(scale.values, dict):
|
||||
self._check_dict_entries(levels, scale.values)
|
||||
values = [scale.values[x] for x in levels]
|
||||
elif isinstance(scale.values, list):
|
||||
values = self._check_list_length(levels, scale.values)
|
||||
else:
|
||||
if scale.values is None:
|
||||
vmin, vmax = self.default_range
|
||||
elif isinstance(scale.values, tuple):
|
||||
vmin, vmax = scale.values
|
||||
else:
|
||||
scale_class = scale.__class__.__name__
|
||||
err = " ".join([
|
||||
f"Values for {self.variable} variables with {scale_class} scale",
|
||||
f"must be a dict, list or tuple; not {type(scale.values)}",
|
||||
])
|
||||
raise TypeError(err)
|
||||
|
||||
vmin, vmax = self._forward([vmin, vmax])
|
||||
values = list(self._inverse(np.linspace(vmax, vmin, len(levels))))
|
||||
|
||||
return values
|
||||
|
||||
|
||||
class PointSize(IntervalProperty):
|
||||
"""Size (diameter) of a point mark, in points, with scaling by area."""
|
||||
_default_range = 2, 8 # TODO use rcparams?
|
||||
|
||||
def _forward(self, values):
|
||||
"""Square native values to implement linear scaling of point area."""
|
||||
return np.square(values)
|
||||
|
||||
def _inverse(self, values):
|
||||
"""Invert areal values back to point diameter."""
|
||||
return np.sqrt(values)
|
||||
|
||||
|
||||
class LineWidth(IntervalProperty):
|
||||
"""Thickness of a line mark, in points."""
|
||||
@property
|
||||
def default_range(self) -> tuple[float, float]:
|
||||
"""Min and max values used by default for semantic mapping."""
|
||||
base = mpl.rcParams["lines.linewidth"]
|
||||
return base * .5, base * 2
|
||||
|
||||
|
||||
class EdgeWidth(IntervalProperty):
|
||||
"""Thickness of the edges on a patch mark, in points."""
|
||||
@property
|
||||
def default_range(self) -> tuple[float, float]:
|
||||
"""Min and max values used by default for semantic mapping."""
|
||||
base = mpl.rcParams["patch.linewidth"]
|
||||
return base * .5, base * 2
|
||||
|
||||
|
||||
class Stroke(IntervalProperty):
|
||||
"""Thickness of lines that define point glyphs."""
|
||||
_default_range = .25, 2.5
|
||||
|
||||
|
||||
class Alpha(IntervalProperty):
|
||||
"""Opacity of the color values for an arbitrary mark."""
|
||||
_default_range = .3, .95
|
||||
# TODO validate / enforce that output is in [0, 1]
|
||||
|
||||
|
||||
class Offset(IntervalProperty):
|
||||
"""Offset for edge-aligned text, in point units."""
|
||||
_default_range = 0, 5
|
||||
_legend = False
|
||||
|
||||
|
||||
class FontSize(IntervalProperty):
|
||||
"""Font size for textual marks, in points."""
|
||||
_legend = False
|
||||
|
||||
@property
|
||||
def default_range(self) -> tuple[float, float]:
|
||||
"""Min and max values used by default for semantic mapping."""
|
||||
base = mpl.rcParams["font.size"]
|
||||
return base * .5, base * 2
|
||||
|
||||
|
||||
# =================================================================================== #
|
||||
# Properties defined by arbitrary objects with inherently nominal scaling
|
||||
# =================================================================================== #
|
||||
|
||||
|
||||
class ObjectProperty(Property):
|
||||
"""A property defined by arbitrary an object, with inherently nominal scaling."""
|
||||
legend = True
|
||||
normed = False
|
||||
|
||||
# Object representing null data, should appear invisible when drawn by matplotlib
|
||||
# Note that we now drop nulls in Plot._plot_layer and thus may not need this
|
||||
null_value: Any = None
|
||||
|
||||
def _default_values(self, n: int) -> list:
|
||||
raise NotImplementedError()
|
||||
|
||||
def default_scale(self, data: Series) -> Scale:
|
||||
var_type = variable_type(data, boolean_type="boolean", strict_boolean=True)
|
||||
return Boolean() if var_type == "boolean" else Nominal()
|
||||
|
||||
def infer_scale(self, arg: Any, data: Series) -> Scale:
|
||||
var_type = variable_type(data, boolean_type="boolean", strict_boolean=True)
|
||||
return Boolean(arg) if var_type == "boolean" else Nominal(arg)
|
||||
|
||||
def get_mapping(self, scale: Scale, data: Series) -> Mapping:
|
||||
"""Define mapping as lookup into list of object values."""
|
||||
boolean_scale = isinstance(scale, Boolean)
|
||||
order = getattr(scale, "order", [True, False] if boolean_scale else None)
|
||||
levels = categorical_order(data, order)
|
||||
values = self._get_values(scale, levels)
|
||||
|
||||
if boolean_scale:
|
||||
values = values[::-1]
|
||||
|
||||
def mapping(x):
|
||||
ixs = np.asarray(np.nan_to_num(x), np.intp)
|
||||
return [
|
||||
values[ix] if np.isfinite(x_i) else self.null_value
|
||||
for x_i, ix in zip(x, ixs)
|
||||
]
|
||||
|
||||
return mapping
|
||||
|
||||
def _get_values(self, scale: Scale, levels: list) -> list:
|
||||
"""Validate scale.values and identify a value for each level."""
|
||||
n = len(levels)
|
||||
if isinstance(scale.values, dict):
|
||||
self._check_dict_entries(levels, scale.values)
|
||||
values = [scale.values[x] for x in levels]
|
||||
elif isinstance(scale.values, list):
|
||||
values = self._check_list_length(levels, scale.values)
|
||||
elif scale.values is None:
|
||||
values = self._default_values(n)
|
||||
else:
|
||||
msg = " ".join([
|
||||
f"Scale values for a {self.variable} variable must be provided",
|
||||
f"in a dict or list; not {type(scale.values)}."
|
||||
])
|
||||
raise TypeError(msg)
|
||||
|
||||
values = [self.standardize(x) for x in values]
|
||||
return values
|
||||
|
||||
|
||||
class Marker(ObjectProperty):
|
||||
"""Shape of points in scatter-type marks or lines with data points marked."""
|
||||
null_value = MarkerStyle("")
|
||||
|
||||
# TODO should we have named marker "palettes"? (e.g. see d3 options)
|
||||
|
||||
# TODO need some sort of "require_scale" functionality
|
||||
# to raise when we get the wrong kind explicitly specified
|
||||
|
||||
def standardize(self, val: MarkerPattern) -> MarkerStyle:
|
||||
return MarkerStyle(val)
|
||||
|
||||
def _default_values(self, n: int) -> list[MarkerStyle]:
|
||||
"""Build an arbitrarily long list of unique marker styles.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
n : int
|
||||
Number of unique marker specs to generate.
|
||||
|
||||
Returns
|
||||
-------
|
||||
markers : list of string or tuples
|
||||
Values for defining :class:`matplotlib.markers.MarkerStyle` objects.
|
||||
All markers will be filled.
|
||||
|
||||
"""
|
||||
# Start with marker specs that are well distinguishable
|
||||
markers = [
|
||||
"o", "X", (4, 0, 45), "P", (4, 0, 0), (4, 1, 0), "^", (4, 1, 45), "v",
|
||||
]
|
||||
|
||||
# Now generate more from regular polygons of increasing order
|
||||
s = 5
|
||||
while len(markers) < n:
|
||||
a = 360 / (s + 1) / 2
|
||||
markers.extend([(s + 1, 1, a), (s + 1, 0, a), (s, 1, 0), (s, 0, 0)])
|
||||
s += 1
|
||||
|
||||
markers = [MarkerStyle(m) for m in markers[:n]]
|
||||
|
||||
return markers
|
||||
|
||||
|
||||
class LineStyle(ObjectProperty):
|
||||
"""Dash pattern for line-type marks."""
|
||||
null_value = ""
|
||||
|
||||
def standardize(self, val: str | DashPattern) -> DashPatternWithOffset:
|
||||
return self._get_dash_pattern(val)
|
||||
|
||||
def _default_values(self, n: int) -> list[DashPatternWithOffset]:
|
||||
"""Build an arbitrarily long list of unique dash styles for lines.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
n : int
|
||||
Number of unique dash specs to generate.
|
||||
|
||||
Returns
|
||||
-------
|
||||
dashes : list of strings or tuples
|
||||
Valid arguments for the ``dashes`` parameter on
|
||||
:class:`matplotlib.lines.Line2D`. The first spec is a solid
|
||||
line (``""``), the remainder are sequences of long and short
|
||||
dashes.
|
||||
|
||||
"""
|
||||
# Start with dash specs that are well distinguishable
|
||||
dashes: list[str | DashPattern] = [
|
||||
"-", (4, 1.5), (1, 1), (3, 1.25, 1.5, 1.25), (5, 1, 1, 1),
|
||||
]
|
||||
|
||||
# Now programmatically build as many as we need
|
||||
p = 3
|
||||
while len(dashes) < n:
|
||||
|
||||
# Take combinations of long and short dashes
|
||||
a = itertools.combinations_with_replacement([3, 1.25], p)
|
||||
b = itertools.combinations_with_replacement([4, 1], p)
|
||||
|
||||
# Interleave the combinations, reversing one of the streams
|
||||
segment_list = itertools.chain(*zip(list(a)[1:-1][::-1], list(b)[1:-1]))
|
||||
|
||||
# Now insert the gaps
|
||||
for segments in segment_list:
|
||||
gap = min(segments)
|
||||
spec = tuple(itertools.chain(*((seg, gap) for seg in segments)))
|
||||
dashes.append(spec)
|
||||
|
||||
p += 1
|
||||
|
||||
return [self._get_dash_pattern(x) for x in dashes]
|
||||
|
||||
@staticmethod
|
||||
def _get_dash_pattern(style: str | DashPattern) -> DashPatternWithOffset:
|
||||
"""Convert linestyle arguments to dash pattern with offset."""
|
||||
# Copied and modified from Matplotlib 3.4
|
||||
# go from short hand -> full strings
|
||||
ls_mapper = {"-": "solid", "--": "dashed", "-.": "dashdot", ":": "dotted"}
|
||||
if isinstance(style, str):
|
||||
style = ls_mapper.get(style, style)
|
||||
# un-dashed styles
|
||||
if style in ["solid", "none", "None"]:
|
||||
offset = 0
|
||||
dashes = None
|
||||
# dashed styles
|
||||
elif style in ["dashed", "dashdot", "dotted"]:
|
||||
offset = 0
|
||||
dashes = tuple(mpl.rcParams[f"lines.{style}_pattern"])
|
||||
else:
|
||||
options = [*ls_mapper.values(), *ls_mapper.keys()]
|
||||
msg = f"Linestyle string must be one of {options}, not {repr(style)}."
|
||||
raise ValueError(msg)
|
||||
|
||||
elif isinstance(style, tuple):
|
||||
if len(style) > 1 and isinstance(style[1], tuple):
|
||||
offset, dashes = style
|
||||
elif len(style) > 1 and style[1] is None:
|
||||
offset, dashes = style
|
||||
else:
|
||||
offset = 0
|
||||
dashes = style
|
||||
else:
|
||||
val_type = type(style).__name__
|
||||
msg = f"Linestyle must be str or tuple, not {val_type}."
|
||||
raise TypeError(msg)
|
||||
|
||||
# Normalize offset to be positive and shorter than the dash cycle
|
||||
if dashes is not None:
|
||||
try:
|
||||
dsum = sum(dashes)
|
||||
except TypeError as err:
|
||||
msg = f"Invalid dash pattern: {dashes}"
|
||||
raise TypeError(msg) from err
|
||||
if dsum:
|
||||
offset %= dsum
|
||||
|
||||
return offset, dashes
|
||||
|
||||
|
||||
class TextAlignment(ObjectProperty):
|
||||
legend = False
|
||||
|
||||
|
||||
class HorizontalAlignment(TextAlignment):
|
||||
|
||||
def _default_values(self, n: int) -> list:
|
||||
vals = itertools.cycle(["left", "right"])
|
||||
return [next(vals) for _ in range(n)]
|
||||
|
||||
|
||||
class VerticalAlignment(TextAlignment):
|
||||
|
||||
def _default_values(self, n: int) -> list:
|
||||
vals = itertools.cycle(["top", "bottom"])
|
||||
return [next(vals) for _ in range(n)]
|
||||
|
||||
|
||||
# =================================================================================== #
|
||||
# Properties with RGB(A) color values
|
||||
# =================================================================================== #
|
||||
|
||||
|
||||
class Color(Property):
|
||||
"""Color, as RGB(A), scalable with nominal palettes or continuous gradients."""
|
||||
legend = True
|
||||
normed = True
|
||||
|
||||
def standardize(self, val: ColorSpec) -> RGBTuple | RGBATuple:
|
||||
# Return color with alpha channel only if the input spec has it
|
||||
# This is so that RGBA colors can override the Alpha property
|
||||
if to_rgba(val) != to_rgba(val, 1):
|
||||
return to_rgba(val)
|
||||
else:
|
||||
return to_rgb(val)
|
||||
|
||||
def _standardize_color_sequence(self, colors: ArrayLike) -> ArrayLike:
|
||||
"""Convert color sequence to RGB(A) array, preserving but not adding alpha."""
|
||||
def has_alpha(x):
|
||||
return to_rgba(x) != to_rgba(x, 1)
|
||||
|
||||
if isinstance(colors, np.ndarray):
|
||||
needs_alpha = colors.shape[1] == 4
|
||||
else:
|
||||
needs_alpha = any(has_alpha(x) for x in colors)
|
||||
|
||||
if needs_alpha:
|
||||
return to_rgba_array(colors)
|
||||
else:
|
||||
return to_rgba_array(colors)[:, :3]
|
||||
|
||||
def infer_scale(self, arg: Any, data: Series) -> Scale:
|
||||
# TODO when inferring Continuous without data, verify type
|
||||
|
||||
# TODO need to rethink the variable type system
|
||||
# (e.g. boolean, ordered categories as Ordinal, etc)..
|
||||
var_type = variable_type(data, boolean_type="boolean", strict_boolean=True)
|
||||
|
||||
if var_type == "boolean":
|
||||
return Boolean(arg)
|
||||
|
||||
if isinstance(arg, (dict, list)):
|
||||
return Nominal(arg)
|
||||
|
||||
if isinstance(arg, tuple):
|
||||
if var_type == "categorical":
|
||||
# TODO It seems reasonable to allow a gradient mapping for nominal
|
||||
# scale but it also feels "technically" wrong. Should this infer
|
||||
# Ordinal with categorical data and, if so, verify orderedness?
|
||||
return Nominal(arg)
|
||||
return Continuous(arg)
|
||||
|
||||
if callable(arg):
|
||||
return Continuous(arg)
|
||||
|
||||
# TODO Do we accept str like "log", "pow", etc. for semantics?
|
||||
|
||||
if not isinstance(arg, str):
|
||||
msg = " ".join([
|
||||
f"A single scale argument for {self.variable} variables must be",
|
||||
f"a string, dict, tuple, list, or callable, not {type(arg)}."
|
||||
])
|
||||
raise TypeError(msg)
|
||||
|
||||
if arg in QUAL_PALETTES:
|
||||
return Nominal(arg)
|
||||
elif var_type == "numeric":
|
||||
return Continuous(arg)
|
||||
# TODO implement scales for date variables and any others.
|
||||
else:
|
||||
return Nominal(arg)
|
||||
|
||||
def get_mapping(self, scale: Scale, data: Series) -> Mapping:
|
||||
"""Return a function that maps from data domain to color values."""
|
||||
# TODO what is best way to do this conditional?
|
||||
# Should it be class-based or should classes have behavioral attributes?
|
||||
if isinstance(scale, Nominal):
|
||||
return self._get_nominal_mapping(scale, data)
|
||||
elif isinstance(scale, Boolean):
|
||||
return self._get_boolean_mapping(scale, data)
|
||||
|
||||
if scale.values is None:
|
||||
# TODO Rethink best default continuous color gradient
|
||||
mapping = color_palette("ch:", as_cmap=True)
|
||||
elif isinstance(scale.values, tuple):
|
||||
# TODO blend_palette will strip alpha, but we should support
|
||||
# interpolation on all four channels
|
||||
mapping = blend_palette(scale.values, as_cmap=True)
|
||||
elif isinstance(scale.values, str):
|
||||
# TODO for matplotlib colormaps this will clip extremes, which is
|
||||
# different from what using the named colormap directly would do
|
||||
# This may or may not be desireable.
|
||||
mapping = color_palette(scale.values, as_cmap=True)
|
||||
elif callable(scale.values):
|
||||
mapping = scale.values
|
||||
else:
|
||||
scale_class = scale.__class__.__name__
|
||||
msg = " ".join([
|
||||
f"Scale values for {self.variable} with a {scale_class} mapping",
|
||||
f"must be string, tuple, or callable; not {type(scale.values)}."
|
||||
])
|
||||
raise TypeError(msg)
|
||||
|
||||
def _mapping(x):
|
||||
# Remove alpha channel so it does not override alpha property downstream
|
||||
# TODO this will need to be more flexible to support RGBA tuples (see above)
|
||||
invalid = ~np.isfinite(x)
|
||||
out = mapping(x)[:, :3]
|
||||
out[invalid] = np.nan
|
||||
return out
|
||||
|
||||
return _mapping
|
||||
|
||||
def _get_nominal_mapping(self, scale: Nominal, data: Series) -> Mapping:
|
||||
|
||||
levels = categorical_order(data, scale.order)
|
||||
colors = self._get_values(scale, levels)
|
||||
|
||||
def mapping(x):
|
||||
ixs = np.asarray(np.nan_to_num(x), np.intp)
|
||||
use = np.isfinite(x)
|
||||
out = np.full((len(ixs), colors.shape[1]), np.nan)
|
||||
out[use] = np.take(colors, ixs[use], axis=0)
|
||||
return out
|
||||
|
||||
return mapping
|
||||
|
||||
def _get_boolean_mapping(self, scale: Boolean, data: Series) -> Mapping:
|
||||
|
||||
colors = self._get_values(scale, [True, False])
|
||||
|
||||
def mapping(x):
|
||||
|
||||
use = np.isfinite(x)
|
||||
x = np.asarray(np.nan_to_num(x)).astype(bool)
|
||||
out = np.full((len(x), colors.shape[1]), np.nan)
|
||||
out[x & use] = colors[0]
|
||||
out[~x & use] = colors[1]
|
||||
return out
|
||||
|
||||
return mapping
|
||||
|
||||
def _get_values(self, scale: Scale, levels: list) -> ArrayLike:
|
||||
"""Validate scale.values and identify a value for each level."""
|
||||
n = len(levels)
|
||||
values = scale.values
|
||||
if isinstance(values, dict):
|
||||
self._check_dict_entries(levels, values)
|
||||
colors = [values[x] for x in levels]
|
||||
elif isinstance(values, list):
|
||||
colors = self._check_list_length(levels, values)
|
||||
elif isinstance(values, tuple):
|
||||
colors = blend_palette(values, n)
|
||||
elif isinstance(values, str):
|
||||
colors = color_palette(values, n)
|
||||
elif values is None:
|
||||
if n <= len(get_color_cycle()):
|
||||
# Use current (global) default palette
|
||||
colors = color_palette(n_colors=n)
|
||||
else:
|
||||
colors = color_palette("husl", n)
|
||||
else:
|
||||
scale_class = scale.__class__.__name__
|
||||
msg = " ".join([
|
||||
f"Scale values for {self.variable} with a {scale_class} mapping",
|
||||
f"must be string, list, tuple, or dict; not {type(scale.values)}."
|
||||
])
|
||||
raise TypeError(msg)
|
||||
|
||||
return self._standardize_color_sequence(colors)
|
||||
|
||||
|
||||
# =================================================================================== #
|
||||
# Properties that can take only two states
|
||||
# =================================================================================== #
|
||||
|
||||
|
||||
class Fill(Property):
|
||||
"""Boolean property of points/bars/patches that can be solid or outlined."""
|
||||
legend = True
|
||||
normed = False
|
||||
|
||||
def default_scale(self, data: Series) -> Scale:
|
||||
var_type = variable_type(data, boolean_type="boolean", strict_boolean=True)
|
||||
return Boolean() if var_type == "boolean" else Nominal()
|
||||
|
||||
def infer_scale(self, arg: Any, data: Series) -> Scale:
|
||||
var_type = variable_type(data, boolean_type="boolean", strict_boolean=True)
|
||||
return Boolean(arg) if var_type == "boolean" else Nominal(arg)
|
||||
|
||||
def standardize(self, val: Any) -> bool:
|
||||
return bool(val)
|
||||
|
||||
def _default_values(self, n: int) -> list:
|
||||
"""Return a list of n values, alternating True and False."""
|
||||
if n > 2:
|
||||
msg = " ".join([
|
||||
f"The variable assigned to {self.variable} has more than two levels,",
|
||||
f"so {self.variable} values will cycle and may be uninterpretable",
|
||||
])
|
||||
# TODO fire in a "nice" way (see above)
|
||||
warnings.warn(msg, UserWarning)
|
||||
return [x for x, _ in zip(itertools.cycle([True, False]), range(n))]
|
||||
|
||||
def get_mapping(self, scale: Scale, data: Series) -> Mapping:
|
||||
"""Return a function that maps each data value to True or False."""
|
||||
boolean_scale = isinstance(scale, Boolean)
|
||||
order = getattr(scale, "order", [True, False] if boolean_scale else None)
|
||||
levels = categorical_order(data, order)
|
||||
values = self._get_values(scale, levels)
|
||||
|
||||
if boolean_scale:
|
||||
values = values[::-1]
|
||||
|
||||
def mapping(x):
|
||||
ixs = np.asarray(np.nan_to_num(x), np.intp)
|
||||
return [
|
||||
values[ix] if np.isfinite(x_i) else False
|
||||
for x_i, ix in zip(x, ixs)
|
||||
]
|
||||
|
||||
return mapping
|
||||
|
||||
def _get_values(self, scale: Scale, levels: list) -> list:
|
||||
"""Validate scale.values and identify a value for each level."""
|
||||
if isinstance(scale.values, list):
|
||||
values = [bool(x) for x in scale.values]
|
||||
elif isinstance(scale.values, dict):
|
||||
values = [bool(scale.values[x]) for x in levels]
|
||||
elif scale.values is None:
|
||||
values = self._default_values(len(levels))
|
||||
else:
|
||||
msg = " ".join([
|
||||
f"Scale values for {self.variable} must be passed in",
|
||||
f"a list or dict; not {type(scale.values)}."
|
||||
])
|
||||
raise TypeError(msg)
|
||||
|
||||
return values
|
||||
|
||||
|
||||
# =================================================================================== #
|
||||
# Enumeration of properties for use by Plot and Mark classes
|
||||
# =================================================================================== #
|
||||
# TODO turn this into a property registry with hooks, etc.
|
||||
# TODO Users do not interact directly with properties, so how to document them?
|
||||
|
||||
|
||||
PROPERTY_CLASSES = {
|
||||
"x": Coordinate,
|
||||
"y": Coordinate,
|
||||
"color": Color,
|
||||
"alpha": Alpha,
|
||||
"fill": Fill,
|
||||
"marker": Marker,
|
||||
"pointsize": PointSize,
|
||||
"stroke": Stroke,
|
||||
"linewidth": LineWidth,
|
||||
"linestyle": LineStyle,
|
||||
"fillcolor": Color,
|
||||
"fillalpha": Alpha,
|
||||
"edgewidth": EdgeWidth,
|
||||
"edgestyle": LineStyle,
|
||||
"edgecolor": Color,
|
||||
"edgealpha": Alpha,
|
||||
"text": Property,
|
||||
"halign": HorizontalAlignment,
|
||||
"valign": VerticalAlignment,
|
||||
"offset": Offset,
|
||||
"fontsize": FontSize,
|
||||
"xmin": Coordinate,
|
||||
"xmax": Coordinate,
|
||||
"ymin": Coordinate,
|
||||
"ymax": Coordinate,
|
||||
"group": Property,
|
||||
# TODO pattern?
|
||||
# TODO gradient?
|
||||
}
|
||||
|
||||
PROPERTIES = {var: cls(var) for var, cls in PROPERTY_CLASSES.items()}
|
||||
173
ftline/lib/python3.12/site-packages/seaborn/_core/rules.py
Normal file
173
ftline/lib/python3.12/site-packages/seaborn/_core/rules.py
Normal file
@@ -0,0 +1,173 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import warnings
|
||||
from collections import UserString
|
||||
from numbers import Number
|
||||
from datetime import datetime
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
if TYPE_CHECKING:
|
||||
from typing import Literal
|
||||
from pandas import Series
|
||||
|
||||
|
||||
class VarType(UserString):
|
||||
"""
|
||||
Prevent comparisons elsewhere in the library from using the wrong name.
|
||||
|
||||
Errors are simple assertions because users should not be able to trigger
|
||||
them. If that changes, they should be more verbose.
|
||||
|
||||
"""
|
||||
# TODO VarType is an awfully overloaded name, but so is DataType ...
|
||||
# TODO adding unknown because we are using this in for scales, is that right?
|
||||
allowed = "numeric", "datetime", "categorical", "boolean", "unknown"
|
||||
|
||||
def __init__(self, data):
|
||||
assert data in self.allowed, data
|
||||
super().__init__(data)
|
||||
|
||||
def __eq__(self, other):
|
||||
assert other in self.allowed, other
|
||||
return self.data == other
|
||||
|
||||
|
||||
def variable_type(
|
||||
vector: Series,
|
||||
boolean_type: Literal["numeric", "categorical", "boolean"] = "numeric",
|
||||
strict_boolean: bool = False,
|
||||
) -> VarType:
|
||||
"""
|
||||
Determine whether a vector contains numeric, categorical, or datetime data.
|
||||
|
||||
This function differs from the pandas typing API in a few ways:
|
||||
|
||||
- Python sequences or object-typed PyData objects are considered numeric if
|
||||
all of their entries are numeric.
|
||||
- String or mixed-type data are considered categorical even if not
|
||||
explicitly represented as a :class:`pandas.api.types.CategoricalDtype`.
|
||||
- There is some flexibility about how to treat binary / boolean data.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
vector : :func:`pandas.Series`, :func:`numpy.ndarray`, or Python sequence
|
||||
Input data to test.
|
||||
boolean_type : 'numeric', 'categorical', or 'boolean'
|
||||
Type to use for vectors containing only 0s and 1s (and NAs).
|
||||
strict_boolean : bool
|
||||
If True, only consider data to be boolean when the dtype is bool or Boolean.
|
||||
|
||||
Returns
|
||||
-------
|
||||
var_type : 'numeric', 'categorical', or 'datetime'
|
||||
Name identifying the type of data in the vector.
|
||||
"""
|
||||
|
||||
# If a categorical dtype is set, infer categorical
|
||||
if isinstance(getattr(vector, 'dtype', None), pd.CategoricalDtype):
|
||||
return VarType("categorical")
|
||||
|
||||
# Special-case all-na data, which is always "numeric"
|
||||
if pd.isna(vector).all():
|
||||
return VarType("numeric")
|
||||
|
||||
# Now drop nulls to simplify further type inference
|
||||
vector = vector.dropna()
|
||||
|
||||
# Special-case binary/boolean data, allow caller to determine
|
||||
# This triggers a numpy warning when vector has strings/objects
|
||||
# https://github.com/numpy/numpy/issues/6784
|
||||
# Because we reduce with .all(), we are agnostic about whether the
|
||||
# comparison returns a scalar or vector, so we will ignore the warning.
|
||||
# It triggers a separate DeprecationWarning when the vector has datetimes:
|
||||
# https://github.com/numpy/numpy/issues/13548
|
||||
# This is considered a bug by numpy and will likely go away.
|
||||
with warnings.catch_warnings():
|
||||
warnings.simplefilter(
|
||||
action='ignore',
|
||||
category=(FutureWarning, DeprecationWarning) # type: ignore # mypy bug?
|
||||
)
|
||||
if strict_boolean:
|
||||
if isinstance(vector.dtype, pd.core.dtypes.base.ExtensionDtype):
|
||||
boolean_dtypes = ["bool", "boolean"]
|
||||
else:
|
||||
boolean_dtypes = ["bool"]
|
||||
boolean_vector = vector.dtype in boolean_dtypes
|
||||
else:
|
||||
try:
|
||||
boolean_vector = bool(np.isin(vector, [0, 1]).all())
|
||||
except TypeError:
|
||||
# .isin comparison is not guaranteed to be possible under NumPy
|
||||
# casting rules, depending on the (unknown) dtype of 'vector'
|
||||
boolean_vector = False
|
||||
if boolean_vector:
|
||||
return VarType(boolean_type)
|
||||
|
||||
# Defer to positive pandas tests
|
||||
if pd.api.types.is_numeric_dtype(vector):
|
||||
return VarType("numeric")
|
||||
|
||||
if pd.api.types.is_datetime64_dtype(vector):
|
||||
return VarType("datetime")
|
||||
|
||||
# --- If we get to here, we need to check the entries
|
||||
|
||||
# Check for a collection where everything is a number
|
||||
|
||||
def all_numeric(x):
|
||||
for x_i in x:
|
||||
if not isinstance(x_i, Number):
|
||||
return False
|
||||
return True
|
||||
|
||||
if all_numeric(vector):
|
||||
return VarType("numeric")
|
||||
|
||||
# Check for a collection where everything is a datetime
|
||||
|
||||
def all_datetime(x):
|
||||
for x_i in x:
|
||||
if not isinstance(x_i, (datetime, np.datetime64)):
|
||||
return False
|
||||
return True
|
||||
|
||||
if all_datetime(vector):
|
||||
return VarType("datetime")
|
||||
|
||||
# Otherwise, our final fallback is to consider things categorical
|
||||
|
||||
return VarType("categorical")
|
||||
|
||||
|
||||
def categorical_order(vector: Series, order: list | None = None) -> list:
|
||||
"""
|
||||
Return a list of unique data values using seaborn's ordering rules.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
vector : Series
|
||||
Vector of "categorical" values
|
||||
order : list
|
||||
Desired order of category levels to override the order determined
|
||||
from the `data` object.
|
||||
|
||||
Returns
|
||||
-------
|
||||
order : list
|
||||
Ordered list of category levels not including null values.
|
||||
|
||||
"""
|
||||
if order is not None:
|
||||
return order
|
||||
|
||||
if vector.dtype.name == "category":
|
||||
order = list(vector.cat.categories)
|
||||
else:
|
||||
order = list(filter(pd.notnull, vector.unique()))
|
||||
if variable_type(pd.Series(order)) == "numeric":
|
||||
order.sort()
|
||||
|
||||
return order
|
||||
1090
ftline/lib/python3.12/site-packages/seaborn/_core/scales.py
Normal file
1090
ftline/lib/python3.12/site-packages/seaborn/_core/scales.py
Normal file
File diff suppressed because it is too large
Load Diff
263
ftline/lib/python3.12/site-packages/seaborn/_core/subplots.py
Normal file
263
ftline/lib/python3.12/site-packages/seaborn/_core/subplots.py
Normal file
@@ -0,0 +1,263 @@
|
||||
from __future__ import annotations
|
||||
from collections.abc import Generator
|
||||
|
||||
import numpy as np
|
||||
import matplotlib as mpl
|
||||
import matplotlib.pyplot as plt
|
||||
|
||||
from matplotlib.axes import Axes
|
||||
from matplotlib.figure import Figure
|
||||
from typing import TYPE_CHECKING
|
||||
if TYPE_CHECKING: # TODO move to seaborn._core.typing?
|
||||
from seaborn._core.plot import FacetSpec, PairSpec
|
||||
from matplotlib.figure import SubFigure
|
||||
|
||||
|
||||
class Subplots:
|
||||
"""
|
||||
Interface for creating and using matplotlib subplots based on seaborn parameters.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
subplot_spec : dict
|
||||
Keyword args for :meth:`matplotlib.figure.Figure.subplots`.
|
||||
facet_spec : dict
|
||||
Parameters that control subplot faceting.
|
||||
pair_spec : dict
|
||||
Parameters that control subplot pairing.
|
||||
data : PlotData
|
||||
Data used to define figure setup.
|
||||
|
||||
"""
|
||||
def __init__(
|
||||
self,
|
||||
subplot_spec: dict, # TODO define as TypedDict
|
||||
facet_spec: FacetSpec,
|
||||
pair_spec: PairSpec,
|
||||
):
|
||||
|
||||
self.subplot_spec = subplot_spec
|
||||
|
||||
self._check_dimension_uniqueness(facet_spec, pair_spec)
|
||||
self._determine_grid_dimensions(facet_spec, pair_spec)
|
||||
self._handle_wrapping(facet_spec, pair_spec)
|
||||
self._determine_axis_sharing(pair_spec)
|
||||
|
||||
def _check_dimension_uniqueness(
|
||||
self, facet_spec: FacetSpec, pair_spec: PairSpec
|
||||
) -> None:
|
||||
"""Reject specs that pair and facet on (or wrap to) same figure dimension."""
|
||||
err = None
|
||||
|
||||
facet_vars = facet_spec.get("variables", {})
|
||||
|
||||
if facet_spec.get("wrap") and {"col", "row"} <= set(facet_vars):
|
||||
err = "Cannot wrap facets when specifying both `col` and `row`."
|
||||
elif (
|
||||
pair_spec.get("wrap")
|
||||
and pair_spec.get("cross", True)
|
||||
and len(pair_spec.get("structure", {}).get("x", [])) > 1
|
||||
and len(pair_spec.get("structure", {}).get("y", [])) > 1
|
||||
):
|
||||
err = "Cannot wrap subplots when pairing on both `x` and `y`."
|
||||
|
||||
collisions = {"x": ["columns", "rows"], "y": ["rows", "columns"]}
|
||||
for pair_axis, (multi_dim, wrap_dim) in collisions.items():
|
||||
if pair_axis not in pair_spec.get("structure", {}):
|
||||
continue
|
||||
elif multi_dim[:3] in facet_vars:
|
||||
err = f"Cannot facet the {multi_dim} while pairing on `{pair_axis}``."
|
||||
elif wrap_dim[:3] in facet_vars and facet_spec.get("wrap"):
|
||||
err = f"Cannot wrap the {wrap_dim} while pairing on `{pair_axis}``."
|
||||
elif wrap_dim[:3] in facet_vars and pair_spec.get("wrap"):
|
||||
err = f"Cannot wrap the {multi_dim} while faceting the {wrap_dim}."
|
||||
|
||||
if err is not None:
|
||||
raise RuntimeError(err) # TODO what err class? Define PlotSpecError?
|
||||
|
||||
def _determine_grid_dimensions(
|
||||
self, facet_spec: FacetSpec, pair_spec: PairSpec
|
||||
) -> None:
|
||||
"""Parse faceting and pairing information to define figure structure."""
|
||||
self.grid_dimensions: dict[str, list] = {}
|
||||
for dim, axis in zip(["col", "row"], ["x", "y"]):
|
||||
|
||||
facet_vars = facet_spec.get("variables", {})
|
||||
if dim in facet_vars:
|
||||
self.grid_dimensions[dim] = facet_spec["structure"][dim]
|
||||
elif axis in pair_spec.get("structure", {}):
|
||||
self.grid_dimensions[dim] = [
|
||||
None for _ in pair_spec.get("structure", {})[axis]
|
||||
]
|
||||
else:
|
||||
self.grid_dimensions[dim] = [None]
|
||||
|
||||
self.subplot_spec[f"n{dim}s"] = len(self.grid_dimensions[dim])
|
||||
|
||||
if not pair_spec.get("cross", True):
|
||||
self.subplot_spec["nrows"] = 1
|
||||
|
||||
self.n_subplots = self.subplot_spec["ncols"] * self.subplot_spec["nrows"]
|
||||
|
||||
def _handle_wrapping(
|
||||
self, facet_spec: FacetSpec, pair_spec: PairSpec
|
||||
) -> None:
|
||||
"""Update figure structure parameters based on facet/pair wrapping."""
|
||||
self.wrap = wrap = facet_spec.get("wrap") or pair_spec.get("wrap")
|
||||
if not wrap:
|
||||
return
|
||||
|
||||
wrap_dim = "row" if self.subplot_spec["nrows"] > 1 else "col"
|
||||
flow_dim = {"row": "col", "col": "row"}[wrap_dim]
|
||||
n_subplots = self.subplot_spec[f"n{wrap_dim}s"]
|
||||
flow = int(np.ceil(n_subplots / wrap))
|
||||
|
||||
if wrap < self.subplot_spec[f"n{wrap_dim}s"]:
|
||||
self.subplot_spec[f"n{wrap_dim}s"] = wrap
|
||||
self.subplot_spec[f"n{flow_dim}s"] = flow
|
||||
self.n_subplots = n_subplots
|
||||
self.wrap_dim = wrap_dim
|
||||
|
||||
def _determine_axis_sharing(self, pair_spec: PairSpec) -> None:
|
||||
"""Update subplot spec with default or specified axis sharing parameters."""
|
||||
axis_to_dim = {"x": "col", "y": "row"}
|
||||
key: str
|
||||
val: str | bool
|
||||
for axis in "xy":
|
||||
key = f"share{axis}"
|
||||
# Always use user-specified value, if present
|
||||
if key not in self.subplot_spec:
|
||||
if axis in pair_spec.get("structure", {}):
|
||||
# Paired axes are shared along one dimension by default
|
||||
if self.wrap is None and pair_spec.get("cross", True):
|
||||
val = axis_to_dim[axis]
|
||||
else:
|
||||
val = False
|
||||
else:
|
||||
# This will pick up faceted plots, as well as single subplot
|
||||
# figures, where the value doesn't really matter
|
||||
val = True
|
||||
self.subplot_spec[key] = val
|
||||
|
||||
def init_figure(
|
||||
self,
|
||||
pair_spec: PairSpec,
|
||||
pyplot: bool = False,
|
||||
figure_kws: dict | None = None,
|
||||
target: Axes | Figure | SubFigure | None = None,
|
||||
) -> Figure:
|
||||
"""Initialize matplotlib objects and add seaborn-relevant metadata."""
|
||||
# TODO reduce need to pass pair_spec here?
|
||||
|
||||
if figure_kws is None:
|
||||
figure_kws = {}
|
||||
|
||||
if isinstance(target, mpl.axes.Axes):
|
||||
|
||||
if max(self.subplot_spec["nrows"], self.subplot_spec["ncols"]) > 1:
|
||||
err = " ".join([
|
||||
"Cannot create multiple subplots after calling `Plot.on` with",
|
||||
f"a {mpl.axes.Axes} object.",
|
||||
f" You may want to use a {mpl.figure.SubFigure} instead.",
|
||||
])
|
||||
raise RuntimeError(err)
|
||||
|
||||
self._subplot_list = [{
|
||||
"ax": target,
|
||||
"left": True,
|
||||
"right": True,
|
||||
"top": True,
|
||||
"bottom": True,
|
||||
"col": None,
|
||||
"row": None,
|
||||
"x": "x",
|
||||
"y": "y",
|
||||
}]
|
||||
self._figure = target.figure
|
||||
return self._figure
|
||||
|
||||
elif isinstance(target, mpl.figure.SubFigure):
|
||||
figure = target.figure
|
||||
elif isinstance(target, mpl.figure.Figure):
|
||||
figure = target
|
||||
else:
|
||||
if pyplot:
|
||||
figure = plt.figure(**figure_kws)
|
||||
else:
|
||||
figure = mpl.figure.Figure(**figure_kws)
|
||||
target = figure
|
||||
self._figure = figure
|
||||
|
||||
axs = target.subplots(**self.subplot_spec, squeeze=False)
|
||||
|
||||
if self.wrap:
|
||||
# Remove unused Axes and flatten the rest into a (2D) vector
|
||||
axs_flat = axs.ravel({"col": "C", "row": "F"}[self.wrap_dim])
|
||||
axs, extra = np.split(axs_flat, [self.n_subplots])
|
||||
for ax in extra:
|
||||
ax.remove()
|
||||
if self.wrap_dim == "col":
|
||||
axs = axs[np.newaxis, :]
|
||||
else:
|
||||
axs = axs[:, np.newaxis]
|
||||
|
||||
# Get i, j coordinates for each Axes object
|
||||
# Note that i, j are with respect to faceting/pairing,
|
||||
# not the subplot grid itself, (which only matters in the case of wrapping).
|
||||
iter_axs: np.ndenumerate | zip
|
||||
if not pair_spec.get("cross", True):
|
||||
indices = np.arange(self.n_subplots)
|
||||
iter_axs = zip(zip(indices, indices), axs.flat)
|
||||
else:
|
||||
iter_axs = np.ndenumerate(axs)
|
||||
|
||||
self._subplot_list = []
|
||||
for (i, j), ax in iter_axs:
|
||||
|
||||
info = {"ax": ax}
|
||||
|
||||
nrows, ncols = self.subplot_spec["nrows"], self.subplot_spec["ncols"]
|
||||
if not self.wrap:
|
||||
info["left"] = j % ncols == 0
|
||||
info["right"] = (j + 1) % ncols == 0
|
||||
info["top"] = i == 0
|
||||
info["bottom"] = i == nrows - 1
|
||||
elif self.wrap_dim == "col":
|
||||
info["left"] = j % ncols == 0
|
||||
info["right"] = ((j + 1) % ncols == 0) or ((j + 1) == self.n_subplots)
|
||||
info["top"] = j < ncols
|
||||
info["bottom"] = j >= (self.n_subplots - ncols)
|
||||
elif self.wrap_dim == "row":
|
||||
info["left"] = i < nrows
|
||||
info["right"] = i >= self.n_subplots - nrows
|
||||
info["top"] = i % nrows == 0
|
||||
info["bottom"] = ((i + 1) % nrows == 0) or ((i + 1) == self.n_subplots)
|
||||
|
||||
if not pair_spec.get("cross", True):
|
||||
info["top"] = j < ncols
|
||||
info["bottom"] = j >= self.n_subplots - ncols
|
||||
|
||||
for dim in ["row", "col"]:
|
||||
idx = {"row": i, "col": j}[dim]
|
||||
info[dim] = self.grid_dimensions[dim][idx]
|
||||
|
||||
for axis in "xy":
|
||||
|
||||
idx = {"x": j, "y": i}[axis]
|
||||
if axis in pair_spec.get("structure", {}):
|
||||
key = f"{axis}{idx}"
|
||||
else:
|
||||
key = axis
|
||||
info[axis] = key
|
||||
|
||||
self._subplot_list.append(info)
|
||||
|
||||
return figure
|
||||
|
||||
def __iter__(self) -> Generator[dict, None, None]: # TODO TypedDict?
|
||||
"""Yield each subplot dictionary with Axes object and metadata."""
|
||||
yield from self._subplot_list
|
||||
|
||||
def __len__(self) -> int:
|
||||
"""Return the number of subplots in this figure."""
|
||||
return len(self._subplot_list)
|
||||
49
ftline/lib/python3.12/site-packages/seaborn/_core/typing.py
Normal file
49
ftline/lib/python3.12/site-packages/seaborn/_core/typing.py
Normal file
@@ -0,0 +1,49 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Iterable, Mapping
|
||||
from datetime import date, datetime, timedelta
|
||||
from typing import Any, Optional, Union, Tuple, List, Dict
|
||||
|
||||
from numpy import ndarray # TODO use ArrayLike?
|
||||
from pandas import Series, Index, Timestamp, Timedelta
|
||||
from matplotlib.colors import Colormap, Normalize
|
||||
|
||||
|
||||
ColumnName = Union[
|
||||
str, bytes, date, datetime, timedelta, bool, complex, Timestamp, Timedelta
|
||||
]
|
||||
Vector = Union[Series, Index, ndarray]
|
||||
|
||||
VariableSpec = Union[ColumnName, Vector, None]
|
||||
VariableSpecList = Union[List[VariableSpec], Index, None]
|
||||
|
||||
# A DataSource can be an object implementing __dataframe__, or a Mapping
|
||||
# (and is optional in all contexts where it is used).
|
||||
# I don't think there's an abc for "has __dataframe__", so we type as object
|
||||
# but keep the (slightly odd) Union alias for better user-facing annotations.
|
||||
DataSource = Union[object, Mapping, None]
|
||||
|
||||
OrderSpec = Union[Iterable, None] # TODO technically str is iterable
|
||||
NormSpec = Union[Tuple[Optional[float], Optional[float]], Normalize, None]
|
||||
|
||||
# TODO for discrete mappings, it would be ideal to use a parameterized type
|
||||
# as the dict values / list entries should be of specific type(s) for each method
|
||||
PaletteSpec = Union[str, list, dict, Colormap, None]
|
||||
DiscreteValueSpec = Union[dict, list, None]
|
||||
ContinuousValueSpec = Union[
|
||||
Tuple[float, float], List[float], Dict[Any, float], None,
|
||||
]
|
||||
|
||||
|
||||
class Default:
|
||||
def __repr__(self):
|
||||
return "<default>"
|
||||
|
||||
|
||||
class Deprecated:
|
||||
def __repr__(self):
|
||||
return "<deprecated>"
|
||||
|
||||
|
||||
default = Default()
|
||||
deprecated = Deprecated()
|
||||
198
ftline/lib/python3.12/site-packages/seaborn/_docstrings.py
Normal file
198
ftline/lib/python3.12/site-packages/seaborn/_docstrings.py
Normal file
@@ -0,0 +1,198 @@
|
||||
import re
|
||||
import pydoc
|
||||
from .external.docscrape import NumpyDocString
|
||||
|
||||
|
||||
class DocstringComponents:
|
||||
|
||||
regexp = re.compile(r"\n((\n|.)+)\n\s*", re.MULTILINE)
|
||||
|
||||
def __init__(self, comp_dict, strip_whitespace=True):
|
||||
"""Read entries from a dict, optionally stripping outer whitespace."""
|
||||
if strip_whitespace:
|
||||
entries = {}
|
||||
for key, val in comp_dict.items():
|
||||
m = re.match(self.regexp, val)
|
||||
if m is None:
|
||||
entries[key] = val
|
||||
else:
|
||||
entries[key] = m.group(1)
|
||||
else:
|
||||
entries = comp_dict.copy()
|
||||
|
||||
self.entries = entries
|
||||
|
||||
def __getattr__(self, attr):
|
||||
"""Provide dot access to entries for clean raw docstrings."""
|
||||
if attr in self.entries:
|
||||
return self.entries[attr]
|
||||
else:
|
||||
try:
|
||||
return self.__getattribute__(attr)
|
||||
except AttributeError as err:
|
||||
# If Python is run with -OO, it will strip docstrings and our lookup
|
||||
# from self.entries will fail. We check for __debug__, which is actually
|
||||
# set to False by -O (it is True for normal execution).
|
||||
# But we only want to see an error when building the docs;
|
||||
# not something users should see, so this slight inconsistency is fine.
|
||||
if __debug__:
|
||||
raise err
|
||||
else:
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def from_nested_components(cls, **kwargs):
|
||||
"""Add multiple sub-sets of components."""
|
||||
return cls(kwargs, strip_whitespace=False)
|
||||
|
||||
@classmethod
|
||||
def from_function_params(cls, func):
|
||||
"""Use the numpydoc parser to extract components from existing func."""
|
||||
params = NumpyDocString(pydoc.getdoc(func))["Parameters"]
|
||||
comp_dict = {}
|
||||
for p in params:
|
||||
name = p.name
|
||||
type = p.type
|
||||
desc = "\n ".join(p.desc)
|
||||
comp_dict[name] = f"{name} : {type}\n {desc}"
|
||||
|
||||
return cls(comp_dict)
|
||||
|
||||
|
||||
# TODO is "vector" the best term here? We mean to imply 1D data with a variety
|
||||
# of types?
|
||||
|
||||
# TODO now that we can parse numpydoc style strings, do we need to define dicts
|
||||
# of docstring components, or just write out a docstring?
|
||||
|
||||
|
||||
_core_params = dict(
|
||||
data="""
|
||||
data : :class:`pandas.DataFrame`, :class:`numpy.ndarray`, mapping, or sequence
|
||||
Input data structure. Either a long-form collection of vectors that can be
|
||||
assigned to named variables or a wide-form dataset that will be internally
|
||||
reshaped.
|
||||
""", # TODO add link to user guide narrative when exists
|
||||
xy="""
|
||||
x, y : vectors or keys in ``data``
|
||||
Variables that specify positions on the x and y axes.
|
||||
""",
|
||||
hue="""
|
||||
hue : vector or key in ``data``
|
||||
Semantic variable that is mapped to determine the color of plot elements.
|
||||
""",
|
||||
palette="""
|
||||
palette : string, list, dict, or :class:`matplotlib.colors.Colormap`
|
||||
Method for choosing the colors to use when mapping the ``hue`` semantic.
|
||||
String values are passed to :func:`color_palette`. List or dict values
|
||||
imply categorical mapping, while a colormap object implies numeric mapping.
|
||||
""", # noqa: E501
|
||||
hue_order="""
|
||||
hue_order : vector of strings
|
||||
Specify the order of processing and plotting for categorical levels of the
|
||||
``hue`` semantic.
|
||||
""",
|
||||
hue_norm="""
|
||||
hue_norm : tuple or :class:`matplotlib.colors.Normalize`
|
||||
Either a pair of values that set the normalization range in data units
|
||||
or an object that will map from data units into a [0, 1] interval. Usage
|
||||
implies numeric mapping.
|
||||
""",
|
||||
color="""
|
||||
color : :mod:`matplotlib color <matplotlib.colors>`
|
||||
Single color specification for when hue mapping is not used. Otherwise, the
|
||||
plot will try to hook into the matplotlib property cycle.
|
||||
""",
|
||||
ax="""
|
||||
ax : :class:`matplotlib.axes.Axes`
|
||||
Pre-existing axes for the plot. Otherwise, call :func:`matplotlib.pyplot.gca`
|
||||
internally.
|
||||
""", # noqa: E501
|
||||
)
|
||||
|
||||
|
||||
_core_returns = dict(
|
||||
ax="""
|
||||
:class:`matplotlib.axes.Axes`
|
||||
The matplotlib axes containing the plot.
|
||||
""",
|
||||
facetgrid="""
|
||||
:class:`FacetGrid`
|
||||
An object managing one or more subplots that correspond to conditional data
|
||||
subsets with convenient methods for batch-setting of axes attributes.
|
||||
""",
|
||||
jointgrid="""
|
||||
:class:`JointGrid`
|
||||
An object managing multiple subplots that correspond to joint and marginal axes
|
||||
for plotting a bivariate relationship or distribution.
|
||||
""",
|
||||
pairgrid="""
|
||||
:class:`PairGrid`
|
||||
An object managing multiple subplots that correspond to joint and marginal axes
|
||||
for pairwise combinations of multiple variables in a dataset.
|
||||
""",
|
||||
)
|
||||
|
||||
|
||||
_seealso_blurbs = dict(
|
||||
|
||||
# Relational plots
|
||||
scatterplot="""
|
||||
scatterplot : Plot data using points.
|
||||
""",
|
||||
lineplot="""
|
||||
lineplot : Plot data using lines.
|
||||
""",
|
||||
|
||||
# Distribution plots
|
||||
displot="""
|
||||
displot : Figure-level interface to distribution plot functions.
|
||||
""",
|
||||
histplot="""
|
||||
histplot : Plot a histogram of binned counts with optional normalization or smoothing.
|
||||
""",
|
||||
kdeplot="""
|
||||
kdeplot : Plot univariate or bivariate distributions using kernel density estimation.
|
||||
""",
|
||||
ecdfplot="""
|
||||
ecdfplot : Plot empirical cumulative distribution functions.
|
||||
""",
|
||||
rugplot="""
|
||||
rugplot : Plot a tick at each observation value along the x and/or y axes.
|
||||
""",
|
||||
|
||||
# Categorical plots
|
||||
stripplot="""
|
||||
stripplot : Plot a categorical scatter with jitter.
|
||||
""",
|
||||
swarmplot="""
|
||||
swarmplot : Plot a categorical scatter with non-overlapping points.
|
||||
""",
|
||||
violinplot="""
|
||||
violinplot : Draw an enhanced boxplot using kernel density estimation.
|
||||
""",
|
||||
pointplot="""
|
||||
pointplot : Plot point estimates and CIs using markers and lines.
|
||||
""",
|
||||
|
||||
# Multiples
|
||||
jointplot="""
|
||||
jointplot : Draw a bivariate plot with univariate marginal distributions.
|
||||
""",
|
||||
pairplot="""
|
||||
jointplot : Draw multiple bivariate plots with univariate marginal distributions.
|
||||
""",
|
||||
jointgrid="""
|
||||
JointGrid : Set up a figure with joint and marginal views on bivariate data.
|
||||
""",
|
||||
pairgrid="""
|
||||
PairGrid : Set up a figure with joint and marginal views on multiple variables.
|
||||
""",
|
||||
)
|
||||
|
||||
|
||||
_core_docs = dict(
|
||||
params=DocstringComponents(_core_params),
|
||||
returns=DocstringComponents(_core_returns),
|
||||
seealso=DocstringComponents(_seealso_blurbs),
|
||||
)
|
||||
170
ftline/lib/python3.12/site-packages/seaborn/_marks/area.py
Normal file
170
ftline/lib/python3.12/site-packages/seaborn/_marks/area.py
Normal file
@@ -0,0 +1,170 @@
|
||||
from __future__ import annotations
|
||||
from collections import defaultdict
|
||||
from dataclasses import dataclass
|
||||
|
||||
import numpy as np
|
||||
import matplotlib as mpl
|
||||
|
||||
from seaborn._marks.base import (
|
||||
Mark,
|
||||
Mappable,
|
||||
MappableBool,
|
||||
MappableFloat,
|
||||
MappableColor,
|
||||
MappableStyle,
|
||||
resolve_properties,
|
||||
resolve_color,
|
||||
document_properties,
|
||||
)
|
||||
|
||||
|
||||
class AreaBase:
|
||||
|
||||
def _plot(self, split_gen, scales, orient):
|
||||
|
||||
patches = defaultdict(list)
|
||||
|
||||
for keys, data, ax in split_gen():
|
||||
|
||||
kws = {}
|
||||
data = self._standardize_coordinate_parameters(data, orient)
|
||||
resolved = resolve_properties(self, keys, scales)
|
||||
verts = self._get_verts(data, orient)
|
||||
ax.update_datalim(verts)
|
||||
|
||||
# TODO should really move this logic into resolve_color
|
||||
fc = resolve_color(self, keys, "", scales)
|
||||
if not resolved["fill"]:
|
||||
fc = mpl.colors.to_rgba(fc, 0)
|
||||
|
||||
kws["facecolor"] = fc
|
||||
kws["edgecolor"] = resolve_color(self, keys, "edge", scales)
|
||||
kws["linewidth"] = resolved["edgewidth"]
|
||||
kws["linestyle"] = resolved["edgestyle"]
|
||||
|
||||
patches[ax].append(mpl.patches.Polygon(verts, **kws))
|
||||
|
||||
for ax, ax_patches in patches.items():
|
||||
|
||||
for patch in ax_patches:
|
||||
self._postprocess_artist(patch, ax, orient)
|
||||
ax.add_patch(patch)
|
||||
|
||||
def _standardize_coordinate_parameters(self, data, orient):
|
||||
return data
|
||||
|
||||
def _postprocess_artist(self, artist, ax, orient):
|
||||
pass
|
||||
|
||||
def _get_verts(self, data, orient):
|
||||
|
||||
dv = {"x": "y", "y": "x"}[orient]
|
||||
data = data.sort_values(orient, kind="mergesort")
|
||||
verts = np.concatenate([
|
||||
data[[orient, f"{dv}min"]].to_numpy(),
|
||||
data[[orient, f"{dv}max"]].to_numpy()[::-1],
|
||||
])
|
||||
if orient == "y":
|
||||
verts = verts[:, ::-1]
|
||||
return verts
|
||||
|
||||
def _legend_artist(self, variables, value, scales):
|
||||
|
||||
keys = {v: value for v in variables}
|
||||
resolved = resolve_properties(self, keys, scales)
|
||||
|
||||
fc = resolve_color(self, keys, "", scales)
|
||||
if not resolved["fill"]:
|
||||
fc = mpl.colors.to_rgba(fc, 0)
|
||||
|
||||
return mpl.patches.Patch(
|
||||
facecolor=fc,
|
||||
edgecolor=resolve_color(self, keys, "edge", scales),
|
||||
linewidth=resolved["edgewidth"],
|
||||
linestyle=resolved["edgestyle"],
|
||||
**self.artist_kws,
|
||||
)
|
||||
|
||||
|
||||
@document_properties
|
||||
@dataclass
|
||||
class Area(AreaBase, Mark):
|
||||
"""
|
||||
A fill mark drawn from a baseline to data values.
|
||||
|
||||
See also
|
||||
--------
|
||||
Band : A fill mark representing an interval between values.
|
||||
|
||||
Examples
|
||||
--------
|
||||
.. include:: ../docstrings/objects.Area.rst
|
||||
|
||||
"""
|
||||
color: MappableColor = Mappable("C0", )
|
||||
alpha: MappableFloat = Mappable(.2, )
|
||||
fill: MappableBool = Mappable(True, )
|
||||
edgecolor: MappableColor = Mappable(depend="color")
|
||||
edgealpha: MappableFloat = Mappable(1, )
|
||||
edgewidth: MappableFloat = Mappable(rc="patch.linewidth", )
|
||||
edgestyle: MappableStyle = Mappable("-", )
|
||||
|
||||
# TODO should this be settable / mappable?
|
||||
baseline: MappableFloat = Mappable(0, grouping=False)
|
||||
|
||||
def _standardize_coordinate_parameters(self, data, orient):
|
||||
dv = {"x": "y", "y": "x"}[orient]
|
||||
return data.rename(columns={"baseline": f"{dv}min", dv: f"{dv}max"})
|
||||
|
||||
def _postprocess_artist(self, artist, ax, orient):
|
||||
|
||||
# TODO copying a lot of code from Bar, let's abstract this
|
||||
# See comments there, I am not going to repeat them too
|
||||
|
||||
artist.set_linewidth(artist.get_linewidth() * 2)
|
||||
|
||||
linestyle = artist.get_linestyle()
|
||||
if linestyle[1]:
|
||||
linestyle = (linestyle[0], tuple(x / 2 for x in linestyle[1]))
|
||||
artist.set_linestyle(linestyle)
|
||||
|
||||
artist.set_clip_path(artist.get_path(), artist.get_transform() + ax.transData)
|
||||
if self.artist_kws.get("clip_on", True):
|
||||
artist.set_clip_box(ax.bbox)
|
||||
|
||||
val_idx = ["y", "x"].index(orient)
|
||||
artist.sticky_edges[val_idx][:] = (0, np.inf)
|
||||
|
||||
|
||||
@document_properties
|
||||
@dataclass
|
||||
class Band(AreaBase, Mark):
|
||||
"""
|
||||
A fill mark representing an interval between values.
|
||||
|
||||
See also
|
||||
--------
|
||||
Area : A fill mark drawn from a baseline to data values.
|
||||
|
||||
Examples
|
||||
--------
|
||||
.. include:: ../docstrings/objects.Band.rst
|
||||
|
||||
"""
|
||||
color: MappableColor = Mappable("C0", )
|
||||
alpha: MappableFloat = Mappable(.2, )
|
||||
fill: MappableBool = Mappable(True, )
|
||||
edgecolor: MappableColor = Mappable(depend="color", )
|
||||
edgealpha: MappableFloat = Mappable(1, )
|
||||
edgewidth: MappableFloat = Mappable(0, )
|
||||
edgestyle: MappableFloat = Mappable("-", )
|
||||
|
||||
def _standardize_coordinate_parameters(self, data, orient):
|
||||
# dv = {"x": "y", "y": "x"}[orient]
|
||||
# TODO assert that all(ymax >= ymin)?
|
||||
# TODO what if only one exist?
|
||||
other = {"x": "y", "y": "x"}[orient]
|
||||
if not set(data.columns) & {f"{other}min", f"{other}max"}:
|
||||
agg = {f"{other}min": (other, "min"), f"{other}max": (other, "max")}
|
||||
data = data.groupby(orient).agg(**agg).reset_index()
|
||||
return data
|
||||
252
ftline/lib/python3.12/site-packages/seaborn/_marks/bar.py
Normal file
252
ftline/lib/python3.12/site-packages/seaborn/_marks/bar.py
Normal file
@@ -0,0 +1,252 @@
|
||||
from __future__ import annotations
|
||||
from collections import defaultdict
|
||||
from dataclasses import dataclass
|
||||
|
||||
import numpy as np
|
||||
import matplotlib as mpl
|
||||
|
||||
from seaborn._marks.base import (
|
||||
Mark,
|
||||
Mappable,
|
||||
MappableBool,
|
||||
MappableColor,
|
||||
MappableFloat,
|
||||
MappableStyle,
|
||||
resolve_properties,
|
||||
resolve_color,
|
||||
document_properties
|
||||
)
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
if TYPE_CHECKING:
|
||||
from typing import Any
|
||||
from matplotlib.artist import Artist
|
||||
from seaborn._core.scales import Scale
|
||||
|
||||
|
||||
class BarBase(Mark):
|
||||
|
||||
def _make_patches(self, data, scales, orient):
|
||||
|
||||
transform = scales[orient]._matplotlib_scale.get_transform()
|
||||
forward = transform.transform
|
||||
reverse = transform.inverted().transform
|
||||
|
||||
other = {"x": "y", "y": "x"}[orient]
|
||||
|
||||
pos = reverse(forward(data[orient]) - data["width"] / 2)
|
||||
width = reverse(forward(data[orient]) + data["width"] / 2) - pos
|
||||
|
||||
val = (data[other] - data["baseline"]).to_numpy()
|
||||
base = data["baseline"].to_numpy()
|
||||
|
||||
kws = self._resolve_properties(data, scales)
|
||||
if orient == "x":
|
||||
kws.update(x=pos, y=base, w=width, h=val)
|
||||
else:
|
||||
kws.update(x=base, y=pos, w=val, h=width)
|
||||
|
||||
kws.pop("width", None)
|
||||
kws.pop("baseline", None)
|
||||
|
||||
val_dim = {"x": "h", "y": "w"}[orient]
|
||||
bars, vals = [], []
|
||||
|
||||
for i in range(len(data)):
|
||||
|
||||
row = {k: v[i] for k, v in kws.items()}
|
||||
|
||||
# Skip bars with no value. It's possible we'll want to make this
|
||||
# an option (i.e so you have an artist for animating or annotating),
|
||||
# but let's keep things simple for now.
|
||||
if not np.nan_to_num(row[val_dim]):
|
||||
continue
|
||||
|
||||
bar = mpl.patches.Rectangle(
|
||||
xy=(row["x"], row["y"]),
|
||||
width=row["w"],
|
||||
height=row["h"],
|
||||
facecolor=row["facecolor"],
|
||||
edgecolor=row["edgecolor"],
|
||||
linestyle=row["edgestyle"],
|
||||
linewidth=row["edgewidth"],
|
||||
**self.artist_kws,
|
||||
)
|
||||
bars.append(bar)
|
||||
vals.append(row[val_dim])
|
||||
|
||||
return bars, vals
|
||||
|
||||
def _resolve_properties(self, data, scales):
|
||||
|
||||
resolved = resolve_properties(self, data, scales)
|
||||
|
||||
resolved["facecolor"] = resolve_color(self, data, "", scales)
|
||||
resolved["edgecolor"] = resolve_color(self, data, "edge", scales)
|
||||
|
||||
fc = resolved["facecolor"]
|
||||
if isinstance(fc, tuple):
|
||||
resolved["facecolor"] = fc[0], fc[1], fc[2], fc[3] * resolved["fill"]
|
||||
else:
|
||||
fc[:, 3] = fc[:, 3] * resolved["fill"] # TODO Is inplace mod a problem?
|
||||
resolved["facecolor"] = fc
|
||||
|
||||
return resolved
|
||||
|
||||
def _legend_artist(
|
||||
self, variables: list[str], value: Any, scales: dict[str, Scale],
|
||||
) -> Artist:
|
||||
# TODO return some sensible default?
|
||||
key = {v: value for v in variables}
|
||||
key = self._resolve_properties(key, scales)
|
||||
artist = mpl.patches.Patch(
|
||||
facecolor=key["facecolor"],
|
||||
edgecolor=key["edgecolor"],
|
||||
linewidth=key["edgewidth"],
|
||||
linestyle=key["edgestyle"],
|
||||
)
|
||||
return artist
|
||||
|
||||
|
||||
@document_properties
|
||||
@dataclass
|
||||
class Bar(BarBase):
|
||||
"""
|
||||
A bar mark drawn between baseline and data values.
|
||||
|
||||
See also
|
||||
--------
|
||||
Bars : A faster bar mark with defaults more suitable for histograms.
|
||||
|
||||
Examples
|
||||
--------
|
||||
.. include:: ../docstrings/objects.Bar.rst
|
||||
|
||||
"""
|
||||
color: MappableColor = Mappable("C0", grouping=False)
|
||||
alpha: MappableFloat = Mappable(.7, grouping=False)
|
||||
fill: MappableBool = Mappable(True, grouping=False)
|
||||
edgecolor: MappableColor = Mappable(depend="color", grouping=False)
|
||||
edgealpha: MappableFloat = Mappable(1, grouping=False)
|
||||
edgewidth: MappableFloat = Mappable(rc="patch.linewidth", grouping=False)
|
||||
edgestyle: MappableStyle = Mappable("-", grouping=False)
|
||||
# pattern: MappableString = Mappable(None) # TODO no Property yet
|
||||
|
||||
width: MappableFloat = Mappable(.8, grouping=False)
|
||||
baseline: MappableFloat = Mappable(0, grouping=False) # TODO *is* this mappable?
|
||||
|
||||
def _plot(self, split_gen, scales, orient):
|
||||
|
||||
val_idx = ["y", "x"].index(orient)
|
||||
|
||||
for _, data, ax in split_gen():
|
||||
|
||||
bars, vals = self._make_patches(data, scales, orient)
|
||||
|
||||
for bar in bars:
|
||||
|
||||
# Because we are clipping the artist (see below), the edges end up
|
||||
# looking half as wide as they actually are. I don't love this clumsy
|
||||
# workaround, which is going to cause surprises if you work with the
|
||||
# artists directly. We may need to revisit after feedback.
|
||||
bar.set_linewidth(bar.get_linewidth() * 2)
|
||||
linestyle = bar.get_linestyle()
|
||||
if linestyle[1]:
|
||||
linestyle = (linestyle[0], tuple(x / 2 for x in linestyle[1]))
|
||||
bar.set_linestyle(linestyle)
|
||||
|
||||
# This is a bit of a hack to handle the fact that the edge lines are
|
||||
# centered on the actual extents of the bar, and overlap when bars are
|
||||
# stacked or dodged. We may discover that this causes problems and needs
|
||||
# to be revisited at some point. Also it should be faster to clip with
|
||||
# a bbox than a path, but I cant't work out how to get the intersection
|
||||
# with the axes bbox.
|
||||
bar.set_clip_path(bar.get_path(), bar.get_transform() + ax.transData)
|
||||
if self.artist_kws.get("clip_on", True):
|
||||
# It seems the above hack undoes the default axes clipping
|
||||
bar.set_clip_box(ax.bbox)
|
||||
bar.sticky_edges[val_idx][:] = (0, np.inf)
|
||||
ax.add_patch(bar)
|
||||
|
||||
# Add a container which is useful for, e.g. Axes.bar_label
|
||||
orientation = {"x": "vertical", "y": "horizontal"}[orient]
|
||||
container_kws = dict(datavalues=vals, orientation=orientation)
|
||||
container = mpl.container.BarContainer(bars, **container_kws)
|
||||
ax.add_container(container)
|
||||
|
||||
|
||||
@document_properties
|
||||
@dataclass
|
||||
class Bars(BarBase):
|
||||
"""
|
||||
A faster bar mark with defaults more suitable for histograms.
|
||||
|
||||
See also
|
||||
--------
|
||||
Bar : A bar mark drawn between baseline and data values.
|
||||
|
||||
Examples
|
||||
--------
|
||||
.. include:: ../docstrings/objects.Bars.rst
|
||||
|
||||
"""
|
||||
color: MappableColor = Mappable("C0", grouping=False)
|
||||
alpha: MappableFloat = Mappable(.7, grouping=False)
|
||||
fill: MappableBool = Mappable(True, grouping=False)
|
||||
edgecolor: MappableColor = Mappable(rc="patch.edgecolor", grouping=False)
|
||||
edgealpha: MappableFloat = Mappable(1, grouping=False)
|
||||
edgewidth: MappableFloat = Mappable(auto=True, grouping=False)
|
||||
edgestyle: MappableStyle = Mappable("-", grouping=False)
|
||||
# pattern: MappableString = Mappable(None) # TODO no Property yet
|
||||
|
||||
width: MappableFloat = Mappable(1, grouping=False)
|
||||
baseline: MappableFloat = Mappable(0, grouping=False) # TODO *is* this mappable?
|
||||
|
||||
def _plot(self, split_gen, scales, orient):
|
||||
|
||||
ori_idx = ["x", "y"].index(orient)
|
||||
val_idx = ["y", "x"].index(orient)
|
||||
|
||||
patches = defaultdict(list)
|
||||
for _, data, ax in split_gen():
|
||||
bars, _ = self._make_patches(data, scales, orient)
|
||||
patches[ax].extend(bars)
|
||||
|
||||
collections = {}
|
||||
for ax, ax_patches in patches.items():
|
||||
|
||||
col = mpl.collections.PatchCollection(ax_patches, match_original=True)
|
||||
col.sticky_edges[val_idx][:] = (0, np.inf)
|
||||
ax.add_collection(col, autolim=False)
|
||||
collections[ax] = col
|
||||
|
||||
# Workaround for matplotlib autoscaling bug
|
||||
# https://github.com/matplotlib/matplotlib/issues/11898
|
||||
# https://github.com/matplotlib/matplotlib/issues/23129
|
||||
xys = np.vstack([path.vertices for path in col.get_paths()])
|
||||
ax.update_datalim(xys)
|
||||
|
||||
if "edgewidth" not in scales and isinstance(self.edgewidth, Mappable):
|
||||
|
||||
for ax in collections:
|
||||
ax.autoscale_view()
|
||||
|
||||
def get_dimensions(collection):
|
||||
edges, widths = [], []
|
||||
for verts in (path.vertices for path in collection.get_paths()):
|
||||
edges.append(min(verts[:, ori_idx]))
|
||||
widths.append(np.ptp(verts[:, ori_idx]))
|
||||
return np.array(edges), np.array(widths)
|
||||
|
||||
min_width = np.inf
|
||||
for ax, col in collections.items():
|
||||
edges, widths = get_dimensions(col)
|
||||
points = 72 / ax.figure.dpi * abs(
|
||||
ax.transData.transform([edges + widths] * 2)
|
||||
- ax.transData.transform([edges] * 2)
|
||||
)
|
||||
min_width = min(min_width, min(points[:, ori_idx]))
|
||||
|
||||
linewidth = min(.1 * min_width, mpl.rcParams["patch.linewidth"])
|
||||
for _, col in collections.items():
|
||||
col.set_linewidth(linewidth)
|
||||
317
ftline/lib/python3.12/site-packages/seaborn/_marks/base.py
Normal file
317
ftline/lib/python3.12/site-packages/seaborn/_marks/base.py
Normal file
@@ -0,0 +1,317 @@
|
||||
from __future__ import annotations
|
||||
from dataclasses import dataclass, fields, field
|
||||
import textwrap
|
||||
from typing import Any, Callable, Union
|
||||
from collections.abc import Generator
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
import matplotlib as mpl
|
||||
|
||||
from numpy import ndarray
|
||||
from pandas import DataFrame
|
||||
from matplotlib.artist import Artist
|
||||
|
||||
from seaborn._core.scales import Scale
|
||||
from seaborn._core.properties import (
|
||||
PROPERTIES,
|
||||
Property,
|
||||
RGBATuple,
|
||||
DashPattern,
|
||||
DashPatternWithOffset,
|
||||
)
|
||||
from seaborn._core.exceptions import PlotSpecError
|
||||
|
||||
|
||||
class Mappable:
|
||||
def __init__(
|
||||
self,
|
||||
val: Any = None,
|
||||
depend: str | None = None,
|
||||
rc: str | None = None,
|
||||
auto: bool = False,
|
||||
grouping: bool = True,
|
||||
):
|
||||
"""
|
||||
Property that can be mapped from data or set directly, with flexible defaults.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
val : Any
|
||||
Use this value as the default.
|
||||
depend : str
|
||||
Use the value of this feature as the default.
|
||||
rc : str
|
||||
Use the value of this rcParam as the default.
|
||||
auto : bool
|
||||
The default value will depend on other parameters at compile time.
|
||||
grouping : bool
|
||||
If True, use the mapped variable to define groups.
|
||||
|
||||
"""
|
||||
if depend is not None:
|
||||
assert depend in PROPERTIES
|
||||
if rc is not None:
|
||||
assert rc in mpl.rcParams
|
||||
|
||||
self._val = val
|
||||
self._rc = rc
|
||||
self._depend = depend
|
||||
self._auto = auto
|
||||
self._grouping = grouping
|
||||
|
||||
def __repr__(self):
|
||||
"""Nice formatting for when object appears in Mark init signature."""
|
||||
if self._val is not None:
|
||||
s = f"<{repr(self._val)}>"
|
||||
elif self._depend is not None:
|
||||
s = f"<depend:{self._depend}>"
|
||||
elif self._rc is not None:
|
||||
s = f"<rc:{self._rc}>"
|
||||
elif self._auto:
|
||||
s = "<auto>"
|
||||
else:
|
||||
s = "<undefined>"
|
||||
return s
|
||||
|
||||
@property
|
||||
def depend(self) -> Any:
|
||||
"""Return the name of the feature to source a default value from."""
|
||||
return self._depend
|
||||
|
||||
@property
|
||||
def grouping(self) -> bool:
|
||||
return self._grouping
|
||||
|
||||
@property
|
||||
def default(self) -> Any:
|
||||
"""Get the default value for this feature, or access the relevant rcParam."""
|
||||
if self._val is not None:
|
||||
return self._val
|
||||
elif self._rc is not None:
|
||||
return mpl.rcParams.get(self._rc)
|
||||
|
||||
|
||||
# TODO where is the right place to put this kind of type aliasing?
|
||||
|
||||
MappableBool = Union[bool, Mappable]
|
||||
MappableString = Union[str, Mappable]
|
||||
MappableFloat = Union[float, Mappable]
|
||||
MappableColor = Union[str, tuple, Mappable]
|
||||
MappableStyle = Union[str, DashPattern, DashPatternWithOffset, Mappable]
|
||||
|
||||
|
||||
@dataclass
|
||||
class Mark:
|
||||
"""Base class for objects that visually represent data."""
|
||||
|
||||
artist_kws: dict = field(default_factory=dict)
|
||||
|
||||
@property
|
||||
def _mappable_props(self):
|
||||
return {
|
||||
f.name: getattr(self, f.name) for f in fields(self)
|
||||
if isinstance(f.default, Mappable)
|
||||
}
|
||||
|
||||
@property
|
||||
def _grouping_props(self):
|
||||
# TODO does it make sense to have variation within a Mark's
|
||||
# properties about whether they are grouping?
|
||||
return [
|
||||
f.name for f in fields(self)
|
||||
if isinstance(f.default, Mappable) and f.default.grouping
|
||||
]
|
||||
|
||||
# TODO make this method private? Would extender every need to call directly?
|
||||
def _resolve(
|
||||
self,
|
||||
data: DataFrame | dict[str, Any],
|
||||
name: str,
|
||||
scales: dict[str, Scale] | None = None,
|
||||
) -> Any:
|
||||
"""Obtain default, specified, or mapped value for a named feature.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
data : DataFrame or dict with scalar values
|
||||
Container with data values for features that will be semantically mapped.
|
||||
name : string
|
||||
Identity of the feature / semantic.
|
||||
scales: dict
|
||||
Mapping from variable to corresponding scale object.
|
||||
|
||||
Returns
|
||||
-------
|
||||
value or array of values
|
||||
Outer return type depends on whether `data` is a dict (implying that
|
||||
we want a single value) or DataFrame (implying that we want an array
|
||||
of values with matching length).
|
||||
|
||||
"""
|
||||
feature = self._mappable_props[name]
|
||||
prop = PROPERTIES.get(name, Property(name))
|
||||
directly_specified = not isinstance(feature, Mappable)
|
||||
return_multiple = isinstance(data, pd.DataFrame)
|
||||
return_array = return_multiple and not name.endswith("style")
|
||||
|
||||
# Special case width because it needs to be resolved and added to the dataframe
|
||||
# during layer prep (so the Move operations use it properly).
|
||||
# TODO how does width *scaling* work, e.g. for violin width by count?
|
||||
if name == "width":
|
||||
directly_specified = directly_specified and name not in data
|
||||
|
||||
if directly_specified:
|
||||
feature = prop.standardize(feature)
|
||||
if return_multiple:
|
||||
feature = [feature] * len(data)
|
||||
if return_array:
|
||||
feature = np.array(feature)
|
||||
return feature
|
||||
|
||||
if name in data:
|
||||
if scales is None or name not in scales:
|
||||
# TODO Might this obviate the identity scale? Just don't add a scale?
|
||||
feature = data[name]
|
||||
else:
|
||||
scale = scales[name]
|
||||
value = data[name]
|
||||
try:
|
||||
feature = scale(value)
|
||||
except Exception as err:
|
||||
raise PlotSpecError._during("Scaling operation", name) from err
|
||||
|
||||
if return_array:
|
||||
feature = np.asarray(feature)
|
||||
return feature
|
||||
|
||||
if feature.depend is not None:
|
||||
# TODO add source_func or similar to transform the source value?
|
||||
# e.g. set linewidth as a proportion of pointsize?
|
||||
return self._resolve(data, feature.depend, scales)
|
||||
|
||||
default = prop.standardize(feature.default)
|
||||
if return_multiple:
|
||||
default = [default] * len(data)
|
||||
if return_array:
|
||||
default = np.array(default)
|
||||
return default
|
||||
|
||||
def _infer_orient(self, scales: dict) -> str: # TODO type scales
|
||||
|
||||
# TODO The original version of this (in seaborn._base) did more checking.
|
||||
# Paring that down here for the prototype to see what restrictions make sense.
|
||||
|
||||
# TODO rethink this to map from scale type to "DV priority" and use that?
|
||||
# e.g. Nominal > Discrete > Continuous
|
||||
|
||||
x = 0 if "x" not in scales else scales["x"]._priority
|
||||
y = 0 if "y" not in scales else scales["y"]._priority
|
||||
|
||||
if y > x:
|
||||
return "y"
|
||||
else:
|
||||
return "x"
|
||||
|
||||
def _plot(
|
||||
self,
|
||||
split_generator: Callable[[], Generator],
|
||||
scales: dict[str, Scale],
|
||||
orient: str,
|
||||
) -> None:
|
||||
"""Main interface for creating a plot."""
|
||||
raise NotImplementedError()
|
||||
|
||||
def _legend_artist(
|
||||
self, variables: list[str], value: Any, scales: dict[str, Scale],
|
||||
) -> Artist | None:
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def resolve_properties(
|
||||
mark: Mark, data: DataFrame, scales: dict[str, Scale]
|
||||
) -> dict[str, Any]:
|
||||
|
||||
props = {
|
||||
name: mark._resolve(data, name, scales) for name in mark._mappable_props
|
||||
}
|
||||
return props
|
||||
|
||||
|
||||
def resolve_color(
|
||||
mark: Mark,
|
||||
data: DataFrame | dict,
|
||||
prefix: str = "",
|
||||
scales: dict[str, Scale] | None = None,
|
||||
) -> RGBATuple | ndarray:
|
||||
"""
|
||||
Obtain a default, specified, or mapped value for a color feature.
|
||||
|
||||
This method exists separately to support the relationship between a
|
||||
color and its corresponding alpha. We want to respect alpha values that
|
||||
are passed in specified (or mapped) color values but also make use of a
|
||||
separate `alpha` variable, which can be mapped. This approach may also
|
||||
be extended to support mapping of specific color channels (i.e.
|
||||
luminance, chroma) in the future.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
mark :
|
||||
Mark with the color property.
|
||||
data :
|
||||
Container with data values for features that will be semantically mapped.
|
||||
prefix :
|
||||
Support "color", "fillcolor", etc.
|
||||
|
||||
"""
|
||||
color = mark._resolve(data, f"{prefix}color", scales)
|
||||
|
||||
if f"{prefix}alpha" in mark._mappable_props:
|
||||
alpha = mark._resolve(data, f"{prefix}alpha", scales)
|
||||
else:
|
||||
alpha = mark._resolve(data, "alpha", scales)
|
||||
|
||||
def visible(x, axis=None):
|
||||
"""Detect "invisible" colors to set alpha appropriately."""
|
||||
# TODO First clause only needed to handle non-rgba arrays,
|
||||
# which we are trying to handle upstream
|
||||
return np.array(x).dtype.kind != "f" or np.isfinite(x).all(axis)
|
||||
|
||||
# Second check here catches vectors of strings with identity scale
|
||||
# It could probably be handled better upstream. This is a tricky problem
|
||||
if np.ndim(color) < 2 and all(isinstance(x, float) for x in color):
|
||||
if len(color) == 4:
|
||||
return mpl.colors.to_rgba(color)
|
||||
alpha = alpha if visible(color) else np.nan
|
||||
return mpl.colors.to_rgba(color, alpha)
|
||||
else:
|
||||
if np.ndim(color) == 2 and color.shape[1] == 4:
|
||||
return mpl.colors.to_rgba_array(color)
|
||||
alpha = np.where(visible(color, axis=1), alpha, np.nan)
|
||||
return mpl.colors.to_rgba_array(color, alpha)
|
||||
|
||||
# TODO should we be implementing fill here too?
|
||||
# (i.e. set fillalpha to 0 when fill=False)
|
||||
|
||||
|
||||
def document_properties(mark):
|
||||
|
||||
properties = [f.name for f in fields(mark) if isinstance(f.default, Mappable)]
|
||||
text = [
|
||||
"",
|
||||
" This mark defines the following properties:",
|
||||
textwrap.fill(
|
||||
", ".join([f"|{p}|" for p in properties]),
|
||||
width=78, initial_indent=" " * 8, subsequent_indent=" " * 8,
|
||||
),
|
||||
]
|
||||
|
||||
docstring_lines = mark.__doc__.split("\n")
|
||||
new_docstring = "\n".join([
|
||||
*docstring_lines[:2],
|
||||
*text,
|
||||
*docstring_lines[2:],
|
||||
])
|
||||
mark.__doc__ = new_docstring
|
||||
return mark
|
||||
200
ftline/lib/python3.12/site-packages/seaborn/_marks/dot.py
Normal file
200
ftline/lib/python3.12/site-packages/seaborn/_marks/dot.py
Normal file
@@ -0,0 +1,200 @@
|
||||
from __future__ import annotations
|
||||
from dataclasses import dataclass
|
||||
|
||||
import numpy as np
|
||||
import matplotlib as mpl
|
||||
|
||||
from seaborn._marks.base import (
|
||||
Mark,
|
||||
Mappable,
|
||||
MappableBool,
|
||||
MappableFloat,
|
||||
MappableString,
|
||||
MappableColor,
|
||||
MappableStyle,
|
||||
resolve_properties,
|
||||
resolve_color,
|
||||
document_properties,
|
||||
)
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
if TYPE_CHECKING:
|
||||
from typing import Any
|
||||
from matplotlib.artist import Artist
|
||||
from seaborn._core.scales import Scale
|
||||
|
||||
|
||||
class DotBase(Mark):
|
||||
|
||||
def _resolve_paths(self, data):
|
||||
|
||||
paths = []
|
||||
path_cache = {}
|
||||
marker = data["marker"]
|
||||
|
||||
def get_transformed_path(m):
|
||||
return m.get_path().transformed(m.get_transform())
|
||||
|
||||
if isinstance(marker, mpl.markers.MarkerStyle):
|
||||
return get_transformed_path(marker)
|
||||
|
||||
for m in marker:
|
||||
if m not in path_cache:
|
||||
path_cache[m] = get_transformed_path(m)
|
||||
paths.append(path_cache[m])
|
||||
return paths
|
||||
|
||||
def _resolve_properties(self, data, scales):
|
||||
|
||||
resolved = resolve_properties(self, data, scales)
|
||||
resolved["path"] = self._resolve_paths(resolved)
|
||||
resolved["size"] = resolved["pointsize"] ** 2
|
||||
|
||||
if isinstance(data, dict): # Properties for single dot
|
||||
filled_marker = resolved["marker"].is_filled()
|
||||
else:
|
||||
filled_marker = [m.is_filled() for m in resolved["marker"]]
|
||||
|
||||
resolved["fill"] = resolved["fill"] * filled_marker
|
||||
|
||||
return resolved
|
||||
|
||||
def _plot(self, split_gen, scales, orient):
|
||||
|
||||
# TODO Not backcompat with allowed (but nonfunctional) univariate plots
|
||||
# (That should be solved upstream by defaulting to "" for unset x/y?)
|
||||
# (Be mindful of xmin/xmax, etc!)
|
||||
|
||||
for _, data, ax in split_gen():
|
||||
|
||||
offsets = np.column_stack([data["x"], data["y"]])
|
||||
data = self._resolve_properties(data, scales)
|
||||
|
||||
points = mpl.collections.PathCollection(
|
||||
offsets=offsets,
|
||||
paths=data["path"],
|
||||
sizes=data["size"],
|
||||
facecolors=data["facecolor"],
|
||||
edgecolors=data["edgecolor"],
|
||||
linewidths=data["linewidth"],
|
||||
linestyles=data["edgestyle"],
|
||||
transOffset=ax.transData,
|
||||
transform=mpl.transforms.IdentityTransform(),
|
||||
**self.artist_kws,
|
||||
)
|
||||
ax.add_collection(points)
|
||||
|
||||
def _legend_artist(
|
||||
self, variables: list[str], value: Any, scales: dict[str, Scale],
|
||||
) -> Artist:
|
||||
|
||||
key = {v: value for v in variables}
|
||||
res = self._resolve_properties(key, scales)
|
||||
|
||||
return mpl.collections.PathCollection(
|
||||
paths=[res["path"]],
|
||||
sizes=[res["size"]],
|
||||
facecolors=[res["facecolor"]],
|
||||
edgecolors=[res["edgecolor"]],
|
||||
linewidths=[res["linewidth"]],
|
||||
linestyles=[res["edgestyle"]],
|
||||
transform=mpl.transforms.IdentityTransform(),
|
||||
**self.artist_kws,
|
||||
)
|
||||
|
||||
|
||||
@document_properties
|
||||
@dataclass
|
||||
class Dot(DotBase):
|
||||
"""
|
||||
A mark suitable for dot plots or less-dense scatterplots.
|
||||
|
||||
See also
|
||||
--------
|
||||
Dots : A dot mark defined by strokes to better handle overplotting.
|
||||
|
||||
Examples
|
||||
--------
|
||||
.. include:: ../docstrings/objects.Dot.rst
|
||||
|
||||
"""
|
||||
marker: MappableString = Mappable("o", grouping=False)
|
||||
pointsize: MappableFloat = Mappable(6, grouping=False) # TODO rcParam?
|
||||
stroke: MappableFloat = Mappable(.75, grouping=False) # TODO rcParam?
|
||||
color: MappableColor = Mappable("C0", grouping=False)
|
||||
alpha: MappableFloat = Mappable(1, grouping=False)
|
||||
fill: MappableBool = Mappable(True, grouping=False)
|
||||
edgecolor: MappableColor = Mappable(depend="color", grouping=False)
|
||||
edgealpha: MappableFloat = Mappable(depend="alpha", grouping=False)
|
||||
edgewidth: MappableFloat = Mappable(.5, grouping=False) # TODO rcParam?
|
||||
edgestyle: MappableStyle = Mappable("-", grouping=False)
|
||||
|
||||
def _resolve_properties(self, data, scales):
|
||||
|
||||
resolved = super()._resolve_properties(data, scales)
|
||||
filled = resolved["fill"]
|
||||
|
||||
main_stroke = resolved["stroke"]
|
||||
edge_stroke = resolved["edgewidth"]
|
||||
resolved["linewidth"] = np.where(filled, edge_stroke, main_stroke)
|
||||
|
||||
main_color = resolve_color(self, data, "", scales)
|
||||
edge_color = resolve_color(self, data, "edge", scales)
|
||||
|
||||
if not np.isscalar(filled):
|
||||
# Expand dims to use in np.where with rgba arrays
|
||||
filled = filled[:, None]
|
||||
resolved["edgecolor"] = np.where(filled, edge_color, main_color)
|
||||
|
||||
filled = np.squeeze(filled)
|
||||
if isinstance(main_color, tuple):
|
||||
# TODO handle this in resolve_color
|
||||
main_color = tuple([*main_color[:3], main_color[3] * filled])
|
||||
else:
|
||||
main_color = np.c_[main_color[:, :3], main_color[:, 3] * filled]
|
||||
resolved["facecolor"] = main_color
|
||||
|
||||
return resolved
|
||||
|
||||
|
||||
@document_properties
|
||||
@dataclass
|
||||
class Dots(DotBase):
|
||||
"""
|
||||
A dot mark defined by strokes to better handle overplotting.
|
||||
|
||||
See also
|
||||
--------
|
||||
Dot : A mark suitable for dot plots or less-dense scatterplots.
|
||||
|
||||
Examples
|
||||
--------
|
||||
.. include:: ../docstrings/objects.Dots.rst
|
||||
|
||||
"""
|
||||
# TODO retype marker as MappableMarker
|
||||
marker: MappableString = Mappable(rc="scatter.marker", grouping=False)
|
||||
pointsize: MappableFloat = Mappable(4, grouping=False) # TODO rcParam?
|
||||
stroke: MappableFloat = Mappable(.75, grouping=False) # TODO rcParam?
|
||||
color: MappableColor = Mappable("C0", grouping=False)
|
||||
alpha: MappableFloat = Mappable(1, grouping=False) # TODO auto alpha?
|
||||
fill: MappableBool = Mappable(True, grouping=False)
|
||||
fillcolor: MappableColor = Mappable(depend="color", grouping=False)
|
||||
fillalpha: MappableFloat = Mappable(.2, grouping=False)
|
||||
|
||||
def _resolve_properties(self, data, scales):
|
||||
|
||||
resolved = super()._resolve_properties(data, scales)
|
||||
resolved["linewidth"] = resolved.pop("stroke")
|
||||
resolved["facecolor"] = resolve_color(self, data, "fill", scales)
|
||||
resolved["edgecolor"] = resolve_color(self, data, "", scales)
|
||||
resolved.setdefault("edgestyle", (0, None))
|
||||
|
||||
fc = resolved["facecolor"]
|
||||
if isinstance(fc, tuple):
|
||||
resolved["facecolor"] = fc[0], fc[1], fc[2], fc[3] * resolved["fill"]
|
||||
else:
|
||||
fc[:, 3] = fc[:, 3] * resolved["fill"] # TODO Is inplace mod a problem?
|
||||
resolved["facecolor"] = fc
|
||||
|
||||
return resolved
|
||||
285
ftline/lib/python3.12/site-packages/seaborn/_marks/line.py
Normal file
285
ftline/lib/python3.12/site-packages/seaborn/_marks/line.py
Normal file
@@ -0,0 +1,285 @@
|
||||
from __future__ import annotations
|
||||
from dataclasses import dataclass
|
||||
from typing import ClassVar
|
||||
|
||||
import numpy as np
|
||||
import matplotlib as mpl
|
||||
|
||||
from seaborn._marks.base import (
|
||||
Mark,
|
||||
Mappable,
|
||||
MappableFloat,
|
||||
MappableString,
|
||||
MappableColor,
|
||||
resolve_properties,
|
||||
resolve_color,
|
||||
document_properties,
|
||||
)
|
||||
|
||||
|
||||
@document_properties
|
||||
@dataclass
|
||||
class Path(Mark):
|
||||
"""
|
||||
A mark connecting data points in the order they appear.
|
||||
|
||||
See also
|
||||
--------
|
||||
Line : A mark connecting data points with sorting along the orientation axis.
|
||||
Paths : A faster but less-flexible mark for drawing many paths.
|
||||
|
||||
Examples
|
||||
--------
|
||||
.. include:: ../docstrings/objects.Path.rst
|
||||
|
||||
"""
|
||||
color: MappableColor = Mappable("C0")
|
||||
alpha: MappableFloat = Mappable(1)
|
||||
linewidth: MappableFloat = Mappable(rc="lines.linewidth")
|
||||
linestyle: MappableString = Mappable(rc="lines.linestyle")
|
||||
marker: MappableString = Mappable(rc="lines.marker")
|
||||
pointsize: MappableFloat = Mappable(rc="lines.markersize")
|
||||
fillcolor: MappableColor = Mappable(depend="color")
|
||||
edgecolor: MappableColor = Mappable(depend="color")
|
||||
edgewidth: MappableFloat = Mappable(rc="lines.markeredgewidth")
|
||||
|
||||
_sort: ClassVar[bool] = False
|
||||
|
||||
def _plot(self, split_gen, scales, orient):
|
||||
|
||||
for keys, data, ax in split_gen(keep_na=not self._sort):
|
||||
|
||||
vals = resolve_properties(self, keys, scales)
|
||||
vals["color"] = resolve_color(self, keys, scales=scales)
|
||||
vals["fillcolor"] = resolve_color(self, keys, prefix="fill", scales=scales)
|
||||
vals["edgecolor"] = resolve_color(self, keys, prefix="edge", scales=scales)
|
||||
|
||||
if self._sort:
|
||||
data = data.sort_values(orient, kind="mergesort")
|
||||
|
||||
artist_kws = self.artist_kws.copy()
|
||||
self._handle_capstyle(artist_kws, vals)
|
||||
|
||||
line = mpl.lines.Line2D(
|
||||
data["x"].to_numpy(),
|
||||
data["y"].to_numpy(),
|
||||
color=vals["color"],
|
||||
linewidth=vals["linewidth"],
|
||||
linestyle=vals["linestyle"],
|
||||
marker=vals["marker"],
|
||||
markersize=vals["pointsize"],
|
||||
markerfacecolor=vals["fillcolor"],
|
||||
markeredgecolor=vals["edgecolor"],
|
||||
markeredgewidth=vals["edgewidth"],
|
||||
**artist_kws,
|
||||
)
|
||||
ax.add_line(line)
|
||||
|
||||
def _legend_artist(self, variables, value, scales):
|
||||
|
||||
keys = {v: value for v in variables}
|
||||
vals = resolve_properties(self, keys, scales)
|
||||
vals["color"] = resolve_color(self, keys, scales=scales)
|
||||
vals["fillcolor"] = resolve_color(self, keys, prefix="fill", scales=scales)
|
||||
vals["edgecolor"] = resolve_color(self, keys, prefix="edge", scales=scales)
|
||||
|
||||
artist_kws = self.artist_kws.copy()
|
||||
self._handle_capstyle(artist_kws, vals)
|
||||
|
||||
return mpl.lines.Line2D(
|
||||
[], [],
|
||||
color=vals["color"],
|
||||
linewidth=vals["linewidth"],
|
||||
linestyle=vals["linestyle"],
|
||||
marker=vals["marker"],
|
||||
markersize=vals["pointsize"],
|
||||
markerfacecolor=vals["fillcolor"],
|
||||
markeredgecolor=vals["edgecolor"],
|
||||
markeredgewidth=vals["edgewidth"],
|
||||
**artist_kws,
|
||||
)
|
||||
|
||||
def _handle_capstyle(self, kws, vals):
|
||||
|
||||
# Work around for this matplotlib issue:
|
||||
# https://github.com/matplotlib/matplotlib/issues/23437
|
||||
if vals["linestyle"][1] is None:
|
||||
capstyle = kws.get("solid_capstyle", mpl.rcParams["lines.solid_capstyle"])
|
||||
kws["dash_capstyle"] = capstyle
|
||||
|
||||
|
||||
@document_properties
|
||||
@dataclass
|
||||
class Line(Path):
|
||||
"""
|
||||
A mark connecting data points with sorting along the orientation axis.
|
||||
|
||||
See also
|
||||
--------
|
||||
Path : A mark connecting data points in the order they appear.
|
||||
Lines : A faster but less-flexible mark for drawing many lines.
|
||||
|
||||
Examples
|
||||
--------
|
||||
.. include:: ../docstrings/objects.Line.rst
|
||||
|
||||
"""
|
||||
_sort: ClassVar[bool] = True
|
||||
|
||||
|
||||
@document_properties
|
||||
@dataclass
|
||||
class Paths(Mark):
|
||||
"""
|
||||
A faster but less-flexible mark for drawing many paths.
|
||||
|
||||
See also
|
||||
--------
|
||||
Path : A mark connecting data points in the order they appear.
|
||||
|
||||
Examples
|
||||
--------
|
||||
.. include:: ../docstrings/objects.Paths.rst
|
||||
|
||||
"""
|
||||
color: MappableColor = Mappable("C0")
|
||||
alpha: MappableFloat = Mappable(1)
|
||||
linewidth: MappableFloat = Mappable(rc="lines.linewidth")
|
||||
linestyle: MappableString = Mappable(rc="lines.linestyle")
|
||||
|
||||
_sort: ClassVar[bool] = False
|
||||
|
||||
def __post_init__(self):
|
||||
|
||||
# LineCollection artists have a capstyle property but don't source its value
|
||||
# from the rc, so we do that manually here. Unfortunately, because we add
|
||||
# only one LineCollection, we have the use the same capstyle for all lines
|
||||
# even when they are dashed. It's a slight inconsistency, but looks fine IMO.
|
||||
self.artist_kws.setdefault("capstyle", mpl.rcParams["lines.solid_capstyle"])
|
||||
|
||||
def _plot(self, split_gen, scales, orient):
|
||||
|
||||
line_data = {}
|
||||
for keys, data, ax in split_gen(keep_na=not self._sort):
|
||||
|
||||
if ax not in line_data:
|
||||
line_data[ax] = {
|
||||
"segments": [],
|
||||
"colors": [],
|
||||
"linewidths": [],
|
||||
"linestyles": [],
|
||||
}
|
||||
|
||||
segments = self._setup_segments(data, orient)
|
||||
line_data[ax]["segments"].extend(segments)
|
||||
n = len(segments)
|
||||
|
||||
vals = resolve_properties(self, keys, scales)
|
||||
vals["color"] = resolve_color(self, keys, scales=scales)
|
||||
|
||||
line_data[ax]["colors"].extend([vals["color"]] * n)
|
||||
line_data[ax]["linewidths"].extend([vals["linewidth"]] * n)
|
||||
line_data[ax]["linestyles"].extend([vals["linestyle"]] * n)
|
||||
|
||||
for ax, ax_data in line_data.items():
|
||||
lines = mpl.collections.LineCollection(**ax_data, **self.artist_kws)
|
||||
# Handle datalim update manually
|
||||
# https://github.com/matplotlib/matplotlib/issues/23129
|
||||
ax.add_collection(lines, autolim=False)
|
||||
if ax_data["segments"]:
|
||||
xy = np.concatenate(ax_data["segments"])
|
||||
ax.update_datalim(xy)
|
||||
|
||||
def _legend_artist(self, variables, value, scales):
|
||||
|
||||
key = resolve_properties(self, {v: value for v in variables}, scales)
|
||||
|
||||
artist_kws = self.artist_kws.copy()
|
||||
capstyle = artist_kws.pop("capstyle")
|
||||
artist_kws["solid_capstyle"] = capstyle
|
||||
artist_kws["dash_capstyle"] = capstyle
|
||||
|
||||
return mpl.lines.Line2D(
|
||||
[], [],
|
||||
color=key["color"],
|
||||
linewidth=key["linewidth"],
|
||||
linestyle=key["linestyle"],
|
||||
**artist_kws,
|
||||
)
|
||||
|
||||
def _setup_segments(self, data, orient):
|
||||
|
||||
if self._sort:
|
||||
data = data.sort_values(orient, kind="mergesort")
|
||||
|
||||
# Column stack to avoid block consolidation
|
||||
xy = np.column_stack([data["x"], data["y"]])
|
||||
|
||||
return [xy]
|
||||
|
||||
|
||||
@document_properties
|
||||
@dataclass
|
||||
class Lines(Paths):
|
||||
"""
|
||||
A faster but less-flexible mark for drawing many lines.
|
||||
|
||||
See also
|
||||
--------
|
||||
Line : A mark connecting data points with sorting along the orientation axis.
|
||||
|
||||
Examples
|
||||
--------
|
||||
.. include:: ../docstrings/objects.Lines.rst
|
||||
|
||||
"""
|
||||
_sort: ClassVar[bool] = True
|
||||
|
||||
|
||||
@document_properties
|
||||
@dataclass
|
||||
class Range(Paths):
|
||||
"""
|
||||
An oriented line mark drawn between min/max values.
|
||||
|
||||
Examples
|
||||
--------
|
||||
.. include:: ../docstrings/objects.Range.rst
|
||||
|
||||
"""
|
||||
def _setup_segments(self, data, orient):
|
||||
|
||||
# TODO better checks on what variables we have
|
||||
# TODO what if only one exist?
|
||||
val = {"x": "y", "y": "x"}[orient]
|
||||
if not set(data.columns) & {f"{val}min", f"{val}max"}:
|
||||
agg = {f"{val}min": (val, "min"), f"{val}max": (val, "max")}
|
||||
data = data.groupby(orient).agg(**agg).reset_index()
|
||||
|
||||
cols = [orient, f"{val}min", f"{val}max"]
|
||||
data = data[cols].melt(orient, value_name=val)[["x", "y"]]
|
||||
segments = [d.to_numpy() for _, d in data.groupby(orient)]
|
||||
return segments
|
||||
|
||||
|
||||
@document_properties
|
||||
@dataclass
|
||||
class Dash(Paths):
|
||||
"""
|
||||
A line mark drawn as an oriented segment for each datapoint.
|
||||
|
||||
Examples
|
||||
--------
|
||||
.. include:: ../docstrings/objects.Dash.rst
|
||||
|
||||
"""
|
||||
width: MappableFloat = Mappable(.8, grouping=False)
|
||||
|
||||
def _setup_segments(self, data, orient):
|
||||
|
||||
ori = ["x", "y"].index(orient)
|
||||
xys = data[["x", "y"]].to_numpy().astype(float)
|
||||
segments = np.stack([xys, xys], axis=1)
|
||||
segments[:, 0, ori] -= data["width"] / 2
|
||||
segments[:, 1, ori] += data["width"] / 2
|
||||
return segments
|
||||
76
ftline/lib/python3.12/site-packages/seaborn/_marks/text.py
Normal file
76
ftline/lib/python3.12/site-packages/seaborn/_marks/text.py
Normal file
@@ -0,0 +1,76 @@
|
||||
from __future__ import annotations
|
||||
from collections import defaultdict
|
||||
from dataclasses import dataclass
|
||||
|
||||
import numpy as np
|
||||
import matplotlib as mpl
|
||||
from matplotlib.transforms import ScaledTranslation
|
||||
|
||||
from seaborn._marks.base import (
|
||||
Mark,
|
||||
Mappable,
|
||||
MappableFloat,
|
||||
MappableString,
|
||||
MappableColor,
|
||||
resolve_properties,
|
||||
resolve_color,
|
||||
document_properties,
|
||||
)
|
||||
|
||||
|
||||
@document_properties
|
||||
@dataclass
|
||||
class Text(Mark):
|
||||
"""
|
||||
A textual mark to annotate or represent data values.
|
||||
|
||||
Examples
|
||||
--------
|
||||
.. include:: ../docstrings/objects.Text.rst
|
||||
|
||||
"""
|
||||
text: MappableString = Mappable("")
|
||||
color: MappableColor = Mappable("k")
|
||||
alpha: MappableFloat = Mappable(1)
|
||||
fontsize: MappableFloat = Mappable(rc="font.size")
|
||||
halign: MappableString = Mappable("center")
|
||||
valign: MappableString = Mappable("center_baseline")
|
||||
offset: MappableFloat = Mappable(4)
|
||||
|
||||
def _plot(self, split_gen, scales, orient):
|
||||
|
||||
ax_data = defaultdict(list)
|
||||
|
||||
for keys, data, ax in split_gen():
|
||||
|
||||
vals = resolve_properties(self, keys, scales)
|
||||
color = resolve_color(self, keys, "", scales)
|
||||
|
||||
halign = vals["halign"]
|
||||
valign = vals["valign"]
|
||||
fontsize = vals["fontsize"]
|
||||
offset = vals["offset"] / 72
|
||||
|
||||
offset_trans = ScaledTranslation(
|
||||
{"right": -offset, "left": +offset}.get(halign, 0),
|
||||
{"top": -offset, "bottom": +offset, "baseline": +offset}.get(valign, 0),
|
||||
ax.figure.dpi_scale_trans,
|
||||
)
|
||||
|
||||
for row in data.to_dict("records"):
|
||||
artist = mpl.text.Text(
|
||||
x=row["x"],
|
||||
y=row["y"],
|
||||
text=str(row.get("text", vals["text"])),
|
||||
color=color,
|
||||
fontsize=fontsize,
|
||||
horizontalalignment=halign,
|
||||
verticalalignment=valign,
|
||||
transform=ax.transData + offset_trans,
|
||||
**self.artist_kws,
|
||||
)
|
||||
ax.add_artist(artist)
|
||||
ax_data[ax].append([row["x"], row["y"]])
|
||||
|
||||
for ax, ax_vals in ax_data.items():
|
||||
ax.update_datalim(np.array(ax_vals))
|
||||
698
ftline/lib/python3.12/site-packages/seaborn/_statistics.py
Normal file
698
ftline/lib/python3.12/site-packages/seaborn/_statistics.py
Normal file
@@ -0,0 +1,698 @@
|
||||
"""Statistical transformations for visualization.
|
||||
|
||||
This module is currently private, but is being written to eventually form part
|
||||
of the public API.
|
||||
|
||||
The classes should behave roughly in the style of scikit-learn.
|
||||
|
||||
- All data-independent parameters should be passed to the class constructor.
|
||||
- Each class should implement a default transformation that is exposed through
|
||||
__call__. These are currently written for vector arguments, but I think
|
||||
consuming a whole `plot_data` DataFrame and return it with transformed
|
||||
variables would make more sense.
|
||||
- Some class have data-dependent preprocessing that should be cached and used
|
||||
multiple times (think defining histogram bins off all data and then counting
|
||||
observations within each bin multiple times per data subsets). These currently
|
||||
have unique names, but it would be good to have a common name. Not quite
|
||||
`fit`, but something similar.
|
||||
- Alternatively, the transform interface could take some information about grouping
|
||||
variables and do a groupby internally.
|
||||
- Some classes should define alternate transforms that might make the most sense
|
||||
with a different function. For example, KDE usually evaluates the distribution
|
||||
on a regular grid, but it would be useful for it to transform at the actual
|
||||
datapoints. Then again, this could be controlled by a parameter at the time of
|
||||
class instantiation.
|
||||
|
||||
"""
|
||||
from numbers import Number
|
||||
from statistics import NormalDist
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
try:
|
||||
from scipy.stats import gaussian_kde
|
||||
_no_scipy = False
|
||||
except ImportError:
|
||||
from .external.kde import gaussian_kde
|
||||
_no_scipy = True
|
||||
|
||||
from .algorithms import bootstrap
|
||||
from .utils import _check_argument
|
||||
|
||||
|
||||
class KDE:
|
||||
"""Univariate and bivariate kernel density estimator."""
|
||||
def __init__(
|
||||
self, *,
|
||||
bw_method=None,
|
||||
bw_adjust=1,
|
||||
gridsize=200,
|
||||
cut=3,
|
||||
clip=None,
|
||||
cumulative=False,
|
||||
):
|
||||
"""Initialize the estimator with its parameters.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
bw_method : string, scalar, or callable, optional
|
||||
Method for determining the smoothing bandwidth to use; passed to
|
||||
:class:`scipy.stats.gaussian_kde`.
|
||||
bw_adjust : number, optional
|
||||
Factor that multiplicatively scales the value chosen using
|
||||
``bw_method``. Increasing will make the curve smoother. See Notes.
|
||||
gridsize : int, optional
|
||||
Number of points on each dimension of the evaluation grid.
|
||||
cut : number, optional
|
||||
Factor, multiplied by the smoothing bandwidth, that determines how
|
||||
far the evaluation grid extends past the extreme datapoints. When
|
||||
set to 0, truncate the curve at the data limits.
|
||||
clip : pair of numbers or None, or a pair of such pairs
|
||||
Do not evaluate the density outside of these limits.
|
||||
cumulative : bool, optional
|
||||
If True, estimate a cumulative distribution function. Requires scipy.
|
||||
|
||||
"""
|
||||
if clip is None:
|
||||
clip = None, None
|
||||
|
||||
self.bw_method = bw_method
|
||||
self.bw_adjust = bw_adjust
|
||||
self.gridsize = gridsize
|
||||
self.cut = cut
|
||||
self.clip = clip
|
||||
self.cumulative = cumulative
|
||||
|
||||
if cumulative and _no_scipy:
|
||||
raise RuntimeError("Cumulative KDE evaluation requires scipy")
|
||||
|
||||
self.support = None
|
||||
|
||||
def _define_support_grid(self, x, bw, cut, clip, gridsize):
|
||||
"""Create the grid of evaluation points depending for vector x."""
|
||||
clip_lo = -np.inf if clip[0] is None else clip[0]
|
||||
clip_hi = +np.inf if clip[1] is None else clip[1]
|
||||
gridmin = max(x.min() - bw * cut, clip_lo)
|
||||
gridmax = min(x.max() + bw * cut, clip_hi)
|
||||
return np.linspace(gridmin, gridmax, gridsize)
|
||||
|
||||
def _define_support_univariate(self, x, weights):
|
||||
"""Create a 1D grid of evaluation points."""
|
||||
kde = self._fit(x, weights)
|
||||
bw = np.sqrt(kde.covariance.squeeze())
|
||||
grid = self._define_support_grid(
|
||||
x, bw, self.cut, self.clip, self.gridsize
|
||||
)
|
||||
return grid
|
||||
|
||||
def _define_support_bivariate(self, x1, x2, weights):
|
||||
"""Create a 2D grid of evaluation points."""
|
||||
clip = self.clip
|
||||
if clip[0] is None or np.isscalar(clip[0]):
|
||||
clip = (clip, clip)
|
||||
|
||||
kde = self._fit([x1, x2], weights)
|
||||
bw = np.sqrt(np.diag(kde.covariance).squeeze())
|
||||
|
||||
grid1 = self._define_support_grid(
|
||||
x1, bw[0], self.cut, clip[0], self.gridsize
|
||||
)
|
||||
grid2 = self._define_support_grid(
|
||||
x2, bw[1], self.cut, clip[1], self.gridsize
|
||||
)
|
||||
|
||||
return grid1, grid2
|
||||
|
||||
def define_support(self, x1, x2=None, weights=None, cache=True):
|
||||
"""Create the evaluation grid for a given data set."""
|
||||
if x2 is None:
|
||||
support = self._define_support_univariate(x1, weights)
|
||||
else:
|
||||
support = self._define_support_bivariate(x1, x2, weights)
|
||||
|
||||
if cache:
|
||||
self.support = support
|
||||
|
||||
return support
|
||||
|
||||
def _fit(self, fit_data, weights=None):
|
||||
"""Fit the scipy kde while adding bw_adjust logic and version check."""
|
||||
fit_kws = {"bw_method": self.bw_method}
|
||||
if weights is not None:
|
||||
fit_kws["weights"] = weights
|
||||
|
||||
kde = gaussian_kde(fit_data, **fit_kws)
|
||||
kde.set_bandwidth(kde.factor * self.bw_adjust)
|
||||
|
||||
return kde
|
||||
|
||||
def _eval_univariate(self, x, weights=None):
|
||||
"""Fit and evaluate a univariate on univariate data."""
|
||||
support = self.support
|
||||
if support is None:
|
||||
support = self.define_support(x, cache=False)
|
||||
|
||||
kde = self._fit(x, weights)
|
||||
|
||||
if self.cumulative:
|
||||
s_0 = support[0]
|
||||
density = np.array([
|
||||
kde.integrate_box_1d(s_0, s_i) for s_i in support
|
||||
])
|
||||
else:
|
||||
density = kde(support)
|
||||
|
||||
return density, support
|
||||
|
||||
def _eval_bivariate(self, x1, x2, weights=None):
|
||||
"""Fit and evaluate a univariate on bivariate data."""
|
||||
support = self.support
|
||||
if support is None:
|
||||
support = self.define_support(x1, x2, cache=False)
|
||||
|
||||
kde = self._fit([x1, x2], weights)
|
||||
|
||||
if self.cumulative:
|
||||
|
||||
grid1, grid2 = support
|
||||
density = np.zeros((grid1.size, grid2.size))
|
||||
p0 = grid1.min(), grid2.min()
|
||||
for i, xi in enumerate(grid1):
|
||||
for j, xj in enumerate(grid2):
|
||||
density[i, j] = kde.integrate_box(p0, (xi, xj))
|
||||
|
||||
else:
|
||||
|
||||
xx1, xx2 = np.meshgrid(*support)
|
||||
density = kde([xx1.ravel(), xx2.ravel()]).reshape(xx1.shape)
|
||||
|
||||
return density, support
|
||||
|
||||
def __call__(self, x1, x2=None, weights=None):
|
||||
"""Fit and evaluate on univariate or bivariate data."""
|
||||
if x2 is None:
|
||||
return self._eval_univariate(x1, weights)
|
||||
else:
|
||||
return self._eval_bivariate(x1, x2, weights)
|
||||
|
||||
|
||||
# Note: we no longer use this for univariate histograms in histplot,
|
||||
# preferring _stats.Hist. We'll deprecate this once we have a bivariate Stat class.
|
||||
class Histogram:
|
||||
"""Univariate and bivariate histogram estimator."""
|
||||
def __init__(
|
||||
self,
|
||||
stat="count",
|
||||
bins="auto",
|
||||
binwidth=None,
|
||||
binrange=None,
|
||||
discrete=False,
|
||||
cumulative=False,
|
||||
):
|
||||
"""Initialize the estimator with its parameters.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
stat : str
|
||||
Aggregate statistic to compute in each bin.
|
||||
|
||||
- `count`: show the number of observations in each bin
|
||||
- `frequency`: show the number of observations divided by the bin width
|
||||
- `probability` or `proportion`: normalize such that bar heights sum to 1
|
||||
- `percent`: normalize such that bar heights sum to 100
|
||||
- `density`: normalize such that the total area of the histogram equals 1
|
||||
|
||||
bins : str, number, vector, or a pair of such values
|
||||
Generic bin parameter that can be the name of a reference rule,
|
||||
the number of bins, or the breaks of the bins.
|
||||
Passed to :func:`numpy.histogram_bin_edges`.
|
||||
binwidth : number or pair of numbers
|
||||
Width of each bin, overrides ``bins`` but can be used with
|
||||
``binrange``.
|
||||
binrange : pair of numbers or a pair of pairs
|
||||
Lowest and highest value for bin edges; can be used either
|
||||
with ``bins`` or ``binwidth``. Defaults to data extremes.
|
||||
discrete : bool or pair of bools
|
||||
If True, set ``binwidth`` and ``binrange`` such that bin
|
||||
edges cover integer values in the dataset.
|
||||
cumulative : bool
|
||||
If True, return the cumulative statistic.
|
||||
|
||||
"""
|
||||
stat_choices = [
|
||||
"count", "frequency", "density", "probability", "proportion", "percent",
|
||||
]
|
||||
_check_argument("stat", stat_choices, stat)
|
||||
|
||||
self.stat = stat
|
||||
self.bins = bins
|
||||
self.binwidth = binwidth
|
||||
self.binrange = binrange
|
||||
self.discrete = discrete
|
||||
self.cumulative = cumulative
|
||||
|
||||
self.bin_kws = None
|
||||
|
||||
def _define_bin_edges(self, x, weights, bins, binwidth, binrange, discrete):
|
||||
"""Inner function that takes bin parameters as arguments."""
|
||||
if binrange is None:
|
||||
start, stop = x.min(), x.max()
|
||||
else:
|
||||
start, stop = binrange
|
||||
|
||||
if discrete:
|
||||
bin_edges = np.arange(start - .5, stop + 1.5)
|
||||
elif binwidth is not None:
|
||||
step = binwidth
|
||||
bin_edges = np.arange(start, stop + step, step)
|
||||
# Handle roundoff error (maybe there is a less clumsy way?)
|
||||
if bin_edges.max() < stop or len(bin_edges) < 2:
|
||||
bin_edges = np.append(bin_edges, bin_edges.max() + step)
|
||||
else:
|
||||
bin_edges = np.histogram_bin_edges(
|
||||
x, bins, binrange, weights,
|
||||
)
|
||||
return bin_edges
|
||||
|
||||
def define_bin_params(self, x1, x2=None, weights=None, cache=True):
|
||||
"""Given data, return numpy.histogram parameters to define bins."""
|
||||
if x2 is None:
|
||||
|
||||
bin_edges = self._define_bin_edges(
|
||||
x1, weights, self.bins, self.binwidth, self.binrange, self.discrete,
|
||||
)
|
||||
|
||||
if isinstance(self.bins, (str, Number)):
|
||||
n_bins = len(bin_edges) - 1
|
||||
bin_range = bin_edges.min(), bin_edges.max()
|
||||
bin_kws = dict(bins=n_bins, range=bin_range)
|
||||
else:
|
||||
bin_kws = dict(bins=bin_edges)
|
||||
|
||||
else:
|
||||
|
||||
bin_edges = []
|
||||
for i, x in enumerate([x1, x2]):
|
||||
|
||||
# Resolve out whether bin parameters are shared
|
||||
# or specific to each variable
|
||||
|
||||
bins = self.bins
|
||||
if not bins or isinstance(bins, (str, Number)):
|
||||
pass
|
||||
elif isinstance(bins[i], str):
|
||||
bins = bins[i]
|
||||
elif len(bins) == 2:
|
||||
bins = bins[i]
|
||||
|
||||
binwidth = self.binwidth
|
||||
if binwidth is None:
|
||||
pass
|
||||
elif not isinstance(binwidth, Number):
|
||||
binwidth = binwidth[i]
|
||||
|
||||
binrange = self.binrange
|
||||
if binrange is None:
|
||||
pass
|
||||
elif not isinstance(binrange[0], Number):
|
||||
binrange = binrange[i]
|
||||
|
||||
discrete = self.discrete
|
||||
if not isinstance(discrete, bool):
|
||||
discrete = discrete[i]
|
||||
|
||||
# Define the bins for this variable
|
||||
|
||||
bin_edges.append(self._define_bin_edges(
|
||||
x, weights, bins, binwidth, binrange, discrete,
|
||||
))
|
||||
|
||||
bin_kws = dict(bins=tuple(bin_edges))
|
||||
|
||||
if cache:
|
||||
self.bin_kws = bin_kws
|
||||
|
||||
return bin_kws
|
||||
|
||||
def _eval_bivariate(self, x1, x2, weights):
|
||||
"""Inner function for histogram of two variables."""
|
||||
bin_kws = self.bin_kws
|
||||
if bin_kws is None:
|
||||
bin_kws = self.define_bin_params(x1, x2, cache=False)
|
||||
|
||||
density = self.stat == "density"
|
||||
|
||||
hist, *bin_edges = np.histogram2d(
|
||||
x1, x2, **bin_kws, weights=weights, density=density
|
||||
)
|
||||
|
||||
area = np.outer(
|
||||
np.diff(bin_edges[0]),
|
||||
np.diff(bin_edges[1]),
|
||||
)
|
||||
|
||||
if self.stat == "probability" or self.stat == "proportion":
|
||||
hist = hist.astype(float) / hist.sum()
|
||||
elif self.stat == "percent":
|
||||
hist = hist.astype(float) / hist.sum() * 100
|
||||
elif self.stat == "frequency":
|
||||
hist = hist.astype(float) / area
|
||||
|
||||
if self.cumulative:
|
||||
if self.stat in ["density", "frequency"]:
|
||||
hist = (hist * area).cumsum(axis=0).cumsum(axis=1)
|
||||
else:
|
||||
hist = hist.cumsum(axis=0).cumsum(axis=1)
|
||||
|
||||
return hist, bin_edges
|
||||
|
||||
def _eval_univariate(self, x, weights):
|
||||
"""Inner function for histogram of one variable."""
|
||||
bin_kws = self.bin_kws
|
||||
if bin_kws is None:
|
||||
bin_kws = self.define_bin_params(x, weights=weights, cache=False)
|
||||
|
||||
density = self.stat == "density"
|
||||
hist, bin_edges = np.histogram(
|
||||
x, **bin_kws, weights=weights, density=density,
|
||||
)
|
||||
|
||||
if self.stat == "probability" or self.stat == "proportion":
|
||||
hist = hist.astype(float) / hist.sum()
|
||||
elif self.stat == "percent":
|
||||
hist = hist.astype(float) / hist.sum() * 100
|
||||
elif self.stat == "frequency":
|
||||
hist = hist.astype(float) / np.diff(bin_edges)
|
||||
|
||||
if self.cumulative:
|
||||
if self.stat in ["density", "frequency"]:
|
||||
hist = (hist * np.diff(bin_edges)).cumsum()
|
||||
else:
|
||||
hist = hist.cumsum()
|
||||
|
||||
return hist, bin_edges
|
||||
|
||||
def __call__(self, x1, x2=None, weights=None):
|
||||
"""Count the occurrences in each bin, maybe normalize."""
|
||||
if x2 is None:
|
||||
return self._eval_univariate(x1, weights)
|
||||
else:
|
||||
return self._eval_bivariate(x1, x2, weights)
|
||||
|
||||
|
||||
class ECDF:
|
||||
"""Univariate empirical cumulative distribution estimator."""
|
||||
def __init__(self, stat="proportion", complementary=False):
|
||||
"""Initialize the class with its parameters
|
||||
|
||||
Parameters
|
||||
----------
|
||||
stat : {{"proportion", "percent", "count"}}
|
||||
Distribution statistic to compute.
|
||||
complementary : bool
|
||||
If True, use the complementary CDF (1 - CDF)
|
||||
|
||||
"""
|
||||
_check_argument("stat", ["count", "percent", "proportion"], stat)
|
||||
self.stat = stat
|
||||
self.complementary = complementary
|
||||
|
||||
def _eval_bivariate(self, x1, x2, weights):
|
||||
"""Inner function for ECDF of two variables."""
|
||||
raise NotImplementedError("Bivariate ECDF is not implemented")
|
||||
|
||||
def _eval_univariate(self, x, weights):
|
||||
"""Inner function for ECDF of one variable."""
|
||||
sorter = x.argsort()
|
||||
x = x[sorter]
|
||||
weights = weights[sorter]
|
||||
y = weights.cumsum()
|
||||
|
||||
if self.stat in ["percent", "proportion"]:
|
||||
y = y / y.max()
|
||||
if self.stat == "percent":
|
||||
y = y * 100
|
||||
|
||||
x = np.r_[-np.inf, x]
|
||||
y = np.r_[0, y]
|
||||
|
||||
if self.complementary:
|
||||
y = y.max() - y
|
||||
|
||||
return y, x
|
||||
|
||||
def __call__(self, x1, x2=None, weights=None):
|
||||
"""Return proportion or count of observations below each sorted datapoint."""
|
||||
x1 = np.asarray(x1)
|
||||
if weights is None:
|
||||
weights = np.ones_like(x1)
|
||||
else:
|
||||
weights = np.asarray(weights)
|
||||
|
||||
if x2 is None:
|
||||
return self._eval_univariate(x1, weights)
|
||||
else:
|
||||
return self._eval_bivariate(x1, x2, weights)
|
||||
|
||||
|
||||
class EstimateAggregator:
|
||||
|
||||
def __init__(self, estimator, errorbar=None, **boot_kws):
|
||||
"""
|
||||
Data aggregator that produces an estimate and error bar interval.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
estimator : callable or string
|
||||
Function (or method name) that maps a vector to a scalar.
|
||||
errorbar : string, (string, number) tuple, or callable
|
||||
Name of errorbar method (either "ci", "pi", "se", or "sd"), or a tuple
|
||||
with a method name and a level parameter, or a function that maps from a
|
||||
vector to a (min, max) interval, or None to hide errorbar. See the
|
||||
:doc:`errorbar tutorial </tutorial/error_bars>` for more information.
|
||||
boot_kws
|
||||
Additional keywords are passed to bootstrap when error_method is "ci".
|
||||
|
||||
"""
|
||||
self.estimator = estimator
|
||||
|
||||
method, level = _validate_errorbar_arg(errorbar)
|
||||
self.error_method = method
|
||||
self.error_level = level
|
||||
|
||||
self.boot_kws = boot_kws
|
||||
|
||||
def __call__(self, data, var):
|
||||
"""Aggregate over `var` column of `data` with estimate and error interval."""
|
||||
vals = data[var]
|
||||
if callable(self.estimator):
|
||||
# You would think we could pass to vals.agg, and yet:
|
||||
# https://github.com/mwaskom/seaborn/issues/2943
|
||||
estimate = self.estimator(vals)
|
||||
else:
|
||||
estimate = vals.agg(self.estimator)
|
||||
|
||||
# Options that produce no error bars
|
||||
if self.error_method is None:
|
||||
err_min = err_max = np.nan
|
||||
elif len(data) <= 1:
|
||||
err_min = err_max = np.nan
|
||||
|
||||
# Generic errorbars from user-supplied function
|
||||
elif callable(self.error_method):
|
||||
err_min, err_max = self.error_method(vals)
|
||||
|
||||
# Parametric options
|
||||
elif self.error_method == "sd":
|
||||
half_interval = vals.std() * self.error_level
|
||||
err_min, err_max = estimate - half_interval, estimate + half_interval
|
||||
elif self.error_method == "se":
|
||||
half_interval = vals.sem() * self.error_level
|
||||
err_min, err_max = estimate - half_interval, estimate + half_interval
|
||||
|
||||
# Nonparametric options
|
||||
elif self.error_method == "pi":
|
||||
err_min, err_max = _percentile_interval(vals, self.error_level)
|
||||
elif self.error_method == "ci":
|
||||
units = data.get("units", None)
|
||||
boots = bootstrap(vals, units=units, func=self.estimator, **self.boot_kws)
|
||||
err_min, err_max = _percentile_interval(boots, self.error_level)
|
||||
|
||||
return pd.Series({var: estimate, f"{var}min": err_min, f"{var}max": err_max})
|
||||
|
||||
|
||||
class WeightedAggregator:
|
||||
|
||||
def __init__(self, estimator, errorbar=None, **boot_kws):
|
||||
"""
|
||||
Data aggregator that produces a weighted estimate and error bar interval.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
estimator : string
|
||||
Function (or method name) that maps a vector to a scalar. Currently
|
||||
supports only "mean".
|
||||
errorbar : string or (string, number) tuple
|
||||
Name of errorbar method or a tuple with a method name and a level parameter.
|
||||
Currently the only supported method is "ci".
|
||||
boot_kws
|
||||
Additional keywords are passed to bootstrap when error_method is "ci".
|
||||
|
||||
"""
|
||||
if estimator != "mean":
|
||||
# Note that, while other weighted estimators may make sense (e.g. median),
|
||||
# I'm not aware of an implementation in our dependencies. We can add one
|
||||
# in seaborn later, if there is sufficient interest. For now, limit to mean.
|
||||
raise ValueError(f"Weighted estimator must be 'mean', not {estimator!r}.")
|
||||
self.estimator = estimator
|
||||
|
||||
method, level = _validate_errorbar_arg(errorbar)
|
||||
if method is not None and method != "ci":
|
||||
# As with the estimator, weighted 'sd' or 'pi' error bars may make sense.
|
||||
# But we'll keep things simple for now and limit to (bootstrap) CI.
|
||||
raise ValueError(f"Error bar method must be 'ci', not {method!r}.")
|
||||
self.error_method = method
|
||||
self.error_level = level
|
||||
|
||||
self.boot_kws = boot_kws
|
||||
|
||||
def __call__(self, data, var):
|
||||
"""Aggregate over `var` column of `data` with estimate and error interval."""
|
||||
vals = data[var]
|
||||
weights = data["weight"]
|
||||
|
||||
estimate = np.average(vals, weights=weights)
|
||||
|
||||
if self.error_method == "ci" and len(data) > 1:
|
||||
|
||||
def error_func(x, w):
|
||||
return np.average(x, weights=w)
|
||||
|
||||
boots = bootstrap(vals, weights, func=error_func, **self.boot_kws)
|
||||
err_min, err_max = _percentile_interval(boots, self.error_level)
|
||||
|
||||
else:
|
||||
err_min = err_max = np.nan
|
||||
|
||||
return pd.Series({var: estimate, f"{var}min": err_min, f"{var}max": err_max})
|
||||
|
||||
|
||||
class LetterValues:
|
||||
|
||||
def __init__(self, k_depth, outlier_prop, trust_alpha):
|
||||
"""
|
||||
Compute percentiles of a distribution using various tail stopping rules.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
k_depth: "tukey", "proportion", "trustworthy", or "full"
|
||||
Stopping rule for choosing tail percentiled to show:
|
||||
|
||||
- tukey: Show a similar number of outliers as in a conventional boxplot.
|
||||
- proportion: Show approximately `outlier_prop` outliers.
|
||||
- trust_alpha: Use `trust_alpha` level for most extreme tail percentile.
|
||||
|
||||
outlier_prop: float
|
||||
Parameter for `k_depth="proportion"` setting the expected outlier rate.
|
||||
trust_alpha: float
|
||||
Parameter for `k_depth="trustworthy"` setting the confidence threshold.
|
||||
|
||||
Notes
|
||||
-----
|
||||
Based on the proposal in this paper:
|
||||
https://vita.had.co.nz/papers/letter-value-plot.pdf
|
||||
|
||||
"""
|
||||
k_options = ["tukey", "proportion", "trustworthy", "full"]
|
||||
if isinstance(k_depth, str):
|
||||
_check_argument("k_depth", k_options, k_depth)
|
||||
elif not isinstance(k_depth, int):
|
||||
err = (
|
||||
"The `k_depth` parameter must be either an integer or string "
|
||||
f"(one of {k_options}), not {k_depth!r}."
|
||||
)
|
||||
raise TypeError(err)
|
||||
|
||||
self.k_depth = k_depth
|
||||
self.outlier_prop = outlier_prop
|
||||
self.trust_alpha = trust_alpha
|
||||
|
||||
def _compute_k(self, n):
|
||||
|
||||
# Select the depth, i.e. number of boxes to draw, based on the method
|
||||
if self.k_depth == "full":
|
||||
# extend boxes to 100% of the data
|
||||
k = int(np.log2(n)) + 1
|
||||
elif self.k_depth == "tukey":
|
||||
# This results with 5-8 points in each tail
|
||||
k = int(np.log2(n)) - 3
|
||||
elif self.k_depth == "proportion":
|
||||
k = int(np.log2(n)) - int(np.log2(n * self.outlier_prop)) + 1
|
||||
elif self.k_depth == "trustworthy":
|
||||
normal_quantile_func = np.vectorize(NormalDist().inv_cdf)
|
||||
point_conf = 2 * normal_quantile_func(1 - self.trust_alpha / 2) ** 2
|
||||
k = int(np.log2(n / point_conf)) + 1
|
||||
else:
|
||||
# Allow having k directly specified as input
|
||||
k = int(self.k_depth)
|
||||
|
||||
return max(k, 1)
|
||||
|
||||
def __call__(self, x):
|
||||
"""Evaluate the letter values."""
|
||||
k = self._compute_k(len(x))
|
||||
exp = np.arange(k + 1, 1, -1), np.arange(2, k + 2)
|
||||
levels = k + 1 - np.concatenate([exp[0], exp[1][1:]])
|
||||
percentiles = 100 * np.concatenate([0.5 ** exp[0], 1 - 0.5 ** exp[1]])
|
||||
if self.k_depth == "full":
|
||||
percentiles[0] = 0
|
||||
percentiles[-1] = 100
|
||||
values = np.percentile(x, percentiles)
|
||||
fliers = np.asarray(x[(x < values.min()) | (x > values.max())])
|
||||
median = np.percentile(x, 50)
|
||||
|
||||
return {
|
||||
"k": k,
|
||||
"levels": levels,
|
||||
"percs": percentiles,
|
||||
"values": values,
|
||||
"fliers": fliers,
|
||||
"median": median,
|
||||
}
|
||||
|
||||
|
||||
def _percentile_interval(data, width):
|
||||
"""Return a percentile interval from data of a given width."""
|
||||
edge = (100 - width) / 2
|
||||
percentiles = edge, 100 - edge
|
||||
return np.nanpercentile(data, percentiles)
|
||||
|
||||
|
||||
def _validate_errorbar_arg(arg):
|
||||
"""Check type and value of errorbar argument and assign default level."""
|
||||
DEFAULT_LEVELS = {
|
||||
"ci": 95,
|
||||
"pi": 95,
|
||||
"se": 1,
|
||||
"sd": 1,
|
||||
}
|
||||
|
||||
usage = "`errorbar` must be a callable, string, or (string, number) tuple"
|
||||
|
||||
if arg is None:
|
||||
return None, None
|
||||
elif callable(arg):
|
||||
return arg, None
|
||||
elif isinstance(arg, str):
|
||||
method = arg
|
||||
level = DEFAULT_LEVELS.get(method, None)
|
||||
else:
|
||||
try:
|
||||
method, level = arg
|
||||
except (ValueError, TypeError) as err:
|
||||
raise err.__class__(usage) from err
|
||||
|
||||
_check_argument("errorbar", list(DEFAULT_LEVELS), method)
|
||||
if level is not None and not isinstance(level, Number):
|
||||
raise TypeError(usage)
|
||||
|
||||
return method, level
|
||||
@@ -0,0 +1,130 @@
|
||||
from __future__ import annotations
|
||||
from dataclasses import dataclass
|
||||
from typing import ClassVar, Callable
|
||||
|
||||
import pandas as pd
|
||||
from pandas import DataFrame
|
||||
|
||||
from seaborn._core.scales import Scale
|
||||
from seaborn._core.groupby import GroupBy
|
||||
from seaborn._stats.base import Stat
|
||||
from seaborn._statistics import (
|
||||
EstimateAggregator,
|
||||
WeightedAggregator,
|
||||
)
|
||||
from seaborn._core.typing import Vector
|
||||
|
||||
|
||||
@dataclass
|
||||
class Agg(Stat):
|
||||
"""
|
||||
Aggregate data along the value axis using given method.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
func : str or callable
|
||||
Name of a :class:`pandas.Series` method or a vector -> scalar function.
|
||||
|
||||
See Also
|
||||
--------
|
||||
objects.Est : Aggregation with error bars.
|
||||
|
||||
Examples
|
||||
--------
|
||||
.. include:: ../docstrings/objects.Agg.rst
|
||||
|
||||
"""
|
||||
func: str | Callable[[Vector], float] = "mean"
|
||||
|
||||
group_by_orient: ClassVar[bool] = True
|
||||
|
||||
def __call__(
|
||||
self, data: DataFrame, groupby: GroupBy, orient: str, scales: dict[str, Scale],
|
||||
) -> DataFrame:
|
||||
|
||||
var = {"x": "y", "y": "x"}.get(orient)
|
||||
res = (
|
||||
groupby
|
||||
.agg(data, {var: self.func})
|
||||
.dropna(subset=[var])
|
||||
.reset_index(drop=True)
|
||||
)
|
||||
return res
|
||||
|
||||
|
||||
@dataclass
|
||||
class Est(Stat):
|
||||
"""
|
||||
Calculate a point estimate and error bar interval.
|
||||
|
||||
For more information about the various `errorbar` choices, see the
|
||||
:doc:`errorbar tutorial </tutorial/error_bars>`.
|
||||
|
||||
Additional variables:
|
||||
|
||||
- **weight**: When passed to a layer that uses this stat, a weighted estimate
|
||||
will be computed. Note that use of weights currently limits the choice of
|
||||
function and error bar method to `"mean"` and `"ci"`, respectively.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
func : str or callable
|
||||
Name of a :class:`numpy.ndarray` method or a vector -> scalar function.
|
||||
errorbar : str, (str, float) tuple, or callable
|
||||
Name of errorbar method (one of "ci", "pi", "se" or "sd"), or a tuple
|
||||
with a method name ane a level parameter, or a function that maps from a
|
||||
vector to a (min, max) interval.
|
||||
n_boot : int
|
||||
Number of bootstrap samples to draw for "ci" errorbars.
|
||||
seed : int
|
||||
Seed for the PRNG used to draw bootstrap samples.
|
||||
|
||||
Examples
|
||||
--------
|
||||
.. include:: ../docstrings/objects.Est.rst
|
||||
|
||||
"""
|
||||
func: str | Callable[[Vector], float] = "mean"
|
||||
errorbar: str | tuple[str, float] = ("ci", 95)
|
||||
n_boot: int = 1000
|
||||
seed: int | None = None
|
||||
|
||||
group_by_orient: ClassVar[bool] = True
|
||||
|
||||
def _process(
|
||||
self, data: DataFrame, var: str, estimator: EstimateAggregator
|
||||
) -> DataFrame:
|
||||
# Needed because GroupBy.apply assumes func is DataFrame -> DataFrame
|
||||
# which we could probably make more general to allow Series return
|
||||
res = estimator(data, var)
|
||||
return pd.DataFrame([res])
|
||||
|
||||
def __call__(
|
||||
self, data: DataFrame, groupby: GroupBy, orient: str, scales: dict[str, Scale],
|
||||
) -> DataFrame:
|
||||
|
||||
boot_kws = {"n_boot": self.n_boot, "seed": self.seed}
|
||||
if "weight" in data:
|
||||
engine = WeightedAggregator(self.func, self.errorbar, **boot_kws)
|
||||
else:
|
||||
engine = EstimateAggregator(self.func, self.errorbar, **boot_kws)
|
||||
|
||||
var = {"x": "y", "y": "x"}[orient]
|
||||
res = (
|
||||
groupby
|
||||
.apply(data, self._process, var, engine)
|
||||
.dropna(subset=[var])
|
||||
.reset_index(drop=True)
|
||||
)
|
||||
|
||||
res = res.fillna({f"{var}min": res[var], f"{var}max": res[var]})
|
||||
|
||||
return res
|
||||
|
||||
|
||||
@dataclass
|
||||
class Rolling(Stat):
|
||||
...
|
||||
|
||||
def __call__(self, data, groupby, orient, scales):
|
||||
...
|
||||
65
ftline/lib/python3.12/site-packages/seaborn/_stats/base.py
Normal file
65
ftline/lib/python3.12/site-packages/seaborn/_stats/base.py
Normal file
@@ -0,0 +1,65 @@
|
||||
"""Base module for statistical transformations."""
|
||||
from __future__ import annotations
|
||||
from collections.abc import Iterable
|
||||
from dataclasses import dataclass
|
||||
from typing import ClassVar, Any
|
||||
import warnings
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
if TYPE_CHECKING:
|
||||
from pandas import DataFrame
|
||||
from seaborn._core.groupby import GroupBy
|
||||
from seaborn._core.scales import Scale
|
||||
|
||||
|
||||
@dataclass
|
||||
class Stat:
|
||||
"""Base class for objects that apply statistical transformations."""
|
||||
|
||||
# The class supports a partial-function application pattern. The object is
|
||||
# initialized with desired parameters and the result is a callable that
|
||||
# accepts and returns dataframes.
|
||||
|
||||
# The statistical transformation logic should not add any state to the instance
|
||||
# beyond what is defined with the initialization parameters.
|
||||
|
||||
# Subclasses can declare whether the orient dimension should be used in grouping
|
||||
# TODO consider whether this should be a parameter. Motivating example:
|
||||
# use the same KDE class violin plots and univariate density estimation.
|
||||
# In the former case, we would expect separate densities for each unique
|
||||
# value on the orient axis, but we would not in the latter case.
|
||||
group_by_orient: ClassVar[bool] = False
|
||||
|
||||
def _check_param_one_of(self, param: str, options: Iterable[Any]) -> None:
|
||||
"""Raise when parameter value is not one of a specified set."""
|
||||
value = getattr(self, param)
|
||||
if value not in options:
|
||||
*most, last = options
|
||||
option_str = ", ".join(f"{x!r}" for x in most[:-1]) + f" or {last!r}"
|
||||
err = " ".join([
|
||||
f"The `{param}` parameter for `{self.__class__.__name__}` must be",
|
||||
f"one of {option_str}; not {value!r}.",
|
||||
])
|
||||
raise ValueError(err)
|
||||
|
||||
def _check_grouping_vars(
|
||||
self, param: str, data_vars: list[str], stacklevel: int = 2,
|
||||
) -> None:
|
||||
"""Warn if vars are named in parameter without being present in the data."""
|
||||
param_vars = getattr(self, param)
|
||||
undefined = set(param_vars) - set(data_vars)
|
||||
if undefined:
|
||||
param = f"{self.__class__.__name__}.{param}"
|
||||
names = ", ".join(f"{x!r}" for x in undefined)
|
||||
msg = f"Undefined variable(s) passed for {param}: {names}."
|
||||
warnings.warn(msg, stacklevel=stacklevel)
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
data: DataFrame,
|
||||
groupby: GroupBy,
|
||||
orient: str,
|
||||
scales: dict[str, Scale],
|
||||
) -> DataFrame:
|
||||
"""Apply statistical transform to data subgroups and return combined result."""
|
||||
return data
|
||||
232
ftline/lib/python3.12/site-packages/seaborn/_stats/counting.py
Normal file
232
ftline/lib/python3.12/site-packages/seaborn/_stats/counting.py
Normal file
@@ -0,0 +1,232 @@
|
||||
from __future__ import annotations
|
||||
from dataclasses import dataclass
|
||||
from typing import ClassVar
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
from pandas import DataFrame
|
||||
|
||||
from seaborn._core.groupby import GroupBy
|
||||
from seaborn._core.scales import Scale
|
||||
from seaborn._stats.base import Stat
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
if TYPE_CHECKING:
|
||||
from numpy.typing import ArrayLike
|
||||
|
||||
|
||||
@dataclass
|
||||
class Count(Stat):
|
||||
"""
|
||||
Count distinct observations within groups.
|
||||
|
||||
See Also
|
||||
--------
|
||||
Hist : A more fully-featured transform including binning and/or normalization.
|
||||
|
||||
Examples
|
||||
--------
|
||||
.. include:: ../docstrings/objects.Count.rst
|
||||
|
||||
"""
|
||||
group_by_orient: ClassVar[bool] = True
|
||||
|
||||
def __call__(
|
||||
self, data: DataFrame, groupby: GroupBy, orient: str, scales: dict[str, Scale],
|
||||
) -> DataFrame:
|
||||
|
||||
var = {"x": "y", "y": "x"}[orient]
|
||||
res = (
|
||||
groupby
|
||||
.agg(data.assign(**{var: data[orient]}), {var: len})
|
||||
.dropna(subset=["x", "y"])
|
||||
.reset_index(drop=True)
|
||||
)
|
||||
return res
|
||||
|
||||
|
||||
@dataclass
|
||||
class Hist(Stat):
|
||||
"""
|
||||
Bin observations, count them, and optionally normalize or cumulate.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
stat : str
|
||||
Aggregate statistic to compute in each bin:
|
||||
|
||||
- `count`: the number of observations
|
||||
- `density`: normalize so that the total area of the histogram equals 1
|
||||
- `percent`: normalize so that bar heights sum to 100
|
||||
- `probability` or `proportion`: normalize so that bar heights sum to 1
|
||||
- `frequency`: divide the number of observations by the bin width
|
||||
|
||||
bins : str, int, or ArrayLike
|
||||
Generic parameter that can be the name of a reference rule, the number
|
||||
of bins, or the bin breaks. Passed to :func:`numpy.histogram_bin_edges`.
|
||||
binwidth : float
|
||||
Width of each bin; overrides `bins` but can be used with `binrange`.
|
||||
Note that if `binwidth` does not evenly divide the bin range, the actual
|
||||
bin width used will be only approximately equal to the parameter value.
|
||||
binrange : (min, max)
|
||||
Lowest and highest value for bin edges; can be used with either
|
||||
`bins` (when a number) or `binwidth`. Defaults to data extremes.
|
||||
common_norm : bool or list of variables
|
||||
When not `False`, the normalization is applied across groups. Use
|
||||
`True` to normalize across all groups, or pass variable name(s) that
|
||||
define normalization groups.
|
||||
common_bins : bool or list of variables
|
||||
When not `False`, the same bins are used for all groups. Use `True` to
|
||||
share bins across all groups, or pass variable name(s) to share within.
|
||||
cumulative : bool
|
||||
If True, cumulate the bin values.
|
||||
discrete : bool
|
||||
If True, set `binwidth` and `binrange` so that bins have unit width and
|
||||
are centered on integer values
|
||||
|
||||
Notes
|
||||
-----
|
||||
The choice of bins for computing and plotting a histogram can exert
|
||||
substantial influence on the insights that one is able to draw from the
|
||||
visualization. If the bins are too large, they may erase important features.
|
||||
On the other hand, bins that are too small may be dominated by random
|
||||
variability, obscuring the shape of the true underlying distribution. The
|
||||
default bin size is determined using a reference rule that depends on the
|
||||
sample size and variance. This works well in many cases, (i.e., with
|
||||
"well-behaved" data) but it fails in others. It is always a good to try
|
||||
different bin sizes to be sure that you are not missing something important.
|
||||
This function allows you to specify bins in several different ways, such as
|
||||
by setting the total number of bins to use, the width of each bin, or the
|
||||
specific locations where the bins should break.
|
||||
|
||||
Examples
|
||||
--------
|
||||
.. include:: ../docstrings/objects.Hist.rst
|
||||
|
||||
"""
|
||||
stat: str = "count"
|
||||
bins: str | int | ArrayLike = "auto"
|
||||
binwidth: float | None = None
|
||||
binrange: tuple[float, float] | None = None
|
||||
common_norm: bool | list[str] = True
|
||||
common_bins: bool | list[str] = True
|
||||
cumulative: bool = False
|
||||
discrete: bool = False
|
||||
|
||||
def __post_init__(self):
|
||||
|
||||
stat_options = [
|
||||
"count", "density", "percent", "probability", "proportion", "frequency"
|
||||
]
|
||||
self._check_param_one_of("stat", stat_options)
|
||||
|
||||
def _define_bin_edges(self, vals, weight, bins, binwidth, binrange, discrete):
|
||||
"""Inner function that takes bin parameters as arguments."""
|
||||
vals = vals.replace(-np.inf, np.nan).replace(np.inf, np.nan).dropna()
|
||||
|
||||
if binrange is None:
|
||||
start, stop = vals.min(), vals.max()
|
||||
else:
|
||||
start, stop = binrange
|
||||
|
||||
if discrete:
|
||||
bin_edges = np.arange(start - .5, stop + 1.5)
|
||||
else:
|
||||
if binwidth is not None:
|
||||
bins = int(round((stop - start) / binwidth))
|
||||
bin_edges = np.histogram_bin_edges(vals, bins, binrange, weight)
|
||||
|
||||
# TODO warning or cap on too many bins?
|
||||
|
||||
return bin_edges
|
||||
|
||||
def _define_bin_params(self, data, orient, scale_type):
|
||||
"""Given data, return numpy.histogram parameters to define bins."""
|
||||
vals = data[orient]
|
||||
weights = data.get("weight", None)
|
||||
|
||||
# TODO We'll want this for ordinal / discrete scales too
|
||||
# (Do we need discrete as a parameter or just infer from scale?)
|
||||
discrete = self.discrete or scale_type == "nominal"
|
||||
|
||||
bin_edges = self._define_bin_edges(
|
||||
vals, weights, self.bins, self.binwidth, self.binrange, discrete,
|
||||
)
|
||||
|
||||
if isinstance(self.bins, (str, int)):
|
||||
n_bins = len(bin_edges) - 1
|
||||
bin_range = bin_edges.min(), bin_edges.max()
|
||||
bin_kws = dict(bins=n_bins, range=bin_range)
|
||||
else:
|
||||
bin_kws = dict(bins=bin_edges)
|
||||
|
||||
return bin_kws
|
||||
|
||||
def _get_bins_and_eval(self, data, orient, groupby, scale_type):
|
||||
|
||||
bin_kws = self._define_bin_params(data, orient, scale_type)
|
||||
return groupby.apply(data, self._eval, orient, bin_kws)
|
||||
|
||||
def _eval(self, data, orient, bin_kws):
|
||||
|
||||
vals = data[orient]
|
||||
weights = data.get("weight", None)
|
||||
|
||||
density = self.stat == "density"
|
||||
hist, edges = np.histogram(vals, **bin_kws, weights=weights, density=density)
|
||||
|
||||
width = np.diff(edges)
|
||||
center = edges[:-1] + width / 2
|
||||
|
||||
return pd.DataFrame({orient: center, "count": hist, "space": width})
|
||||
|
||||
def _normalize(self, data):
|
||||
|
||||
hist = data["count"]
|
||||
if self.stat == "probability" or self.stat == "proportion":
|
||||
hist = hist.astype(float) / hist.sum()
|
||||
elif self.stat == "percent":
|
||||
hist = hist.astype(float) / hist.sum() * 100
|
||||
elif self.stat == "frequency":
|
||||
hist = hist.astype(float) / data["space"]
|
||||
|
||||
if self.cumulative:
|
||||
if self.stat in ["density", "frequency"]:
|
||||
hist = (hist * data["space"]).cumsum()
|
||||
else:
|
||||
hist = hist.cumsum()
|
||||
|
||||
return data.assign(**{self.stat: hist})
|
||||
|
||||
def __call__(
|
||||
self, data: DataFrame, groupby: GroupBy, orient: str, scales: dict[str, Scale],
|
||||
) -> DataFrame:
|
||||
|
||||
scale_type = scales[orient].__class__.__name__.lower()
|
||||
grouping_vars = [str(v) for v in data if v in groupby.order]
|
||||
if not grouping_vars or self.common_bins is True:
|
||||
bin_kws = self._define_bin_params(data, orient, scale_type)
|
||||
data = groupby.apply(data, self._eval, orient, bin_kws)
|
||||
else:
|
||||
if self.common_bins is False:
|
||||
bin_groupby = GroupBy(grouping_vars)
|
||||
else:
|
||||
bin_groupby = GroupBy(self.common_bins)
|
||||
self._check_grouping_vars("common_bins", grouping_vars)
|
||||
|
||||
data = bin_groupby.apply(
|
||||
data, self._get_bins_and_eval, orient, groupby, scale_type,
|
||||
)
|
||||
|
||||
if not grouping_vars or self.common_norm is True:
|
||||
data = self._normalize(data)
|
||||
else:
|
||||
if self.common_norm is False:
|
||||
norm_groupby = GroupBy(grouping_vars)
|
||||
else:
|
||||
norm_groupby = GroupBy(self.common_norm)
|
||||
self._check_grouping_vars("common_norm", grouping_vars)
|
||||
data = norm_groupby.apply(data, self._normalize)
|
||||
|
||||
other = {"x": "y", "y": "x"}[orient]
|
||||
return data.assign(**{other: data[self.stat]})
|
||||
214
ftline/lib/python3.12/site-packages/seaborn/_stats/density.py
Normal file
214
ftline/lib/python3.12/site-packages/seaborn/_stats/density.py
Normal file
@@ -0,0 +1,214 @@
|
||||
from __future__ import annotations
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Callable
|
||||
|
||||
import numpy as np
|
||||
from numpy import ndarray
|
||||
import pandas as pd
|
||||
from pandas import DataFrame
|
||||
try:
|
||||
from scipy.stats import gaussian_kde
|
||||
_no_scipy = False
|
||||
except ImportError:
|
||||
from seaborn.external.kde import gaussian_kde
|
||||
_no_scipy = True
|
||||
|
||||
from seaborn._core.groupby import GroupBy
|
||||
from seaborn._core.scales import Scale
|
||||
from seaborn._stats.base import Stat
|
||||
|
||||
|
||||
@dataclass
|
||||
class KDE(Stat):
|
||||
"""
|
||||
Compute a univariate kernel density estimate.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
bw_adjust : float
|
||||
Factor that multiplicatively scales the value chosen using
|
||||
`bw_method`. Increasing will make the curve smoother. See Notes.
|
||||
bw_method : string, scalar, or callable
|
||||
Method for determining the smoothing bandwidth to use. Passed directly
|
||||
to :class:`scipy.stats.gaussian_kde`; see there for options.
|
||||
common_norm : bool or list of variables
|
||||
If `True`, normalize so that the areas of all curves sums to 1.
|
||||
If `False`, normalize each curve independently. If a list, defines
|
||||
variable(s) to group by and normalize within.
|
||||
common_grid : bool or list of variables
|
||||
If `True`, all curves will share the same evaluation grid.
|
||||
If `False`, each evaluation grid is independent. If a list, defines
|
||||
variable(s) to group by and share a grid within.
|
||||
gridsize : int or None
|
||||
Number of points in the evaluation grid. If None, the density is
|
||||
evaluated at the original datapoints.
|
||||
cut : float
|
||||
Factor, multiplied by the kernel bandwidth, that determines how far
|
||||
the evaluation grid extends past the extreme datapoints. When set to 0,
|
||||
the curve is truncated at the data limits.
|
||||
cumulative : bool
|
||||
If True, estimate a cumulative distribution function. Requires scipy.
|
||||
|
||||
Notes
|
||||
-----
|
||||
The *bandwidth*, or standard deviation of the smoothing kernel, is an
|
||||
important parameter. Much like histogram bin width, using the wrong
|
||||
bandwidth can produce a distorted representation. Over-smoothing can erase
|
||||
true features, while under-smoothing can create false ones. The default
|
||||
uses a rule-of-thumb that works best for distributions that are roughly
|
||||
bell-shaped. It is a good idea to check the default by varying `bw_adjust`.
|
||||
|
||||
Because the smoothing is performed with a Gaussian kernel, the estimated
|
||||
density curve can extend to values that may not make sense. For example, the
|
||||
curve may be drawn over negative values when data that are naturally
|
||||
positive. The `cut` parameter can be used to control the evaluation range,
|
||||
but datasets that have many observations close to a natural boundary may be
|
||||
better served by a different method.
|
||||
|
||||
Similar distortions may arise when a dataset is naturally discrete or "spiky"
|
||||
(containing many repeated observations of the same value). KDEs will always
|
||||
produce a smooth curve, which could be misleading.
|
||||
|
||||
The units on the density axis are a common source of confusion. While kernel
|
||||
density estimation produces a probability distribution, the height of the curve
|
||||
at each point gives a density, not a probability. A probability can be obtained
|
||||
only by integrating the density across a range. The curve is normalized so
|
||||
that the integral over all possible values is 1, meaning that the scale of
|
||||
the density axis depends on the data values.
|
||||
|
||||
If scipy is installed, its cython-accelerated implementation will be used.
|
||||
|
||||
Examples
|
||||
--------
|
||||
.. include:: ../docstrings/objects.KDE.rst
|
||||
|
||||
"""
|
||||
bw_adjust: float = 1
|
||||
bw_method: str | float | Callable[[gaussian_kde], float] = "scott"
|
||||
common_norm: bool | list[str] = True
|
||||
common_grid: bool | list[str] = True
|
||||
gridsize: int | None = 200
|
||||
cut: float = 3
|
||||
cumulative: bool = False
|
||||
|
||||
def __post_init__(self):
|
||||
|
||||
if self.cumulative and _no_scipy:
|
||||
raise RuntimeError("Cumulative KDE evaluation requires scipy")
|
||||
|
||||
def _check_var_list_or_boolean(self, param: str, grouping_vars: Any) -> None:
|
||||
"""Do input checks on grouping parameters."""
|
||||
value = getattr(self, param)
|
||||
if not (
|
||||
isinstance(value, bool)
|
||||
or (isinstance(value, list) and all(isinstance(v, str) for v in value))
|
||||
):
|
||||
param_name = f"{self.__class__.__name__}.{param}"
|
||||
raise TypeError(f"{param_name} must be a boolean or list of strings.")
|
||||
self._check_grouping_vars(param, grouping_vars, stacklevel=3)
|
||||
|
||||
def _fit(self, data: DataFrame, orient: str) -> gaussian_kde:
|
||||
"""Fit and return a KDE object."""
|
||||
# TODO need to handle singular data
|
||||
|
||||
fit_kws: dict[str, Any] = {"bw_method": self.bw_method}
|
||||
if "weight" in data:
|
||||
fit_kws["weights"] = data["weight"]
|
||||
kde = gaussian_kde(data[orient], **fit_kws)
|
||||
kde.set_bandwidth(kde.factor * self.bw_adjust)
|
||||
|
||||
return kde
|
||||
|
||||
def _get_support(self, data: DataFrame, orient: str) -> ndarray:
|
||||
"""Define the grid that the KDE will be evaluated on."""
|
||||
if self.gridsize is None:
|
||||
return data[orient].to_numpy()
|
||||
|
||||
kde = self._fit(data, orient)
|
||||
bw = np.sqrt(kde.covariance.squeeze())
|
||||
gridmin = data[orient].min() - bw * self.cut
|
||||
gridmax = data[orient].max() + bw * self.cut
|
||||
return np.linspace(gridmin, gridmax, self.gridsize)
|
||||
|
||||
def _fit_and_evaluate(
|
||||
self, data: DataFrame, orient: str, support: ndarray
|
||||
) -> DataFrame:
|
||||
"""Transform single group by fitting a KDE and evaluating on a support grid."""
|
||||
empty = pd.DataFrame(columns=[orient, "weight", "density"], dtype=float)
|
||||
if len(data) < 2:
|
||||
return empty
|
||||
try:
|
||||
kde = self._fit(data, orient)
|
||||
except np.linalg.LinAlgError:
|
||||
return empty
|
||||
|
||||
if self.cumulative:
|
||||
s_0 = support[0]
|
||||
density = np.array([kde.integrate_box_1d(s_0, s_i) for s_i in support])
|
||||
else:
|
||||
density = kde(support)
|
||||
|
||||
weight = data["weight"].sum()
|
||||
return pd.DataFrame({orient: support, "weight": weight, "density": density})
|
||||
|
||||
def _transform(
|
||||
self, data: DataFrame, orient: str, grouping_vars: list[str]
|
||||
) -> DataFrame:
|
||||
"""Transform multiple groups by fitting KDEs and evaluating."""
|
||||
empty = pd.DataFrame(columns=[*data.columns, "density"], dtype=float)
|
||||
if len(data) < 2:
|
||||
return empty
|
||||
try:
|
||||
support = self._get_support(data, orient)
|
||||
except np.linalg.LinAlgError:
|
||||
return empty
|
||||
|
||||
grouping_vars = [x for x in grouping_vars if data[x].nunique() > 1]
|
||||
if not grouping_vars:
|
||||
return self._fit_and_evaluate(data, orient, support)
|
||||
groupby = GroupBy(grouping_vars)
|
||||
return groupby.apply(data, self._fit_and_evaluate, orient, support)
|
||||
|
||||
def __call__(
|
||||
self, data: DataFrame, groupby: GroupBy, orient: str, scales: dict[str, Scale],
|
||||
) -> DataFrame:
|
||||
|
||||
if "weight" not in data:
|
||||
data = data.assign(weight=1)
|
||||
data = data.dropna(subset=[orient, "weight"])
|
||||
|
||||
# Transform each group separately
|
||||
grouping_vars = [str(v) for v in data if v in groupby.order]
|
||||
if not grouping_vars or self.common_grid is True:
|
||||
res = self._transform(data, orient, grouping_vars)
|
||||
else:
|
||||
if self.common_grid is False:
|
||||
grid_vars = grouping_vars
|
||||
else:
|
||||
self._check_var_list_or_boolean("common_grid", grouping_vars)
|
||||
grid_vars = [v for v in self.common_grid if v in grouping_vars]
|
||||
|
||||
res = (
|
||||
GroupBy(grid_vars)
|
||||
.apply(data, self._transform, orient, grouping_vars)
|
||||
)
|
||||
|
||||
# Normalize, potentially within groups
|
||||
if not grouping_vars or self.common_norm is True:
|
||||
res = res.assign(group_weight=data["weight"].sum())
|
||||
else:
|
||||
if self.common_norm is False:
|
||||
norm_vars = grouping_vars
|
||||
else:
|
||||
self._check_var_list_or_boolean("common_norm", grouping_vars)
|
||||
norm_vars = [v for v in self.common_norm if v in grouping_vars]
|
||||
|
||||
res = res.join(
|
||||
data.groupby(norm_vars)["weight"].sum().rename("group_weight"),
|
||||
on=norm_vars,
|
||||
)
|
||||
|
||||
res["density"] *= res.eval("weight / group_weight")
|
||||
value = {"x": "y", "y": "x"}[orient]
|
||||
res[value] = res["density"]
|
||||
return res.drop(["weight", "group_weight"], axis=1)
|
||||
78
ftline/lib/python3.12/site-packages/seaborn/_stats/order.py
Normal file
78
ftline/lib/python3.12/site-packages/seaborn/_stats/order.py
Normal file
@@ -0,0 +1,78 @@
|
||||
|
||||
from __future__ import annotations
|
||||
from dataclasses import dataclass
|
||||
from typing import ClassVar, cast
|
||||
try:
|
||||
from typing import Literal
|
||||
except ImportError:
|
||||
from typing_extensions import Literal # type: ignore
|
||||
|
||||
import numpy as np
|
||||
from pandas import DataFrame
|
||||
|
||||
from seaborn._core.scales import Scale
|
||||
from seaborn._core.groupby import GroupBy
|
||||
from seaborn._stats.base import Stat
|
||||
from seaborn.utils import _version_predates
|
||||
|
||||
|
||||
# From https://github.com/numpy/numpy/blob/main/numpy/lib/function_base.pyi
|
||||
_MethodKind = Literal[
|
||||
"inverted_cdf",
|
||||
"averaged_inverted_cdf",
|
||||
"closest_observation",
|
||||
"interpolated_inverted_cdf",
|
||||
"hazen",
|
||||
"weibull",
|
||||
"linear",
|
||||
"median_unbiased",
|
||||
"normal_unbiased",
|
||||
"lower",
|
||||
"higher",
|
||||
"midpoint",
|
||||
"nearest",
|
||||
]
|
||||
|
||||
|
||||
@dataclass
|
||||
class Perc(Stat):
|
||||
"""
|
||||
Replace observations with percentile values.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
k : list of numbers or int
|
||||
If a list of numbers, this gives the percentiles (in [0, 100]) to compute.
|
||||
If an integer, compute `k` evenly-spaced percentiles between 0 and 100.
|
||||
For example, `k=5` computes the 0, 25, 50, 75, and 100th percentiles.
|
||||
method : str
|
||||
Method for interpolating percentiles between observed datapoints.
|
||||
See :func:`numpy.percentile` for valid options and more information.
|
||||
|
||||
Examples
|
||||
--------
|
||||
.. include:: ../docstrings/objects.Perc.rst
|
||||
|
||||
"""
|
||||
k: int | list[float] = 5
|
||||
method: str = "linear"
|
||||
|
||||
group_by_orient: ClassVar[bool] = True
|
||||
|
||||
def _percentile(self, data: DataFrame, var: str) -> DataFrame:
|
||||
|
||||
k = list(np.linspace(0, 100, self.k)) if isinstance(self.k, int) else self.k
|
||||
method = cast(_MethodKind, self.method)
|
||||
values = data[var].dropna()
|
||||
if _version_predates(np, "1.22"):
|
||||
res = np.percentile(values, k, interpolation=method) # type: ignore
|
||||
else:
|
||||
res = np.percentile(data[var].dropna(), k, method=method)
|
||||
return DataFrame({var: res, "percentile": k})
|
||||
|
||||
def __call__(
|
||||
self, data: DataFrame, groupby: GroupBy, orient: str, scales: dict[str, Scale],
|
||||
) -> DataFrame:
|
||||
|
||||
var = {"x": "y", "y": "x"}[orient]
|
||||
return groupby.apply(data, self._percentile, var)
|
||||
@@ -0,0 +1,50 @@
|
||||
from __future__ import annotations
|
||||
from dataclasses import dataclass
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
from seaborn._stats.base import Stat
|
||||
|
||||
|
||||
@dataclass
|
||||
class PolyFit(Stat):
|
||||
"""
|
||||
Fit a polynomial of the given order and resample data onto predicted curve.
|
||||
"""
|
||||
# This is a provisional class that is useful for building out functionality.
|
||||
# It may or may not change substantially in form or dissappear as we think
|
||||
# through the organization of the stats subpackage.
|
||||
|
||||
order: int = 2
|
||||
gridsize: int = 100
|
||||
|
||||
def _fit_predict(self, data):
|
||||
|
||||
x = data["x"]
|
||||
y = data["y"]
|
||||
if x.nunique() <= self.order:
|
||||
# TODO warn?
|
||||
xx = yy = []
|
||||
else:
|
||||
p = np.polyfit(x, y, self.order)
|
||||
xx = np.linspace(x.min(), x.max(), self.gridsize)
|
||||
yy = np.polyval(p, xx)
|
||||
|
||||
return pd.DataFrame(dict(x=xx, y=yy))
|
||||
|
||||
# TODO we should have a way of identifying the method that will be applied
|
||||
# and then only define __call__ on a base-class of stats with this pattern
|
||||
|
||||
def __call__(self, data, groupby, orient, scales):
|
||||
|
||||
return (
|
||||
groupby
|
||||
.apply(data.dropna(subset=["x", "y"]), self._fit_predict)
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class OLSFit(Stat):
|
||||
|
||||
...
|
||||
90
ftline/lib/python3.12/site-packages/seaborn/_testing.py
Normal file
90
ftline/lib/python3.12/site-packages/seaborn/_testing.py
Normal file
@@ -0,0 +1,90 @@
|
||||
import numpy as np
|
||||
import matplotlib as mpl
|
||||
from matplotlib.colors import to_rgb, to_rgba
|
||||
from numpy.testing import assert_array_equal
|
||||
|
||||
|
||||
USE_PROPS = [
|
||||
"alpha",
|
||||
"edgecolor",
|
||||
"facecolor",
|
||||
"fill",
|
||||
"hatch",
|
||||
"height",
|
||||
"linestyle",
|
||||
"linewidth",
|
||||
"paths",
|
||||
"xy",
|
||||
"xydata",
|
||||
"sizes",
|
||||
"zorder",
|
||||
]
|
||||
|
||||
|
||||
def assert_artists_equal(list1, list2):
|
||||
|
||||
assert len(list1) == len(list2)
|
||||
for a1, a2 in zip(list1, list2):
|
||||
assert a1.__class__ == a2.__class__
|
||||
prop1 = a1.properties()
|
||||
prop2 = a2.properties()
|
||||
for key in USE_PROPS:
|
||||
if key not in prop1:
|
||||
continue
|
||||
v1 = prop1[key]
|
||||
v2 = prop2[key]
|
||||
if key == "paths":
|
||||
for p1, p2 in zip(v1, v2):
|
||||
assert_array_equal(p1.vertices, p2.vertices)
|
||||
assert_array_equal(p1.codes, p2.codes)
|
||||
elif key == "color":
|
||||
v1 = mpl.colors.to_rgba(v1)
|
||||
v2 = mpl.colors.to_rgba(v2)
|
||||
assert v1 == v2
|
||||
elif isinstance(v1, np.ndarray):
|
||||
assert_array_equal(v1, v2)
|
||||
else:
|
||||
assert v1 == v2
|
||||
|
||||
|
||||
def assert_legends_equal(leg1, leg2):
|
||||
|
||||
assert leg1.get_title().get_text() == leg2.get_title().get_text()
|
||||
for t1, t2 in zip(leg1.get_texts(), leg2.get_texts()):
|
||||
assert t1.get_text() == t2.get_text()
|
||||
|
||||
assert_artists_equal(
|
||||
leg1.get_patches(), leg2.get_patches(),
|
||||
)
|
||||
assert_artists_equal(
|
||||
leg1.get_lines(), leg2.get_lines(),
|
||||
)
|
||||
|
||||
|
||||
def assert_plots_equal(ax1, ax2, labels=True):
|
||||
|
||||
assert_artists_equal(ax1.patches, ax2.patches)
|
||||
assert_artists_equal(ax1.lines, ax2.lines)
|
||||
assert_artists_equal(ax1.collections, ax2.collections)
|
||||
|
||||
if labels:
|
||||
assert ax1.get_xlabel() == ax2.get_xlabel()
|
||||
assert ax1.get_ylabel() == ax2.get_ylabel()
|
||||
|
||||
|
||||
def assert_colors_equal(a, b, check_alpha=True):
|
||||
|
||||
def handle_array(x):
|
||||
|
||||
if isinstance(x, np.ndarray):
|
||||
if x.ndim > 1:
|
||||
x = np.unique(x, axis=0).squeeze()
|
||||
if x.ndim > 1:
|
||||
raise ValueError("Color arrays must be 1 dimensional")
|
||||
return x
|
||||
|
||||
a = handle_array(a)
|
||||
b = handle_array(b)
|
||||
|
||||
f = to_rgba if check_alpha else to_rgb
|
||||
assert f(a) == f(b)
|
||||
120
ftline/lib/python3.12/site-packages/seaborn/algorithms.py
Normal file
120
ftline/lib/python3.12/site-packages/seaborn/algorithms.py
Normal file
@@ -0,0 +1,120 @@
|
||||
"""Algorithms to support fitting routines in seaborn plotting functions."""
|
||||
import numpy as np
|
||||
import warnings
|
||||
|
||||
|
||||
def bootstrap(*args, **kwargs):
|
||||
"""Resample one or more arrays with replacement and store aggregate values.
|
||||
|
||||
Positional arguments are a sequence of arrays to bootstrap along the first
|
||||
axis and pass to a summary function.
|
||||
|
||||
Keyword arguments:
|
||||
n_boot : int, default=10000
|
||||
Number of iterations
|
||||
axis : int, default=None
|
||||
Will pass axis to ``func`` as a keyword argument.
|
||||
units : array, default=None
|
||||
Array of sampling unit IDs. When used the bootstrap resamples units
|
||||
and then observations within units instead of individual
|
||||
datapoints.
|
||||
func : string or callable, default="mean"
|
||||
Function to call on the args that are passed in. If string, uses as
|
||||
name of function in the numpy namespace. If nans are present in the
|
||||
data, will try to use nan-aware version of named function.
|
||||
seed : Generator | SeedSequence | RandomState | int | None
|
||||
Seed for the random number generator; useful if you want
|
||||
reproducible resamples.
|
||||
|
||||
Returns
|
||||
-------
|
||||
boot_dist: array
|
||||
array of bootstrapped statistic values
|
||||
|
||||
"""
|
||||
# Ensure list of arrays are same length
|
||||
if len(np.unique(list(map(len, args)))) > 1:
|
||||
raise ValueError("All input arrays must have the same length")
|
||||
n = len(args[0])
|
||||
|
||||
# Default keyword arguments
|
||||
n_boot = kwargs.get("n_boot", 10000)
|
||||
func = kwargs.get("func", "mean")
|
||||
axis = kwargs.get("axis", None)
|
||||
units = kwargs.get("units", None)
|
||||
random_seed = kwargs.get("random_seed", None)
|
||||
if random_seed is not None:
|
||||
msg = "`random_seed` has been renamed to `seed` and will be removed"
|
||||
warnings.warn(msg)
|
||||
seed = kwargs.get("seed", random_seed)
|
||||
if axis is None:
|
||||
func_kwargs = dict()
|
||||
else:
|
||||
func_kwargs = dict(axis=axis)
|
||||
|
||||
# Initialize the resampler
|
||||
if isinstance(seed, np.random.RandomState):
|
||||
rng = seed
|
||||
else:
|
||||
rng = np.random.default_rng(seed)
|
||||
|
||||
# Coerce to arrays
|
||||
args = list(map(np.asarray, args))
|
||||
if units is not None:
|
||||
units = np.asarray(units)
|
||||
|
||||
if isinstance(func, str):
|
||||
|
||||
# Allow named numpy functions
|
||||
f = getattr(np, func)
|
||||
|
||||
# Try to use nan-aware version of function if necessary
|
||||
missing_data = np.isnan(np.sum(np.column_stack(args)))
|
||||
|
||||
if missing_data and not func.startswith("nan"):
|
||||
nanf = getattr(np, f"nan{func}", None)
|
||||
if nanf is None:
|
||||
msg = f"Data contain nans but no nan-aware version of `{func}` found"
|
||||
warnings.warn(msg, UserWarning)
|
||||
else:
|
||||
f = nanf
|
||||
|
||||
else:
|
||||
f = func
|
||||
|
||||
# Handle numpy changes
|
||||
try:
|
||||
integers = rng.integers
|
||||
except AttributeError:
|
||||
integers = rng.randint
|
||||
|
||||
# Do the bootstrap
|
||||
if units is not None:
|
||||
return _structured_bootstrap(args, n_boot, units, f,
|
||||
func_kwargs, integers)
|
||||
|
||||
boot_dist = []
|
||||
for i in range(int(n_boot)):
|
||||
resampler = integers(0, n, n, dtype=np.intp) # intp is indexing dtype
|
||||
sample = [a.take(resampler, axis=0) for a in args]
|
||||
boot_dist.append(f(*sample, **func_kwargs))
|
||||
return np.array(boot_dist)
|
||||
|
||||
|
||||
def _structured_bootstrap(args, n_boot, units, func, func_kwargs, integers):
|
||||
"""Resample units instead of datapoints."""
|
||||
unique_units = np.unique(units)
|
||||
n_units = len(unique_units)
|
||||
|
||||
args = [[a[units == unit] for unit in unique_units] for a in args]
|
||||
|
||||
boot_dist = []
|
||||
for i in range(int(n_boot)):
|
||||
resampler = integers(0, n_units, n_units, dtype=np.intp)
|
||||
sample = [[a[i] for i in resampler] for a in args]
|
||||
lengths = map(len, sample[0])
|
||||
resampler = [integers(0, n, n, dtype=np.intp) for n in lengths]
|
||||
sample = [[c.take(r, axis=0) for c, r in zip(a, resampler)] for a in sample]
|
||||
sample = list(map(np.concatenate, sample))
|
||||
boot_dist.append(func(*sample, **func_kwargs))
|
||||
return np.array(boot_dist)
|
||||
2401
ftline/lib/python3.12/site-packages/seaborn/axisgrid.py
Normal file
2401
ftline/lib/python3.12/site-packages/seaborn/axisgrid.py
Normal file
File diff suppressed because it is too large
Load Diff
3456
ftline/lib/python3.12/site-packages/seaborn/categorical.py
Normal file
3456
ftline/lib/python3.12/site-packages/seaborn/categorical.py
Normal file
File diff suppressed because it is too large
Load Diff
1586
ftline/lib/python3.12/site-packages/seaborn/cm.py
Normal file
1586
ftline/lib/python3.12/site-packages/seaborn/cm.py
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,2 @@
|
||||
from .xkcd_rgb import xkcd_rgb # noqa: F401
|
||||
from .crayons import crayons # noqa: F401
|
||||
120
ftline/lib/python3.12/site-packages/seaborn/colors/crayons.py
Normal file
120
ftline/lib/python3.12/site-packages/seaborn/colors/crayons.py
Normal file
@@ -0,0 +1,120 @@
|
||||
crayons = {'Almond': '#EFDECD',
|
||||
'Antique Brass': '#CD9575',
|
||||
'Apricot': '#FDD9B5',
|
||||
'Aquamarine': '#78DBE2',
|
||||
'Asparagus': '#87A96B',
|
||||
'Atomic Tangerine': '#FFA474',
|
||||
'Banana Mania': '#FAE7B5',
|
||||
'Beaver': '#9F8170',
|
||||
'Bittersweet': '#FD7C6E',
|
||||
'Black': '#000000',
|
||||
'Blue': '#1F75FE',
|
||||
'Blue Bell': '#A2A2D0',
|
||||
'Blue Green': '#0D98BA',
|
||||
'Blue Violet': '#7366BD',
|
||||
'Blush': '#DE5D83',
|
||||
'Brick Red': '#CB4154',
|
||||
'Brown': '#B4674D',
|
||||
'Burnt Orange': '#FF7F49',
|
||||
'Burnt Sienna': '#EA7E5D',
|
||||
'Cadet Blue': '#B0B7C6',
|
||||
'Canary': '#FFFF99',
|
||||
'Caribbean Green': '#00CC99',
|
||||
'Carnation Pink': '#FFAACC',
|
||||
'Cerise': '#DD4492',
|
||||
'Cerulean': '#1DACD6',
|
||||
'Chestnut': '#BC5D58',
|
||||
'Copper': '#DD9475',
|
||||
'Cornflower': '#9ACEEB',
|
||||
'Cotton Candy': '#FFBCD9',
|
||||
'Dandelion': '#FDDB6D',
|
||||
'Denim': '#2B6CC4',
|
||||
'Desert Sand': '#EFCDB8',
|
||||
'Eggplant': '#6E5160',
|
||||
'Electric Lime': '#CEFF1D',
|
||||
'Fern': '#71BC78',
|
||||
'Forest Green': '#6DAE81',
|
||||
'Fuchsia': '#C364C5',
|
||||
'Fuzzy Wuzzy': '#CC6666',
|
||||
'Gold': '#E7C697',
|
||||
'Goldenrod': '#FCD975',
|
||||
'Granny Smith Apple': '#A8E4A0',
|
||||
'Gray': '#95918C',
|
||||
'Green': '#1CAC78',
|
||||
'Green Yellow': '#F0E891',
|
||||
'Hot Magenta': '#FF1DCE',
|
||||
'Inchworm': '#B2EC5D',
|
||||
'Indigo': '#5D76CB',
|
||||
'Jazzberry Jam': '#CA3767',
|
||||
'Jungle Green': '#3BB08F',
|
||||
'Laser Lemon': '#FEFE22',
|
||||
'Lavender': '#FCB4D5',
|
||||
'Macaroni and Cheese': '#FFBD88',
|
||||
'Magenta': '#F664AF',
|
||||
'Mahogany': '#CD4A4C',
|
||||
'Manatee': '#979AAA',
|
||||
'Mango Tango': '#FF8243',
|
||||
'Maroon': '#C8385A',
|
||||
'Mauvelous': '#EF98AA',
|
||||
'Melon': '#FDBCB4',
|
||||
'Midnight Blue': '#1A4876',
|
||||
'Mountain Meadow': '#30BA8F',
|
||||
'Navy Blue': '#1974D2',
|
||||
'Neon Carrot': '#FFA343',
|
||||
'Olive Green': '#BAB86C',
|
||||
'Orange': '#FF7538',
|
||||
'Orchid': '#E6A8D7',
|
||||
'Outer Space': '#414A4C',
|
||||
'Outrageous Orange': '#FF6E4A',
|
||||
'Pacific Blue': '#1CA9C9',
|
||||
'Peach': '#FFCFAB',
|
||||
'Periwinkle': '#C5D0E6',
|
||||
'Piggy Pink': '#FDDDE6',
|
||||
'Pine Green': '#158078',
|
||||
'Pink Flamingo': '#FC74FD',
|
||||
'Pink Sherbert': '#F78FA7',
|
||||
'Plum': '#8E4585',
|
||||
'Purple Heart': '#7442C8',
|
||||
"Purple Mountains' Majesty": '#9D81BA',
|
||||
'Purple Pizzazz': '#FE4EDA',
|
||||
'Radical Red': '#FF496C',
|
||||
'Raw Sienna': '#D68A59',
|
||||
'Razzle Dazzle Rose': '#FF48D0',
|
||||
'Razzmatazz': '#E3256B',
|
||||
'Red': '#EE204D',
|
||||
'Red Orange': '#FF5349',
|
||||
'Red Violet': '#C0448F',
|
||||
"Robin's Egg Blue": '#1FCECB',
|
||||
'Royal Purple': '#7851A9',
|
||||
'Salmon': '#FF9BAA',
|
||||
'Scarlet': '#FC2847',
|
||||
"Screamin' Green": '#76FF7A',
|
||||
'Sea Green': '#93DFB8',
|
||||
'Sepia': '#A5694F',
|
||||
'Shadow': '#8A795D',
|
||||
'Shamrock': '#45CEA2',
|
||||
'Shocking Pink': '#FB7EFD',
|
||||
'Silver': '#CDC5C2',
|
||||
'Sky Blue': '#80DAEB',
|
||||
'Spring Green': '#ECEABE',
|
||||
'Sunglow': '#FFCF48',
|
||||
'Sunset Orange': '#FD5E53',
|
||||
'Tan': '#FAA76C',
|
||||
'Tickle Me Pink': '#FC89AC',
|
||||
'Timberwolf': '#DBD7D2',
|
||||
'Tropical Rain Forest': '#17806D',
|
||||
'Tumbleweed': '#DEAA88',
|
||||
'Turquoise Blue': '#77DDE7',
|
||||
'Unmellow Yellow': '#FFFF66',
|
||||
'Violet (Purple)': '#926EAE',
|
||||
'Violet Red': '#F75394',
|
||||
'Vivid Tangerine': '#FFA089',
|
||||
'Vivid Violet': '#8F509D',
|
||||
'White': '#FFFFFF',
|
||||
'Wild Blue Yonder': '#A2ADD0',
|
||||
'Wild Strawberry': '#FF43A4',
|
||||
'Wild Watermelon': '#FC6C85',
|
||||
'Wisteria': '#CDA4DE',
|
||||
'Yellow': '#FCE883',
|
||||
'Yellow Green': '#C5E384',
|
||||
'Yellow Orange': '#FFAE42'}
|
||||
949
ftline/lib/python3.12/site-packages/seaborn/colors/xkcd_rgb.py
Normal file
949
ftline/lib/python3.12/site-packages/seaborn/colors/xkcd_rgb.py
Normal file
@@ -0,0 +1,949 @@
|
||||
xkcd_rgb = {'acid green': '#8ffe09',
|
||||
'adobe': '#bd6c48',
|
||||
'algae': '#54ac68',
|
||||
'algae green': '#21c36f',
|
||||
'almost black': '#070d0d',
|
||||
'amber': '#feb308',
|
||||
'amethyst': '#9b5fc0',
|
||||
'apple': '#6ecb3c',
|
||||
'apple green': '#76cd26',
|
||||
'apricot': '#ffb16d',
|
||||
'aqua': '#13eac9',
|
||||
'aqua blue': '#02d8e9',
|
||||
'aqua green': '#12e193',
|
||||
'aqua marine': '#2ee8bb',
|
||||
'aquamarine': '#04d8b2',
|
||||
'army green': '#4b5d16',
|
||||
'asparagus': '#77ab56',
|
||||
'aubergine': '#3d0734',
|
||||
'auburn': '#9a3001',
|
||||
'avocado': '#90b134',
|
||||
'avocado green': '#87a922',
|
||||
'azul': '#1d5dec',
|
||||
'azure': '#069af3',
|
||||
'baby blue': '#a2cffe',
|
||||
'baby green': '#8cff9e',
|
||||
'baby pink': '#ffb7ce',
|
||||
'baby poo': '#ab9004',
|
||||
'baby poop': '#937c00',
|
||||
'baby poop green': '#8f9805',
|
||||
'baby puke green': '#b6c406',
|
||||
'baby purple': '#ca9bf7',
|
||||
'baby shit brown': '#ad900d',
|
||||
'baby shit green': '#889717',
|
||||
'banana': '#ffff7e',
|
||||
'banana yellow': '#fafe4b',
|
||||
'barbie pink': '#fe46a5',
|
||||
'barf green': '#94ac02',
|
||||
'barney': '#ac1db8',
|
||||
'barney purple': '#a00498',
|
||||
'battleship grey': '#6b7c85',
|
||||
'beige': '#e6daa6',
|
||||
'berry': '#990f4b',
|
||||
'bile': '#b5c306',
|
||||
'black': '#000000',
|
||||
'bland': '#afa88b',
|
||||
'blood': '#770001',
|
||||
'blood orange': '#fe4b03',
|
||||
'blood red': '#980002',
|
||||
'blue': '#0343df',
|
||||
'blue blue': '#2242c7',
|
||||
'blue green': '#137e6d',
|
||||
'blue grey': '#607c8e',
|
||||
'blue purple': '#5729ce',
|
||||
'blue violet': '#5d06e9',
|
||||
'blue with a hint of purple': '#533cc6',
|
||||
'blue/green': '#0f9b8e',
|
||||
'blue/grey': '#758da3',
|
||||
'blue/purple': '#5a06ef',
|
||||
'blueberry': '#464196',
|
||||
'bluegreen': '#017a79',
|
||||
'bluegrey': '#85a3b2',
|
||||
'bluey green': '#2bb179',
|
||||
'bluey grey': '#89a0b0',
|
||||
'bluey purple': '#6241c7',
|
||||
'bluish': '#2976bb',
|
||||
'bluish green': '#10a674',
|
||||
'bluish grey': '#748b97',
|
||||
'bluish purple': '#703be7',
|
||||
'blurple': '#5539cc',
|
||||
'blush': '#f29e8e',
|
||||
'blush pink': '#fe828c',
|
||||
'booger': '#9bb53c',
|
||||
'booger green': '#96b403',
|
||||
'bordeaux': '#7b002c',
|
||||
'boring green': '#63b365',
|
||||
'bottle green': '#044a05',
|
||||
'brick': '#a03623',
|
||||
'brick orange': '#c14a09',
|
||||
'brick red': '#8f1402',
|
||||
'bright aqua': '#0bf9ea',
|
||||
'bright blue': '#0165fc',
|
||||
'bright cyan': '#41fdfe',
|
||||
'bright green': '#01ff07',
|
||||
'bright lavender': '#c760ff',
|
||||
'bright light blue': '#26f7fd',
|
||||
'bright light green': '#2dfe54',
|
||||
'bright lilac': '#c95efb',
|
||||
'bright lime': '#87fd05',
|
||||
'bright lime green': '#65fe08',
|
||||
'bright magenta': '#ff08e8',
|
||||
'bright olive': '#9cbb04',
|
||||
'bright orange': '#ff5b00',
|
||||
'bright pink': '#fe01b1',
|
||||
'bright purple': '#be03fd',
|
||||
'bright red': '#ff000d',
|
||||
'bright sea green': '#05ffa6',
|
||||
'bright sky blue': '#02ccfe',
|
||||
'bright teal': '#01f9c6',
|
||||
'bright turquoise': '#0ffef9',
|
||||
'bright violet': '#ad0afd',
|
||||
'bright yellow': '#fffd01',
|
||||
'bright yellow green': '#9dff00',
|
||||
'british racing green': '#05480d',
|
||||
'bronze': '#a87900',
|
||||
'brown': '#653700',
|
||||
'brown green': '#706c11',
|
||||
'brown grey': '#8d8468',
|
||||
'brown orange': '#b96902',
|
||||
'brown red': '#922b05',
|
||||
'brown yellow': '#b29705',
|
||||
'brownish': '#9c6d57',
|
||||
'brownish green': '#6a6e09',
|
||||
'brownish grey': '#86775f',
|
||||
'brownish orange': '#cb7723',
|
||||
'brownish pink': '#c27e79',
|
||||
'brownish purple': '#76424e',
|
||||
'brownish red': '#9e3623',
|
||||
'brownish yellow': '#c9b003',
|
||||
'browny green': '#6f6c0a',
|
||||
'browny orange': '#ca6b02',
|
||||
'bruise': '#7e4071',
|
||||
'bubble gum pink': '#ff69af',
|
||||
'bubblegum': '#ff6cb5',
|
||||
'bubblegum pink': '#fe83cc',
|
||||
'buff': '#fef69e',
|
||||
'burgundy': '#610023',
|
||||
'burnt orange': '#c04e01',
|
||||
'burnt red': '#9f2305',
|
||||
'burnt siena': '#b75203',
|
||||
'burnt sienna': '#b04e0f',
|
||||
'burnt umber': '#a0450e',
|
||||
'burnt yellow': '#d5ab09',
|
||||
'burple': '#6832e3',
|
||||
'butter': '#ffff81',
|
||||
'butter yellow': '#fffd74',
|
||||
'butterscotch': '#fdb147',
|
||||
'cadet blue': '#4e7496',
|
||||
'camel': '#c69f59',
|
||||
'camo': '#7f8f4e',
|
||||
'camo green': '#526525',
|
||||
'camouflage green': '#4b6113',
|
||||
'canary': '#fdff63',
|
||||
'canary yellow': '#fffe40',
|
||||
'candy pink': '#ff63e9',
|
||||
'caramel': '#af6f09',
|
||||
'carmine': '#9d0216',
|
||||
'carnation': '#fd798f',
|
||||
'carnation pink': '#ff7fa7',
|
||||
'carolina blue': '#8ab8fe',
|
||||
'celadon': '#befdb7',
|
||||
'celery': '#c1fd95',
|
||||
'cement': '#a5a391',
|
||||
'cerise': '#de0c62',
|
||||
'cerulean': '#0485d1',
|
||||
'cerulean blue': '#056eee',
|
||||
'charcoal': '#343837',
|
||||
'charcoal grey': '#3c4142',
|
||||
'chartreuse': '#c1f80a',
|
||||
'cherry': '#cf0234',
|
||||
'cherry red': '#f7022a',
|
||||
'chestnut': '#742802',
|
||||
'chocolate': '#3d1c02',
|
||||
'chocolate brown': '#411900',
|
||||
'cinnamon': '#ac4f06',
|
||||
'claret': '#680018',
|
||||
'clay': '#b66a50',
|
||||
'clay brown': '#b2713d',
|
||||
'clear blue': '#247afd',
|
||||
'cloudy blue': '#acc2d9',
|
||||
'cobalt': '#1e488f',
|
||||
'cobalt blue': '#030aa7',
|
||||
'cocoa': '#875f42',
|
||||
'coffee': '#a6814c',
|
||||
'cool blue': '#4984b8',
|
||||
'cool green': '#33b864',
|
||||
'cool grey': '#95a3a6',
|
||||
'copper': '#b66325',
|
||||
'coral': '#fc5a50',
|
||||
'coral pink': '#ff6163',
|
||||
'cornflower': '#6a79f7',
|
||||
'cornflower blue': '#5170d7',
|
||||
'cranberry': '#9e003a',
|
||||
'cream': '#ffffc2',
|
||||
'creme': '#ffffb6',
|
||||
'crimson': '#8c000f',
|
||||
'custard': '#fffd78',
|
||||
'cyan': '#00ffff',
|
||||
'dandelion': '#fedf08',
|
||||
'dark': '#1b2431',
|
||||
'dark aqua': '#05696b',
|
||||
'dark aquamarine': '#017371',
|
||||
'dark beige': '#ac9362',
|
||||
'dark blue': '#00035b',
|
||||
'dark blue green': '#005249',
|
||||
'dark blue grey': '#1f3b4d',
|
||||
'dark brown': '#341c02',
|
||||
'dark coral': '#cf524e',
|
||||
'dark cream': '#fff39a',
|
||||
'dark cyan': '#0a888a',
|
||||
'dark forest green': '#002d04',
|
||||
'dark fuchsia': '#9d0759',
|
||||
'dark gold': '#b59410',
|
||||
'dark grass green': '#388004',
|
||||
'dark green': '#033500',
|
||||
'dark green blue': '#1f6357',
|
||||
'dark grey': '#363737',
|
||||
'dark grey blue': '#29465b',
|
||||
'dark hot pink': '#d90166',
|
||||
'dark indigo': '#1f0954',
|
||||
'dark khaki': '#9b8f55',
|
||||
'dark lavender': '#856798',
|
||||
'dark lilac': '#9c6da5',
|
||||
'dark lime': '#84b701',
|
||||
'dark lime green': '#7ebd01',
|
||||
'dark magenta': '#960056',
|
||||
'dark maroon': '#3c0008',
|
||||
'dark mauve': '#874c62',
|
||||
'dark mint': '#48c072',
|
||||
'dark mint green': '#20c073',
|
||||
'dark mustard': '#a88905',
|
||||
'dark navy': '#000435',
|
||||
'dark navy blue': '#00022e',
|
||||
'dark olive': '#373e02',
|
||||
'dark olive green': '#3c4d03',
|
||||
'dark orange': '#c65102',
|
||||
'dark pastel green': '#56ae57',
|
||||
'dark peach': '#de7e5d',
|
||||
'dark periwinkle': '#665fd1',
|
||||
'dark pink': '#cb416b',
|
||||
'dark plum': '#3f012c',
|
||||
'dark purple': '#35063e',
|
||||
'dark red': '#840000',
|
||||
'dark rose': '#b5485d',
|
||||
'dark royal blue': '#02066f',
|
||||
'dark sage': '#598556',
|
||||
'dark salmon': '#c85a53',
|
||||
'dark sand': '#a88f59',
|
||||
'dark sea green': '#11875d',
|
||||
'dark seafoam': '#1fb57a',
|
||||
'dark seafoam green': '#3eaf76',
|
||||
'dark sky blue': '#448ee4',
|
||||
'dark slate blue': '#214761',
|
||||
'dark tan': '#af884a',
|
||||
'dark taupe': '#7f684e',
|
||||
'dark teal': '#014d4e',
|
||||
'dark turquoise': '#045c5a',
|
||||
'dark violet': '#34013f',
|
||||
'dark yellow': '#d5b60a',
|
||||
'dark yellow green': '#728f02',
|
||||
'darkblue': '#030764',
|
||||
'darkgreen': '#054907',
|
||||
'darkish blue': '#014182',
|
||||
'darkish green': '#287c37',
|
||||
'darkish pink': '#da467d',
|
||||
'darkish purple': '#751973',
|
||||
'darkish red': '#a90308',
|
||||
'deep aqua': '#08787f',
|
||||
'deep blue': '#040273',
|
||||
'deep brown': '#410200',
|
||||
'deep green': '#02590f',
|
||||
'deep lavender': '#8d5eb7',
|
||||
'deep lilac': '#966ebd',
|
||||
'deep magenta': '#a0025c',
|
||||
'deep orange': '#dc4d01',
|
||||
'deep pink': '#cb0162',
|
||||
'deep purple': '#36013f',
|
||||
'deep red': '#9a0200',
|
||||
'deep rose': '#c74767',
|
||||
'deep sea blue': '#015482',
|
||||
'deep sky blue': '#0d75f8',
|
||||
'deep teal': '#00555a',
|
||||
'deep turquoise': '#017374',
|
||||
'deep violet': '#490648',
|
||||
'denim': '#3b638c',
|
||||
'denim blue': '#3b5b92',
|
||||
'desert': '#ccad60',
|
||||
'diarrhea': '#9f8303',
|
||||
'dirt': '#8a6e45',
|
||||
'dirt brown': '#836539',
|
||||
'dirty blue': '#3f829d',
|
||||
'dirty green': '#667e2c',
|
||||
'dirty orange': '#c87606',
|
||||
'dirty pink': '#ca7b80',
|
||||
'dirty purple': '#734a65',
|
||||
'dirty yellow': '#cdc50a',
|
||||
'dodger blue': '#3e82fc',
|
||||
'drab': '#828344',
|
||||
'drab green': '#749551',
|
||||
'dried blood': '#4b0101',
|
||||
'duck egg blue': '#c3fbf4',
|
||||
'dull blue': '#49759c',
|
||||
'dull brown': '#876e4b',
|
||||
'dull green': '#74a662',
|
||||
'dull orange': '#d8863b',
|
||||
'dull pink': '#d5869d',
|
||||
'dull purple': '#84597e',
|
||||
'dull red': '#bb3f3f',
|
||||
'dull teal': '#5f9e8f',
|
||||
'dull yellow': '#eedc5b',
|
||||
'dusk': '#4e5481',
|
||||
'dusk blue': '#26538d',
|
||||
'dusky blue': '#475f94',
|
||||
'dusky pink': '#cc7a8b',
|
||||
'dusky purple': '#895b7b',
|
||||
'dusky rose': '#ba6873',
|
||||
'dust': '#b2996e',
|
||||
'dusty blue': '#5a86ad',
|
||||
'dusty green': '#76a973',
|
||||
'dusty lavender': '#ac86a8',
|
||||
'dusty orange': '#f0833a',
|
||||
'dusty pink': '#d58a94',
|
||||
'dusty purple': '#825f87',
|
||||
'dusty red': '#b9484e',
|
||||
'dusty rose': '#c0737a',
|
||||
'dusty teal': '#4c9085',
|
||||
'earth': '#a2653e',
|
||||
'easter green': '#8cfd7e',
|
||||
'easter purple': '#c071fe',
|
||||
'ecru': '#feffca',
|
||||
'egg shell': '#fffcc4',
|
||||
'eggplant': '#380835',
|
||||
'eggplant purple': '#430541',
|
||||
'eggshell': '#ffffd4',
|
||||
'eggshell blue': '#c4fff7',
|
||||
'electric blue': '#0652ff',
|
||||
'electric green': '#21fc0d',
|
||||
'electric lime': '#a8ff04',
|
||||
'electric pink': '#ff0490',
|
||||
'electric purple': '#aa23ff',
|
||||
'emerald': '#01a049',
|
||||
'emerald green': '#028f1e',
|
||||
'evergreen': '#05472a',
|
||||
'faded blue': '#658cbb',
|
||||
'faded green': '#7bb274',
|
||||
'faded orange': '#f0944d',
|
||||
'faded pink': '#de9dac',
|
||||
'faded purple': '#916e99',
|
||||
'faded red': '#d3494e',
|
||||
'faded yellow': '#feff7f',
|
||||
'fawn': '#cfaf7b',
|
||||
'fern': '#63a950',
|
||||
'fern green': '#548d44',
|
||||
'fire engine red': '#fe0002',
|
||||
'flat blue': '#3c73a8',
|
||||
'flat green': '#699d4c',
|
||||
'fluorescent green': '#08ff08',
|
||||
'fluro green': '#0aff02',
|
||||
'foam green': '#90fda9',
|
||||
'forest': '#0b5509',
|
||||
'forest green': '#06470c',
|
||||
'forrest green': '#154406',
|
||||
'french blue': '#436bad',
|
||||
'fresh green': '#69d84f',
|
||||
'frog green': '#58bc08',
|
||||
'fuchsia': '#ed0dd9',
|
||||
'gold': '#dbb40c',
|
||||
'golden': '#f5bf03',
|
||||
'golden brown': '#b27a01',
|
||||
'golden rod': '#f9bc08',
|
||||
'golden yellow': '#fec615',
|
||||
'goldenrod': '#fac205',
|
||||
'grape': '#6c3461',
|
||||
'grape purple': '#5d1451',
|
||||
'grapefruit': '#fd5956',
|
||||
'grass': '#5cac2d',
|
||||
'grass green': '#3f9b0b',
|
||||
'grassy green': '#419c03',
|
||||
'green': '#15b01a',
|
||||
'green apple': '#5edc1f',
|
||||
'green blue': '#06b48b',
|
||||
'green brown': '#544e03',
|
||||
'green grey': '#77926f',
|
||||
'green teal': '#0cb577',
|
||||
'green yellow': '#c9ff27',
|
||||
'green/blue': '#01c08d',
|
||||
'green/yellow': '#b5ce08',
|
||||
'greenblue': '#23c48b',
|
||||
'greenish': '#40a368',
|
||||
'greenish beige': '#c9d179',
|
||||
'greenish blue': '#0b8b87',
|
||||
'greenish brown': '#696112',
|
||||
'greenish cyan': '#2afeb7',
|
||||
'greenish grey': '#96ae8d',
|
||||
'greenish tan': '#bccb7a',
|
||||
'greenish teal': '#32bf84',
|
||||
'greenish turquoise': '#00fbb0',
|
||||
'greenish yellow': '#cdfd02',
|
||||
'greeny blue': '#42b395',
|
||||
'greeny brown': '#696006',
|
||||
'greeny grey': '#7ea07a',
|
||||
'greeny yellow': '#c6f808',
|
||||
'grey': '#929591',
|
||||
'grey blue': '#6b8ba4',
|
||||
'grey brown': '#7f7053',
|
||||
'grey green': '#789b73',
|
||||
'grey pink': '#c3909b',
|
||||
'grey purple': '#826d8c',
|
||||
'grey teal': '#5e9b8a',
|
||||
'grey/blue': '#647d8e',
|
||||
'grey/green': '#86a17d',
|
||||
'greyblue': '#77a1b5',
|
||||
'greyish': '#a8a495',
|
||||
'greyish blue': '#5e819d',
|
||||
'greyish brown': '#7a6a4f',
|
||||
'greyish green': '#82a67d',
|
||||
'greyish pink': '#c88d94',
|
||||
'greyish purple': '#887191',
|
||||
'greyish teal': '#719f91',
|
||||
'gross green': '#a0bf16',
|
||||
'gunmetal': '#536267',
|
||||
'hazel': '#8e7618',
|
||||
'heather': '#a484ac',
|
||||
'heliotrope': '#d94ff5',
|
||||
'highlighter green': '#1bfc06',
|
||||
'hospital green': '#9be5aa',
|
||||
'hot green': '#25ff29',
|
||||
'hot magenta': '#f504c9',
|
||||
'hot pink': '#ff028d',
|
||||
'hot purple': '#cb00f5',
|
||||
'hunter green': '#0b4008',
|
||||
'ice': '#d6fffa',
|
||||
'ice blue': '#d7fffe',
|
||||
'icky green': '#8fae22',
|
||||
'indian red': '#850e04',
|
||||
'indigo': '#380282',
|
||||
'indigo blue': '#3a18b1',
|
||||
'iris': '#6258c4',
|
||||
'irish green': '#019529',
|
||||
'ivory': '#ffffcb',
|
||||
'jade': '#1fa774',
|
||||
'jade green': '#2baf6a',
|
||||
'jungle green': '#048243',
|
||||
'kelley green': '#009337',
|
||||
'kelly green': '#02ab2e',
|
||||
'kermit green': '#5cb200',
|
||||
'key lime': '#aeff6e',
|
||||
'khaki': '#aaa662',
|
||||
'khaki green': '#728639',
|
||||
'kiwi': '#9cef43',
|
||||
'kiwi green': '#8ee53f',
|
||||
'lavender': '#c79fef',
|
||||
'lavender blue': '#8b88f8',
|
||||
'lavender pink': '#dd85d7',
|
||||
'lawn green': '#4da409',
|
||||
'leaf': '#71aa34',
|
||||
'leaf green': '#5ca904',
|
||||
'leafy green': '#51b73b',
|
||||
'leather': '#ac7434',
|
||||
'lemon': '#fdff52',
|
||||
'lemon green': '#adf802',
|
||||
'lemon lime': '#bffe28',
|
||||
'lemon yellow': '#fdff38',
|
||||
'lichen': '#8fb67b',
|
||||
'light aqua': '#8cffdb',
|
||||
'light aquamarine': '#7bfdc7',
|
||||
'light beige': '#fffeb6',
|
||||
'light blue': '#95d0fc',
|
||||
'light blue green': '#7efbb3',
|
||||
'light blue grey': '#b7c9e2',
|
||||
'light bluish green': '#76fda8',
|
||||
'light bright green': '#53fe5c',
|
||||
'light brown': '#ad8150',
|
||||
'light burgundy': '#a8415b',
|
||||
'light cyan': '#acfffc',
|
||||
'light eggplant': '#894585',
|
||||
'light forest green': '#4f9153',
|
||||
'light gold': '#fddc5c',
|
||||
'light grass green': '#9af764',
|
||||
'light green': '#96f97b',
|
||||
'light green blue': '#56fca2',
|
||||
'light greenish blue': '#63f7b4',
|
||||
'light grey': '#d8dcd6',
|
||||
'light grey blue': '#9dbcd4',
|
||||
'light grey green': '#b7e1a1',
|
||||
'light indigo': '#6d5acf',
|
||||
'light khaki': '#e6f2a2',
|
||||
'light lavendar': '#efc0fe',
|
||||
'light lavender': '#dfc5fe',
|
||||
'light light blue': '#cafffb',
|
||||
'light light green': '#c8ffb0',
|
||||
'light lilac': '#edc8ff',
|
||||
'light lime': '#aefd6c',
|
||||
'light lime green': '#b9ff66',
|
||||
'light magenta': '#fa5ff7',
|
||||
'light maroon': '#a24857',
|
||||
'light mauve': '#c292a1',
|
||||
'light mint': '#b6ffbb',
|
||||
'light mint green': '#a6fbb2',
|
||||
'light moss green': '#a6c875',
|
||||
'light mustard': '#f7d560',
|
||||
'light navy': '#155084',
|
||||
'light navy blue': '#2e5a88',
|
||||
'light neon green': '#4efd54',
|
||||
'light olive': '#acbf69',
|
||||
'light olive green': '#a4be5c',
|
||||
'light orange': '#fdaa48',
|
||||
'light pastel green': '#b2fba5',
|
||||
'light pea green': '#c4fe82',
|
||||
'light peach': '#ffd8b1',
|
||||
'light periwinkle': '#c1c6fc',
|
||||
'light pink': '#ffd1df',
|
||||
'light plum': '#9d5783',
|
||||
'light purple': '#bf77f6',
|
||||
'light red': '#ff474c',
|
||||
'light rose': '#ffc5cb',
|
||||
'light royal blue': '#3a2efe',
|
||||
'light sage': '#bcecac',
|
||||
'light salmon': '#fea993',
|
||||
'light sea green': '#98f6b0',
|
||||
'light seafoam': '#a0febf',
|
||||
'light seafoam green': '#a7ffb5',
|
||||
'light sky blue': '#c6fcff',
|
||||
'light tan': '#fbeeac',
|
||||
'light teal': '#90e4c1',
|
||||
'light turquoise': '#7ef4cc',
|
||||
'light urple': '#b36ff6',
|
||||
'light violet': '#d6b4fc',
|
||||
'light yellow': '#fffe7a',
|
||||
'light yellow green': '#ccfd7f',
|
||||
'light yellowish green': '#c2ff89',
|
||||
'lightblue': '#7bc8f6',
|
||||
'lighter green': '#75fd63',
|
||||
'lighter purple': '#a55af4',
|
||||
'lightgreen': '#76ff7b',
|
||||
'lightish blue': '#3d7afd',
|
||||
'lightish green': '#61e160',
|
||||
'lightish purple': '#a552e6',
|
||||
'lightish red': '#fe2f4a',
|
||||
'lilac': '#cea2fd',
|
||||
'liliac': '#c48efd',
|
||||
'lime': '#aaff32',
|
||||
'lime green': '#89fe05',
|
||||
'lime yellow': '#d0fe1d',
|
||||
'lipstick': '#d5174e',
|
||||
'lipstick red': '#c0022f',
|
||||
'macaroni and cheese': '#efb435',
|
||||
'magenta': '#c20078',
|
||||
'mahogany': '#4a0100',
|
||||
'maize': '#f4d054',
|
||||
'mango': '#ffa62b',
|
||||
'manilla': '#fffa86',
|
||||
'marigold': '#fcc006',
|
||||
'marine': '#042e60',
|
||||
'marine blue': '#01386a',
|
||||
'maroon': '#650021',
|
||||
'mauve': '#ae7181',
|
||||
'medium blue': '#2c6fbb',
|
||||
'medium brown': '#7f5112',
|
||||
'medium green': '#39ad48',
|
||||
'medium grey': '#7d7f7c',
|
||||
'medium pink': '#f36196',
|
||||
'medium purple': '#9e43a2',
|
||||
'melon': '#ff7855',
|
||||
'merlot': '#730039',
|
||||
'metallic blue': '#4f738e',
|
||||
'mid blue': '#276ab3',
|
||||
'mid green': '#50a747',
|
||||
'midnight': '#03012d',
|
||||
'midnight blue': '#020035',
|
||||
'midnight purple': '#280137',
|
||||
'military green': '#667c3e',
|
||||
'milk chocolate': '#7f4e1e',
|
||||
'mint': '#9ffeb0',
|
||||
'mint green': '#8fff9f',
|
||||
'minty green': '#0bf77d',
|
||||
'mocha': '#9d7651',
|
||||
'moss': '#769958',
|
||||
'moss green': '#658b38',
|
||||
'mossy green': '#638b27',
|
||||
'mud': '#735c12',
|
||||
'mud brown': '#60460f',
|
||||
'mud green': '#606602',
|
||||
'muddy brown': '#886806',
|
||||
'muddy green': '#657432',
|
||||
'muddy yellow': '#bfac05',
|
||||
'mulberry': '#920a4e',
|
||||
'murky green': '#6c7a0e',
|
||||
'mushroom': '#ba9e88',
|
||||
'mustard': '#ceb301',
|
||||
'mustard brown': '#ac7e04',
|
||||
'mustard green': '#a8b504',
|
||||
'mustard yellow': '#d2bd0a',
|
||||
'muted blue': '#3b719f',
|
||||
'muted green': '#5fa052',
|
||||
'muted pink': '#d1768f',
|
||||
'muted purple': '#805b87',
|
||||
'nasty green': '#70b23f',
|
||||
'navy': '#01153e',
|
||||
'navy blue': '#001146',
|
||||
'navy green': '#35530a',
|
||||
'neon blue': '#04d9ff',
|
||||
'neon green': '#0cff0c',
|
||||
'neon pink': '#fe019a',
|
||||
'neon purple': '#bc13fe',
|
||||
'neon red': '#ff073a',
|
||||
'neon yellow': '#cfff04',
|
||||
'nice blue': '#107ab0',
|
||||
'night blue': '#040348',
|
||||
'ocean': '#017b92',
|
||||
'ocean blue': '#03719c',
|
||||
'ocean green': '#3d9973',
|
||||
'ocher': '#bf9b0c',
|
||||
'ochre': '#bf9005',
|
||||
'ocre': '#c69c04',
|
||||
'off blue': '#5684ae',
|
||||
'off green': '#6ba353',
|
||||
'off white': '#ffffe4',
|
||||
'off yellow': '#f1f33f',
|
||||
'old pink': '#c77986',
|
||||
'old rose': '#c87f89',
|
||||
'olive': '#6e750e',
|
||||
'olive brown': '#645403',
|
||||
'olive drab': '#6f7632',
|
||||
'olive green': '#677a04',
|
||||
'olive yellow': '#c2b709',
|
||||
'orange': '#f97306',
|
||||
'orange brown': '#be6400',
|
||||
'orange pink': '#ff6f52',
|
||||
'orange red': '#fd411e',
|
||||
'orange yellow': '#ffad01',
|
||||
'orangeish': '#fd8d49',
|
||||
'orangered': '#fe420f',
|
||||
'orangey brown': '#b16002',
|
||||
'orangey red': '#fa4224',
|
||||
'orangey yellow': '#fdb915',
|
||||
'orangish': '#fc824a',
|
||||
'orangish brown': '#b25f03',
|
||||
'orangish red': '#f43605',
|
||||
'orchid': '#c875c4',
|
||||
'pale': '#fff9d0',
|
||||
'pale aqua': '#b8ffeb',
|
||||
'pale blue': '#d0fefe',
|
||||
'pale brown': '#b1916e',
|
||||
'pale cyan': '#b7fffa',
|
||||
'pale gold': '#fdde6c',
|
||||
'pale green': '#c7fdb5',
|
||||
'pale grey': '#fdfdfe',
|
||||
'pale lavender': '#eecffe',
|
||||
'pale light green': '#b1fc99',
|
||||
'pale lilac': '#e4cbff',
|
||||
'pale lime': '#befd73',
|
||||
'pale lime green': '#b1ff65',
|
||||
'pale magenta': '#d767ad',
|
||||
'pale mauve': '#fed0fc',
|
||||
'pale olive': '#b9cc81',
|
||||
'pale olive green': '#b1d27b',
|
||||
'pale orange': '#ffa756',
|
||||
'pale peach': '#ffe5ad',
|
||||
'pale pink': '#ffcfdc',
|
||||
'pale purple': '#b790d4',
|
||||
'pale red': '#d9544d',
|
||||
'pale rose': '#fdc1c5',
|
||||
'pale salmon': '#ffb19a',
|
||||
'pale sky blue': '#bdf6fe',
|
||||
'pale teal': '#82cbb2',
|
||||
'pale turquoise': '#a5fbd5',
|
||||
'pale violet': '#ceaefa',
|
||||
'pale yellow': '#ffff84',
|
||||
'parchment': '#fefcaf',
|
||||
'pastel blue': '#a2bffe',
|
||||
'pastel green': '#b0ff9d',
|
||||
'pastel orange': '#ff964f',
|
||||
'pastel pink': '#ffbacd',
|
||||
'pastel purple': '#caa0ff',
|
||||
'pastel red': '#db5856',
|
||||
'pastel yellow': '#fffe71',
|
||||
'pea': '#a4bf20',
|
||||
'pea green': '#8eab12',
|
||||
'pea soup': '#929901',
|
||||
'pea soup green': '#94a617',
|
||||
'peach': '#ffb07c',
|
||||
'peachy pink': '#ff9a8a',
|
||||
'peacock blue': '#016795',
|
||||
'pear': '#cbf85f',
|
||||
'periwinkle': '#8e82fe',
|
||||
'periwinkle blue': '#8f99fb',
|
||||
'perrywinkle': '#8f8ce7',
|
||||
'petrol': '#005f6a',
|
||||
'pig pink': '#e78ea5',
|
||||
'pine': '#2b5d34',
|
||||
'pine green': '#0a481e',
|
||||
'pink': '#ff81c0',
|
||||
'pink purple': '#db4bda',
|
||||
'pink red': '#f5054f',
|
||||
'pink/purple': '#ef1de7',
|
||||
'pinkish': '#d46a7e',
|
||||
'pinkish brown': '#b17261',
|
||||
'pinkish grey': '#c8aca9',
|
||||
'pinkish orange': '#ff724c',
|
||||
'pinkish purple': '#d648d7',
|
||||
'pinkish red': '#f10c45',
|
||||
'pinkish tan': '#d99b82',
|
||||
'pinky': '#fc86aa',
|
||||
'pinky purple': '#c94cbe',
|
||||
'pinky red': '#fc2647',
|
||||
'piss yellow': '#ddd618',
|
||||
'pistachio': '#c0fa8b',
|
||||
'plum': '#580f41',
|
||||
'plum purple': '#4e0550',
|
||||
'poison green': '#40fd14',
|
||||
'poo': '#8f7303',
|
||||
'poo brown': '#885f01',
|
||||
'poop': '#7f5e00',
|
||||
'poop brown': '#7a5901',
|
||||
'poop green': '#6f7c00',
|
||||
'powder blue': '#b1d1fc',
|
||||
'powder pink': '#ffb2d0',
|
||||
'primary blue': '#0804f9',
|
||||
'prussian blue': '#004577',
|
||||
'puce': '#a57e52',
|
||||
'puke': '#a5a502',
|
||||
'puke brown': '#947706',
|
||||
'puke green': '#9aae07',
|
||||
'puke yellow': '#c2be0e',
|
||||
'pumpkin': '#e17701',
|
||||
'pumpkin orange': '#fb7d07',
|
||||
'pure blue': '#0203e2',
|
||||
'purple': '#7e1e9c',
|
||||
'purple blue': '#632de9',
|
||||
'purple brown': '#673a3f',
|
||||
'purple grey': '#866f85',
|
||||
'purple pink': '#e03fd8',
|
||||
'purple red': '#990147',
|
||||
'purple/blue': '#5d21d0',
|
||||
'purple/pink': '#d725de',
|
||||
'purpleish': '#98568d',
|
||||
'purpleish blue': '#6140ef',
|
||||
'purpleish pink': '#df4ec8',
|
||||
'purpley': '#8756e4',
|
||||
'purpley blue': '#5f34e7',
|
||||
'purpley grey': '#947e94',
|
||||
'purpley pink': '#c83cb9',
|
||||
'purplish': '#94568c',
|
||||
'purplish blue': '#601ef9',
|
||||
'purplish brown': '#6b4247',
|
||||
'purplish grey': '#7a687f',
|
||||
'purplish pink': '#ce5dae',
|
||||
'purplish red': '#b0054b',
|
||||
'purply': '#983fb2',
|
||||
'purply blue': '#661aee',
|
||||
'purply pink': '#f075e6',
|
||||
'putty': '#beae8a',
|
||||
'racing green': '#014600',
|
||||
'radioactive green': '#2cfa1f',
|
||||
'raspberry': '#b00149',
|
||||
'raw sienna': '#9a6200',
|
||||
'raw umber': '#a75e09',
|
||||
'really light blue': '#d4ffff',
|
||||
'red': '#e50000',
|
||||
'red brown': '#8b2e16',
|
||||
'red orange': '#fd3c06',
|
||||
'red pink': '#fa2a55',
|
||||
'red purple': '#820747',
|
||||
'red violet': '#9e0168',
|
||||
'red wine': '#8c0034',
|
||||
'reddish': '#c44240',
|
||||
'reddish brown': '#7f2b0a',
|
||||
'reddish grey': '#997570',
|
||||
'reddish orange': '#f8481c',
|
||||
'reddish pink': '#fe2c54',
|
||||
'reddish purple': '#910951',
|
||||
'reddy brown': '#6e1005',
|
||||
'rich blue': '#021bf9',
|
||||
'rich purple': '#720058',
|
||||
'robin egg blue': '#8af1fe',
|
||||
"robin's egg": '#6dedfd',
|
||||
"robin's egg blue": '#98eff9',
|
||||
'rosa': '#fe86a4',
|
||||
'rose': '#cf6275',
|
||||
'rose pink': '#f7879a',
|
||||
'rose red': '#be013c',
|
||||
'rosy pink': '#f6688e',
|
||||
'rouge': '#ab1239',
|
||||
'royal': '#0c1793',
|
||||
'royal blue': '#0504aa',
|
||||
'royal purple': '#4b006e',
|
||||
'ruby': '#ca0147',
|
||||
'russet': '#a13905',
|
||||
'rust': '#a83c09',
|
||||
'rust brown': '#8b3103',
|
||||
'rust orange': '#c45508',
|
||||
'rust red': '#aa2704',
|
||||
'rusty orange': '#cd5909',
|
||||
'rusty red': '#af2f0d',
|
||||
'saffron': '#feb209',
|
||||
'sage': '#87ae73',
|
||||
'sage green': '#88b378',
|
||||
'salmon': '#ff796c',
|
||||
'salmon pink': '#fe7b7c',
|
||||
'sand': '#e2ca76',
|
||||
'sand brown': '#cba560',
|
||||
'sand yellow': '#fce166',
|
||||
'sandstone': '#c9ae74',
|
||||
'sandy': '#f1da7a',
|
||||
'sandy brown': '#c4a661',
|
||||
'sandy yellow': '#fdee73',
|
||||
'sap green': '#5c8b15',
|
||||
'sapphire': '#2138ab',
|
||||
'scarlet': '#be0119',
|
||||
'sea': '#3c9992',
|
||||
'sea blue': '#047495',
|
||||
'sea green': '#53fca1',
|
||||
'seafoam': '#80f9ad',
|
||||
'seafoam blue': '#78d1b6',
|
||||
'seafoam green': '#7af9ab',
|
||||
'seaweed': '#18d17b',
|
||||
'seaweed green': '#35ad6b',
|
||||
'sepia': '#985e2b',
|
||||
'shamrock': '#01b44c',
|
||||
'shamrock green': '#02c14d',
|
||||
'shit': '#7f5f00',
|
||||
'shit brown': '#7b5804',
|
||||
'shit green': '#758000',
|
||||
'shocking pink': '#fe02a2',
|
||||
'sick green': '#9db92c',
|
||||
'sickly green': '#94b21c',
|
||||
'sickly yellow': '#d0e429',
|
||||
'sienna': '#a9561e',
|
||||
'silver': '#c5c9c7',
|
||||
'sky': '#82cafc',
|
||||
'sky blue': '#75bbfd',
|
||||
'slate': '#516572',
|
||||
'slate blue': '#5b7c99',
|
||||
'slate green': '#658d6d',
|
||||
'slate grey': '#59656d',
|
||||
'slime green': '#99cc04',
|
||||
'snot': '#acbb0d',
|
||||
'snot green': '#9dc100',
|
||||
'soft blue': '#6488ea',
|
||||
'soft green': '#6fc276',
|
||||
'soft pink': '#fdb0c0',
|
||||
'soft purple': '#a66fb5',
|
||||
'spearmint': '#1ef876',
|
||||
'spring green': '#a9f971',
|
||||
'spruce': '#0a5f38',
|
||||
'squash': '#f2ab15',
|
||||
'steel': '#738595',
|
||||
'steel blue': '#5a7d9a',
|
||||
'steel grey': '#6f828a',
|
||||
'stone': '#ada587',
|
||||
'stormy blue': '#507b9c',
|
||||
'straw': '#fcf679',
|
||||
'strawberry': '#fb2943',
|
||||
'strong blue': '#0c06f7',
|
||||
'strong pink': '#ff0789',
|
||||
'sun yellow': '#ffdf22',
|
||||
'sunflower': '#ffc512',
|
||||
'sunflower yellow': '#ffda03',
|
||||
'sunny yellow': '#fff917',
|
||||
'sunshine yellow': '#fffd37',
|
||||
'swamp': '#698339',
|
||||
'swamp green': '#748500',
|
||||
'tan': '#d1b26f',
|
||||
'tan brown': '#ab7e4c',
|
||||
'tan green': '#a9be70',
|
||||
'tangerine': '#ff9408',
|
||||
'taupe': '#b9a281',
|
||||
'tea': '#65ab7c',
|
||||
'tea green': '#bdf8a3',
|
||||
'teal': '#029386',
|
||||
'teal blue': '#01889f',
|
||||
'teal green': '#25a36f',
|
||||
'tealish': '#24bca8',
|
||||
'tealish green': '#0cdc73',
|
||||
'terra cotta': '#c9643b',
|
||||
'terracota': '#cb6843',
|
||||
'terracotta': '#ca6641',
|
||||
'tiffany blue': '#7bf2da',
|
||||
'tomato': '#ef4026',
|
||||
'tomato red': '#ec2d01',
|
||||
'topaz': '#13bbaf',
|
||||
'toupe': '#c7ac7d',
|
||||
'toxic green': '#61de2a',
|
||||
'tree green': '#2a7e19',
|
||||
'true blue': '#010fcc',
|
||||
'true green': '#089404',
|
||||
'turquoise': '#06c2ac',
|
||||
'turquoise blue': '#06b1c4',
|
||||
'turquoise green': '#04f489',
|
||||
'turtle green': '#75b84f',
|
||||
'twilight': '#4e518b',
|
||||
'twilight blue': '#0a437a',
|
||||
'ugly blue': '#31668a',
|
||||
'ugly brown': '#7d7103',
|
||||
'ugly green': '#7a9703',
|
||||
'ugly pink': '#cd7584',
|
||||
'ugly purple': '#a442a0',
|
||||
'ugly yellow': '#d0c101',
|
||||
'ultramarine': '#2000b1',
|
||||
'ultramarine blue': '#1805db',
|
||||
'umber': '#b26400',
|
||||
'velvet': '#750851',
|
||||
'vermillion': '#f4320c',
|
||||
'very dark blue': '#000133',
|
||||
'very dark brown': '#1d0200',
|
||||
'very dark green': '#062e03',
|
||||
'very dark purple': '#2a0134',
|
||||
'very light blue': '#d5ffff',
|
||||
'very light brown': '#d3b683',
|
||||
'very light green': '#d1ffbd',
|
||||
'very light pink': '#fff4f2',
|
||||
'very light purple': '#f6cefc',
|
||||
'very pale blue': '#d6fffe',
|
||||
'very pale green': '#cffdbc',
|
||||
'vibrant blue': '#0339f8',
|
||||
'vibrant green': '#0add08',
|
||||
'vibrant purple': '#ad03de',
|
||||
'violet': '#9a0eea',
|
||||
'violet blue': '#510ac9',
|
||||
'violet pink': '#fb5ffc',
|
||||
'violet red': '#a50055',
|
||||
'viridian': '#1e9167',
|
||||
'vivid blue': '#152eff',
|
||||
'vivid green': '#2fef10',
|
||||
'vivid purple': '#9900fa',
|
||||
'vomit': '#a2a415',
|
||||
'vomit green': '#89a203',
|
||||
'vomit yellow': '#c7c10c',
|
||||
'warm blue': '#4b57db',
|
||||
'warm brown': '#964e02',
|
||||
'warm grey': '#978a84',
|
||||
'warm pink': '#fb5581',
|
||||
'warm purple': '#952e8f',
|
||||
'washed out green': '#bcf5a6',
|
||||
'water blue': '#0e87cc',
|
||||
'watermelon': '#fd4659',
|
||||
'weird green': '#3ae57f',
|
||||
'wheat': '#fbdd7e',
|
||||
'white': '#ffffff',
|
||||
'windows blue': '#3778bf',
|
||||
'wine': '#80013f',
|
||||
'wine red': '#7b0323',
|
||||
'wintergreen': '#20f986',
|
||||
'wisteria': '#a87dc2',
|
||||
'yellow': '#ffff14',
|
||||
'yellow brown': '#b79400',
|
||||
'yellow green': '#c0fb2d',
|
||||
'yellow ochre': '#cb9d06',
|
||||
'yellow orange': '#fcb001',
|
||||
'yellow tan': '#ffe36e',
|
||||
'yellow/green': '#c8fd3d',
|
||||
'yellowgreen': '#bbf90f',
|
||||
'yellowish': '#faee66',
|
||||
'yellowish brown': '#9b7a01',
|
||||
'yellowish green': '#b0dd16',
|
||||
'yellowish orange': '#ffab0f',
|
||||
'yellowish tan': '#fcfc81',
|
||||
'yellowy brown': '#ae8b0c',
|
||||
'yellowy green': '#bff128'}
|
||||
2531
ftline/lib/python3.12/site-packages/seaborn/distributions.py
Normal file
2531
ftline/lib/python3.12/site-packages/seaborn/distributions.py
Normal file
File diff suppressed because it is too large
Load Diff
0
ftline/lib/python3.12/site-packages/seaborn/external/__init__.py
vendored
Normal file
0
ftline/lib/python3.12/site-packages/seaborn/external/__init__.py
vendored
Normal file
245
ftline/lib/python3.12/site-packages/seaborn/external/appdirs.py
vendored
Normal file
245
ftline/lib/python3.12/site-packages/seaborn/external/appdirs.py
vendored
Normal file
@@ -0,0 +1,245 @@
|
||||
#!/usr/bin/env python3
|
||||
# Copyright (c) 2005-2010 ActiveState Software Inc.
|
||||
# Copyright (c) 2013 Eddy Petrișor
|
||||
|
||||
# flake8: noqa
|
||||
|
||||
"""
|
||||
This file is directly from
|
||||
https://github.com/ActiveState/appdirs/blob/3fe6a83776843a46f20c2e5587afcffe05e03b39/appdirs.py
|
||||
|
||||
The license of https://github.com/ActiveState/appdirs copied below:
|
||||
|
||||
|
||||
# This is the MIT license
|
||||
|
||||
Copyright (c) 2010 ActiveState Software Inc.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of this software and associated documentation files (the
|
||||
"Software"), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish,
|
||||
distribute, sublicense, and/or sell copies of the Software, and to
|
||||
permit persons to whom the Software is furnished to do so, subject to
|
||||
the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included
|
||||
in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
"""
|
||||
|
||||
"""Utilities for determining application-specific dirs.
|
||||
|
||||
See <https://github.com/ActiveState/appdirs> for details and usage.
|
||||
"""
|
||||
# Dev Notes:
|
||||
# - MSDN on where to store app data files:
|
||||
# http://support.microsoft.com/default.aspx?scid=kb;en-us;310294#XSLTH3194121123120121120120
|
||||
# - Mac OS X: http://developer.apple.com/documentation/MacOSX/Conceptual/BPFileSystem/index.html
|
||||
# - XDG spec for Un*x: https://standards.freedesktop.org/basedir-spec/basedir-spec-latest.html
|
||||
|
||||
__version__ = "1.4.4"
|
||||
__version_info__ = tuple(int(segment) for segment in __version__.split("."))
|
||||
|
||||
|
||||
import sys
|
||||
import os
|
||||
|
||||
unicode = str
|
||||
|
||||
if sys.platform.startswith('java'):
|
||||
import platform
|
||||
os_name = platform.java_ver()[3][0]
|
||||
if os_name.startswith('Windows'): # "Windows XP", "Windows 7", etc.
|
||||
system = 'win32'
|
||||
elif os_name.startswith('Mac'): # "Mac OS X", etc.
|
||||
system = 'darwin'
|
||||
else: # "Linux", "SunOS", "FreeBSD", etc.
|
||||
# Setting this to "linux2" is not ideal, but only Windows or Mac
|
||||
# are actually checked for and the rest of the module expects
|
||||
# *sys.platform* style strings.
|
||||
system = 'linux2'
|
||||
else:
|
||||
system = sys.platform
|
||||
|
||||
|
||||
def user_cache_dir(appname=None, appauthor=None, version=None, opinion=True):
|
||||
r"""Return full path to the user-specific cache dir for this application.
|
||||
|
||||
"appname" is the name of application.
|
||||
If None, just the system directory is returned.
|
||||
"appauthor" (only used on Windows) is the name of the
|
||||
appauthor or distributing body for this application. Typically
|
||||
it is the owning company name. This falls back to appname. You may
|
||||
pass False to disable it.
|
||||
"version" is an optional version path element to append to the
|
||||
path. You might want to use this if you want multiple versions
|
||||
of your app to be able to run independently. If used, this
|
||||
would typically be "<major>.<minor>".
|
||||
Only applied when appname is present.
|
||||
"opinion" (boolean) can be False to disable the appending of
|
||||
"Cache" to the base app data dir for Windows. See
|
||||
discussion below.
|
||||
|
||||
Typical user cache directories are:
|
||||
Mac OS X: ~/Library/Caches/<AppName>
|
||||
Unix: ~/.cache/<AppName> (XDG default)
|
||||
Win XP: C:\Documents and Settings\<username>\Local Settings\Application Data\<AppAuthor>\<AppName>\Cache
|
||||
Vista: C:\Users\<username>\AppData\Local\<AppAuthor>\<AppName>\Cache
|
||||
|
||||
On Windows the only suggestion in the MSDN docs is that local settings go in
|
||||
the `CSIDL_LOCAL_APPDATA` directory. This is identical to the non-roaming
|
||||
app data dir (the default returned by `user_data_dir` above). Apps typically
|
||||
put cache data somewhere *under* the given dir here. Some examples:
|
||||
...\Mozilla\Firefox\Profiles\<ProfileName>\Cache
|
||||
...\Acme\SuperApp\Cache\1.0
|
||||
OPINION: This function appends "Cache" to the `CSIDL_LOCAL_APPDATA` value.
|
||||
This can be disabled with the `opinion=False` option.
|
||||
"""
|
||||
if system == "win32":
|
||||
if appauthor is None:
|
||||
appauthor = appname
|
||||
path = os.path.normpath(_get_win_folder("CSIDL_LOCAL_APPDATA"))
|
||||
if appname:
|
||||
if appauthor is not False:
|
||||
path = os.path.join(path, appauthor, appname)
|
||||
else:
|
||||
path = os.path.join(path, appname)
|
||||
if opinion:
|
||||
path = os.path.join(path, "Cache")
|
||||
elif system == 'darwin':
|
||||
path = os.path.expanduser('~/Library/Caches')
|
||||
if appname:
|
||||
path = os.path.join(path, appname)
|
||||
else:
|
||||
path = os.getenv('XDG_CACHE_HOME', os.path.expanduser('~/.cache'))
|
||||
if appname:
|
||||
path = os.path.join(path, appname)
|
||||
if appname and version:
|
||||
path = os.path.join(path, version)
|
||||
return path
|
||||
|
||||
|
||||
#---- internal support stuff
|
||||
|
||||
def _get_win_folder_from_registry(csidl_name):
|
||||
"""This is a fallback technique at best. I'm not sure if using the
|
||||
registry for this guarantees us the correct answer for all CSIDL_*
|
||||
names.
|
||||
"""
|
||||
import winreg as _winreg
|
||||
|
||||
shell_folder_name = {
|
||||
"CSIDL_APPDATA": "AppData",
|
||||
"CSIDL_COMMON_APPDATA": "Common AppData",
|
||||
"CSIDL_LOCAL_APPDATA": "Local AppData",
|
||||
}[csidl_name]
|
||||
|
||||
key = _winreg.OpenKey(
|
||||
_winreg.HKEY_CURRENT_USER,
|
||||
r"Software\Microsoft\Windows\CurrentVersion\Explorer\Shell Folders"
|
||||
)
|
||||
dir, type = _winreg.QueryValueEx(key, shell_folder_name)
|
||||
return dir
|
||||
|
||||
|
||||
def _get_win_folder_with_pywin32(csidl_name):
|
||||
from win32com.shell import shellcon, shell
|
||||
dir = shell.SHGetFolderPath(0, getattr(shellcon, csidl_name), 0, 0)
|
||||
# Try to make this a unicode path because SHGetFolderPath does
|
||||
# not return unicode strings when there is unicode data in the
|
||||
# path.
|
||||
try:
|
||||
dir = unicode(dir)
|
||||
|
||||
# Downgrade to short path name if have highbit chars. See
|
||||
# <http://bugs.activestate.com/show_bug.cgi?id=85099>.
|
||||
has_high_char = False
|
||||
for c in dir:
|
||||
if ord(c) > 255:
|
||||
has_high_char = True
|
||||
break
|
||||
if has_high_char:
|
||||
try:
|
||||
import win32api
|
||||
dir = win32api.GetShortPathName(dir)
|
||||
except ImportError:
|
||||
pass
|
||||
except UnicodeError:
|
||||
pass
|
||||
return dir
|
||||
|
||||
|
||||
def _get_win_folder_with_ctypes(csidl_name):
|
||||
import ctypes
|
||||
|
||||
csidl_const = {
|
||||
"CSIDL_APPDATA": 26,
|
||||
"CSIDL_COMMON_APPDATA": 35,
|
||||
"CSIDL_LOCAL_APPDATA": 28,
|
||||
}[csidl_name]
|
||||
|
||||
buf = ctypes.create_unicode_buffer(1024)
|
||||
ctypes.windll.shell32.SHGetFolderPathW(None, csidl_const, None, 0, buf)
|
||||
|
||||
# Downgrade to short path name if have highbit chars. See
|
||||
# <http://bugs.activestate.com/show_bug.cgi?id=85099>.
|
||||
has_high_char = False
|
||||
for c in buf:
|
||||
if ord(c) > 255:
|
||||
has_high_char = True
|
||||
break
|
||||
if has_high_char:
|
||||
buf2 = ctypes.create_unicode_buffer(1024)
|
||||
if ctypes.windll.kernel32.GetShortPathNameW(buf.value, buf2, 1024):
|
||||
buf = buf2
|
||||
|
||||
return buf.value
|
||||
|
||||
def _get_win_folder_with_jna(csidl_name):
|
||||
import array
|
||||
from com.sun import jna
|
||||
from com.sun.jna.platform import win32
|
||||
|
||||
buf_size = win32.WinDef.MAX_PATH * 2
|
||||
buf = array.zeros('c', buf_size)
|
||||
shell = win32.Shell32.INSTANCE
|
||||
shell.SHGetFolderPath(None, getattr(win32.ShlObj, csidl_name), None, win32.ShlObj.SHGFP_TYPE_CURRENT, buf)
|
||||
dir = jna.Native.toString(buf.tostring()).rstrip("\0")
|
||||
|
||||
# Downgrade to short path name if have highbit chars. See
|
||||
# <http://bugs.activestate.com/show_bug.cgi?id=85099>.
|
||||
has_high_char = False
|
||||
for c in dir:
|
||||
if ord(c) > 255:
|
||||
has_high_char = True
|
||||
break
|
||||
if has_high_char:
|
||||
buf = array.zeros('c', buf_size)
|
||||
kernel = win32.Kernel32.INSTANCE
|
||||
if kernel.GetShortPathName(dir, buf, buf_size):
|
||||
dir = jna.Native.toString(buf.tostring()).rstrip("\0")
|
||||
|
||||
return dir
|
||||
|
||||
if system == "win32":
|
||||
try:
|
||||
import win32com.shell
|
||||
_get_win_folder = _get_win_folder_with_pywin32
|
||||
except ImportError:
|
||||
try:
|
||||
from ctypes import windll
|
||||
_get_win_folder = _get_win_folder_with_ctypes
|
||||
except ImportError:
|
||||
try:
|
||||
import com.sun.jna
|
||||
_get_win_folder = _get_win_folder_with_jna
|
||||
except ImportError:
|
||||
_get_win_folder = _get_win_folder_from_registry
|
||||
715
ftline/lib/python3.12/site-packages/seaborn/external/docscrape.py
vendored
Normal file
715
ftline/lib/python3.12/site-packages/seaborn/external/docscrape.py
vendored
Normal file
@@ -0,0 +1,715 @@
|
||||
"""Extract reference documentation from the NumPy source tree.
|
||||
|
||||
Copyright (C) 2008 Stefan van der Walt <stefan@mentat.za.net>, Pauli Virtanen <pav@iki.fi>
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
2. Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in
|
||||
the documentation and/or other materials provided with the
|
||||
distribution.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
|
||||
IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT,
|
||||
INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
|
||||
HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
|
||||
STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING
|
||||
IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
"""
|
||||
import inspect
|
||||
import textwrap
|
||||
import re
|
||||
import pydoc
|
||||
from warnings import warn
|
||||
from collections import namedtuple
|
||||
from collections.abc import Callable, Mapping
|
||||
import copy
|
||||
import sys
|
||||
|
||||
|
||||
def strip_blank_lines(l):
|
||||
"Remove leading and trailing blank lines from a list of lines"
|
||||
while l and not l[0].strip():
|
||||
del l[0]
|
||||
while l and not l[-1].strip():
|
||||
del l[-1]
|
||||
return l
|
||||
|
||||
|
||||
class Reader:
|
||||
"""A line-based string reader.
|
||||
|
||||
"""
|
||||
def __init__(self, data):
|
||||
"""
|
||||
Parameters
|
||||
----------
|
||||
data : str
|
||||
String with lines separated by '\n'.
|
||||
|
||||
"""
|
||||
if isinstance(data, list):
|
||||
self._str = data
|
||||
else:
|
||||
self._str = data.split('\n') # store string as list of lines
|
||||
|
||||
self.reset()
|
||||
|
||||
def __getitem__(self, n):
|
||||
return self._str[n]
|
||||
|
||||
def reset(self):
|
||||
self._l = 0 # current line nr
|
||||
|
||||
def read(self):
|
||||
if not self.eof():
|
||||
out = self[self._l]
|
||||
self._l += 1
|
||||
return out
|
||||
else:
|
||||
return ''
|
||||
|
||||
def seek_next_non_empty_line(self):
|
||||
for l in self[self._l:]:
|
||||
if l.strip():
|
||||
break
|
||||
else:
|
||||
self._l += 1
|
||||
|
||||
def eof(self):
|
||||
return self._l >= len(self._str)
|
||||
|
||||
def read_to_condition(self, condition_func):
|
||||
start = self._l
|
||||
for line in self[start:]:
|
||||
if condition_func(line):
|
||||
return self[start:self._l]
|
||||
self._l += 1
|
||||
if self.eof():
|
||||
return self[start:self._l+1]
|
||||
return []
|
||||
|
||||
def read_to_next_empty_line(self):
|
||||
self.seek_next_non_empty_line()
|
||||
|
||||
def is_empty(line):
|
||||
return not line.strip()
|
||||
|
||||
return self.read_to_condition(is_empty)
|
||||
|
||||
def read_to_next_unindented_line(self):
|
||||
def is_unindented(line):
|
||||
return (line.strip() and (len(line.lstrip()) == len(line)))
|
||||
return self.read_to_condition(is_unindented)
|
||||
|
||||
def peek(self, n=0):
|
||||
if self._l + n < len(self._str):
|
||||
return self[self._l + n]
|
||||
else:
|
||||
return ''
|
||||
|
||||
def is_empty(self):
|
||||
return not ''.join(self._str).strip()
|
||||
|
||||
|
||||
class ParseError(Exception):
|
||||
def __str__(self):
|
||||
message = self.args[0]
|
||||
if hasattr(self, 'docstring'):
|
||||
message = f"{message} in {self.docstring!r}"
|
||||
return message
|
||||
|
||||
|
||||
Parameter = namedtuple('Parameter', ['name', 'type', 'desc'])
|
||||
|
||||
|
||||
class NumpyDocString(Mapping):
|
||||
"""Parses a numpydoc string to an abstract representation
|
||||
|
||||
Instances define a mapping from section title to structured data.
|
||||
|
||||
"""
|
||||
|
||||
sections = {
|
||||
'Signature': '',
|
||||
'Summary': [''],
|
||||
'Extended Summary': [],
|
||||
'Parameters': [],
|
||||
'Returns': [],
|
||||
'Yields': [],
|
||||
'Receives': [],
|
||||
'Raises': [],
|
||||
'Warns': [],
|
||||
'Other Parameters': [],
|
||||
'Attributes': [],
|
||||
'Methods': [],
|
||||
'See Also': [],
|
||||
'Notes': [],
|
||||
'Warnings': [],
|
||||
'References': '',
|
||||
'Examples': '',
|
||||
'index': {}
|
||||
}
|
||||
|
||||
def __init__(self, docstring, config={}):
|
||||
orig_docstring = docstring
|
||||
docstring = textwrap.dedent(docstring).split('\n')
|
||||
|
||||
self._doc = Reader(docstring)
|
||||
self._parsed_data = copy.deepcopy(self.sections)
|
||||
|
||||
try:
|
||||
self._parse()
|
||||
except ParseError as e:
|
||||
e.docstring = orig_docstring
|
||||
raise
|
||||
|
||||
def __getitem__(self, key):
|
||||
return self._parsed_data[key]
|
||||
|
||||
def __setitem__(self, key, val):
|
||||
if key not in self._parsed_data:
|
||||
self._error_location(f"Unknown section {key}", error=False)
|
||||
else:
|
||||
self._parsed_data[key] = val
|
||||
|
||||
def __iter__(self):
|
||||
return iter(self._parsed_data)
|
||||
|
||||
def __len__(self):
|
||||
return len(self._parsed_data)
|
||||
|
||||
def _is_at_section(self):
|
||||
self._doc.seek_next_non_empty_line()
|
||||
|
||||
if self._doc.eof():
|
||||
return False
|
||||
|
||||
l1 = self._doc.peek().strip() # e.g. Parameters
|
||||
|
||||
if l1.startswith('.. index::'):
|
||||
return True
|
||||
|
||||
l2 = self._doc.peek(1).strip() # ---------- or ==========
|
||||
return l2.startswith('-'*len(l1)) or l2.startswith('='*len(l1))
|
||||
|
||||
def _strip(self, doc):
|
||||
i = 0
|
||||
j = 0
|
||||
for i, line in enumerate(doc):
|
||||
if line.strip():
|
||||
break
|
||||
|
||||
for j, line in enumerate(doc[::-1]):
|
||||
if line.strip():
|
||||
break
|
||||
|
||||
return doc[i:len(doc)-j]
|
||||
|
||||
def _read_to_next_section(self):
|
||||
section = self._doc.read_to_next_empty_line()
|
||||
|
||||
while not self._is_at_section() and not self._doc.eof():
|
||||
if not self._doc.peek(-1).strip(): # previous line was empty
|
||||
section += ['']
|
||||
|
||||
section += self._doc.read_to_next_empty_line()
|
||||
|
||||
return section
|
||||
|
||||
def _read_sections(self):
|
||||
while not self._doc.eof():
|
||||
data = self._read_to_next_section()
|
||||
name = data[0].strip()
|
||||
|
||||
if name.startswith('..'): # index section
|
||||
yield name, data[1:]
|
||||
elif len(data) < 2:
|
||||
yield StopIteration
|
||||
else:
|
||||
yield name, self._strip(data[2:])
|
||||
|
||||
def _parse_param_list(self, content, single_element_is_type=False):
|
||||
r = Reader(content)
|
||||
params = []
|
||||
while not r.eof():
|
||||
header = r.read().strip()
|
||||
if ' : ' in header:
|
||||
arg_name, arg_type = header.split(' : ')[:2]
|
||||
else:
|
||||
if single_element_is_type:
|
||||
arg_name, arg_type = '', header
|
||||
else:
|
||||
arg_name, arg_type = header, ''
|
||||
|
||||
desc = r.read_to_next_unindented_line()
|
||||
desc = dedent_lines(desc)
|
||||
desc = strip_blank_lines(desc)
|
||||
|
||||
params.append(Parameter(arg_name, arg_type, desc))
|
||||
|
||||
return params
|
||||
|
||||
# See also supports the following formats.
|
||||
#
|
||||
# <FUNCNAME>
|
||||
# <FUNCNAME> SPACE* COLON SPACE+ <DESC> SPACE*
|
||||
# <FUNCNAME> ( COMMA SPACE+ <FUNCNAME>)+ (COMMA | PERIOD)? SPACE*
|
||||
# <FUNCNAME> ( COMMA SPACE+ <FUNCNAME>)* SPACE* COLON SPACE+ <DESC> SPACE*
|
||||
|
||||
# <FUNCNAME> is one of
|
||||
# <PLAIN_FUNCNAME>
|
||||
# COLON <ROLE> COLON BACKTICK <PLAIN_FUNCNAME> BACKTICK
|
||||
# where
|
||||
# <PLAIN_FUNCNAME> is a legal function name, and
|
||||
# <ROLE> is any nonempty sequence of word characters.
|
||||
# Examples: func_f1 :meth:`func_h1` :obj:`~baz.obj_r` :class:`class_j`
|
||||
# <DESC> is a string describing the function.
|
||||
|
||||
_role = r":(?P<role>\w+):"
|
||||
_funcbacktick = r"`(?P<name>(?:~\w+\.)?[a-zA-Z0-9_\.-]+)`"
|
||||
_funcplain = r"(?P<name2>[a-zA-Z0-9_\.-]+)"
|
||||
_funcname = r"(" + _role + _funcbacktick + r"|" + _funcplain + r")"
|
||||
_funcnamenext = _funcname.replace('role', 'rolenext')
|
||||
_funcnamenext = _funcnamenext.replace('name', 'namenext')
|
||||
_description = r"(?P<description>\s*:(\s+(?P<desc>\S+.*))?)?\s*$"
|
||||
_func_rgx = re.compile(r"^\s*" + _funcname + r"\s*")
|
||||
_line_rgx = re.compile(
|
||||
r"^\s*" +
|
||||
r"(?P<allfuncs>" + # group for all function names
|
||||
_funcname +
|
||||
r"(?P<morefuncs>([,]\s+" + _funcnamenext + r")*)" +
|
||||
r")" + # end of "allfuncs"
|
||||
r"(?P<trailing>[,\.])?" + # Some function lists have a trailing comma (or period) '\s*'
|
||||
_description)
|
||||
|
||||
# Empty <DESC> elements are replaced with '..'
|
||||
empty_description = '..'
|
||||
|
||||
def _parse_see_also(self, content):
|
||||
"""
|
||||
func_name : Descriptive text
|
||||
continued text
|
||||
another_func_name : Descriptive text
|
||||
func_name1, func_name2, :meth:`func_name`, func_name3
|
||||
|
||||
"""
|
||||
|
||||
items = []
|
||||
|
||||
def parse_item_name(text):
|
||||
"""Match ':role:`name`' or 'name'."""
|
||||
m = self._func_rgx.match(text)
|
||||
if not m:
|
||||
raise ParseError(f"{text} is not a item name")
|
||||
role = m.group('role')
|
||||
name = m.group('name') if role else m.group('name2')
|
||||
return name, role, m.end()
|
||||
|
||||
rest = []
|
||||
for line in content:
|
||||
if not line.strip():
|
||||
continue
|
||||
|
||||
line_match = self._line_rgx.match(line)
|
||||
description = None
|
||||
if line_match:
|
||||
description = line_match.group('desc')
|
||||
if line_match.group('trailing') and description:
|
||||
self._error_location(
|
||||
'Unexpected comma or period after function list at index %d of '
|
||||
'line "%s"' % (line_match.end('trailing'), line),
|
||||
error=False)
|
||||
if not description and line.startswith(' '):
|
||||
rest.append(line.strip())
|
||||
elif line_match:
|
||||
funcs = []
|
||||
text = line_match.group('allfuncs')
|
||||
while True:
|
||||
if not text.strip():
|
||||
break
|
||||
name, role, match_end = parse_item_name(text)
|
||||
funcs.append((name, role))
|
||||
text = text[match_end:].strip()
|
||||
if text and text[0] == ',':
|
||||
text = text[1:].strip()
|
||||
rest = list(filter(None, [description]))
|
||||
items.append((funcs, rest))
|
||||
else:
|
||||
raise ParseError(f"{line} is not a item name")
|
||||
return items
|
||||
|
||||
def _parse_index(self, section, content):
|
||||
"""
|
||||
.. index: default
|
||||
:refguide: something, else, and more
|
||||
|
||||
"""
|
||||
def strip_each_in(lst):
|
||||
return [s.strip() for s in lst]
|
||||
|
||||
out = {}
|
||||
section = section.split('::')
|
||||
if len(section) > 1:
|
||||
out['default'] = strip_each_in(section[1].split(','))[0]
|
||||
for line in content:
|
||||
line = line.split(':')
|
||||
if len(line) > 2:
|
||||
out[line[1]] = strip_each_in(line[2].split(','))
|
||||
return out
|
||||
|
||||
def _parse_summary(self):
|
||||
"""Grab signature (if given) and summary"""
|
||||
if self._is_at_section():
|
||||
return
|
||||
|
||||
# If several signatures present, take the last one
|
||||
while True:
|
||||
summary = self._doc.read_to_next_empty_line()
|
||||
summary_str = " ".join([s.strip() for s in summary]).strip()
|
||||
compiled = re.compile(r'^([\w., ]+=)?\s*[\w\.]+\(.*\)$')
|
||||
if compiled.match(summary_str):
|
||||
self['Signature'] = summary_str
|
||||
if not self._is_at_section():
|
||||
continue
|
||||
break
|
||||
|
||||
if summary is not None:
|
||||
self['Summary'] = summary
|
||||
|
||||
if not self._is_at_section():
|
||||
self['Extended Summary'] = self._read_to_next_section()
|
||||
|
||||
def _parse(self):
|
||||
self._doc.reset()
|
||||
self._parse_summary()
|
||||
|
||||
sections = list(self._read_sections())
|
||||
section_names = {section for section, content in sections}
|
||||
|
||||
has_returns = 'Returns' in section_names
|
||||
has_yields = 'Yields' in section_names
|
||||
# We could do more tests, but we are not. Arbitrarily.
|
||||
if has_returns and has_yields:
|
||||
msg = 'Docstring contains both a Returns and Yields section.'
|
||||
raise ValueError(msg)
|
||||
if not has_yields and 'Receives' in section_names:
|
||||
msg = 'Docstring contains a Receives section but not Yields.'
|
||||
raise ValueError(msg)
|
||||
|
||||
for (section, content) in sections:
|
||||
if not section.startswith('..'):
|
||||
section = (s.capitalize() for s in section.split(' '))
|
||||
section = ' '.join(section)
|
||||
if self.get(section):
|
||||
self._error_location(f"The section {section} appears twice")
|
||||
|
||||
if section in ('Parameters', 'Other Parameters', 'Attributes',
|
||||
'Methods'):
|
||||
self[section] = self._parse_param_list(content)
|
||||
elif section in ('Returns', 'Yields', 'Raises', 'Warns', 'Receives'):
|
||||
self[section] = self._parse_param_list(
|
||||
content, single_element_is_type=True)
|
||||
elif section.startswith('.. index::'):
|
||||
self['index'] = self._parse_index(section, content)
|
||||
elif section == 'See Also':
|
||||
self['See Also'] = self._parse_see_also(content)
|
||||
else:
|
||||
self[section] = content
|
||||
|
||||
def _error_location(self, msg, error=True):
|
||||
if hasattr(self, '_obj'):
|
||||
# we know where the docs came from:
|
||||
try:
|
||||
filename = inspect.getsourcefile(self._obj)
|
||||
except TypeError:
|
||||
filename = None
|
||||
msg = msg + f" in the docstring of {self._obj} in {filename}."
|
||||
if error:
|
||||
raise ValueError(msg)
|
||||
else:
|
||||
warn(msg)
|
||||
|
||||
# string conversion routines
|
||||
|
||||
def _str_header(self, name, symbol='-'):
|
||||
return [name, len(name)*symbol]
|
||||
|
||||
def _str_indent(self, doc, indent=4):
|
||||
out = []
|
||||
for line in doc:
|
||||
out += [' '*indent + line]
|
||||
return out
|
||||
|
||||
def _str_signature(self):
|
||||
if self['Signature']:
|
||||
return [self['Signature'].replace('*', r'\*')] + ['']
|
||||
else:
|
||||
return ['']
|
||||
|
||||
def _str_summary(self):
|
||||
if self['Summary']:
|
||||
return self['Summary'] + ['']
|
||||
else:
|
||||
return []
|
||||
|
||||
def _str_extended_summary(self):
|
||||
if self['Extended Summary']:
|
||||
return self['Extended Summary'] + ['']
|
||||
else:
|
||||
return []
|
||||
|
||||
def _str_param_list(self, name):
|
||||
out = []
|
||||
if self[name]:
|
||||
out += self._str_header(name)
|
||||
for param in self[name]:
|
||||
parts = []
|
||||
if param.name:
|
||||
parts.append(param.name)
|
||||
if param.type:
|
||||
parts.append(param.type)
|
||||
out += [' : '.join(parts)]
|
||||
if param.desc and ''.join(param.desc).strip():
|
||||
out += self._str_indent(param.desc)
|
||||
out += ['']
|
||||
return out
|
||||
|
||||
def _str_section(self, name):
|
||||
out = []
|
||||
if self[name]:
|
||||
out += self._str_header(name)
|
||||
out += self[name]
|
||||
out += ['']
|
||||
return out
|
||||
|
||||
def _str_see_also(self, func_role):
|
||||
if not self['See Also']:
|
||||
return []
|
||||
out = []
|
||||
out += self._str_header("See Also")
|
||||
out += ['']
|
||||
last_had_desc = True
|
||||
for funcs, desc in self['See Also']:
|
||||
assert isinstance(funcs, list)
|
||||
links = []
|
||||
for func, role in funcs:
|
||||
if role:
|
||||
link = f':{role}:`{func}`'
|
||||
elif func_role:
|
||||
link = f':{func_role}:`{func}`'
|
||||
else:
|
||||
link = f"`{func}`_"
|
||||
links.append(link)
|
||||
link = ', '.join(links)
|
||||
out += [link]
|
||||
if desc:
|
||||
out += self._str_indent([' '.join(desc)])
|
||||
last_had_desc = True
|
||||
else:
|
||||
last_had_desc = False
|
||||
out += self._str_indent([self.empty_description])
|
||||
|
||||
if last_had_desc:
|
||||
out += ['']
|
||||
out += ['']
|
||||
return out
|
||||
|
||||
def _str_index(self):
|
||||
idx = self['index']
|
||||
out = []
|
||||
output_index = False
|
||||
default_index = idx.get('default', '')
|
||||
if default_index:
|
||||
output_index = True
|
||||
out += [f'.. index:: {default_index}']
|
||||
for section, references in idx.items():
|
||||
if section == 'default':
|
||||
continue
|
||||
output_index = True
|
||||
out += [f" :{section}: {', '.join(references)}"]
|
||||
if output_index:
|
||||
return out
|
||||
else:
|
||||
return ''
|
||||
|
||||
def __str__(self, func_role=''):
|
||||
out = []
|
||||
out += self._str_signature()
|
||||
out += self._str_summary()
|
||||
out += self._str_extended_summary()
|
||||
for param_list in ('Parameters', 'Returns', 'Yields', 'Receives',
|
||||
'Other Parameters', 'Raises', 'Warns'):
|
||||
out += self._str_param_list(param_list)
|
||||
out += self._str_section('Warnings')
|
||||
out += self._str_see_also(func_role)
|
||||
for s in ('Notes', 'References', 'Examples'):
|
||||
out += self._str_section(s)
|
||||
for param_list in ('Attributes', 'Methods'):
|
||||
out += self._str_param_list(param_list)
|
||||
out += self._str_index()
|
||||
return '\n'.join(out)
|
||||
|
||||
|
||||
def indent(str, indent=4):
|
||||
indent_str = ' '*indent
|
||||
if str is None:
|
||||
return indent_str
|
||||
lines = str.split('\n')
|
||||
return '\n'.join(indent_str + l for l in lines)
|
||||
|
||||
|
||||
def dedent_lines(lines):
|
||||
"""Deindent a list of lines maximally"""
|
||||
return textwrap.dedent("\n".join(lines)).split("\n")
|
||||
|
||||
|
||||
def header(text, style='-'):
|
||||
return text + '\n' + style*len(text) + '\n'
|
||||
|
||||
|
||||
class FunctionDoc(NumpyDocString):
|
||||
def __init__(self, func, role='func', doc=None, config={}):
|
||||
self._f = func
|
||||
self._role = role # e.g. "func" or "meth"
|
||||
|
||||
if doc is None:
|
||||
if func is None:
|
||||
raise ValueError("No function or docstring given")
|
||||
doc = inspect.getdoc(func) or ''
|
||||
NumpyDocString.__init__(self, doc, config)
|
||||
|
||||
if not self['Signature'] and func is not None:
|
||||
func, func_name = self.get_func()
|
||||
try:
|
||||
try:
|
||||
signature = str(inspect.signature(func))
|
||||
except (AttributeError, ValueError):
|
||||
# try to read signature, backward compat for older Python
|
||||
if sys.version_info[0] >= 3:
|
||||
argspec = inspect.getfullargspec(func)
|
||||
else:
|
||||
argspec = inspect.getargspec(func)
|
||||
signature = inspect.formatargspec(*argspec)
|
||||
signature = f'{func_name}{signature}'
|
||||
except TypeError:
|
||||
signature = f'{func_name}()'
|
||||
self['Signature'] = signature
|
||||
|
||||
def get_func(self):
|
||||
func_name = getattr(self._f, '__name__', self.__class__.__name__)
|
||||
if inspect.isclass(self._f):
|
||||
func = getattr(self._f, '__call__', self._f.__init__)
|
||||
else:
|
||||
func = self._f
|
||||
return func, func_name
|
||||
|
||||
def __str__(self):
|
||||
out = ''
|
||||
|
||||
func, func_name = self.get_func()
|
||||
|
||||
roles = {'func': 'function',
|
||||
'meth': 'method'}
|
||||
|
||||
if self._role:
|
||||
if self._role not in roles:
|
||||
print(f"Warning: invalid role {self._role}")
|
||||
out += f".. {roles.get(self._role, '')}:: {func_name}\n \n\n"
|
||||
|
||||
out += super().__str__(func_role=self._role)
|
||||
return out
|
||||
|
||||
|
||||
class ClassDoc(NumpyDocString):
|
||||
|
||||
extra_public_methods = ['__call__']
|
||||
|
||||
def __init__(self, cls, doc=None, modulename='', func_doc=FunctionDoc,
|
||||
config={}):
|
||||
if not inspect.isclass(cls) and cls is not None:
|
||||
raise ValueError(f"Expected a class or None, but got {cls!r}")
|
||||
self._cls = cls
|
||||
|
||||
if 'sphinx' in sys.modules:
|
||||
from sphinx.ext.autodoc import ALL
|
||||
else:
|
||||
ALL = object()
|
||||
|
||||
self.show_inherited_members = config.get(
|
||||
'show_inherited_class_members', True)
|
||||
|
||||
if modulename and not modulename.endswith('.'):
|
||||
modulename += '.'
|
||||
self._mod = modulename
|
||||
|
||||
if doc is None:
|
||||
if cls is None:
|
||||
raise ValueError("No class or documentation string given")
|
||||
doc = pydoc.getdoc(cls)
|
||||
|
||||
NumpyDocString.__init__(self, doc)
|
||||
|
||||
_members = config.get('members', [])
|
||||
if _members is ALL:
|
||||
_members = None
|
||||
_exclude = config.get('exclude-members', [])
|
||||
|
||||
if config.get('show_class_members', True) and _exclude is not ALL:
|
||||
def splitlines_x(s):
|
||||
if not s:
|
||||
return []
|
||||
else:
|
||||
return s.splitlines()
|
||||
for field, items in [('Methods', self.methods),
|
||||
('Attributes', self.properties)]:
|
||||
if not self[field]:
|
||||
doc_list = []
|
||||
for name in sorted(items):
|
||||
if (name in _exclude or
|
||||
(_members and name not in _members)):
|
||||
continue
|
||||
try:
|
||||
doc_item = pydoc.getdoc(getattr(self._cls, name))
|
||||
doc_list.append(
|
||||
Parameter(name, '', splitlines_x(doc_item)))
|
||||
except AttributeError:
|
||||
pass # method doesn't exist
|
||||
self[field] = doc_list
|
||||
|
||||
@property
|
||||
def methods(self):
|
||||
if self._cls is None:
|
||||
return []
|
||||
return [name for name, func in inspect.getmembers(self._cls)
|
||||
if ((not name.startswith('_')
|
||||
or name in self.extra_public_methods)
|
||||
and isinstance(func, Callable)
|
||||
and self._is_show_member(name))]
|
||||
|
||||
@property
|
||||
def properties(self):
|
||||
if self._cls is None:
|
||||
return []
|
||||
return [name for name, func in inspect.getmembers(self._cls)
|
||||
if (not name.startswith('_') and
|
||||
(func is None or isinstance(func, property) or
|
||||
inspect.isdatadescriptor(func))
|
||||
and self._is_show_member(name))]
|
||||
|
||||
def _is_show_member(self, name):
|
||||
if self.show_inherited_members:
|
||||
return True # show all class members
|
||||
if name not in self._cls.__dict__:
|
||||
return False # class member is inherited, we do not show it
|
||||
return True
|
||||
313
ftline/lib/python3.12/site-packages/seaborn/external/husl.py
vendored
Normal file
313
ftline/lib/python3.12/site-packages/seaborn/external/husl.py
vendored
Normal file
@@ -0,0 +1,313 @@
|
||||
import operator
|
||||
import math
|
||||
|
||||
__version__ = "2.1.0"
|
||||
|
||||
|
||||
m = [
|
||||
[3.2406, -1.5372, -0.4986],
|
||||
[-0.9689, 1.8758, 0.0415],
|
||||
[0.0557, -0.2040, 1.0570]
|
||||
]
|
||||
|
||||
m_inv = [
|
||||
[0.4124, 0.3576, 0.1805],
|
||||
[0.2126, 0.7152, 0.0722],
|
||||
[0.0193, 0.1192, 0.9505]
|
||||
]
|
||||
|
||||
# Hard-coded D65 illuminant
|
||||
refX = 0.95047
|
||||
refY = 1.00000
|
||||
refZ = 1.08883
|
||||
refU = 0.19784
|
||||
refV = 0.46834
|
||||
lab_e = 0.008856
|
||||
lab_k = 903.3
|
||||
|
||||
|
||||
# Public API
|
||||
|
||||
def husl_to_rgb(h, s, l):
|
||||
return lch_to_rgb(*husl_to_lch([h, s, l]))
|
||||
|
||||
|
||||
def husl_to_hex(h, s, l):
|
||||
return rgb_to_hex(husl_to_rgb(h, s, l))
|
||||
|
||||
|
||||
def rgb_to_husl(r, g, b):
|
||||
return lch_to_husl(rgb_to_lch(r, g, b))
|
||||
|
||||
|
||||
def hex_to_husl(hex):
|
||||
return rgb_to_husl(*hex_to_rgb(hex))
|
||||
|
||||
|
||||
def huslp_to_rgb(h, s, l):
|
||||
return lch_to_rgb(*huslp_to_lch([h, s, l]))
|
||||
|
||||
|
||||
def huslp_to_hex(h, s, l):
|
||||
return rgb_to_hex(huslp_to_rgb(h, s, l))
|
||||
|
||||
|
||||
def rgb_to_huslp(r, g, b):
|
||||
return lch_to_huslp(rgb_to_lch(r, g, b))
|
||||
|
||||
|
||||
def hex_to_huslp(hex):
|
||||
return rgb_to_huslp(*hex_to_rgb(hex))
|
||||
|
||||
|
||||
def lch_to_rgb(l, c, h):
|
||||
return xyz_to_rgb(luv_to_xyz(lch_to_luv([l, c, h])))
|
||||
|
||||
|
||||
def rgb_to_lch(r, g, b):
|
||||
return luv_to_lch(xyz_to_luv(rgb_to_xyz([r, g, b])))
|
||||
|
||||
|
||||
def max_chroma(L, H):
|
||||
hrad = math.radians(H)
|
||||
sinH = (math.sin(hrad))
|
||||
cosH = (math.cos(hrad))
|
||||
sub1 = (math.pow(L + 16, 3.0) / 1560896.0)
|
||||
sub2 = sub1 if sub1 > 0.008856 else (L / 903.3)
|
||||
result = float("inf")
|
||||
for row in m:
|
||||
m1 = row[0]
|
||||
m2 = row[1]
|
||||
m3 = row[2]
|
||||
top = ((0.99915 * m1 + 1.05122 * m2 + 1.14460 * m3) * sub2)
|
||||
rbottom = (0.86330 * m3 - 0.17266 * m2)
|
||||
lbottom = (0.12949 * m3 - 0.38848 * m1)
|
||||
bottom = (rbottom * sinH + lbottom * cosH) * sub2
|
||||
|
||||
for t in (0.0, 1.0):
|
||||
C = (L * (top - 1.05122 * t) / (bottom + 0.17266 * sinH * t))
|
||||
if C > 0.0 and C < result:
|
||||
result = C
|
||||
return result
|
||||
|
||||
|
||||
def _hrad_extremum(L):
|
||||
lhs = (math.pow(L, 3.0) + 48.0 * math.pow(L, 2.0) + 768.0 * L + 4096.0) / 1560896.0
|
||||
rhs = 1107.0 / 125000.0
|
||||
sub = lhs if lhs > rhs else 10.0 * L / 9033.0
|
||||
chroma = float("inf")
|
||||
result = None
|
||||
for row in m:
|
||||
for limit in (0.0, 1.0):
|
||||
[m1, m2, m3] = row
|
||||
top = -3015466475.0 * m3 * sub + 603093295.0 * m2 * sub - 603093295.0 * limit
|
||||
bottom = 1356959916.0 * m1 * sub - 452319972.0 * m3 * sub
|
||||
hrad = math.atan2(top, bottom)
|
||||
# This is a math hack to deal with tan quadrants, I'm too lazy to figure
|
||||
# out how to do this properly
|
||||
if limit == 0.0:
|
||||
hrad += math.pi
|
||||
test = max_chroma(L, math.degrees(hrad))
|
||||
if test < chroma:
|
||||
chroma = test
|
||||
result = hrad
|
||||
return result
|
||||
|
||||
|
||||
def max_chroma_pastel(L):
|
||||
H = math.degrees(_hrad_extremum(L))
|
||||
return max_chroma(L, H)
|
||||
|
||||
|
||||
def dot_product(a, b):
|
||||
return sum(map(operator.mul, a, b))
|
||||
|
||||
|
||||
def f(t):
|
||||
if t > lab_e:
|
||||
return (math.pow(t, 1.0 / 3.0))
|
||||
else:
|
||||
return (7.787 * t + 16.0 / 116.0)
|
||||
|
||||
|
||||
def f_inv(t):
|
||||
if math.pow(t, 3.0) > lab_e:
|
||||
return (math.pow(t, 3.0))
|
||||
else:
|
||||
return (116.0 * t - 16.0) / lab_k
|
||||
|
||||
|
||||
def from_linear(c):
|
||||
if c <= 0.0031308:
|
||||
return 12.92 * c
|
||||
else:
|
||||
return (1.055 * math.pow(c, 1.0 / 2.4) - 0.055)
|
||||
|
||||
|
||||
def to_linear(c):
|
||||
a = 0.055
|
||||
|
||||
if c > 0.04045:
|
||||
return (math.pow((c + a) / (1.0 + a), 2.4))
|
||||
else:
|
||||
return (c / 12.92)
|
||||
|
||||
|
||||
def rgb_prepare(triple):
|
||||
ret = []
|
||||
for ch in triple:
|
||||
ch = round(ch, 3)
|
||||
|
||||
if ch < -0.0001 or ch > 1.0001:
|
||||
raise Exception(f"Illegal RGB value {ch:f}")
|
||||
|
||||
if ch < 0:
|
||||
ch = 0
|
||||
if ch > 1:
|
||||
ch = 1
|
||||
|
||||
# Fix for Python 3 which by default rounds 4.5 down to 4.0
|
||||
# instead of Python 2 which is rounded to 5.0 which caused
|
||||
# a couple off by one errors in the tests. Tests now all pass
|
||||
# in Python 2 and Python 3
|
||||
ret.append(int(round(ch * 255 + 0.001, 0)))
|
||||
|
||||
return ret
|
||||
|
||||
|
||||
def hex_to_rgb(hex):
|
||||
if hex.startswith('#'):
|
||||
hex = hex[1:]
|
||||
r = int(hex[0:2], 16) / 255.0
|
||||
g = int(hex[2:4], 16) / 255.0
|
||||
b = int(hex[4:6], 16) / 255.0
|
||||
return [r, g, b]
|
||||
|
||||
|
||||
def rgb_to_hex(triple):
|
||||
[r, g, b] = triple
|
||||
return '#%02x%02x%02x' % tuple(rgb_prepare([r, g, b]))
|
||||
|
||||
|
||||
def xyz_to_rgb(triple):
|
||||
xyz = map(lambda row: dot_product(row, triple), m)
|
||||
return list(map(from_linear, xyz))
|
||||
|
||||
|
||||
def rgb_to_xyz(triple):
|
||||
rgbl = list(map(to_linear, triple))
|
||||
return list(map(lambda row: dot_product(row, rgbl), m_inv))
|
||||
|
||||
|
||||
def xyz_to_luv(triple):
|
||||
X, Y, Z = triple
|
||||
|
||||
if X == Y == Z == 0.0:
|
||||
return [0.0, 0.0, 0.0]
|
||||
|
||||
varU = (4.0 * X) / (X + (15.0 * Y) + (3.0 * Z))
|
||||
varV = (9.0 * Y) / (X + (15.0 * Y) + (3.0 * Z))
|
||||
L = 116.0 * f(Y / refY) - 16.0
|
||||
|
||||
# Black will create a divide-by-zero error
|
||||
if L == 0.0:
|
||||
return [0.0, 0.0, 0.0]
|
||||
|
||||
U = 13.0 * L * (varU - refU)
|
||||
V = 13.0 * L * (varV - refV)
|
||||
|
||||
return [L, U, V]
|
||||
|
||||
|
||||
def luv_to_xyz(triple):
|
||||
L, U, V = triple
|
||||
|
||||
if L == 0:
|
||||
return [0.0, 0.0, 0.0]
|
||||
|
||||
varY = f_inv((L + 16.0) / 116.0)
|
||||
varU = U / (13.0 * L) + refU
|
||||
varV = V / (13.0 * L) + refV
|
||||
Y = varY * refY
|
||||
X = 0.0 - (9.0 * Y * varU) / ((varU - 4.0) * varV - varU * varV)
|
||||
Z = (9.0 * Y - (15.0 * varV * Y) - (varV * X)) / (3.0 * varV)
|
||||
|
||||
return [X, Y, Z]
|
||||
|
||||
|
||||
def luv_to_lch(triple):
|
||||
L, U, V = triple
|
||||
|
||||
C = (math.pow(math.pow(U, 2) + math.pow(V, 2), (1.0 / 2.0)))
|
||||
hrad = (math.atan2(V, U))
|
||||
H = math.degrees(hrad)
|
||||
if H < 0.0:
|
||||
H = 360.0 + H
|
||||
|
||||
return [L, C, H]
|
||||
|
||||
|
||||
def lch_to_luv(triple):
|
||||
L, C, H = triple
|
||||
|
||||
Hrad = math.radians(H)
|
||||
U = (math.cos(Hrad) * C)
|
||||
V = (math.sin(Hrad) * C)
|
||||
|
||||
return [L, U, V]
|
||||
|
||||
|
||||
def husl_to_lch(triple):
|
||||
H, S, L = triple
|
||||
|
||||
if L > 99.9999999:
|
||||
return [100, 0.0, H]
|
||||
if L < 0.00000001:
|
||||
return [0.0, 0.0, H]
|
||||
|
||||
mx = max_chroma(L, H)
|
||||
C = mx / 100.0 * S
|
||||
|
||||
return [L, C, H]
|
||||
|
||||
|
||||
def lch_to_husl(triple):
|
||||
L, C, H = triple
|
||||
|
||||
if L > 99.9999999:
|
||||
return [H, 0.0, 100.0]
|
||||
if L < 0.00000001:
|
||||
return [H, 0.0, 0.0]
|
||||
|
||||
mx = max_chroma(L, H)
|
||||
S = C / mx * 100.0
|
||||
|
||||
return [H, S, L]
|
||||
|
||||
|
||||
def huslp_to_lch(triple):
|
||||
H, S, L = triple
|
||||
|
||||
if L > 99.9999999:
|
||||
return [100, 0.0, H]
|
||||
if L < 0.00000001:
|
||||
return [0.0, 0.0, H]
|
||||
|
||||
mx = max_chroma_pastel(L)
|
||||
C = mx / 100.0 * S
|
||||
|
||||
return [L, C, H]
|
||||
|
||||
|
||||
def lch_to_huslp(triple):
|
||||
L, C, H = triple
|
||||
|
||||
if L > 99.9999999:
|
||||
return [H, 0.0, 100.0]
|
||||
if L < 0.00000001:
|
||||
return [H, 0.0, 0.0]
|
||||
|
||||
mx = max_chroma_pastel(L)
|
||||
S = C / mx * 100.0
|
||||
|
||||
return [H, S, L]
|
||||
380
ftline/lib/python3.12/site-packages/seaborn/external/kde.py
vendored
Normal file
380
ftline/lib/python3.12/site-packages/seaborn/external/kde.py
vendored
Normal file
@@ -0,0 +1,380 @@
|
||||
"""
|
||||
This module was copied from the scipy project.
|
||||
|
||||
In the process of copying, some methods were removed because they depended on
|
||||
other parts of scipy (especially on compiled components), allowing seaborn to
|
||||
have a simple and pure Python implementation. These include:
|
||||
|
||||
- integrate_gaussian
|
||||
- integrate_box
|
||||
- integrate_box_1d
|
||||
- integrate_kde
|
||||
- logpdf
|
||||
- resample
|
||||
|
||||
Additionally, the numpy.linalg module was substituted for scipy.linalg,
|
||||
and the examples section (with doctests) was removed from the docstring
|
||||
|
||||
The original scipy license is copied below:
|
||||
|
||||
Copyright (c) 2001-2002 Enthought, Inc. 2003-2019, SciPy Developers.
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions
|
||||
are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above
|
||||
copyright notice, this list of conditions and the following
|
||||
disclaimer in the documentation and/or other materials provided
|
||||
with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder nor the names of its
|
||||
contributors may be used to endorse or promote products derived
|
||||
from this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
"""
|
||||
|
||||
# -------------------------------------------------------------------------------
|
||||
#
|
||||
# Define classes for (uni/multi)-variate kernel density estimation.
|
||||
#
|
||||
# Currently, only Gaussian kernels are implemented.
|
||||
#
|
||||
# Written by: Robert Kern
|
||||
#
|
||||
# Date: 2004-08-09
|
||||
#
|
||||
# Modified: 2005-02-10 by Robert Kern.
|
||||
# Contributed to SciPy
|
||||
# 2005-10-07 by Robert Kern.
|
||||
# Some fixes to match the new scipy_core
|
||||
#
|
||||
# Copyright 2004-2005 by Enthought, Inc.
|
||||
#
|
||||
# -------------------------------------------------------------------------------
|
||||
|
||||
import numpy as np
|
||||
from numpy import (asarray, atleast_2d, reshape, zeros, newaxis, dot, exp, pi,
|
||||
sqrt, power, atleast_1d, sum, ones, cov)
|
||||
from numpy import linalg
|
||||
|
||||
|
||||
__all__ = ['gaussian_kde']
|
||||
|
||||
|
||||
class gaussian_kde:
|
||||
"""Representation of a kernel-density estimate using Gaussian kernels.
|
||||
|
||||
Kernel density estimation is a way to estimate the probability density
|
||||
function (PDF) of a random variable in a non-parametric way.
|
||||
`gaussian_kde` works for both uni-variate and multi-variate data. It
|
||||
includes automatic bandwidth determination. The estimation works best for
|
||||
a unimodal distribution; bimodal or multi-modal distributions tend to be
|
||||
oversmoothed.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
dataset : array_like
|
||||
Datapoints to estimate from. In case of univariate data this is a 1-D
|
||||
array, otherwise a 2-D array with shape (# of dims, # of data).
|
||||
bw_method : str, scalar or callable, optional
|
||||
The method used to calculate the estimator bandwidth. This can be
|
||||
'scott', 'silverman', a scalar constant or a callable. If a scalar,
|
||||
this will be used directly as `kde.factor`. If a callable, it should
|
||||
take a `gaussian_kde` instance as only parameter and return a scalar.
|
||||
If None (default), 'scott' is used. See Notes for more details.
|
||||
weights : array_like, optional
|
||||
weights of datapoints. This must be the same shape as dataset.
|
||||
If None (default), the samples are assumed to be equally weighted
|
||||
|
||||
Attributes
|
||||
----------
|
||||
dataset : ndarray
|
||||
The dataset with which `gaussian_kde` was initialized.
|
||||
d : int
|
||||
Number of dimensions.
|
||||
n : int
|
||||
Number of datapoints.
|
||||
neff : int
|
||||
Effective number of datapoints.
|
||||
|
||||
.. versionadded:: 1.2.0
|
||||
factor : float
|
||||
The bandwidth factor, obtained from `kde.covariance_factor`, with which
|
||||
the covariance matrix is multiplied.
|
||||
covariance : ndarray
|
||||
The covariance matrix of `dataset`, scaled by the calculated bandwidth
|
||||
(`kde.factor`).
|
||||
inv_cov : ndarray
|
||||
The inverse of `covariance`.
|
||||
|
||||
Methods
|
||||
-------
|
||||
evaluate
|
||||
__call__
|
||||
integrate_gaussian
|
||||
integrate_box_1d
|
||||
integrate_box
|
||||
integrate_kde
|
||||
pdf
|
||||
logpdf
|
||||
resample
|
||||
set_bandwidth
|
||||
covariance_factor
|
||||
|
||||
Notes
|
||||
-----
|
||||
Bandwidth selection strongly influences the estimate obtained from the KDE
|
||||
(much more so than the actual shape of the kernel). Bandwidth selection
|
||||
can be done by a "rule of thumb", by cross-validation, by "plug-in
|
||||
methods" or by other means; see [3]_, [4]_ for reviews. `gaussian_kde`
|
||||
uses a rule of thumb, the default is Scott's Rule.
|
||||
|
||||
Scott's Rule [1]_, implemented as `scotts_factor`, is::
|
||||
|
||||
n**(-1./(d+4)),
|
||||
|
||||
with ``n`` the number of data points and ``d`` the number of dimensions.
|
||||
In the case of unequally weighted points, `scotts_factor` becomes::
|
||||
|
||||
neff**(-1./(d+4)),
|
||||
|
||||
with ``neff`` the effective number of datapoints.
|
||||
Silverman's Rule [2]_, implemented as `silverman_factor`, is::
|
||||
|
||||
(n * (d + 2) / 4.)**(-1. / (d + 4)).
|
||||
|
||||
or in the case of unequally weighted points::
|
||||
|
||||
(neff * (d + 2) / 4.)**(-1. / (d + 4)).
|
||||
|
||||
Good general descriptions of kernel density estimation can be found in [1]_
|
||||
and [2]_, the mathematics for this multi-dimensional implementation can be
|
||||
found in [1]_.
|
||||
|
||||
With a set of weighted samples, the effective number of datapoints ``neff``
|
||||
is defined by::
|
||||
|
||||
neff = sum(weights)^2 / sum(weights^2)
|
||||
|
||||
as detailed in [5]_.
|
||||
|
||||
References
|
||||
----------
|
||||
.. [1] D.W. Scott, "Multivariate Density Estimation: Theory, Practice, and
|
||||
Visualization", John Wiley & Sons, New York, Chicester, 1992.
|
||||
.. [2] B.W. Silverman, "Density Estimation for Statistics and Data
|
||||
Analysis", Vol. 26, Monographs on Statistics and Applied Probability,
|
||||
Chapman and Hall, London, 1986.
|
||||
.. [3] B.A. Turlach, "Bandwidth Selection in Kernel Density Estimation: A
|
||||
Review", CORE and Institut de Statistique, Vol. 19, pp. 1-33, 1993.
|
||||
.. [4] D.M. Bashtannyk and R.J. Hyndman, "Bandwidth selection for kernel
|
||||
conditional density estimation", Computational Statistics & Data
|
||||
Analysis, Vol. 36, pp. 279-298, 2001.
|
||||
.. [5] Gray P. G., 1969, Journal of the Royal Statistical Society.
|
||||
Series A (General), 132, 272
|
||||
|
||||
"""
|
||||
def __init__(self, dataset, bw_method=None, weights=None):
|
||||
self.dataset = atleast_2d(asarray(dataset))
|
||||
if not self.dataset.size > 1:
|
||||
raise ValueError("`dataset` input should have multiple elements.")
|
||||
|
||||
self.d, self.n = self.dataset.shape
|
||||
|
||||
if weights is not None:
|
||||
self._weights = atleast_1d(weights).astype(float)
|
||||
self._weights /= sum(self._weights)
|
||||
if self.weights.ndim != 1:
|
||||
raise ValueError("`weights` input should be one-dimensional.")
|
||||
if len(self._weights) != self.n:
|
||||
raise ValueError("`weights` input should be of length n")
|
||||
self._neff = 1/sum(self._weights**2)
|
||||
|
||||
self.set_bandwidth(bw_method=bw_method)
|
||||
|
||||
def evaluate(self, points):
|
||||
"""Evaluate the estimated pdf on a set of points.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
points : (# of dimensions, # of points)-array
|
||||
Alternatively, a (# of dimensions,) vector can be passed in and
|
||||
treated as a single point.
|
||||
|
||||
Returns
|
||||
-------
|
||||
values : (# of points,)-array
|
||||
The values at each point.
|
||||
|
||||
Raises
|
||||
------
|
||||
ValueError : if the dimensionality of the input points is different than
|
||||
the dimensionality of the KDE.
|
||||
|
||||
"""
|
||||
points = atleast_2d(asarray(points))
|
||||
|
||||
d, m = points.shape
|
||||
if d != self.d:
|
||||
if d == 1 and m == self.d:
|
||||
# points was passed in as a row vector
|
||||
points = reshape(points, (self.d, 1))
|
||||
m = 1
|
||||
else:
|
||||
msg = f"points have dimension {d}, dataset has dimension {self.d}"
|
||||
raise ValueError(msg)
|
||||
|
||||
output_dtype = np.common_type(self.covariance, points)
|
||||
result = zeros((m,), dtype=output_dtype)
|
||||
|
||||
whitening = linalg.cholesky(self.inv_cov)
|
||||
scaled_dataset = dot(whitening, self.dataset)
|
||||
scaled_points = dot(whitening, points)
|
||||
|
||||
if m >= self.n:
|
||||
# there are more points than data, so loop over data
|
||||
for i in range(self.n):
|
||||
diff = scaled_dataset[:, i, newaxis] - scaled_points
|
||||
energy = sum(diff * diff, axis=0) / 2.0
|
||||
result += self.weights[i]*exp(-energy)
|
||||
else:
|
||||
# loop over points
|
||||
for i in range(m):
|
||||
diff = scaled_dataset - scaled_points[:, i, newaxis]
|
||||
energy = sum(diff * diff, axis=0) / 2.0
|
||||
result[i] = sum(exp(-energy)*self.weights, axis=0)
|
||||
|
||||
result = result / self._norm_factor
|
||||
|
||||
return result
|
||||
|
||||
__call__ = evaluate
|
||||
|
||||
def scotts_factor(self):
|
||||
"""Compute Scott's factor.
|
||||
|
||||
Returns
|
||||
-------
|
||||
s : float
|
||||
Scott's factor.
|
||||
"""
|
||||
return power(self.neff, -1./(self.d+4))
|
||||
|
||||
def silverman_factor(self):
|
||||
"""Compute the Silverman factor.
|
||||
|
||||
Returns
|
||||
-------
|
||||
s : float
|
||||
The silverman factor.
|
||||
"""
|
||||
return power(self.neff*(self.d+2.0)/4.0, -1./(self.d+4))
|
||||
|
||||
# Default method to calculate bandwidth, can be overwritten by subclass
|
||||
covariance_factor = scotts_factor
|
||||
covariance_factor.__doc__ = """Computes the coefficient (`kde.factor`) that
|
||||
multiplies the data covariance matrix to obtain the kernel covariance
|
||||
matrix. The default is `scotts_factor`. A subclass can overwrite this
|
||||
method to provide a different method, or set it through a call to
|
||||
`kde.set_bandwidth`."""
|
||||
|
||||
def set_bandwidth(self, bw_method=None):
|
||||
"""Compute the estimator bandwidth with given method.
|
||||
|
||||
The new bandwidth calculated after a call to `set_bandwidth` is used
|
||||
for subsequent evaluations of the estimated density.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
bw_method : str, scalar or callable, optional
|
||||
The method used to calculate the estimator bandwidth. This can be
|
||||
'scott', 'silverman', a scalar constant or a callable. If a
|
||||
scalar, this will be used directly as `kde.factor`. If a callable,
|
||||
it should take a `gaussian_kde` instance as only parameter and
|
||||
return a scalar. If None (default), nothing happens; the current
|
||||
`kde.covariance_factor` method is kept.
|
||||
|
||||
Notes
|
||||
-----
|
||||
.. versionadded:: 0.11
|
||||
|
||||
"""
|
||||
if bw_method is None:
|
||||
pass
|
||||
elif bw_method == 'scott':
|
||||
self.covariance_factor = self.scotts_factor
|
||||
elif bw_method == 'silverman':
|
||||
self.covariance_factor = self.silverman_factor
|
||||
elif np.isscalar(bw_method) and not isinstance(bw_method, str):
|
||||
self._bw_method = 'use constant'
|
||||
self.covariance_factor = lambda: bw_method
|
||||
elif callable(bw_method):
|
||||
self._bw_method = bw_method
|
||||
self.covariance_factor = lambda: self._bw_method(self)
|
||||
else:
|
||||
msg = "`bw_method` should be 'scott', 'silverman', a scalar " \
|
||||
"or a callable."
|
||||
raise ValueError(msg)
|
||||
|
||||
self._compute_covariance()
|
||||
|
||||
def _compute_covariance(self):
|
||||
"""Computes the covariance matrix for each Gaussian kernel using
|
||||
covariance_factor().
|
||||
"""
|
||||
self.factor = self.covariance_factor()
|
||||
# Cache covariance and inverse covariance of the data
|
||||
if not hasattr(self, '_data_inv_cov'):
|
||||
self._data_covariance = atleast_2d(cov(self.dataset, rowvar=1,
|
||||
bias=False,
|
||||
aweights=self.weights))
|
||||
self._data_inv_cov = linalg.inv(self._data_covariance)
|
||||
|
||||
self.covariance = self._data_covariance * self.factor**2
|
||||
self.inv_cov = self._data_inv_cov / self.factor**2
|
||||
self._norm_factor = sqrt(linalg.det(2*pi*self.covariance))
|
||||
|
||||
def pdf(self, x):
|
||||
"""
|
||||
Evaluate the estimated pdf on a provided set of points.
|
||||
|
||||
Notes
|
||||
-----
|
||||
This is an alias for `gaussian_kde.evaluate`. See the ``evaluate``
|
||||
docstring for more details.
|
||||
|
||||
"""
|
||||
return self.evaluate(x)
|
||||
|
||||
@property
|
||||
def weights(self):
|
||||
try:
|
||||
return self._weights
|
||||
except AttributeError:
|
||||
self._weights = ones(self.n)/self.n
|
||||
return self._weights
|
||||
|
||||
@property
|
||||
def neff(self):
|
||||
try:
|
||||
return self._neff
|
||||
except AttributeError:
|
||||
self._neff = 1/sum(self.weights**2)
|
||||
return self._neff
|
||||
461
ftline/lib/python3.12/site-packages/seaborn/external/version.py
vendored
Normal file
461
ftline/lib/python3.12/site-packages/seaborn/external/version.py
vendored
Normal file
@@ -0,0 +1,461 @@
|
||||
"""Extract reference documentation from the pypa/packaging source tree.
|
||||
|
||||
In the process of copying, some unused methods / classes were removed.
|
||||
These include:
|
||||
|
||||
- parse()
|
||||
- anything involving LegacyVersion
|
||||
|
||||
This software is made available under the terms of *either* of the licenses
|
||||
found in LICENSE.APACHE or LICENSE.BSD. Contributions to this software is made
|
||||
under the terms of *both* these licenses.
|
||||
|
||||
Vendored from:
|
||||
- https://github.com/pypa/packaging/
|
||||
- commit ba07d8287b4554754ac7178d177033ea3f75d489 (09/09/2021)
|
||||
"""
|
||||
|
||||
|
||||
# This file is dual licensed under the terms of the Apache License, Version
|
||||
# 2.0, and the BSD License. See the LICENSE file in the root of this repository
|
||||
# for complete details.
|
||||
|
||||
|
||||
import collections
|
||||
import itertools
|
||||
import re
|
||||
from typing import Callable, Optional, SupportsInt, Tuple, Union
|
||||
|
||||
__all__ = ["Version", "InvalidVersion", "VERSION_PATTERN"]
|
||||
|
||||
|
||||
# Vendored from https://github.com/pypa/packaging/blob/main/packaging/_structures.py
|
||||
|
||||
class InfinityType:
|
||||
def __repr__(self) -> str:
|
||||
return "Infinity"
|
||||
|
||||
def __hash__(self) -> int:
|
||||
return hash(repr(self))
|
||||
|
||||
def __lt__(self, other: object) -> bool:
|
||||
return False
|
||||
|
||||
def __le__(self, other: object) -> bool:
|
||||
return False
|
||||
|
||||
def __eq__(self, other: object) -> bool:
|
||||
return isinstance(other, self.__class__)
|
||||
|
||||
def __ne__(self, other: object) -> bool:
|
||||
return not isinstance(other, self.__class__)
|
||||
|
||||
def __gt__(self, other: object) -> bool:
|
||||
return True
|
||||
|
||||
def __ge__(self, other: object) -> bool:
|
||||
return True
|
||||
|
||||
def __neg__(self: object) -> "NegativeInfinityType":
|
||||
return NegativeInfinity
|
||||
|
||||
|
||||
Infinity = InfinityType()
|
||||
|
||||
|
||||
class NegativeInfinityType:
|
||||
def __repr__(self) -> str:
|
||||
return "-Infinity"
|
||||
|
||||
def __hash__(self) -> int:
|
||||
return hash(repr(self))
|
||||
|
||||
def __lt__(self, other: object) -> bool:
|
||||
return True
|
||||
|
||||
def __le__(self, other: object) -> bool:
|
||||
return True
|
||||
|
||||
def __eq__(self, other: object) -> bool:
|
||||
return isinstance(other, self.__class__)
|
||||
|
||||
def __ne__(self, other: object) -> bool:
|
||||
return not isinstance(other, self.__class__)
|
||||
|
||||
def __gt__(self, other: object) -> bool:
|
||||
return False
|
||||
|
||||
def __ge__(self, other: object) -> bool:
|
||||
return False
|
||||
|
||||
def __neg__(self: object) -> InfinityType:
|
||||
return Infinity
|
||||
|
||||
|
||||
NegativeInfinity = NegativeInfinityType()
|
||||
|
||||
|
||||
# Vendored from https://github.com/pypa/packaging/blob/main/packaging/version.py
|
||||
|
||||
InfiniteTypes = Union[InfinityType, NegativeInfinityType]
|
||||
PrePostDevType = Union[InfiniteTypes, Tuple[str, int]]
|
||||
SubLocalType = Union[InfiniteTypes, int, str]
|
||||
LocalType = Union[
|
||||
NegativeInfinityType,
|
||||
Tuple[
|
||||
Union[
|
||||
SubLocalType,
|
||||
Tuple[SubLocalType, str],
|
||||
Tuple[NegativeInfinityType, SubLocalType],
|
||||
],
|
||||
...,
|
||||
],
|
||||
]
|
||||
CmpKey = Tuple[
|
||||
int, Tuple[int, ...], PrePostDevType, PrePostDevType, PrePostDevType, LocalType
|
||||
]
|
||||
LegacyCmpKey = Tuple[int, Tuple[str, ...]]
|
||||
VersionComparisonMethod = Callable[
|
||||
[Union[CmpKey, LegacyCmpKey], Union[CmpKey, LegacyCmpKey]], bool
|
||||
]
|
||||
|
||||
_Version = collections.namedtuple(
|
||||
"_Version", ["epoch", "release", "dev", "pre", "post", "local"]
|
||||
)
|
||||
|
||||
|
||||
|
||||
class InvalidVersion(ValueError):
|
||||
"""
|
||||
An invalid version was found, users should refer to PEP 440.
|
||||
"""
|
||||
|
||||
|
||||
class _BaseVersion:
|
||||
_key: Union[CmpKey, LegacyCmpKey]
|
||||
|
||||
def __hash__(self) -> int:
|
||||
return hash(self._key)
|
||||
|
||||
# Please keep the duplicated `isinstance` check
|
||||
# in the six comparisons hereunder
|
||||
# unless you find a way to avoid adding overhead function calls.
|
||||
def __lt__(self, other: "_BaseVersion") -> bool:
|
||||
if not isinstance(other, _BaseVersion):
|
||||
return NotImplemented
|
||||
|
||||
return self._key < other._key
|
||||
|
||||
def __le__(self, other: "_BaseVersion") -> bool:
|
||||
if not isinstance(other, _BaseVersion):
|
||||
return NotImplemented
|
||||
|
||||
return self._key <= other._key
|
||||
|
||||
def __eq__(self, other: object) -> bool:
|
||||
if not isinstance(other, _BaseVersion):
|
||||
return NotImplemented
|
||||
|
||||
return self._key == other._key
|
||||
|
||||
def __ge__(self, other: "_BaseVersion") -> bool:
|
||||
if not isinstance(other, _BaseVersion):
|
||||
return NotImplemented
|
||||
|
||||
return self._key >= other._key
|
||||
|
||||
def __gt__(self, other: "_BaseVersion") -> bool:
|
||||
if not isinstance(other, _BaseVersion):
|
||||
return NotImplemented
|
||||
|
||||
return self._key > other._key
|
||||
|
||||
def __ne__(self, other: object) -> bool:
|
||||
if not isinstance(other, _BaseVersion):
|
||||
return NotImplemented
|
||||
|
||||
return self._key != other._key
|
||||
|
||||
|
||||
# Deliberately not anchored to the start and end of the string, to make it
|
||||
# easier for 3rd party code to reuse
|
||||
VERSION_PATTERN = r"""
|
||||
v?
|
||||
(?:
|
||||
(?:(?P<epoch>[0-9]+)!)? # epoch
|
||||
(?P<release>[0-9]+(?:\.[0-9]+)*) # release segment
|
||||
(?P<pre> # pre-release
|
||||
[-_\.]?
|
||||
(?P<pre_l>(a|b|c|rc|alpha|beta|pre|preview))
|
||||
[-_\.]?
|
||||
(?P<pre_n>[0-9]+)?
|
||||
)?
|
||||
(?P<post> # post release
|
||||
(?:-(?P<post_n1>[0-9]+))
|
||||
|
|
||||
(?:
|
||||
[-_\.]?
|
||||
(?P<post_l>post|rev|r)
|
||||
[-_\.]?
|
||||
(?P<post_n2>[0-9]+)?
|
||||
)
|
||||
)?
|
||||
(?P<dev> # dev release
|
||||
[-_\.]?
|
||||
(?P<dev_l>dev)
|
||||
[-_\.]?
|
||||
(?P<dev_n>[0-9]+)?
|
||||
)?
|
||||
)
|
||||
(?:\+(?P<local>[a-z0-9]+(?:[-_\.][a-z0-9]+)*))? # local version
|
||||
"""
|
||||
|
||||
|
||||
class Version(_BaseVersion):
|
||||
|
||||
_regex = re.compile(r"^\s*" + VERSION_PATTERN + r"\s*$", re.VERBOSE | re.IGNORECASE)
|
||||
|
||||
def __init__(self, version: str) -> None:
|
||||
|
||||
# Validate the version and parse it into pieces
|
||||
match = self._regex.search(version)
|
||||
if not match:
|
||||
raise InvalidVersion(f"Invalid version: '{version}'")
|
||||
|
||||
# Store the parsed out pieces of the version
|
||||
self._version = _Version(
|
||||
epoch=int(match.group("epoch")) if match.group("epoch") else 0,
|
||||
release=tuple(int(i) for i in match.group("release").split(".")),
|
||||
pre=_parse_letter_version(match.group("pre_l"), match.group("pre_n")),
|
||||
post=_parse_letter_version(
|
||||
match.group("post_l"), match.group("post_n1") or match.group("post_n2")
|
||||
),
|
||||
dev=_parse_letter_version(match.group("dev_l"), match.group("dev_n")),
|
||||
local=_parse_local_version(match.group("local")),
|
||||
)
|
||||
|
||||
# Generate a key which will be used for sorting
|
||||
self._key = _cmpkey(
|
||||
self._version.epoch,
|
||||
self._version.release,
|
||||
self._version.pre,
|
||||
self._version.post,
|
||||
self._version.dev,
|
||||
self._version.local,
|
||||
)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<Version('{self}')>"
|
||||
|
||||
def __str__(self) -> str:
|
||||
parts = []
|
||||
|
||||
# Epoch
|
||||
if self.epoch != 0:
|
||||
parts.append(f"{self.epoch}!")
|
||||
|
||||
# Release segment
|
||||
parts.append(".".join(str(x) for x in self.release))
|
||||
|
||||
# Pre-release
|
||||
if self.pre is not None:
|
||||
parts.append("".join(str(x) for x in self.pre))
|
||||
|
||||
# Post-release
|
||||
if self.post is not None:
|
||||
parts.append(f".post{self.post}")
|
||||
|
||||
# Development release
|
||||
if self.dev is not None:
|
||||
parts.append(f".dev{self.dev}")
|
||||
|
||||
# Local version segment
|
||||
if self.local is not None:
|
||||
parts.append(f"+{self.local}")
|
||||
|
||||
return "".join(parts)
|
||||
|
||||
@property
|
||||
def epoch(self) -> int:
|
||||
_epoch: int = self._version.epoch
|
||||
return _epoch
|
||||
|
||||
@property
|
||||
def release(self) -> Tuple[int, ...]:
|
||||
_release: Tuple[int, ...] = self._version.release
|
||||
return _release
|
||||
|
||||
@property
|
||||
def pre(self) -> Optional[Tuple[str, int]]:
|
||||
_pre: Optional[Tuple[str, int]] = self._version.pre
|
||||
return _pre
|
||||
|
||||
@property
|
||||
def post(self) -> Optional[int]:
|
||||
return self._version.post[1] if self._version.post else None
|
||||
|
||||
@property
|
||||
def dev(self) -> Optional[int]:
|
||||
return self._version.dev[1] if self._version.dev else None
|
||||
|
||||
@property
|
||||
def local(self) -> Optional[str]:
|
||||
if self._version.local:
|
||||
return ".".join(str(x) for x in self._version.local)
|
||||
else:
|
||||
return None
|
||||
|
||||
@property
|
||||
def public(self) -> str:
|
||||
return str(self).split("+", 1)[0]
|
||||
|
||||
@property
|
||||
def base_version(self) -> str:
|
||||
parts = []
|
||||
|
||||
# Epoch
|
||||
if self.epoch != 0:
|
||||
parts.append(f"{self.epoch}!")
|
||||
|
||||
# Release segment
|
||||
parts.append(".".join(str(x) for x in self.release))
|
||||
|
||||
return "".join(parts)
|
||||
|
||||
@property
|
||||
def is_prerelease(self) -> bool:
|
||||
return self.dev is not None or self.pre is not None
|
||||
|
||||
@property
|
||||
def is_postrelease(self) -> bool:
|
||||
return self.post is not None
|
||||
|
||||
@property
|
||||
def is_devrelease(self) -> bool:
|
||||
return self.dev is not None
|
||||
|
||||
@property
|
||||
def major(self) -> int:
|
||||
return self.release[0] if len(self.release) >= 1 else 0
|
||||
|
||||
@property
|
||||
def minor(self) -> int:
|
||||
return self.release[1] if len(self.release) >= 2 else 0
|
||||
|
||||
@property
|
||||
def micro(self) -> int:
|
||||
return self.release[2] if len(self.release) >= 3 else 0
|
||||
|
||||
|
||||
def _parse_letter_version(
|
||||
letter: str, number: Union[str, bytes, SupportsInt]
|
||||
) -> Optional[Tuple[str, int]]:
|
||||
|
||||
if letter:
|
||||
# We consider there to be an implicit 0 in a pre-release if there is
|
||||
# not a numeral associated with it.
|
||||
if number is None:
|
||||
number = 0
|
||||
|
||||
# We normalize any letters to their lower case form
|
||||
letter = letter.lower()
|
||||
|
||||
# We consider some words to be alternate spellings of other words and
|
||||
# in those cases we want to normalize the spellings to our preferred
|
||||
# spelling.
|
||||
if letter == "alpha":
|
||||
letter = "a"
|
||||
elif letter == "beta":
|
||||
letter = "b"
|
||||
elif letter in ["c", "pre", "preview"]:
|
||||
letter = "rc"
|
||||
elif letter in ["rev", "r"]:
|
||||
letter = "post"
|
||||
|
||||
return letter, int(number)
|
||||
if not letter and number:
|
||||
# We assume if we are given a number, but we are not given a letter
|
||||
# then this is using the implicit post release syntax (e.g. 1.0-1)
|
||||
letter = "post"
|
||||
|
||||
return letter, int(number)
|
||||
|
||||
return None
|
||||
|
||||
|
||||
_local_version_separators = re.compile(r"[\._-]")
|
||||
|
||||
|
||||
def _parse_local_version(local: str) -> Optional[LocalType]:
|
||||
"""
|
||||
Takes a string like abc.1.twelve and turns it into ("abc", 1, "twelve").
|
||||
"""
|
||||
if local is not None:
|
||||
return tuple(
|
||||
part.lower() if not part.isdigit() else int(part)
|
||||
for part in _local_version_separators.split(local)
|
||||
)
|
||||
return None
|
||||
|
||||
|
||||
def _cmpkey(
|
||||
epoch: int,
|
||||
release: Tuple[int, ...],
|
||||
pre: Optional[Tuple[str, int]],
|
||||
post: Optional[Tuple[str, int]],
|
||||
dev: Optional[Tuple[str, int]],
|
||||
local: Optional[Tuple[SubLocalType]],
|
||||
) -> CmpKey:
|
||||
|
||||
# When we compare a release version, we want to compare it with all of the
|
||||
# trailing zeros removed. So we'll use a reverse the list, drop all the now
|
||||
# leading zeros until we come to something non zero, then take the rest
|
||||
# re-reverse it back into the correct order and make it a tuple and use
|
||||
# that for our sorting key.
|
||||
_release = tuple(
|
||||
reversed(list(itertools.dropwhile(lambda x: x == 0, reversed(release))))
|
||||
)
|
||||
|
||||
# We need to "trick" the sorting algorithm to put 1.0.dev0 before 1.0a0.
|
||||
# We'll do this by abusing the pre segment, but we _only_ want to do this
|
||||
# if there is not a pre or a post segment. If we have one of those then
|
||||
# the normal sorting rules will handle this case correctly.
|
||||
if pre is None and post is None and dev is not None:
|
||||
_pre: PrePostDevType = NegativeInfinity
|
||||
# Versions without a pre-release (except as noted above) should sort after
|
||||
# those with one.
|
||||
elif pre is None:
|
||||
_pre = Infinity
|
||||
else:
|
||||
_pre = pre
|
||||
|
||||
# Versions without a post segment should sort before those with one.
|
||||
if post is None:
|
||||
_post: PrePostDevType = NegativeInfinity
|
||||
|
||||
else:
|
||||
_post = post
|
||||
|
||||
# Versions without a development segment should sort after those with one.
|
||||
if dev is None:
|
||||
_dev: PrePostDevType = Infinity
|
||||
|
||||
else:
|
||||
_dev = dev
|
||||
|
||||
if local is None:
|
||||
# Versions without a local segment should sort before those with one.
|
||||
_local: LocalType = NegativeInfinity
|
||||
else:
|
||||
# Versions with a local segment need that segment parsed to implement
|
||||
# the sorting rules in PEP440.
|
||||
# - Alpha numeric segments sort before numeric segments
|
||||
# - Alpha numeric segments sort lexicographically
|
||||
# - Numeric segments sort numerically
|
||||
# - Shorter versions sort before longer versions when the prefixes
|
||||
# match exactly
|
||||
_local = tuple(
|
||||
(i, "") if isinstance(i, int) else (NegativeInfinity, i) for i in local
|
||||
)
|
||||
|
||||
return epoch, _release, _pre, _post, _dev, _local
|
||||
1262
ftline/lib/python3.12/site-packages/seaborn/matrix.py
Normal file
1262
ftline/lib/python3.12/site-packages/seaborn/matrix.py
Normal file
File diff suppressed because it is too large
Load Diff
45
ftline/lib/python3.12/site-packages/seaborn/miscplot.py
Normal file
45
ftline/lib/python3.12/site-packages/seaborn/miscplot.py
Normal file
@@ -0,0 +1,45 @@
|
||||
import numpy as np
|
||||
import matplotlib as mpl
|
||||
import matplotlib.pyplot as plt
|
||||
import matplotlib.ticker as ticker
|
||||
|
||||
__all__ = ["palplot", "dogplot"]
|
||||
|
||||
|
||||
def palplot(pal, size=1):
|
||||
"""Plot the values in a color palette as a horizontal array.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
pal : sequence of matplotlib colors
|
||||
colors, i.e. as returned by seaborn.color_palette()
|
||||
size :
|
||||
scaling factor for size of plot
|
||||
|
||||
"""
|
||||
n = len(pal)
|
||||
_, ax = plt.subplots(1, 1, figsize=(n * size, size))
|
||||
ax.imshow(np.arange(n).reshape(1, n),
|
||||
cmap=mpl.colors.ListedColormap(list(pal)),
|
||||
interpolation="nearest", aspect="auto")
|
||||
ax.set_xticks(np.arange(n) - .5)
|
||||
ax.set_yticks([-.5, .5])
|
||||
# Ensure nice border between colors
|
||||
ax.set_xticklabels(["" for _ in range(n)])
|
||||
# The proper way to set no ticks
|
||||
ax.yaxis.set_major_locator(ticker.NullLocator())
|
||||
|
||||
|
||||
def dogplot(*_, **__):
|
||||
"""Who's a good boy?"""
|
||||
from urllib.request import urlopen
|
||||
from io import BytesIO
|
||||
|
||||
url = "https://github.com/mwaskom/seaborn-data/raw/master/png/img{}.png"
|
||||
pic = np.random.randint(2, 7)
|
||||
data = BytesIO(urlopen(url.format(pic)).read())
|
||||
img = plt.imread(data)
|
||||
f, ax = plt.subplots(figsize=(5, 5), dpi=100)
|
||||
f.subplots_adjust(0, 0, 1, 1)
|
||||
ax.imshow(img)
|
||||
ax.set_axis_off()
|
||||
49
ftline/lib/python3.12/site-packages/seaborn/objects.py
Normal file
49
ftline/lib/python3.12/site-packages/seaborn/objects.py
Normal file
@@ -0,0 +1,49 @@
|
||||
"""
|
||||
A declarative, object-oriented interface for creating statistical graphics.
|
||||
|
||||
The seaborn.objects namespace contains a number of classes that can be composed
|
||||
together to build a customized visualization.
|
||||
|
||||
The main object is :class:`Plot`, which is the starting point for all figures.
|
||||
Pass :class:`Plot` a dataset and specify assignments from its variables to
|
||||
roles in the plot. Build up the visualization by calling its methods.
|
||||
|
||||
There are four other general types of objects in this interface:
|
||||
|
||||
- :class:`Mark` subclasses, which create matplotlib artists for visualization
|
||||
- :class:`Stat` subclasses, which apply statistical transforms before plotting
|
||||
- :class:`Move` subclasses, which make further adjustments to reduce overplotting
|
||||
|
||||
These classes are passed to :meth:`Plot.add` to define a layer in the plot.
|
||||
Each layer has a :class:`Mark` and optional :class:`Stat` and/or :class:`Move`.
|
||||
Plots can have multiple layers.
|
||||
|
||||
The other general type of object is a :class:`Scale` subclass, which provide an
|
||||
interface for controlling the mappings between data values and visual properties.
|
||||
Pass :class:`Scale` objects to :meth:`Plot.scale`.
|
||||
|
||||
See the documentation for other :class:`Plot` methods to learn about the many
|
||||
ways that a plot can be enhanced and customized.
|
||||
|
||||
"""
|
||||
from seaborn._core.plot import Plot # noqa: F401
|
||||
|
||||
from seaborn._marks.base import Mark # noqa: F401
|
||||
from seaborn._marks.area import Area, Band # noqa: F401
|
||||
from seaborn._marks.bar import Bar, Bars # noqa: F401
|
||||
from seaborn._marks.dot import Dot, Dots # noqa: F401
|
||||
from seaborn._marks.line import Dash, Line, Lines, Path, Paths, Range # noqa: F401
|
||||
from seaborn._marks.text import Text # noqa: F401
|
||||
|
||||
from seaborn._stats.base import Stat # noqa: F401
|
||||
from seaborn._stats.aggregation import Agg, Est # noqa: F401
|
||||
from seaborn._stats.counting import Count, Hist # noqa: F401
|
||||
from seaborn._stats.density import KDE # noqa: F401
|
||||
from seaborn._stats.order import Perc # noqa: F401
|
||||
from seaborn._stats.regression import PolyFit # noqa: F401
|
||||
|
||||
from seaborn._core.moves import Dodge, Jitter, Norm, Shift, Stack, Move # noqa: F401
|
||||
|
||||
from seaborn._core.scales import ( # noqa: F401
|
||||
Boolean, Continuous, Nominal, Temporal, Scale
|
||||
)
|
||||
841
ftline/lib/python3.12/site-packages/seaborn/palettes.py
Normal file
841
ftline/lib/python3.12/site-packages/seaborn/palettes.py
Normal file
@@ -0,0 +1,841 @@
|
||||
import colorsys
|
||||
from itertools import cycle
|
||||
|
||||
import numpy as np
|
||||
import matplotlib as mpl
|
||||
|
||||
from .external import husl
|
||||
|
||||
from .utils import desaturate, get_color_cycle
|
||||
from .colors import xkcd_rgb, crayons
|
||||
from ._compat import get_colormap
|
||||
|
||||
|
||||
__all__ = ["color_palette", "hls_palette", "husl_palette", "mpl_palette",
|
||||
"dark_palette", "light_palette", "diverging_palette",
|
||||
"blend_palette", "xkcd_palette", "crayon_palette",
|
||||
"cubehelix_palette", "set_color_codes"]
|
||||
|
||||
|
||||
SEABORN_PALETTES = dict(
|
||||
deep=["#4C72B0", "#DD8452", "#55A868", "#C44E52", "#8172B3",
|
||||
"#937860", "#DA8BC3", "#8C8C8C", "#CCB974", "#64B5CD"],
|
||||
deep6=["#4C72B0", "#55A868", "#C44E52",
|
||||
"#8172B3", "#CCB974", "#64B5CD"],
|
||||
muted=["#4878D0", "#EE854A", "#6ACC64", "#D65F5F", "#956CB4",
|
||||
"#8C613C", "#DC7EC0", "#797979", "#D5BB67", "#82C6E2"],
|
||||
muted6=["#4878D0", "#6ACC64", "#D65F5F",
|
||||
"#956CB4", "#D5BB67", "#82C6E2"],
|
||||
pastel=["#A1C9F4", "#FFB482", "#8DE5A1", "#FF9F9B", "#D0BBFF",
|
||||
"#DEBB9B", "#FAB0E4", "#CFCFCF", "#FFFEA3", "#B9F2F0"],
|
||||
pastel6=["#A1C9F4", "#8DE5A1", "#FF9F9B",
|
||||
"#D0BBFF", "#FFFEA3", "#B9F2F0"],
|
||||
bright=["#023EFF", "#FF7C00", "#1AC938", "#E8000B", "#8B2BE2",
|
||||
"#9F4800", "#F14CC1", "#A3A3A3", "#FFC400", "#00D7FF"],
|
||||
bright6=["#023EFF", "#1AC938", "#E8000B",
|
||||
"#8B2BE2", "#FFC400", "#00D7FF"],
|
||||
dark=["#001C7F", "#B1400D", "#12711C", "#8C0800", "#591E71",
|
||||
"#592F0D", "#A23582", "#3C3C3C", "#B8850A", "#006374"],
|
||||
dark6=["#001C7F", "#12711C", "#8C0800",
|
||||
"#591E71", "#B8850A", "#006374"],
|
||||
colorblind=["#0173B2", "#DE8F05", "#029E73", "#D55E00", "#CC78BC",
|
||||
"#CA9161", "#FBAFE4", "#949494", "#ECE133", "#56B4E9"],
|
||||
colorblind6=["#0173B2", "#029E73", "#D55E00",
|
||||
"#CC78BC", "#ECE133", "#56B4E9"]
|
||||
)
|
||||
|
||||
|
||||
MPL_QUAL_PALS = {
|
||||
"tab10": 10, "tab20": 20, "tab20b": 20, "tab20c": 20,
|
||||
"Set1": 9, "Set2": 8, "Set3": 12,
|
||||
"Accent": 8, "Paired": 12,
|
||||
"Pastel1": 9, "Pastel2": 8, "Dark2": 8,
|
||||
}
|
||||
|
||||
|
||||
QUAL_PALETTE_SIZES = MPL_QUAL_PALS.copy()
|
||||
QUAL_PALETTE_SIZES.update({k: len(v) for k, v in SEABORN_PALETTES.items()})
|
||||
QUAL_PALETTES = list(QUAL_PALETTE_SIZES.keys())
|
||||
|
||||
|
||||
class _ColorPalette(list):
|
||||
"""Set the color palette in a with statement, otherwise be a list."""
|
||||
def __enter__(self):
|
||||
"""Open the context."""
|
||||
from .rcmod import set_palette
|
||||
self._orig_palette = color_palette()
|
||||
set_palette(self)
|
||||
return self
|
||||
|
||||
def __exit__(self, *args):
|
||||
"""Close the context."""
|
||||
from .rcmod import set_palette
|
||||
set_palette(self._orig_palette)
|
||||
|
||||
def as_hex(self):
|
||||
"""Return a color palette with hex codes instead of RGB values."""
|
||||
hex = [mpl.colors.rgb2hex(rgb) for rgb in self]
|
||||
return _ColorPalette(hex)
|
||||
|
||||
def _repr_html_(self):
|
||||
"""Rich display of the color palette in an HTML frontend."""
|
||||
s = 55
|
||||
n = len(self)
|
||||
html = f'<svg width="{n * s}" height="{s}">'
|
||||
for i, c in enumerate(self.as_hex()):
|
||||
html += (
|
||||
f'<rect x="{i * s}" y="0" width="{s}" height="{s}" style="fill:{c};'
|
||||
'stroke-width:2;stroke:rgb(255,255,255)"/>'
|
||||
)
|
||||
html += '</svg>'
|
||||
return html
|
||||
|
||||
|
||||
def _patch_colormap_display():
|
||||
"""Simplify the rich display of matplotlib color maps in a notebook."""
|
||||
def _repr_png_(self):
|
||||
"""Generate a PNG representation of the Colormap."""
|
||||
import io
|
||||
from PIL import Image
|
||||
import numpy as np
|
||||
IMAGE_SIZE = (400, 50)
|
||||
X = np.tile(np.linspace(0, 1, IMAGE_SIZE[0]), (IMAGE_SIZE[1], 1))
|
||||
pixels = self(X, bytes=True)
|
||||
png_bytes = io.BytesIO()
|
||||
Image.fromarray(pixels).save(png_bytes, format='png')
|
||||
return png_bytes.getvalue()
|
||||
|
||||
def _repr_html_(self):
|
||||
"""Generate an HTML representation of the Colormap."""
|
||||
import base64
|
||||
png_bytes = self._repr_png_()
|
||||
png_base64 = base64.b64encode(png_bytes).decode('ascii')
|
||||
return ('<img '
|
||||
+ 'alt="' + self.name + ' color map" '
|
||||
+ 'title="' + self.name + '"'
|
||||
+ 'src="data:image/png;base64,' + png_base64 + '">')
|
||||
|
||||
mpl.colors.Colormap._repr_png_ = _repr_png_
|
||||
mpl.colors.Colormap._repr_html_ = _repr_html_
|
||||
|
||||
|
||||
def color_palette(palette=None, n_colors=None, desat=None, as_cmap=False):
|
||||
"""Return a list of colors or continuous colormap defining a palette.
|
||||
|
||||
Possible ``palette`` values include:
|
||||
- Name of a seaborn palette (deep, muted, bright, pastel, dark, colorblind)
|
||||
- Name of matplotlib colormap
|
||||
- 'husl' or 'hls'
|
||||
- 'ch:<cubehelix arguments>'
|
||||
- 'light:<color>', 'dark:<color>', 'blend:<color>,<color>',
|
||||
- A sequence of colors in any format matplotlib accepts
|
||||
|
||||
Calling this function with ``palette=None`` will return the current
|
||||
matplotlib color cycle.
|
||||
|
||||
This function can also be used in a ``with`` statement to temporarily
|
||||
set the color cycle for a plot or set of plots.
|
||||
|
||||
See the :ref:`tutorial <palette_tutorial>` for more information.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
palette : None, string, or sequence, optional
|
||||
Name of palette or None to return current palette. If a sequence, input
|
||||
colors are used but possibly cycled and desaturated.
|
||||
n_colors : int, optional
|
||||
Number of colors in the palette. If ``None``, the default will depend
|
||||
on how ``palette`` is specified. Named palettes default to 6 colors,
|
||||
but grabbing the current palette or passing in a list of colors will
|
||||
not change the number of colors unless this is specified. Asking for
|
||||
more colors than exist in the palette will cause it to cycle. Ignored
|
||||
when ``as_cmap`` is True.
|
||||
desat : float, optional
|
||||
Proportion to desaturate each color by.
|
||||
as_cmap : bool
|
||||
If True, return a :class:`matplotlib.colors.ListedColormap`.
|
||||
|
||||
Returns
|
||||
-------
|
||||
list of RGB tuples or :class:`matplotlib.colors.ListedColormap`
|
||||
|
||||
See Also
|
||||
--------
|
||||
set_palette : Set the default color cycle for all plots.
|
||||
set_color_codes : Reassign color codes like ``"b"``, ``"g"``, etc. to
|
||||
colors from one of the seaborn palettes.
|
||||
|
||||
Examples
|
||||
--------
|
||||
|
||||
.. include:: ../docstrings/color_palette.rst
|
||||
|
||||
"""
|
||||
if palette is None:
|
||||
palette = get_color_cycle()
|
||||
if n_colors is None:
|
||||
n_colors = len(palette)
|
||||
|
||||
elif not isinstance(palette, str):
|
||||
palette = palette
|
||||
if n_colors is None:
|
||||
n_colors = len(palette)
|
||||
else:
|
||||
|
||||
if n_colors is None:
|
||||
# Use all colors in a qualitative palette or 6 of another kind
|
||||
n_colors = QUAL_PALETTE_SIZES.get(palette, 6)
|
||||
|
||||
if palette in SEABORN_PALETTES:
|
||||
# Named "seaborn variant" of matplotlib default color cycle
|
||||
palette = SEABORN_PALETTES[palette]
|
||||
|
||||
elif palette == "hls":
|
||||
# Evenly spaced colors in cylindrical RGB space
|
||||
palette = hls_palette(n_colors, as_cmap=as_cmap)
|
||||
|
||||
elif palette == "husl":
|
||||
# Evenly spaced colors in cylindrical Lab space
|
||||
palette = husl_palette(n_colors, as_cmap=as_cmap)
|
||||
|
||||
elif palette.lower() == "jet":
|
||||
# Paternalism
|
||||
raise ValueError("No.")
|
||||
|
||||
elif palette.startswith("ch:"):
|
||||
# Cubehelix palette with params specified in string
|
||||
args, kwargs = _parse_cubehelix_args(palette)
|
||||
palette = cubehelix_palette(n_colors, *args, **kwargs, as_cmap=as_cmap)
|
||||
|
||||
elif palette.startswith("light:"):
|
||||
# light palette to color specified in string
|
||||
_, color = palette.split(":")
|
||||
reverse = color.endswith("_r")
|
||||
if reverse:
|
||||
color = color[:-2]
|
||||
palette = light_palette(color, n_colors, reverse=reverse, as_cmap=as_cmap)
|
||||
|
||||
elif palette.startswith("dark:"):
|
||||
# light palette to color specified in string
|
||||
_, color = palette.split(":")
|
||||
reverse = color.endswith("_r")
|
||||
if reverse:
|
||||
color = color[:-2]
|
||||
palette = dark_palette(color, n_colors, reverse=reverse, as_cmap=as_cmap)
|
||||
|
||||
elif palette.startswith("blend:"):
|
||||
# blend palette between colors specified in string
|
||||
_, colors = palette.split(":")
|
||||
colors = colors.split(",")
|
||||
palette = blend_palette(colors, n_colors, as_cmap=as_cmap)
|
||||
|
||||
else:
|
||||
try:
|
||||
# Perhaps a named matplotlib colormap?
|
||||
palette = mpl_palette(palette, n_colors, as_cmap=as_cmap)
|
||||
except (ValueError, KeyError): # Error class changed in mpl36
|
||||
raise ValueError(f"{palette!r} is not a valid palette name")
|
||||
|
||||
if desat is not None:
|
||||
palette = [desaturate(c, desat) for c in palette]
|
||||
|
||||
if not as_cmap:
|
||||
|
||||
# Always return as many colors as we asked for
|
||||
pal_cycle = cycle(palette)
|
||||
palette = [next(pal_cycle) for _ in range(n_colors)]
|
||||
|
||||
# Always return in r, g, b tuple format
|
||||
try:
|
||||
palette = map(mpl.colors.colorConverter.to_rgb, palette)
|
||||
palette = _ColorPalette(palette)
|
||||
except ValueError:
|
||||
raise ValueError(f"Could not generate a palette for {palette}")
|
||||
|
||||
return palette
|
||||
|
||||
|
||||
def hls_palette(n_colors=6, h=.01, l=.6, s=.65, as_cmap=False): # noqa
|
||||
"""
|
||||
Return hues with constant lightness and saturation in the HLS system.
|
||||
|
||||
The hues are evenly sampled along a circular path. The resulting palette will be
|
||||
appropriate for categorical or cyclical data.
|
||||
|
||||
The `h`, `l`, and `s` values should be between 0 and 1.
|
||||
|
||||
.. note::
|
||||
While the separation of the resulting colors will be mathematically
|
||||
constant, the HLS system does not construct a perceptually-uniform space,
|
||||
so their apparent intensity will vary.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
n_colors : int
|
||||
Number of colors in the palette.
|
||||
h : float
|
||||
The value of the first hue.
|
||||
l : float
|
||||
The lightness value.
|
||||
s : float
|
||||
The saturation intensity.
|
||||
as_cmap : bool
|
||||
If True, return a matplotlib colormap object.
|
||||
|
||||
Returns
|
||||
-------
|
||||
palette
|
||||
list of RGB tuples or :class:`matplotlib.colors.ListedColormap`
|
||||
|
||||
See Also
|
||||
--------
|
||||
husl_palette : Make a palette using evenly spaced hues in the HUSL system.
|
||||
|
||||
Examples
|
||||
--------
|
||||
.. include:: ../docstrings/hls_palette.rst
|
||||
|
||||
"""
|
||||
if as_cmap:
|
||||
n_colors = 256
|
||||
hues = np.linspace(0, 1, int(n_colors) + 1)[:-1]
|
||||
hues += h
|
||||
hues %= 1
|
||||
hues -= hues.astype(int)
|
||||
palette = [colorsys.hls_to_rgb(h_i, l, s) for h_i in hues]
|
||||
if as_cmap:
|
||||
return mpl.colors.ListedColormap(palette, "hls")
|
||||
else:
|
||||
return _ColorPalette(palette)
|
||||
|
||||
|
||||
def husl_palette(n_colors=6, h=.01, s=.9, l=.65, as_cmap=False): # noqa
|
||||
"""
|
||||
Return hues with constant lightness and saturation in the HUSL system.
|
||||
|
||||
The hues are evenly sampled along a circular path. The resulting palette will be
|
||||
appropriate for categorical or cyclical data.
|
||||
|
||||
The `h`, `l`, and `s` values should be between 0 and 1.
|
||||
|
||||
This function is similar to :func:`hls_palette`, but it uses a nonlinear color
|
||||
space that is more perceptually uniform.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
n_colors : int
|
||||
Number of colors in the palette.
|
||||
h : float
|
||||
The value of the first hue.
|
||||
l : float
|
||||
The lightness value.
|
||||
s : float
|
||||
The saturation intensity.
|
||||
as_cmap : bool
|
||||
If True, return a matplotlib colormap object.
|
||||
|
||||
Returns
|
||||
-------
|
||||
palette
|
||||
list of RGB tuples or :class:`matplotlib.colors.ListedColormap`
|
||||
|
||||
See Also
|
||||
--------
|
||||
hls_palette : Make a palette using evenly spaced hues in the HSL system.
|
||||
|
||||
Examples
|
||||
--------
|
||||
.. include:: ../docstrings/husl_palette.rst
|
||||
|
||||
"""
|
||||
if as_cmap:
|
||||
n_colors = 256
|
||||
hues = np.linspace(0, 1, int(n_colors) + 1)[:-1]
|
||||
hues += h
|
||||
hues %= 1
|
||||
hues *= 359
|
||||
s *= 99
|
||||
l *= 99 # noqa
|
||||
palette = [_color_to_rgb((h_i, s, l), input="husl") for h_i in hues]
|
||||
if as_cmap:
|
||||
return mpl.colors.ListedColormap(palette, "hsl")
|
||||
else:
|
||||
return _ColorPalette(palette)
|
||||
|
||||
|
||||
def mpl_palette(name, n_colors=6, as_cmap=False):
|
||||
"""
|
||||
Return a palette or colormap from the matplotlib registry.
|
||||
|
||||
For continuous palettes, evenly-spaced discrete samples are chosen while
|
||||
excluding the minimum and maximum value in the colormap to provide better
|
||||
contrast at the extremes.
|
||||
|
||||
For qualitative palettes (e.g. those from colorbrewer), exact values are
|
||||
indexed (rather than interpolated), but fewer than `n_colors` can be returned
|
||||
if the palette does not define that many.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
name : string
|
||||
Name of the palette. This should be a named matplotlib colormap.
|
||||
n_colors : int
|
||||
Number of discrete colors in the palette.
|
||||
|
||||
Returns
|
||||
-------
|
||||
list of RGB tuples or :class:`matplotlib.colors.ListedColormap`
|
||||
|
||||
Examples
|
||||
--------
|
||||
.. include:: ../docstrings/mpl_palette.rst
|
||||
|
||||
"""
|
||||
if name.endswith("_d"):
|
||||
sub_name = name[:-2]
|
||||
if sub_name.endswith("_r"):
|
||||
reverse = True
|
||||
sub_name = sub_name[:-2]
|
||||
else:
|
||||
reverse = False
|
||||
pal = color_palette(sub_name, 2) + ["#333333"]
|
||||
if reverse:
|
||||
pal = pal[::-1]
|
||||
cmap = blend_palette(pal, n_colors, as_cmap=True)
|
||||
else:
|
||||
cmap = get_colormap(name)
|
||||
|
||||
if name in MPL_QUAL_PALS:
|
||||
bins = np.linspace(0, 1, MPL_QUAL_PALS[name])[:n_colors]
|
||||
else:
|
||||
bins = np.linspace(0, 1, int(n_colors) + 2)[1:-1]
|
||||
palette = list(map(tuple, cmap(bins)[:, :3]))
|
||||
|
||||
if as_cmap:
|
||||
return cmap
|
||||
else:
|
||||
return _ColorPalette(palette)
|
||||
|
||||
|
||||
def _color_to_rgb(color, input):
|
||||
"""Add some more flexibility to color choices."""
|
||||
if input == "hls":
|
||||
color = colorsys.hls_to_rgb(*color)
|
||||
elif input == "husl":
|
||||
color = husl.husl_to_rgb(*color)
|
||||
color = tuple(np.clip(color, 0, 1))
|
||||
elif input == "xkcd":
|
||||
color = xkcd_rgb[color]
|
||||
|
||||
return mpl.colors.to_rgb(color)
|
||||
|
||||
|
||||
def dark_palette(color, n_colors=6, reverse=False, as_cmap=False, input="rgb"):
|
||||
"""Make a sequential palette that blends from dark to ``color``.
|
||||
|
||||
This kind of palette is good for data that range between relatively
|
||||
uninteresting low values and interesting high values.
|
||||
|
||||
The ``color`` parameter can be specified in a number of ways, including
|
||||
all options for defining a color in matplotlib and several additional
|
||||
color spaces that are handled by seaborn. You can also use the database
|
||||
of named colors from the XKCD color survey.
|
||||
|
||||
If you are using the IPython notebook, you can also choose this palette
|
||||
interactively with the :func:`choose_dark_palette` function.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
color : base color for high values
|
||||
hex, rgb-tuple, or html color name
|
||||
n_colors : int, optional
|
||||
number of colors in the palette
|
||||
reverse : bool, optional
|
||||
if True, reverse the direction of the blend
|
||||
as_cmap : bool, optional
|
||||
If True, return a :class:`matplotlib.colors.ListedColormap`.
|
||||
input : {'rgb', 'hls', 'husl', xkcd'}
|
||||
Color space to interpret the input color. The first three options
|
||||
apply to tuple inputs and the latter applies to string inputs.
|
||||
|
||||
Returns
|
||||
-------
|
||||
palette
|
||||
list of RGB tuples or :class:`matplotlib.colors.ListedColormap`
|
||||
|
||||
See Also
|
||||
--------
|
||||
light_palette : Create a sequential palette with bright low values.
|
||||
diverging_palette : Create a diverging palette with two colors.
|
||||
|
||||
Examples
|
||||
--------
|
||||
.. include:: ../docstrings/dark_palette.rst
|
||||
|
||||
"""
|
||||
rgb = _color_to_rgb(color, input)
|
||||
hue, sat, _ = husl.rgb_to_husl(*rgb)
|
||||
gray_s, gray_l = .15 * sat, 15
|
||||
gray = _color_to_rgb((hue, gray_s, gray_l), input="husl")
|
||||
colors = [rgb, gray] if reverse else [gray, rgb]
|
||||
return blend_palette(colors, n_colors, as_cmap)
|
||||
|
||||
|
||||
def light_palette(color, n_colors=6, reverse=False, as_cmap=False, input="rgb"):
|
||||
"""Make a sequential palette that blends from light to ``color``.
|
||||
|
||||
The ``color`` parameter can be specified in a number of ways, including
|
||||
all options for defining a color in matplotlib and several additional
|
||||
color spaces that are handled by seaborn. You can also use the database
|
||||
of named colors from the XKCD color survey.
|
||||
|
||||
If you are using a Jupyter notebook, you can also choose this palette
|
||||
interactively with the :func:`choose_light_palette` function.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
color : base color for high values
|
||||
hex code, html color name, or tuple in `input` space.
|
||||
n_colors : int, optional
|
||||
number of colors in the palette
|
||||
reverse : bool, optional
|
||||
if True, reverse the direction of the blend
|
||||
as_cmap : bool, optional
|
||||
If True, return a :class:`matplotlib.colors.ListedColormap`.
|
||||
input : {'rgb', 'hls', 'husl', xkcd'}
|
||||
Color space to interpret the input color. The first three options
|
||||
apply to tuple inputs and the latter applies to string inputs.
|
||||
|
||||
Returns
|
||||
-------
|
||||
palette
|
||||
list of RGB tuples or :class:`matplotlib.colors.ListedColormap`
|
||||
|
||||
See Also
|
||||
--------
|
||||
dark_palette : Create a sequential palette with dark low values.
|
||||
diverging_palette : Create a diverging palette with two colors.
|
||||
|
||||
Examples
|
||||
--------
|
||||
.. include:: ../docstrings/light_palette.rst
|
||||
|
||||
"""
|
||||
rgb = _color_to_rgb(color, input)
|
||||
hue, sat, _ = husl.rgb_to_husl(*rgb)
|
||||
gray_s, gray_l = .15 * sat, 95
|
||||
gray = _color_to_rgb((hue, gray_s, gray_l), input="husl")
|
||||
colors = [rgb, gray] if reverse else [gray, rgb]
|
||||
return blend_palette(colors, n_colors, as_cmap)
|
||||
|
||||
|
||||
def diverging_palette(h_neg, h_pos, s=75, l=50, sep=1, n=6, # noqa
|
||||
center="light", as_cmap=False):
|
||||
"""Make a diverging palette between two HUSL colors.
|
||||
|
||||
If you are using the IPython notebook, you can also choose this palette
|
||||
interactively with the :func:`choose_diverging_palette` function.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
h_neg, h_pos : float in [0, 359]
|
||||
Anchor hues for negative and positive extents of the map.
|
||||
s : float in [0, 100], optional
|
||||
Anchor saturation for both extents of the map.
|
||||
l : float in [0, 100], optional
|
||||
Anchor lightness for both extents of the map.
|
||||
sep : int, optional
|
||||
Size of the intermediate region.
|
||||
n : int, optional
|
||||
Number of colors in the palette (if not returning a cmap)
|
||||
center : {"light", "dark"}, optional
|
||||
Whether the center of the palette is light or dark
|
||||
as_cmap : bool, optional
|
||||
If True, return a :class:`matplotlib.colors.ListedColormap`.
|
||||
|
||||
Returns
|
||||
-------
|
||||
palette
|
||||
list of RGB tuples or :class:`matplotlib.colors.ListedColormap`
|
||||
|
||||
See Also
|
||||
--------
|
||||
dark_palette : Create a sequential palette with dark values.
|
||||
light_palette : Create a sequential palette with light values.
|
||||
|
||||
Examples
|
||||
--------
|
||||
.. include: ../docstrings/diverging_palette.rst
|
||||
|
||||
"""
|
||||
palfunc = dict(dark=dark_palette, light=light_palette)[center]
|
||||
n_half = int(128 - (sep // 2))
|
||||
neg = palfunc((h_neg, s, l), n_half, reverse=True, input="husl")
|
||||
pos = palfunc((h_pos, s, l), n_half, input="husl")
|
||||
midpoint = dict(light=[(.95, .95, .95)], dark=[(.133, .133, .133)])[center]
|
||||
mid = midpoint * sep
|
||||
pal = blend_palette(np.concatenate([neg, mid, pos]), n, as_cmap=as_cmap)
|
||||
return pal
|
||||
|
||||
|
||||
def blend_palette(colors, n_colors=6, as_cmap=False, input="rgb"):
|
||||
"""Make a palette that blends between a list of colors.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
colors : sequence of colors in various formats interpreted by `input`
|
||||
hex code, html color name, or tuple in `input` space.
|
||||
n_colors : int, optional
|
||||
Number of colors in the palette.
|
||||
as_cmap : bool, optional
|
||||
If True, return a :class:`matplotlib.colors.ListedColormap`.
|
||||
|
||||
Returns
|
||||
-------
|
||||
palette
|
||||
list of RGB tuples or :class:`matplotlib.colors.ListedColormap`
|
||||
|
||||
Examples
|
||||
--------
|
||||
.. include: ../docstrings/blend_palette.rst
|
||||
|
||||
"""
|
||||
colors = [_color_to_rgb(color, input) for color in colors]
|
||||
name = "blend"
|
||||
pal = mpl.colors.LinearSegmentedColormap.from_list(name, colors)
|
||||
if not as_cmap:
|
||||
rgb_array = pal(np.linspace(0, 1, int(n_colors)))[:, :3] # no alpha
|
||||
pal = _ColorPalette(map(tuple, rgb_array))
|
||||
return pal
|
||||
|
||||
|
||||
def xkcd_palette(colors):
|
||||
"""Make a palette with color names from the xkcd color survey.
|
||||
|
||||
See xkcd for the full list of colors: https://xkcd.com/color/rgb/
|
||||
|
||||
This is just a simple wrapper around the `seaborn.xkcd_rgb` dictionary.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
colors : list of strings
|
||||
List of keys in the `seaborn.xkcd_rgb` dictionary.
|
||||
|
||||
Returns
|
||||
-------
|
||||
palette
|
||||
A list of colors as RGB tuples.
|
||||
|
||||
See Also
|
||||
--------
|
||||
crayon_palette : Make a palette with Crayola crayon colors.
|
||||
|
||||
"""
|
||||
palette = [xkcd_rgb[name] for name in colors]
|
||||
return color_palette(palette, len(palette))
|
||||
|
||||
|
||||
def crayon_palette(colors):
|
||||
"""Make a palette with color names from Crayola crayons.
|
||||
|
||||
Colors are taken from here:
|
||||
https://en.wikipedia.org/wiki/List_of_Crayola_crayon_colors
|
||||
|
||||
This is just a simple wrapper around the `seaborn.crayons` dictionary.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
colors : list of strings
|
||||
List of keys in the `seaborn.crayons` dictionary.
|
||||
|
||||
Returns
|
||||
-------
|
||||
palette
|
||||
A list of colors as RGB tuples.
|
||||
|
||||
See Also
|
||||
--------
|
||||
xkcd_palette : Make a palette with named colors from the XKCD color survey.
|
||||
|
||||
"""
|
||||
palette = [crayons[name] for name in colors]
|
||||
return color_palette(palette, len(palette))
|
||||
|
||||
|
||||
def cubehelix_palette(n_colors=6, start=0, rot=.4, gamma=1.0, hue=0.8,
|
||||
light=.85, dark=.15, reverse=False, as_cmap=False):
|
||||
"""Make a sequential palette from the cubehelix system.
|
||||
|
||||
This produces a colormap with linearly-decreasing (or increasing)
|
||||
brightness. That means that information will be preserved if printed to
|
||||
black and white or viewed by someone who is colorblind. "cubehelix" is
|
||||
also available as a matplotlib-based palette, but this function gives the
|
||||
user more control over the look of the palette and has a different set of
|
||||
defaults.
|
||||
|
||||
In addition to using this function, it is also possible to generate a
|
||||
cubehelix palette generally in seaborn using a string starting with
|
||||
`ch:` and containing other parameters (e.g. `"ch:s=.25,r=-.5"`).
|
||||
|
||||
Parameters
|
||||
----------
|
||||
n_colors : int
|
||||
Number of colors in the palette.
|
||||
start : float, 0 <= start <= 3
|
||||
The hue value at the start of the helix.
|
||||
rot : float
|
||||
Rotations around the hue wheel over the range of the palette.
|
||||
gamma : float 0 <= gamma
|
||||
Nonlinearity to emphasize dark (gamma < 1) or light (gamma > 1) colors.
|
||||
hue : float, 0 <= hue <= 1
|
||||
Saturation of the colors.
|
||||
dark : float 0 <= dark <= 1
|
||||
Intensity of the darkest color in the palette.
|
||||
light : float 0 <= light <= 1
|
||||
Intensity of the lightest color in the palette.
|
||||
reverse : bool
|
||||
If True, the palette will go from dark to light.
|
||||
as_cmap : bool
|
||||
If True, return a :class:`matplotlib.colors.ListedColormap`.
|
||||
|
||||
Returns
|
||||
-------
|
||||
palette
|
||||
list of RGB tuples or :class:`matplotlib.colors.ListedColormap`
|
||||
|
||||
See Also
|
||||
--------
|
||||
choose_cubehelix_palette : Launch an interactive widget to select cubehelix
|
||||
palette parameters.
|
||||
dark_palette : Create a sequential palette with dark low values.
|
||||
light_palette : Create a sequential palette with bright low values.
|
||||
|
||||
References
|
||||
----------
|
||||
Green, D. A. (2011). "A colour scheme for the display of astronomical
|
||||
intensity images". Bulletin of the Astromical Society of India, Vol. 39,
|
||||
p. 289-295.
|
||||
|
||||
Examples
|
||||
--------
|
||||
.. include:: ../docstrings/cubehelix_palette.rst
|
||||
|
||||
"""
|
||||
def get_color_function(p0, p1):
|
||||
# Copied from matplotlib because it lives in private module
|
||||
def color(x):
|
||||
# Apply gamma factor to emphasise low or high intensity values
|
||||
xg = x ** gamma
|
||||
|
||||
# Calculate amplitude and angle of deviation from the black
|
||||
# to white diagonal in the plane of constant
|
||||
# perceived intensity.
|
||||
a = hue * xg * (1 - xg) / 2
|
||||
|
||||
phi = 2 * np.pi * (start / 3 + rot * x)
|
||||
|
||||
return xg + a * (p0 * np.cos(phi) + p1 * np.sin(phi))
|
||||
return color
|
||||
|
||||
cdict = {
|
||||
"red": get_color_function(-0.14861, 1.78277),
|
||||
"green": get_color_function(-0.29227, -0.90649),
|
||||
"blue": get_color_function(1.97294, 0.0),
|
||||
}
|
||||
|
||||
cmap = mpl.colors.LinearSegmentedColormap("cubehelix", cdict)
|
||||
|
||||
x = np.linspace(light, dark, int(n_colors))
|
||||
pal = cmap(x)[:, :3].tolist()
|
||||
if reverse:
|
||||
pal = pal[::-1]
|
||||
|
||||
if as_cmap:
|
||||
x_256 = np.linspace(light, dark, 256)
|
||||
if reverse:
|
||||
x_256 = x_256[::-1]
|
||||
pal_256 = cmap(x_256)
|
||||
cmap = mpl.colors.ListedColormap(pal_256, "seaborn_cubehelix")
|
||||
return cmap
|
||||
else:
|
||||
return _ColorPalette(pal)
|
||||
|
||||
|
||||
def _parse_cubehelix_args(argstr):
|
||||
"""Turn stringified cubehelix params into args/kwargs."""
|
||||
|
||||
if argstr.startswith("ch:"):
|
||||
argstr = argstr[3:]
|
||||
|
||||
if argstr.endswith("_r"):
|
||||
reverse = True
|
||||
argstr = argstr[:-2]
|
||||
else:
|
||||
reverse = False
|
||||
|
||||
if not argstr:
|
||||
return [], {"reverse": reverse}
|
||||
|
||||
all_args = argstr.split(",")
|
||||
|
||||
args = [float(a.strip(" ")) for a in all_args if "=" not in a]
|
||||
|
||||
kwargs = [a.split("=") for a in all_args if "=" in a]
|
||||
kwargs = {k.strip(" "): float(v.strip(" ")) for k, v in kwargs}
|
||||
|
||||
kwarg_map = dict(
|
||||
s="start", r="rot", g="gamma",
|
||||
h="hue", l="light", d="dark", # noqa: E741
|
||||
)
|
||||
|
||||
kwargs = {kwarg_map.get(k, k): v for k, v in kwargs.items()}
|
||||
|
||||
if reverse:
|
||||
kwargs["reverse"] = True
|
||||
|
||||
return args, kwargs
|
||||
|
||||
|
||||
def set_color_codes(palette="deep"):
|
||||
"""Change how matplotlib color shorthands are interpreted.
|
||||
|
||||
Calling this will change how shorthand codes like "b" or "g"
|
||||
are interpreted by matplotlib in subsequent plots.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
palette : {deep, muted, pastel, dark, bright, colorblind}
|
||||
Named seaborn palette to use as the source of colors.
|
||||
|
||||
See Also
|
||||
--------
|
||||
set : Color codes can be set through the high-level seaborn style
|
||||
manager.
|
||||
set_palette : Color codes can also be set through the function that
|
||||
sets the matplotlib color cycle.
|
||||
|
||||
"""
|
||||
if palette == "reset":
|
||||
colors = [
|
||||
(0., 0., 1.),
|
||||
(0., .5, 0.),
|
||||
(1., 0., 0.),
|
||||
(.75, 0., .75),
|
||||
(.75, .75, 0.),
|
||||
(0., .75, .75),
|
||||
(0., 0., 0.)
|
||||
]
|
||||
elif not isinstance(palette, str):
|
||||
err = "set_color_codes requires a named seaborn palette"
|
||||
raise TypeError(err)
|
||||
elif palette in SEABORN_PALETTES:
|
||||
if not palette.endswith("6"):
|
||||
palette = palette + "6"
|
||||
colors = SEABORN_PALETTES[palette] + [(.1, .1, .1)]
|
||||
else:
|
||||
err = f"Cannot set colors with palette '{palette}'"
|
||||
raise ValueError(err)
|
||||
|
||||
for code, color in zip("bgrmyck", colors):
|
||||
rgb = mpl.colors.colorConverter.to_rgb(color)
|
||||
mpl.colors.colorConverter.colors[code] = rgb
|
||||
533
ftline/lib/python3.12/site-packages/seaborn/rcmod.py
Normal file
533
ftline/lib/python3.12/site-packages/seaborn/rcmod.py
Normal file
@@ -0,0 +1,533 @@
|
||||
"""Control plot style and scaling using the matplotlib rcParams interface."""
|
||||
import functools
|
||||
import matplotlib as mpl
|
||||
from cycler import cycler
|
||||
from . import palettes
|
||||
|
||||
|
||||
__all__ = ["set_theme", "set", "reset_defaults", "reset_orig",
|
||||
"axes_style", "set_style", "plotting_context", "set_context",
|
||||
"set_palette"]
|
||||
|
||||
|
||||
_style_keys = [
|
||||
|
||||
"axes.facecolor",
|
||||
"axes.edgecolor",
|
||||
"axes.grid",
|
||||
"axes.axisbelow",
|
||||
"axes.labelcolor",
|
||||
|
||||
"figure.facecolor",
|
||||
|
||||
"grid.color",
|
||||
"grid.linestyle",
|
||||
|
||||
"text.color",
|
||||
|
||||
"xtick.color",
|
||||
"ytick.color",
|
||||
"xtick.direction",
|
||||
"ytick.direction",
|
||||
"lines.solid_capstyle",
|
||||
|
||||
"patch.edgecolor",
|
||||
"patch.force_edgecolor",
|
||||
|
||||
"image.cmap",
|
||||
"font.family",
|
||||
"font.sans-serif",
|
||||
|
||||
"xtick.bottom",
|
||||
"xtick.top",
|
||||
"ytick.left",
|
||||
"ytick.right",
|
||||
|
||||
"axes.spines.left",
|
||||
"axes.spines.bottom",
|
||||
"axes.spines.right",
|
||||
"axes.spines.top",
|
||||
|
||||
]
|
||||
|
||||
_context_keys = [
|
||||
|
||||
"font.size",
|
||||
"axes.labelsize",
|
||||
"axes.titlesize",
|
||||
"xtick.labelsize",
|
||||
"ytick.labelsize",
|
||||
"legend.fontsize",
|
||||
"legend.title_fontsize",
|
||||
|
||||
"axes.linewidth",
|
||||
"grid.linewidth",
|
||||
"lines.linewidth",
|
||||
"lines.markersize",
|
||||
"patch.linewidth",
|
||||
|
||||
"xtick.major.width",
|
||||
"ytick.major.width",
|
||||
"xtick.minor.width",
|
||||
"ytick.minor.width",
|
||||
|
||||
"xtick.major.size",
|
||||
"ytick.major.size",
|
||||
"xtick.minor.size",
|
||||
"ytick.minor.size",
|
||||
|
||||
]
|
||||
|
||||
|
||||
def set_theme(context="notebook", style="darkgrid", palette="deep",
|
||||
font="sans-serif", font_scale=1, color_codes=True, rc=None):
|
||||
"""
|
||||
Set aspects of the visual theme for all matplotlib and seaborn plots.
|
||||
|
||||
This function changes the global defaults for all plots using the
|
||||
matplotlib rcParams system. The themeing is decomposed into several distinct
|
||||
sets of parameter values.
|
||||
|
||||
The options are illustrated in the :doc:`aesthetics <../tutorial/aesthetics>`
|
||||
and :doc:`color palette <../tutorial/color_palettes>` tutorials.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
context : string or dict
|
||||
Scaling parameters, see :func:`plotting_context`.
|
||||
style : string or dict
|
||||
Axes style parameters, see :func:`axes_style`.
|
||||
palette : string or sequence
|
||||
Color palette, see :func:`color_palette`.
|
||||
font : string
|
||||
Font family, see matplotlib font manager.
|
||||
font_scale : float, optional
|
||||
Separate scaling factor to independently scale the size of the
|
||||
font elements.
|
||||
color_codes : bool
|
||||
If ``True`` and ``palette`` is a seaborn palette, remap the shorthand
|
||||
color codes (e.g. "b", "g", "r", etc.) to the colors from this palette.
|
||||
rc : dict or None
|
||||
Dictionary of rc parameter mappings to override the above.
|
||||
|
||||
Examples
|
||||
--------
|
||||
|
||||
.. include:: ../docstrings/set_theme.rst
|
||||
|
||||
"""
|
||||
set_context(context, font_scale)
|
||||
set_style(style, rc={"font.family": font})
|
||||
set_palette(palette, color_codes=color_codes)
|
||||
if rc is not None:
|
||||
mpl.rcParams.update(rc)
|
||||
|
||||
|
||||
def set(*args, **kwargs):
|
||||
"""
|
||||
Alias for :func:`set_theme`, which is the preferred interface.
|
||||
|
||||
This function may be removed in the future.
|
||||
"""
|
||||
set_theme(*args, **kwargs)
|
||||
|
||||
|
||||
def reset_defaults():
|
||||
"""Restore all RC params to default settings."""
|
||||
mpl.rcParams.update(mpl.rcParamsDefault)
|
||||
|
||||
|
||||
def reset_orig():
|
||||
"""Restore all RC params to original settings (respects custom rc)."""
|
||||
from . import _orig_rc_params
|
||||
mpl.rcParams.update(_orig_rc_params)
|
||||
|
||||
|
||||
def axes_style(style=None, rc=None):
|
||||
"""
|
||||
Get the parameters that control the general style of the plots.
|
||||
|
||||
The style parameters control properties like the color of the background and
|
||||
whether a grid is enabled by default. This is accomplished using the
|
||||
matplotlib rcParams system.
|
||||
|
||||
The options are illustrated in the
|
||||
:doc:`aesthetics tutorial <../tutorial/aesthetics>`.
|
||||
|
||||
This function can also be used as a context manager to temporarily
|
||||
alter the global defaults. See :func:`set_theme` or :func:`set_style`
|
||||
to modify the global defaults for all plots.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
style : None, dict, or one of {darkgrid, whitegrid, dark, white, ticks}
|
||||
A dictionary of parameters or the name of a preconfigured style.
|
||||
rc : dict, optional
|
||||
Parameter mappings to override the values in the preset seaborn
|
||||
style dictionaries. This only updates parameters that are
|
||||
considered part of the style definition.
|
||||
|
||||
Examples
|
||||
--------
|
||||
|
||||
.. include:: ../docstrings/axes_style.rst
|
||||
|
||||
"""
|
||||
if style is None:
|
||||
style_dict = {k: mpl.rcParams[k] for k in _style_keys}
|
||||
|
||||
elif isinstance(style, dict):
|
||||
style_dict = style
|
||||
|
||||
else:
|
||||
styles = ["white", "dark", "whitegrid", "darkgrid", "ticks"]
|
||||
if style not in styles:
|
||||
raise ValueError(f"style must be one of {', '.join(styles)}")
|
||||
|
||||
# Define colors here
|
||||
dark_gray = ".15"
|
||||
light_gray = ".8"
|
||||
|
||||
# Common parameters
|
||||
style_dict = {
|
||||
|
||||
"figure.facecolor": "white",
|
||||
"axes.labelcolor": dark_gray,
|
||||
|
||||
"xtick.direction": "out",
|
||||
"ytick.direction": "out",
|
||||
"xtick.color": dark_gray,
|
||||
"ytick.color": dark_gray,
|
||||
|
||||
"axes.axisbelow": True,
|
||||
"grid.linestyle": "-",
|
||||
|
||||
|
||||
"text.color": dark_gray,
|
||||
"font.family": ["sans-serif"],
|
||||
"font.sans-serif": ["Arial", "DejaVu Sans", "Liberation Sans",
|
||||
"Bitstream Vera Sans", "sans-serif"],
|
||||
|
||||
|
||||
"lines.solid_capstyle": "round",
|
||||
"patch.edgecolor": "w",
|
||||
"patch.force_edgecolor": True,
|
||||
|
||||
"image.cmap": "rocket",
|
||||
|
||||
"xtick.top": False,
|
||||
"ytick.right": False,
|
||||
|
||||
}
|
||||
|
||||
# Set grid on or off
|
||||
if "grid" in style:
|
||||
style_dict.update({
|
||||
"axes.grid": True,
|
||||
})
|
||||
else:
|
||||
style_dict.update({
|
||||
"axes.grid": False,
|
||||
})
|
||||
|
||||
# Set the color of the background, spines, and grids
|
||||
if style.startswith("dark"):
|
||||
style_dict.update({
|
||||
|
||||
"axes.facecolor": "#EAEAF2",
|
||||
"axes.edgecolor": "white",
|
||||
"grid.color": "white",
|
||||
|
||||
"axes.spines.left": True,
|
||||
"axes.spines.bottom": True,
|
||||
"axes.spines.right": True,
|
||||
"axes.spines.top": True,
|
||||
|
||||
})
|
||||
|
||||
elif style == "whitegrid":
|
||||
style_dict.update({
|
||||
|
||||
"axes.facecolor": "white",
|
||||
"axes.edgecolor": light_gray,
|
||||
"grid.color": light_gray,
|
||||
|
||||
"axes.spines.left": True,
|
||||
"axes.spines.bottom": True,
|
||||
"axes.spines.right": True,
|
||||
"axes.spines.top": True,
|
||||
|
||||
})
|
||||
|
||||
elif style in ["white", "ticks"]:
|
||||
style_dict.update({
|
||||
|
||||
"axes.facecolor": "white",
|
||||
"axes.edgecolor": dark_gray,
|
||||
"grid.color": light_gray,
|
||||
|
||||
"axes.spines.left": True,
|
||||
"axes.spines.bottom": True,
|
||||
"axes.spines.right": True,
|
||||
"axes.spines.top": True,
|
||||
|
||||
})
|
||||
|
||||
# Show or hide the axes ticks
|
||||
if style == "ticks":
|
||||
style_dict.update({
|
||||
"xtick.bottom": True,
|
||||
"ytick.left": True,
|
||||
})
|
||||
else:
|
||||
style_dict.update({
|
||||
"xtick.bottom": False,
|
||||
"ytick.left": False,
|
||||
})
|
||||
|
||||
# Remove entries that are not defined in the base list of valid keys
|
||||
# This lets us handle matplotlib <=/> 2.0
|
||||
style_dict = {k: v for k, v in style_dict.items() if k in _style_keys}
|
||||
|
||||
# Override these settings with the provided rc dictionary
|
||||
if rc is not None:
|
||||
rc = {k: v for k, v in rc.items() if k in _style_keys}
|
||||
style_dict.update(rc)
|
||||
|
||||
# Wrap in an _AxesStyle object so this can be used in a with statement
|
||||
style_object = _AxesStyle(style_dict)
|
||||
|
||||
return style_object
|
||||
|
||||
|
||||
def set_style(style=None, rc=None):
|
||||
"""
|
||||
Set the parameters that control the general style of the plots.
|
||||
|
||||
The style parameters control properties like the color of the background and
|
||||
whether a grid is enabled by default. This is accomplished using the
|
||||
matplotlib rcParams system.
|
||||
|
||||
The options are illustrated in the
|
||||
:doc:`aesthetics tutorial <../tutorial/aesthetics>`.
|
||||
|
||||
See :func:`axes_style` to get the parameter values.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
style : dict, or one of {darkgrid, whitegrid, dark, white, ticks}
|
||||
A dictionary of parameters or the name of a preconfigured style.
|
||||
rc : dict, optional
|
||||
Parameter mappings to override the values in the preset seaborn
|
||||
style dictionaries. This only updates parameters that are
|
||||
considered part of the style definition.
|
||||
|
||||
Examples
|
||||
--------
|
||||
|
||||
.. include:: ../docstrings/set_style.rst
|
||||
|
||||
"""
|
||||
style_object = axes_style(style, rc)
|
||||
mpl.rcParams.update(style_object)
|
||||
|
||||
|
||||
def plotting_context(context=None, font_scale=1, rc=None):
|
||||
"""
|
||||
Get the parameters that control the scaling of plot elements.
|
||||
|
||||
These parameters correspond to label size, line thickness, etc. For more
|
||||
information, see the :doc:`aesthetics tutorial <../tutorial/aesthetics>`.
|
||||
|
||||
The base context is "notebook", and the other contexts are "paper", "talk",
|
||||
and "poster", which are version of the notebook parameters scaled by different
|
||||
values. Font elements can also be scaled independently of (but relative to)
|
||||
the other values.
|
||||
|
||||
This function can also be used as a context manager to temporarily
|
||||
alter the global defaults. See :func:`set_theme` or :func:`set_context`
|
||||
to modify the global defaults for all plots.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
context : None, dict, or one of {paper, notebook, talk, poster}
|
||||
A dictionary of parameters or the name of a preconfigured set.
|
||||
font_scale : float, optional
|
||||
Separate scaling factor to independently scale the size of the
|
||||
font elements.
|
||||
rc : dict, optional
|
||||
Parameter mappings to override the values in the preset seaborn
|
||||
context dictionaries. This only updates parameters that are
|
||||
considered part of the context definition.
|
||||
|
||||
Examples
|
||||
--------
|
||||
|
||||
.. include:: ../docstrings/plotting_context.rst
|
||||
|
||||
"""
|
||||
if context is None:
|
||||
context_dict = {k: mpl.rcParams[k] for k in _context_keys}
|
||||
|
||||
elif isinstance(context, dict):
|
||||
context_dict = context
|
||||
|
||||
else:
|
||||
|
||||
contexts = ["paper", "notebook", "talk", "poster"]
|
||||
if context not in contexts:
|
||||
raise ValueError(f"context must be in {', '.join(contexts)}")
|
||||
|
||||
# Set up dictionary of default parameters
|
||||
texts_base_context = {
|
||||
|
||||
"font.size": 12,
|
||||
"axes.labelsize": 12,
|
||||
"axes.titlesize": 12,
|
||||
"xtick.labelsize": 11,
|
||||
"ytick.labelsize": 11,
|
||||
"legend.fontsize": 11,
|
||||
"legend.title_fontsize": 12,
|
||||
|
||||
}
|
||||
|
||||
base_context = {
|
||||
|
||||
"axes.linewidth": 1.25,
|
||||
"grid.linewidth": 1,
|
||||
"lines.linewidth": 1.5,
|
||||
"lines.markersize": 6,
|
||||
"patch.linewidth": 1,
|
||||
|
||||
"xtick.major.width": 1.25,
|
||||
"ytick.major.width": 1.25,
|
||||
"xtick.minor.width": 1,
|
||||
"ytick.minor.width": 1,
|
||||
|
||||
"xtick.major.size": 6,
|
||||
"ytick.major.size": 6,
|
||||
"xtick.minor.size": 4,
|
||||
"ytick.minor.size": 4,
|
||||
|
||||
}
|
||||
base_context.update(texts_base_context)
|
||||
|
||||
# Scale all the parameters by the same factor depending on the context
|
||||
scaling = dict(paper=.8, notebook=1, talk=1.5, poster=2)[context]
|
||||
context_dict = {k: v * scaling for k, v in base_context.items()}
|
||||
|
||||
# Now independently scale the fonts
|
||||
font_keys = texts_base_context.keys()
|
||||
font_dict = {k: context_dict[k] * font_scale for k in font_keys}
|
||||
context_dict.update(font_dict)
|
||||
|
||||
# Override these settings with the provided rc dictionary
|
||||
if rc is not None:
|
||||
rc = {k: v for k, v in rc.items() if k in _context_keys}
|
||||
context_dict.update(rc)
|
||||
|
||||
# Wrap in a _PlottingContext object so this can be used in a with statement
|
||||
context_object = _PlottingContext(context_dict)
|
||||
|
||||
return context_object
|
||||
|
||||
|
||||
def set_context(context=None, font_scale=1, rc=None):
|
||||
"""
|
||||
Set the parameters that control the scaling of plot elements.
|
||||
|
||||
These parameters correspond to label size, line thickness, etc.
|
||||
Calling this function modifies the global matplotlib `rcParams`. For more
|
||||
information, see the :doc:`aesthetics tutorial <../tutorial/aesthetics>`.
|
||||
|
||||
The base context is "notebook", and the other contexts are "paper", "talk",
|
||||
and "poster", which are version of the notebook parameters scaled by different
|
||||
values. Font elements can also be scaled independently of (but relative to)
|
||||
the other values.
|
||||
|
||||
See :func:`plotting_context` to get the parameter values.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
context : dict, or one of {paper, notebook, talk, poster}
|
||||
A dictionary of parameters or the name of a preconfigured set.
|
||||
font_scale : float, optional
|
||||
Separate scaling factor to independently scale the size of the
|
||||
font elements.
|
||||
rc : dict, optional
|
||||
Parameter mappings to override the values in the preset seaborn
|
||||
context dictionaries. This only updates parameters that are
|
||||
considered part of the context definition.
|
||||
|
||||
Examples
|
||||
--------
|
||||
|
||||
.. include:: ../docstrings/set_context.rst
|
||||
|
||||
"""
|
||||
context_object = plotting_context(context, font_scale, rc)
|
||||
mpl.rcParams.update(context_object)
|
||||
|
||||
|
||||
class _RCAesthetics(dict):
|
||||
def __enter__(self):
|
||||
rc = mpl.rcParams
|
||||
self._orig = {k: rc[k] for k in self._keys}
|
||||
self._set(self)
|
||||
|
||||
def __exit__(self, exc_type, exc_value, exc_tb):
|
||||
self._set(self._orig)
|
||||
|
||||
def __call__(self, func):
|
||||
@functools.wraps(func)
|
||||
def wrapper(*args, **kwargs):
|
||||
with self:
|
||||
return func(*args, **kwargs)
|
||||
return wrapper
|
||||
|
||||
|
||||
class _AxesStyle(_RCAesthetics):
|
||||
"""Light wrapper on a dict to set style temporarily."""
|
||||
_keys = _style_keys
|
||||
_set = staticmethod(set_style)
|
||||
|
||||
|
||||
class _PlottingContext(_RCAesthetics):
|
||||
"""Light wrapper on a dict to set context temporarily."""
|
||||
_keys = _context_keys
|
||||
_set = staticmethod(set_context)
|
||||
|
||||
|
||||
def set_palette(palette, n_colors=None, desat=None, color_codes=False):
|
||||
"""Set the matplotlib color cycle using a seaborn palette.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
palette : seaborn color palette | matplotlib colormap | hls | husl
|
||||
Palette definition. Should be something :func:`color_palette` can process.
|
||||
n_colors : int
|
||||
Number of colors in the cycle. The default number of colors will depend
|
||||
on the format of ``palette``, see the :func:`color_palette`
|
||||
documentation for more information.
|
||||
desat : float
|
||||
Proportion to desaturate each color by.
|
||||
color_codes : bool
|
||||
If ``True`` and ``palette`` is a seaborn palette, remap the shorthand
|
||||
color codes (e.g. "b", "g", "r", etc.) to the colors from this palette.
|
||||
|
||||
See Also
|
||||
--------
|
||||
color_palette : build a color palette or set the color cycle temporarily
|
||||
in a ``with`` statement.
|
||||
set_context : set parameters to scale plot elements
|
||||
set_style : set the default parameters for figure style
|
||||
|
||||
"""
|
||||
colors = palettes.color_palette(palette, n_colors, desat)
|
||||
cyl = cycler('color', colors)
|
||||
mpl.rcParams['axes.prop_cycle'] = cyl
|
||||
if color_codes:
|
||||
try:
|
||||
palettes.set_color_codes(palette)
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
940
ftline/lib/python3.12/site-packages/seaborn/regression.py
Normal file
940
ftline/lib/python3.12/site-packages/seaborn/regression.py
Normal file
@@ -0,0 +1,940 @@
|
||||
"""Plotting functions for linear models (broadly construed)."""
|
||||
import copy
|
||||
from textwrap import dedent
|
||||
import warnings
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
import matplotlib as mpl
|
||||
import matplotlib.pyplot as plt
|
||||
|
||||
try:
|
||||
import statsmodels
|
||||
assert statsmodels
|
||||
_has_statsmodels = True
|
||||
except ImportError:
|
||||
_has_statsmodels = False
|
||||
|
||||
from . import utils
|
||||
from . import algorithms as algo
|
||||
from .axisgrid import FacetGrid, _facet_docs
|
||||
|
||||
|
||||
__all__ = ["lmplot", "regplot", "residplot"]
|
||||
|
||||
|
||||
class _LinearPlotter:
|
||||
"""Base class for plotting relational data in tidy format.
|
||||
|
||||
To get anything useful done you'll have to inherit from this, but setup
|
||||
code that can be abstracted out should be put here.
|
||||
|
||||
"""
|
||||
def establish_variables(self, data, **kws):
|
||||
"""Extract variables from data or use directly."""
|
||||
self.data = data
|
||||
|
||||
# Validate the inputs
|
||||
any_strings = any([isinstance(v, str) for v in kws.values()])
|
||||
if any_strings and data is None:
|
||||
raise ValueError("Must pass `data` if using named variables.")
|
||||
|
||||
# Set the variables
|
||||
for var, val in kws.items():
|
||||
if isinstance(val, str):
|
||||
vector = data[val]
|
||||
elif isinstance(val, list):
|
||||
vector = np.asarray(val)
|
||||
else:
|
||||
vector = val
|
||||
if vector is not None and vector.shape != (1,):
|
||||
vector = np.squeeze(vector)
|
||||
if np.ndim(vector) > 1:
|
||||
err = "regplot inputs must be 1d"
|
||||
raise ValueError(err)
|
||||
setattr(self, var, vector)
|
||||
|
||||
def dropna(self, *vars):
|
||||
"""Remove observations with missing data."""
|
||||
vals = [getattr(self, var) for var in vars]
|
||||
vals = [v for v in vals if v is not None]
|
||||
not_na = np.all(np.column_stack([pd.notnull(v) for v in vals]), axis=1)
|
||||
for var in vars:
|
||||
val = getattr(self, var)
|
||||
if val is not None:
|
||||
setattr(self, var, val[not_na])
|
||||
|
||||
def plot(self, ax):
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class _RegressionPlotter(_LinearPlotter):
|
||||
"""Plotter for numeric independent variables with regression model.
|
||||
|
||||
This does the computations and drawing for the `regplot` function, and
|
||||
is thus also used indirectly by `lmplot`.
|
||||
"""
|
||||
def __init__(self, x, y, data=None, x_estimator=None, x_bins=None,
|
||||
x_ci="ci", scatter=True, fit_reg=True, ci=95, n_boot=1000,
|
||||
units=None, seed=None, order=1, logistic=False, lowess=False,
|
||||
robust=False, logx=False, x_partial=None, y_partial=None,
|
||||
truncate=False, dropna=True, x_jitter=None, y_jitter=None,
|
||||
color=None, label=None):
|
||||
|
||||
# Set member attributes
|
||||
self.x_estimator = x_estimator
|
||||
self.ci = ci
|
||||
self.x_ci = ci if x_ci == "ci" else x_ci
|
||||
self.n_boot = n_boot
|
||||
self.seed = seed
|
||||
self.scatter = scatter
|
||||
self.fit_reg = fit_reg
|
||||
self.order = order
|
||||
self.logistic = logistic
|
||||
self.lowess = lowess
|
||||
self.robust = robust
|
||||
self.logx = logx
|
||||
self.truncate = truncate
|
||||
self.x_jitter = x_jitter
|
||||
self.y_jitter = y_jitter
|
||||
self.color = color
|
||||
self.label = label
|
||||
|
||||
# Validate the regression options:
|
||||
if sum((order > 1, logistic, robust, lowess, logx)) > 1:
|
||||
raise ValueError("Mutually exclusive regression options.")
|
||||
|
||||
# Extract the data vals from the arguments or passed dataframe
|
||||
self.establish_variables(data, x=x, y=y, units=units,
|
||||
x_partial=x_partial, y_partial=y_partial)
|
||||
|
||||
# Drop null observations
|
||||
if dropna:
|
||||
self.dropna("x", "y", "units", "x_partial", "y_partial")
|
||||
|
||||
# Regress nuisance variables out of the data
|
||||
if self.x_partial is not None:
|
||||
self.x = self.regress_out(self.x, self.x_partial)
|
||||
if self.y_partial is not None:
|
||||
self.y = self.regress_out(self.y, self.y_partial)
|
||||
|
||||
# Possibly bin the predictor variable, which implies a point estimate
|
||||
if x_bins is not None:
|
||||
self.x_estimator = np.mean if x_estimator is None else x_estimator
|
||||
x_discrete, x_bins = self.bin_predictor(x_bins)
|
||||
self.x_discrete = x_discrete
|
||||
else:
|
||||
self.x_discrete = self.x
|
||||
|
||||
# Disable regression in case of singleton inputs
|
||||
if len(self.x) <= 1:
|
||||
self.fit_reg = False
|
||||
|
||||
# Save the range of the x variable for the grid later
|
||||
if self.fit_reg:
|
||||
self.x_range = self.x.min(), self.x.max()
|
||||
|
||||
@property
|
||||
def scatter_data(self):
|
||||
"""Data where each observation is a point."""
|
||||
x_j = self.x_jitter
|
||||
if x_j is None:
|
||||
x = self.x
|
||||
else:
|
||||
x = self.x + np.random.uniform(-x_j, x_j, len(self.x))
|
||||
|
||||
y_j = self.y_jitter
|
||||
if y_j is None:
|
||||
y = self.y
|
||||
else:
|
||||
y = self.y + np.random.uniform(-y_j, y_j, len(self.y))
|
||||
|
||||
return x, y
|
||||
|
||||
@property
|
||||
def estimate_data(self):
|
||||
"""Data with a point estimate and CI for each discrete x value."""
|
||||
x, y = self.x_discrete, self.y
|
||||
vals = sorted(np.unique(x))
|
||||
points, cis = [], []
|
||||
|
||||
for val in vals:
|
||||
|
||||
# Get the point estimate of the y variable
|
||||
_y = y[x == val]
|
||||
est = self.x_estimator(_y)
|
||||
points.append(est)
|
||||
|
||||
# Compute the confidence interval for this estimate
|
||||
if self.x_ci is None:
|
||||
cis.append(None)
|
||||
else:
|
||||
units = None
|
||||
if self.x_ci == "sd":
|
||||
sd = np.std(_y)
|
||||
_ci = est - sd, est + sd
|
||||
else:
|
||||
if self.units is not None:
|
||||
units = self.units[x == val]
|
||||
boots = algo.bootstrap(_y,
|
||||
func=self.x_estimator,
|
||||
n_boot=self.n_boot,
|
||||
units=units,
|
||||
seed=self.seed)
|
||||
_ci = utils.ci(boots, self.x_ci)
|
||||
cis.append(_ci)
|
||||
|
||||
return vals, points, cis
|
||||
|
||||
def _check_statsmodels(self):
|
||||
"""Check whether statsmodels is installed if any boolean options require it."""
|
||||
options = "logistic", "robust", "lowess"
|
||||
err = "`{}=True` requires statsmodels, an optional dependency, to be installed."
|
||||
for option in options:
|
||||
if getattr(self, option) and not _has_statsmodels:
|
||||
raise RuntimeError(err.format(option))
|
||||
|
||||
def fit_regression(self, ax=None, x_range=None, grid=None):
|
||||
"""Fit the regression model."""
|
||||
self._check_statsmodels()
|
||||
|
||||
# Create the grid for the regression
|
||||
if grid is None:
|
||||
if self.truncate:
|
||||
x_min, x_max = self.x_range
|
||||
else:
|
||||
if ax is None:
|
||||
x_min, x_max = x_range
|
||||
else:
|
||||
x_min, x_max = ax.get_xlim()
|
||||
grid = np.linspace(x_min, x_max, 100)
|
||||
ci = self.ci
|
||||
|
||||
# Fit the regression
|
||||
if self.order > 1:
|
||||
yhat, yhat_boots = self.fit_poly(grid, self.order)
|
||||
elif self.logistic:
|
||||
from statsmodels.genmod.generalized_linear_model import GLM
|
||||
from statsmodels.genmod.families import Binomial
|
||||
yhat, yhat_boots = self.fit_statsmodels(grid, GLM,
|
||||
family=Binomial())
|
||||
elif self.lowess:
|
||||
ci = None
|
||||
grid, yhat = self.fit_lowess()
|
||||
elif self.robust:
|
||||
from statsmodels.robust.robust_linear_model import RLM
|
||||
yhat, yhat_boots = self.fit_statsmodels(grid, RLM)
|
||||
elif self.logx:
|
||||
yhat, yhat_boots = self.fit_logx(grid)
|
||||
else:
|
||||
yhat, yhat_boots = self.fit_fast(grid)
|
||||
|
||||
# Compute the confidence interval at each grid point
|
||||
if ci is None:
|
||||
err_bands = None
|
||||
else:
|
||||
err_bands = utils.ci(yhat_boots, ci, axis=0)
|
||||
|
||||
return grid, yhat, err_bands
|
||||
|
||||
def fit_fast(self, grid):
|
||||
"""Low-level regression and prediction using linear algebra."""
|
||||
def reg_func(_x, _y):
|
||||
return np.linalg.pinv(_x).dot(_y)
|
||||
|
||||
X, y = np.c_[np.ones(len(self.x)), self.x], self.y
|
||||
grid = np.c_[np.ones(len(grid)), grid]
|
||||
yhat = grid.dot(reg_func(X, y))
|
||||
if self.ci is None:
|
||||
return yhat, None
|
||||
|
||||
beta_boots = algo.bootstrap(X, y,
|
||||
func=reg_func,
|
||||
n_boot=self.n_boot,
|
||||
units=self.units,
|
||||
seed=self.seed).T
|
||||
yhat_boots = grid.dot(beta_boots).T
|
||||
return yhat, yhat_boots
|
||||
|
||||
def fit_poly(self, grid, order):
|
||||
"""Regression using numpy polyfit for higher-order trends."""
|
||||
def reg_func(_x, _y):
|
||||
return np.polyval(np.polyfit(_x, _y, order), grid)
|
||||
|
||||
x, y = self.x, self.y
|
||||
yhat = reg_func(x, y)
|
||||
if self.ci is None:
|
||||
return yhat, None
|
||||
|
||||
yhat_boots = algo.bootstrap(x, y,
|
||||
func=reg_func,
|
||||
n_boot=self.n_boot,
|
||||
units=self.units,
|
||||
seed=self.seed)
|
||||
return yhat, yhat_boots
|
||||
|
||||
def fit_statsmodels(self, grid, model, **kwargs):
|
||||
"""More general regression function using statsmodels objects."""
|
||||
import statsmodels.tools.sm_exceptions as sme
|
||||
X, y = np.c_[np.ones(len(self.x)), self.x], self.y
|
||||
grid = np.c_[np.ones(len(grid)), grid]
|
||||
|
||||
def reg_func(_x, _y):
|
||||
err_classes = (sme.PerfectSeparationError,)
|
||||
try:
|
||||
with warnings.catch_warnings():
|
||||
if hasattr(sme, "PerfectSeparationWarning"):
|
||||
# statsmodels>=0.14.0
|
||||
warnings.simplefilter("error", sme.PerfectSeparationWarning)
|
||||
err_classes = (*err_classes, sme.PerfectSeparationWarning)
|
||||
yhat = model(_y, _x, **kwargs).fit().predict(grid)
|
||||
except err_classes:
|
||||
yhat = np.empty(len(grid))
|
||||
yhat.fill(np.nan)
|
||||
return yhat
|
||||
|
||||
yhat = reg_func(X, y)
|
||||
if self.ci is None:
|
||||
return yhat, None
|
||||
|
||||
yhat_boots = algo.bootstrap(X, y,
|
||||
func=reg_func,
|
||||
n_boot=self.n_boot,
|
||||
units=self.units,
|
||||
seed=self.seed)
|
||||
return yhat, yhat_boots
|
||||
|
||||
def fit_lowess(self):
|
||||
"""Fit a locally-weighted regression, which returns its own grid."""
|
||||
from statsmodels.nonparametric.smoothers_lowess import lowess
|
||||
grid, yhat = lowess(self.y, self.x).T
|
||||
return grid, yhat
|
||||
|
||||
def fit_logx(self, grid):
|
||||
"""Fit the model in log-space."""
|
||||
X, y = np.c_[np.ones(len(self.x)), self.x], self.y
|
||||
grid = np.c_[np.ones(len(grid)), np.log(grid)]
|
||||
|
||||
def reg_func(_x, _y):
|
||||
_x = np.c_[_x[:, 0], np.log(_x[:, 1])]
|
||||
return np.linalg.pinv(_x).dot(_y)
|
||||
|
||||
yhat = grid.dot(reg_func(X, y))
|
||||
if self.ci is None:
|
||||
return yhat, None
|
||||
|
||||
beta_boots = algo.bootstrap(X, y,
|
||||
func=reg_func,
|
||||
n_boot=self.n_boot,
|
||||
units=self.units,
|
||||
seed=self.seed).T
|
||||
yhat_boots = grid.dot(beta_boots).T
|
||||
return yhat, yhat_boots
|
||||
|
||||
def bin_predictor(self, bins):
|
||||
"""Discretize a predictor by assigning value to closest bin."""
|
||||
x = np.asarray(self.x)
|
||||
if np.isscalar(bins):
|
||||
percentiles = np.linspace(0, 100, bins + 2)[1:-1]
|
||||
bins = np.percentile(x, percentiles)
|
||||
else:
|
||||
bins = np.ravel(bins)
|
||||
|
||||
dist = np.abs(np.subtract.outer(x, bins))
|
||||
x_binned = bins[np.argmin(dist, axis=1)].ravel()
|
||||
|
||||
return x_binned, bins
|
||||
|
||||
def regress_out(self, a, b):
|
||||
"""Regress b from a keeping a's original mean."""
|
||||
a_mean = a.mean()
|
||||
a = a - a_mean
|
||||
b = b - b.mean()
|
||||
b = np.c_[b]
|
||||
a_prime = a - b.dot(np.linalg.pinv(b).dot(a))
|
||||
return np.asarray(a_prime + a_mean).reshape(a.shape)
|
||||
|
||||
def plot(self, ax, scatter_kws, line_kws):
|
||||
"""Draw the full plot."""
|
||||
# Insert the plot label into the correct set of keyword arguments
|
||||
if self.scatter:
|
||||
scatter_kws["label"] = self.label
|
||||
else:
|
||||
line_kws["label"] = self.label
|
||||
|
||||
# Use the current color cycle state as a default
|
||||
if self.color is None:
|
||||
lines, = ax.plot([], [])
|
||||
color = lines.get_color()
|
||||
lines.remove()
|
||||
else:
|
||||
color = self.color
|
||||
|
||||
# Ensure that color is hex to avoid matplotlib weirdness
|
||||
color = mpl.colors.rgb2hex(mpl.colors.colorConverter.to_rgb(color))
|
||||
|
||||
# Let color in keyword arguments override overall plot color
|
||||
scatter_kws.setdefault("color", color)
|
||||
line_kws.setdefault("color", color)
|
||||
|
||||
# Draw the constituent plots
|
||||
if self.scatter:
|
||||
self.scatterplot(ax, scatter_kws)
|
||||
|
||||
if self.fit_reg:
|
||||
self.lineplot(ax, line_kws)
|
||||
|
||||
# Label the axes
|
||||
if hasattr(self.x, "name"):
|
||||
ax.set_xlabel(self.x.name)
|
||||
if hasattr(self.y, "name"):
|
||||
ax.set_ylabel(self.y.name)
|
||||
|
||||
def scatterplot(self, ax, kws):
|
||||
"""Draw the data."""
|
||||
# Treat the line-based markers specially, explicitly setting larger
|
||||
# linewidth than is provided by the seaborn style defaults.
|
||||
# This would ideally be handled better in matplotlib (i.e., distinguish
|
||||
# between edgewidth for solid glyphs and linewidth for line glyphs
|
||||
# but this should do for now.
|
||||
line_markers = ["1", "2", "3", "4", "+", "x", "|", "_"]
|
||||
if self.x_estimator is None:
|
||||
if "marker" in kws and kws["marker"] in line_markers:
|
||||
lw = mpl.rcParams["lines.linewidth"]
|
||||
else:
|
||||
lw = mpl.rcParams["lines.markeredgewidth"]
|
||||
kws.setdefault("linewidths", lw)
|
||||
|
||||
if not hasattr(kws['color'], 'shape') or kws['color'].shape[1] < 4:
|
||||
kws.setdefault("alpha", .8)
|
||||
|
||||
x, y = self.scatter_data
|
||||
ax.scatter(x, y, **kws)
|
||||
else:
|
||||
# TODO abstraction
|
||||
ci_kws = {"color": kws["color"]}
|
||||
if "alpha" in kws:
|
||||
ci_kws["alpha"] = kws["alpha"]
|
||||
ci_kws["linewidth"] = mpl.rcParams["lines.linewidth"] * 1.75
|
||||
kws.setdefault("s", 50)
|
||||
|
||||
xs, ys, cis = self.estimate_data
|
||||
if [ci for ci in cis if ci is not None]:
|
||||
for x, ci in zip(xs, cis):
|
||||
ax.plot([x, x], ci, **ci_kws)
|
||||
ax.scatter(xs, ys, **kws)
|
||||
|
||||
def lineplot(self, ax, kws):
|
||||
"""Draw the model."""
|
||||
# Fit the regression model
|
||||
grid, yhat, err_bands = self.fit_regression(ax)
|
||||
edges = grid[0], grid[-1]
|
||||
|
||||
# Get set default aesthetics
|
||||
fill_color = kws["color"]
|
||||
lw = kws.pop("lw", mpl.rcParams["lines.linewidth"] * 1.5)
|
||||
kws.setdefault("linewidth", lw)
|
||||
|
||||
# Draw the regression line and confidence interval
|
||||
line, = ax.plot(grid, yhat, **kws)
|
||||
if not self.truncate:
|
||||
line.sticky_edges.x[:] = edges # Prevent mpl from adding margin
|
||||
if err_bands is not None:
|
||||
ax.fill_between(grid, *err_bands, facecolor=fill_color, alpha=.15)
|
||||
|
||||
|
||||
_regression_docs = dict(
|
||||
|
||||
model_api=dedent("""\
|
||||
There are a number of mutually exclusive options for estimating the
|
||||
regression model. See the :ref:`tutorial <regression_tutorial>` for more
|
||||
information.\
|
||||
"""),
|
||||
regplot_vs_lmplot=dedent("""\
|
||||
The :func:`regplot` and :func:`lmplot` functions are closely related, but
|
||||
the former is an axes-level function while the latter is a figure-level
|
||||
function that combines :func:`regplot` and :class:`FacetGrid`.\
|
||||
"""),
|
||||
x_estimator=dedent("""\
|
||||
x_estimator : callable that maps vector -> scalar, optional
|
||||
Apply this function to each unique value of ``x`` and plot the
|
||||
resulting estimate. This is useful when ``x`` is a discrete variable.
|
||||
If ``x_ci`` is given, this estimate will be bootstrapped and a
|
||||
confidence interval will be drawn.\
|
||||
"""),
|
||||
x_bins=dedent("""\
|
||||
x_bins : int or vector, optional
|
||||
Bin the ``x`` variable into discrete bins and then estimate the central
|
||||
tendency and a confidence interval. This binning only influences how
|
||||
the scatterplot is drawn; the regression is still fit to the original
|
||||
data. This parameter is interpreted either as the number of
|
||||
evenly-sized (not necessary spaced) bins or the positions of the bin
|
||||
centers. When this parameter is used, it implies that the default of
|
||||
``x_estimator`` is ``numpy.mean``.\
|
||||
"""),
|
||||
x_ci=dedent("""\
|
||||
x_ci : "ci", "sd", int in [0, 100] or None, optional
|
||||
Size of the confidence interval used when plotting a central tendency
|
||||
for discrete values of ``x``. If ``"ci"``, defer to the value of the
|
||||
``ci`` parameter. If ``"sd"``, skip bootstrapping and show the
|
||||
standard deviation of the observations in each bin.\
|
||||
"""),
|
||||
scatter=dedent("""\
|
||||
scatter : bool, optional
|
||||
If ``True``, draw a scatterplot with the underlying observations (or
|
||||
the ``x_estimator`` values).\
|
||||
"""),
|
||||
fit_reg=dedent("""\
|
||||
fit_reg : bool, optional
|
||||
If ``True``, estimate and plot a regression model relating the ``x``
|
||||
and ``y`` variables.\
|
||||
"""),
|
||||
ci=dedent("""\
|
||||
ci : int in [0, 100] or None, optional
|
||||
Size of the confidence interval for the regression estimate. This will
|
||||
be drawn using translucent bands around the regression line. The
|
||||
confidence interval is estimated using a bootstrap; for large
|
||||
datasets, it may be advisable to avoid that computation by setting
|
||||
this parameter to None.\
|
||||
"""),
|
||||
n_boot=dedent("""\
|
||||
n_boot : int, optional
|
||||
Number of bootstrap resamples used to estimate the ``ci``. The default
|
||||
value attempts to balance time and stability; you may want to increase
|
||||
this value for "final" versions of plots.\
|
||||
"""),
|
||||
units=dedent("""\
|
||||
units : variable name in ``data``, optional
|
||||
If the ``x`` and ``y`` observations are nested within sampling units,
|
||||
those can be specified here. This will be taken into account when
|
||||
computing the confidence intervals by performing a multilevel bootstrap
|
||||
that resamples both units and observations (within unit). This does not
|
||||
otherwise influence how the regression is estimated or drawn.\
|
||||
"""),
|
||||
seed=dedent("""\
|
||||
seed : int, numpy.random.Generator, or numpy.random.RandomState, optional
|
||||
Seed or random number generator for reproducible bootstrapping.\
|
||||
"""),
|
||||
order=dedent("""\
|
||||
order : int, optional
|
||||
If ``order`` is greater than 1, use ``numpy.polyfit`` to estimate a
|
||||
polynomial regression.\
|
||||
"""),
|
||||
logistic=dedent("""\
|
||||
logistic : bool, optional
|
||||
If ``True``, assume that ``y`` is a binary variable and use
|
||||
``statsmodels`` to estimate a logistic regression model. Note that this
|
||||
is substantially more computationally intensive than linear regression,
|
||||
so you may wish to decrease the number of bootstrap resamples
|
||||
(``n_boot``) or set ``ci`` to None.\
|
||||
"""),
|
||||
lowess=dedent("""\
|
||||
lowess : bool, optional
|
||||
If ``True``, use ``statsmodels`` to estimate a nonparametric lowess
|
||||
model (locally weighted linear regression). Note that confidence
|
||||
intervals cannot currently be drawn for this kind of model.\
|
||||
"""),
|
||||
robust=dedent("""\
|
||||
robust : bool, optional
|
||||
If ``True``, use ``statsmodels`` to estimate a robust regression. This
|
||||
will de-weight outliers. Note that this is substantially more
|
||||
computationally intensive than standard linear regression, so you may
|
||||
wish to decrease the number of bootstrap resamples (``n_boot``) or set
|
||||
``ci`` to None.\
|
||||
"""),
|
||||
logx=dedent("""\
|
||||
logx : bool, optional
|
||||
If ``True``, estimate a linear regression of the form y ~ log(x), but
|
||||
plot the scatterplot and regression model in the input space. Note that
|
||||
``x`` must be positive for this to work.\
|
||||
"""),
|
||||
xy_partial=dedent("""\
|
||||
{x,y}_partial : strings in ``data`` or matrices
|
||||
Confounding variables to regress out of the ``x`` or ``y`` variables
|
||||
before plotting.\
|
||||
"""),
|
||||
truncate=dedent("""\
|
||||
truncate : bool, optional
|
||||
If ``True``, the regression line is bounded by the data limits. If
|
||||
``False``, it extends to the ``x`` axis limits.
|
||||
"""),
|
||||
xy_jitter=dedent("""\
|
||||
{x,y}_jitter : floats, optional
|
||||
Add uniform random noise of this size to either the ``x`` or ``y``
|
||||
variables. The noise is added to a copy of the data after fitting the
|
||||
regression, and only influences the look of the scatterplot. This can
|
||||
be helpful when plotting variables that take discrete values.\
|
||||
"""),
|
||||
scatter_line_kws=dedent("""\
|
||||
{scatter,line}_kws : dictionaries
|
||||
Additional keyword arguments to pass to ``plt.scatter`` and
|
||||
``plt.plot``.\
|
||||
"""),
|
||||
)
|
||||
_regression_docs.update(_facet_docs)
|
||||
|
||||
|
||||
def lmplot(
|
||||
data, *,
|
||||
x=None, y=None, hue=None, col=None, row=None,
|
||||
palette=None, col_wrap=None, height=5, aspect=1, markers="o",
|
||||
sharex=None, sharey=None, hue_order=None, col_order=None, row_order=None,
|
||||
legend=True, legend_out=None, x_estimator=None, x_bins=None,
|
||||
x_ci="ci", scatter=True, fit_reg=True, ci=95, n_boot=1000,
|
||||
units=None, seed=None, order=1, logistic=False, lowess=False,
|
||||
robust=False, logx=False, x_partial=None, y_partial=None,
|
||||
truncate=True, x_jitter=None, y_jitter=None, scatter_kws=None,
|
||||
line_kws=None, facet_kws=None,
|
||||
):
|
||||
|
||||
if facet_kws is None:
|
||||
facet_kws = {}
|
||||
|
||||
def facet_kw_deprecation(key, val):
|
||||
msg = (
|
||||
f"{key} is deprecated from the `lmplot` function signature. "
|
||||
"Please update your code to pass it using `facet_kws`."
|
||||
)
|
||||
if val is not None:
|
||||
warnings.warn(msg, UserWarning)
|
||||
facet_kws[key] = val
|
||||
|
||||
facet_kw_deprecation("sharex", sharex)
|
||||
facet_kw_deprecation("sharey", sharey)
|
||||
facet_kw_deprecation("legend_out", legend_out)
|
||||
|
||||
if data is None:
|
||||
raise TypeError("Missing required keyword argument `data`.")
|
||||
|
||||
# Reduce the dataframe to only needed columns
|
||||
need_cols = [x, y, hue, col, row, units, x_partial, y_partial]
|
||||
cols = np.unique([a for a in need_cols if a is not None]).tolist()
|
||||
data = data[cols]
|
||||
|
||||
# Initialize the grid
|
||||
facets = FacetGrid(
|
||||
data, row=row, col=col, hue=hue,
|
||||
palette=palette,
|
||||
row_order=row_order, col_order=col_order, hue_order=hue_order,
|
||||
height=height, aspect=aspect, col_wrap=col_wrap,
|
||||
**facet_kws,
|
||||
)
|
||||
|
||||
# Add the markers here as FacetGrid has figured out how many levels of the
|
||||
# hue variable are needed and we don't want to duplicate that process
|
||||
if facets.hue_names is None:
|
||||
n_markers = 1
|
||||
else:
|
||||
n_markers = len(facets.hue_names)
|
||||
if not isinstance(markers, list):
|
||||
markers = [markers] * n_markers
|
||||
if len(markers) != n_markers:
|
||||
raise ValueError("markers must be a singleton or a list of markers "
|
||||
"for each level of the hue variable")
|
||||
facets.hue_kws = {"marker": markers}
|
||||
|
||||
def update_datalim(data, x, y, ax, **kws):
|
||||
xys = data[[x, y]].to_numpy().astype(float)
|
||||
ax.update_datalim(xys, updatey=False)
|
||||
ax.autoscale_view(scaley=False)
|
||||
|
||||
facets.map_dataframe(update_datalim, x=x, y=y)
|
||||
|
||||
# Draw the regression plot on each facet
|
||||
regplot_kws = dict(
|
||||
x_estimator=x_estimator, x_bins=x_bins, x_ci=x_ci,
|
||||
scatter=scatter, fit_reg=fit_reg, ci=ci, n_boot=n_boot, units=units,
|
||||
seed=seed, order=order, logistic=logistic, lowess=lowess,
|
||||
robust=robust, logx=logx, x_partial=x_partial, y_partial=y_partial,
|
||||
truncate=truncate, x_jitter=x_jitter, y_jitter=y_jitter,
|
||||
scatter_kws=scatter_kws, line_kws=line_kws,
|
||||
)
|
||||
facets.map_dataframe(regplot, x=x, y=y, **regplot_kws)
|
||||
facets.set_axis_labels(x, y)
|
||||
|
||||
# Add a legend
|
||||
if legend and (hue is not None) and (hue not in [col, row]):
|
||||
facets.add_legend()
|
||||
return facets
|
||||
|
||||
|
||||
lmplot.__doc__ = dedent("""\
|
||||
Plot data and regression model fits across a FacetGrid.
|
||||
|
||||
This function combines :func:`regplot` and :class:`FacetGrid`. It is
|
||||
intended as a convenient interface to fit regression models across
|
||||
conditional subsets of a dataset.
|
||||
|
||||
When thinking about how to assign variables to different facets, a general
|
||||
rule is that it makes sense to use ``hue`` for the most important
|
||||
comparison, followed by ``col`` and ``row``. However, always think about
|
||||
your particular dataset and the goals of the visualization you are
|
||||
creating.
|
||||
|
||||
{model_api}
|
||||
|
||||
The parameters to this function span most of the options in
|
||||
:class:`FacetGrid`, although there may be occasional cases where you will
|
||||
want to use that class and :func:`regplot` directly.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
{data}
|
||||
x, y : strings, optional
|
||||
Input variables; these should be column names in ``data``.
|
||||
hue, col, row : strings
|
||||
Variables that define subsets of the data, which will be drawn on
|
||||
separate facets in the grid. See the ``*_order`` parameters to control
|
||||
the order of levels of this variable.
|
||||
{palette}
|
||||
{col_wrap}
|
||||
{height}
|
||||
{aspect}
|
||||
markers : matplotlib marker code or list of marker codes, optional
|
||||
Markers for the scatterplot. If a list, each marker in the list will be
|
||||
used for each level of the ``hue`` variable.
|
||||
{share_xy}
|
||||
|
||||
.. deprecated:: 0.12.0
|
||||
Pass using the `facet_kws` dictionary.
|
||||
|
||||
{{hue,col,row}}_order : lists, optional
|
||||
Order for the levels of the faceting variables. By default, this will
|
||||
be the order that the levels appear in ``data`` or, if the variables
|
||||
are pandas categoricals, the category order.
|
||||
legend : bool, optional
|
||||
If ``True`` and there is a ``hue`` variable, add a legend.
|
||||
{legend_out}
|
||||
|
||||
.. deprecated:: 0.12.0
|
||||
Pass using the `facet_kws` dictionary.
|
||||
|
||||
{x_estimator}
|
||||
{x_bins}
|
||||
{x_ci}
|
||||
{scatter}
|
||||
{fit_reg}
|
||||
{ci}
|
||||
{n_boot}
|
||||
{units}
|
||||
{seed}
|
||||
{order}
|
||||
{logistic}
|
||||
{lowess}
|
||||
{robust}
|
||||
{logx}
|
||||
{xy_partial}
|
||||
{truncate}
|
||||
{xy_jitter}
|
||||
{scatter_line_kws}
|
||||
facet_kws : dict
|
||||
Dictionary of keyword arguments for :class:`FacetGrid`.
|
||||
|
||||
See Also
|
||||
--------
|
||||
regplot : Plot data and a conditional model fit.
|
||||
FacetGrid : Subplot grid for plotting conditional relationships.
|
||||
pairplot : Combine :func:`regplot` and :class:`PairGrid` (when used with
|
||||
``kind="reg"``).
|
||||
|
||||
Notes
|
||||
-----
|
||||
|
||||
{regplot_vs_lmplot}
|
||||
|
||||
Examples
|
||||
--------
|
||||
|
||||
.. include:: ../docstrings/lmplot.rst
|
||||
|
||||
""").format(**_regression_docs)
|
||||
|
||||
|
||||
def regplot(
|
||||
data=None, *, x=None, y=None,
|
||||
x_estimator=None, x_bins=None, x_ci="ci",
|
||||
scatter=True, fit_reg=True, ci=95, n_boot=1000, units=None,
|
||||
seed=None, order=1, logistic=False, lowess=False, robust=False,
|
||||
logx=False, x_partial=None, y_partial=None,
|
||||
truncate=True, dropna=True, x_jitter=None, y_jitter=None,
|
||||
label=None, color=None, marker="o",
|
||||
scatter_kws=None, line_kws=None, ax=None
|
||||
):
|
||||
|
||||
plotter = _RegressionPlotter(x, y, data, x_estimator, x_bins, x_ci,
|
||||
scatter, fit_reg, ci, n_boot, units, seed,
|
||||
order, logistic, lowess, robust, logx,
|
||||
x_partial, y_partial, truncate, dropna,
|
||||
x_jitter, y_jitter, color, label)
|
||||
|
||||
if ax is None:
|
||||
ax = plt.gca()
|
||||
|
||||
scatter_kws = {} if scatter_kws is None else copy.copy(scatter_kws)
|
||||
scatter_kws["marker"] = marker
|
||||
line_kws = {} if line_kws is None else copy.copy(line_kws)
|
||||
plotter.plot(ax, scatter_kws, line_kws)
|
||||
return ax
|
||||
|
||||
|
||||
regplot.__doc__ = dedent("""\
|
||||
Plot data and a linear regression model fit.
|
||||
|
||||
{model_api}
|
||||
|
||||
Parameters
|
||||
----------
|
||||
x, y: string, series, or vector array
|
||||
Input variables. If strings, these should correspond with column names
|
||||
in ``data``. When pandas objects are used, axes will be labeled with
|
||||
the series name.
|
||||
{data}
|
||||
{x_estimator}
|
||||
{x_bins}
|
||||
{x_ci}
|
||||
{scatter}
|
||||
{fit_reg}
|
||||
{ci}
|
||||
{n_boot}
|
||||
{units}
|
||||
{seed}
|
||||
{order}
|
||||
{logistic}
|
||||
{lowess}
|
||||
{robust}
|
||||
{logx}
|
||||
{xy_partial}
|
||||
{truncate}
|
||||
{xy_jitter}
|
||||
label : string
|
||||
Label to apply to either the scatterplot or regression line (if
|
||||
``scatter`` is ``False``) for use in a legend.
|
||||
color : matplotlib color
|
||||
Color to apply to all plot elements; will be superseded by colors
|
||||
passed in ``scatter_kws`` or ``line_kws``.
|
||||
marker : matplotlib marker code
|
||||
Marker to use for the scatterplot glyphs.
|
||||
{scatter_line_kws}
|
||||
ax : matplotlib Axes, optional
|
||||
Axes object to draw the plot onto, otherwise uses the current Axes.
|
||||
|
||||
Returns
|
||||
-------
|
||||
ax : matplotlib Axes
|
||||
The Axes object containing the plot.
|
||||
|
||||
See Also
|
||||
--------
|
||||
lmplot : Combine :func:`regplot` and :class:`FacetGrid` to plot multiple
|
||||
linear relationships in a dataset.
|
||||
jointplot : Combine :func:`regplot` and :class:`JointGrid` (when used with
|
||||
``kind="reg"``).
|
||||
pairplot : Combine :func:`regplot` and :class:`PairGrid` (when used with
|
||||
``kind="reg"``).
|
||||
residplot : Plot the residuals of a linear regression model.
|
||||
|
||||
Notes
|
||||
-----
|
||||
|
||||
{regplot_vs_lmplot}
|
||||
|
||||
|
||||
It's also easy to combine :func:`regplot` and :class:`JointGrid` or
|
||||
:class:`PairGrid` through the :func:`jointplot` and :func:`pairplot`
|
||||
functions, although these do not directly accept all of :func:`regplot`'s
|
||||
parameters.
|
||||
|
||||
Examples
|
||||
--------
|
||||
|
||||
.. include:: ../docstrings/regplot.rst
|
||||
|
||||
""").format(**_regression_docs)
|
||||
|
||||
|
||||
def residplot(
|
||||
data=None, *, x=None, y=None,
|
||||
x_partial=None, y_partial=None, lowess=False,
|
||||
order=1, robust=False, dropna=True, label=None, color=None,
|
||||
scatter_kws=None, line_kws=None, ax=None
|
||||
):
|
||||
"""Plot the residuals of a linear regression.
|
||||
|
||||
This function will regress y on x (possibly as a robust or polynomial
|
||||
regression) and then draw a scatterplot of the residuals. You can
|
||||
optionally fit a lowess smoother to the residual plot, which can
|
||||
help in determining if there is structure to the residuals.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
data : DataFrame, optional
|
||||
DataFrame to use if `x` and `y` are column names.
|
||||
x : vector or string
|
||||
Data or column name in `data` for the predictor variable.
|
||||
y : vector or string
|
||||
Data or column name in `data` for the response variable.
|
||||
{x, y}_partial : vectors or string(s) , optional
|
||||
These variables are treated as confounding and are removed from
|
||||
the `x` or `y` variables before plotting.
|
||||
lowess : boolean, optional
|
||||
Fit a lowess smoother to the residual scatterplot.
|
||||
order : int, optional
|
||||
Order of the polynomial to fit when calculating the residuals.
|
||||
robust : boolean, optional
|
||||
Fit a robust linear regression when calculating the residuals.
|
||||
dropna : boolean, optional
|
||||
If True, ignore observations with missing data when fitting and
|
||||
plotting.
|
||||
label : string, optional
|
||||
Label that will be used in any plot legends.
|
||||
color : matplotlib color, optional
|
||||
Color to use for all elements of the plot.
|
||||
{scatter, line}_kws : dictionaries, optional
|
||||
Additional keyword arguments passed to scatter() and plot() for drawing
|
||||
the components of the plot.
|
||||
ax : matplotlib axis, optional
|
||||
Plot into this axis, otherwise grab the current axis or make a new
|
||||
one if not existing.
|
||||
|
||||
Returns
|
||||
-------
|
||||
ax: matplotlib axes
|
||||
Axes with the regression plot.
|
||||
|
||||
See Also
|
||||
--------
|
||||
regplot : Plot a simple linear regression model.
|
||||
jointplot : Draw a :func:`residplot` with univariate marginal distributions
|
||||
(when used with ``kind="resid"``).
|
||||
|
||||
Examples
|
||||
--------
|
||||
|
||||
.. include:: ../docstrings/residplot.rst
|
||||
|
||||
"""
|
||||
plotter = _RegressionPlotter(x, y, data, ci=None,
|
||||
order=order, robust=robust,
|
||||
x_partial=x_partial, y_partial=y_partial,
|
||||
dropna=dropna, color=color, label=label)
|
||||
|
||||
if ax is None:
|
||||
ax = plt.gca()
|
||||
|
||||
# Calculate the residual from a linear regression
|
||||
_, yhat, _ = plotter.fit_regression(grid=plotter.x)
|
||||
plotter.y = plotter.y - yhat
|
||||
|
||||
# Set the regression option on the plotter
|
||||
if lowess:
|
||||
plotter.lowess = True
|
||||
else:
|
||||
plotter.fit_reg = False
|
||||
|
||||
# Plot a horizontal line at 0
|
||||
ax.axhline(0, ls=":", c=".2")
|
||||
|
||||
# Draw the scatterplot
|
||||
scatter_kws = {} if scatter_kws is None else scatter_kws.copy()
|
||||
line_kws = {} if line_kws is None else line_kws.copy()
|
||||
plotter.plot(ax, scatter_kws, line_kws)
|
||||
return ax
|
||||
982
ftline/lib/python3.12/site-packages/seaborn/relational.py
Normal file
982
ftline/lib/python3.12/site-packages/seaborn/relational.py
Normal file
@@ -0,0 +1,982 @@
|
||||
from functools import partial
|
||||
import warnings
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
import matplotlib as mpl
|
||||
import matplotlib.pyplot as plt
|
||||
from matplotlib.cbook import normalize_kwargs
|
||||
|
||||
from ._base import (
|
||||
VectorPlotter,
|
||||
)
|
||||
from .utils import (
|
||||
adjust_legend_subtitles,
|
||||
_default_color,
|
||||
_deprecate_ci,
|
||||
_get_transform_functions,
|
||||
_scatter_legend_artist,
|
||||
)
|
||||
from ._compat import groupby_apply_include_groups
|
||||
from ._statistics import EstimateAggregator, WeightedAggregator
|
||||
from .axisgrid import FacetGrid, _facet_docs
|
||||
from ._docstrings import DocstringComponents, _core_docs
|
||||
|
||||
|
||||
__all__ = ["relplot", "scatterplot", "lineplot"]
|
||||
|
||||
|
||||
_relational_narrative = DocstringComponents(dict(
|
||||
|
||||
# --- Introductory prose
|
||||
main_api="""
|
||||
The relationship between `x` and `y` can be shown for different subsets
|
||||
of the data using the `hue`, `size`, and `style` parameters. These
|
||||
parameters control what visual semantics are used to identify the different
|
||||
subsets. It is possible to show up to three dimensions independently by
|
||||
using all three semantic types, but this style of plot can be hard to
|
||||
interpret and is often ineffective. Using redundant semantics (i.e. both
|
||||
`hue` and `style` for the same variable) can be helpful for making
|
||||
graphics more accessible.
|
||||
|
||||
See the :ref:`tutorial <relational_tutorial>` for more information.
|
||||
""",
|
||||
|
||||
relational_semantic="""
|
||||
The default treatment of the `hue` (and to a lesser extent, `size`)
|
||||
semantic, if present, depends on whether the variable is inferred to
|
||||
represent "numeric" or "categorical" data. In particular, numeric variables
|
||||
are represented with a sequential colormap by default, and the legend
|
||||
entries show regular "ticks" with values that may or may not exist in the
|
||||
data. This behavior can be controlled through various parameters, as
|
||||
described and illustrated below.
|
||||
""",
|
||||
))
|
||||
|
||||
_relational_docs = dict(
|
||||
|
||||
# --- Shared function parameters
|
||||
data_vars="""
|
||||
x, y : names of variables in `data` or vector data
|
||||
Input data variables; must be numeric. Can pass data directly or
|
||||
reference columns in `data`.
|
||||
""",
|
||||
data="""
|
||||
data : DataFrame, array, or list of arrays
|
||||
Input data structure. If `x` and `y` are specified as names, this
|
||||
should be a "long-form" DataFrame containing those columns. Otherwise
|
||||
it is treated as "wide-form" data and grouping variables are ignored.
|
||||
See the examples for the various ways this parameter can be specified
|
||||
and the different effects of each.
|
||||
""",
|
||||
palette="""
|
||||
palette : string, list, dict, or matplotlib colormap
|
||||
An object that determines how colors are chosen when `hue` is used.
|
||||
It can be the name of a seaborn palette or matplotlib colormap, a list
|
||||
of colors (anything matplotlib understands), a dict mapping levels
|
||||
of the `hue` variable to colors, or a matplotlib colormap object.
|
||||
""",
|
||||
hue_order="""
|
||||
hue_order : list
|
||||
Specified order for the appearance of the `hue` variable levels,
|
||||
otherwise they are determined from the data. Not relevant when the
|
||||
`hue` variable is numeric.
|
||||
""",
|
||||
hue_norm="""
|
||||
hue_norm : tuple or :class:`matplotlib.colors.Normalize` object
|
||||
Normalization in data units for colormap applied to the `hue`
|
||||
variable when it is numeric. Not relevant if `hue` is categorical.
|
||||
""",
|
||||
sizes="""
|
||||
sizes : list, dict, or tuple
|
||||
An object that determines how sizes are chosen when `size` is used.
|
||||
List or dict arguments should provide a size for each unique data value,
|
||||
which forces a categorical interpretation. The argument may also be a
|
||||
min, max tuple.
|
||||
""",
|
||||
size_order="""
|
||||
size_order : list
|
||||
Specified order for appearance of the `size` variable levels,
|
||||
otherwise they are determined from the data. Not relevant when the
|
||||
`size` variable is numeric.
|
||||
""",
|
||||
size_norm="""
|
||||
size_norm : tuple or Normalize object
|
||||
Normalization in data units for scaling plot objects when the
|
||||
`size` variable is numeric.
|
||||
""",
|
||||
dashes="""
|
||||
dashes : boolean, list, or dictionary
|
||||
Object determining how to draw the lines for different levels of the
|
||||
`style` variable. Setting to `True` will use default dash codes, or
|
||||
you can pass a list of dash codes or a dictionary mapping levels of the
|
||||
`style` variable to dash codes. Setting to `False` will use solid
|
||||
lines for all subsets. Dashes are specified as in matplotlib: a tuple
|
||||
of `(segment, gap)` lengths, or an empty string to draw a solid line.
|
||||
""",
|
||||
markers="""
|
||||
markers : boolean, list, or dictionary
|
||||
Object determining how to draw the markers for different levels of the
|
||||
`style` variable. Setting to `True` will use default markers, or
|
||||
you can pass a list of markers or a dictionary mapping levels of the
|
||||
`style` variable to markers. Setting to `False` will draw
|
||||
marker-less lines. Markers are specified as in matplotlib.
|
||||
""",
|
||||
style_order="""
|
||||
style_order : list
|
||||
Specified order for appearance of the `style` variable levels
|
||||
otherwise they are determined from the data. Not relevant when the
|
||||
`style` variable is numeric.
|
||||
""",
|
||||
units="""
|
||||
units : vector or key in `data`
|
||||
Grouping variable identifying sampling units. When used, a separate
|
||||
line will be drawn for each unit with appropriate semantics, but no
|
||||
legend entry will be added. Useful for showing distribution of
|
||||
experimental replicates when exact identities are not needed.
|
||||
""",
|
||||
estimator="""
|
||||
estimator : name of pandas method or callable or None
|
||||
Method for aggregating across multiple observations of the `y`
|
||||
variable at the same `x` level. If `None`, all observations will
|
||||
be drawn.
|
||||
""",
|
||||
ci="""
|
||||
ci : int or "sd" or None
|
||||
Size of the confidence interval to draw when aggregating.
|
||||
|
||||
.. deprecated:: 0.12.0
|
||||
Use the new `errorbar` parameter for more flexibility.
|
||||
|
||||
""",
|
||||
n_boot="""
|
||||
n_boot : int
|
||||
Number of bootstraps to use for computing the confidence interval.
|
||||
""",
|
||||
seed="""
|
||||
seed : int, numpy.random.Generator, or numpy.random.RandomState
|
||||
Seed or random number generator for reproducible bootstrapping.
|
||||
""",
|
||||
legend="""
|
||||
legend : "auto", "brief", "full", or False
|
||||
How to draw the legend. If "brief", numeric `hue` and `size`
|
||||
variables will be represented with a sample of evenly spaced values.
|
||||
If "full", every group will get an entry in the legend. If "auto",
|
||||
choose between brief or full representation based on number of levels.
|
||||
If `False`, no legend data is added and no legend is drawn.
|
||||
""",
|
||||
ax_in="""
|
||||
ax : matplotlib Axes
|
||||
Axes object to draw the plot onto, otherwise uses the current Axes.
|
||||
""",
|
||||
ax_out="""
|
||||
ax : matplotlib Axes
|
||||
Returns the Axes object with the plot drawn onto it.
|
||||
""",
|
||||
|
||||
)
|
||||
|
||||
|
||||
_param_docs = DocstringComponents.from_nested_components(
|
||||
core=_core_docs["params"],
|
||||
facets=DocstringComponents(_facet_docs),
|
||||
rel=DocstringComponents(_relational_docs),
|
||||
stat=DocstringComponents.from_function_params(EstimateAggregator.__init__),
|
||||
)
|
||||
|
||||
|
||||
class _RelationalPlotter(VectorPlotter):
|
||||
|
||||
wide_structure = {
|
||||
"x": "@index", "y": "@values", "hue": "@columns", "style": "@columns",
|
||||
}
|
||||
|
||||
# TODO where best to define default parameters?
|
||||
sort = True
|
||||
|
||||
|
||||
class _LinePlotter(_RelationalPlotter):
|
||||
|
||||
_legend_attributes = ["color", "linewidth", "marker", "dashes"]
|
||||
|
||||
def __init__(
|
||||
self, *,
|
||||
data=None, variables={},
|
||||
estimator=None, n_boot=None, seed=None, errorbar=None,
|
||||
sort=True, orient="x", err_style=None, err_kws=None, legend=None
|
||||
):
|
||||
|
||||
# TODO this is messy, we want the mapping to be agnostic about
|
||||
# the kind of plot to draw, but for the time being we need to set
|
||||
# this information so the SizeMapping can use it
|
||||
self._default_size_range = (
|
||||
np.r_[.5, 2] * mpl.rcParams["lines.linewidth"]
|
||||
)
|
||||
|
||||
super().__init__(data=data, variables=variables)
|
||||
|
||||
self.estimator = estimator
|
||||
self.errorbar = errorbar
|
||||
self.n_boot = n_boot
|
||||
self.seed = seed
|
||||
self.sort = sort
|
||||
self.orient = orient
|
||||
self.err_style = err_style
|
||||
self.err_kws = {} if err_kws is None else err_kws
|
||||
|
||||
self.legend = legend
|
||||
|
||||
def plot(self, ax, kws):
|
||||
"""Draw the plot onto an axes, passing matplotlib kwargs."""
|
||||
|
||||
# Draw a test plot, using the passed in kwargs. The goal here is to
|
||||
# honor both (a) the current state of the plot cycler and (b) the
|
||||
# specified kwargs on all the lines we will draw, overriding when
|
||||
# relevant with the data semantics. Note that we won't cycle
|
||||
# internally; in other words, if `hue` is not used, all elements will
|
||||
# have the same color, but they will have the color that you would have
|
||||
# gotten from the corresponding matplotlib function, and calling the
|
||||
# function will advance the axes property cycle.
|
||||
|
||||
kws = normalize_kwargs(kws, mpl.lines.Line2D)
|
||||
kws.setdefault("markeredgewidth", 0.75)
|
||||
kws.setdefault("markeredgecolor", "w")
|
||||
|
||||
# Set default error kwargs
|
||||
err_kws = self.err_kws.copy()
|
||||
if self.err_style == "band":
|
||||
err_kws.setdefault("alpha", .2)
|
||||
elif self.err_style == "bars":
|
||||
pass
|
||||
elif self.err_style is not None:
|
||||
err = "`err_style` must be 'band' or 'bars', not {}"
|
||||
raise ValueError(err.format(self.err_style))
|
||||
|
||||
# Initialize the aggregation object
|
||||
weighted = "weight" in self.plot_data
|
||||
agg = (WeightedAggregator if weighted else EstimateAggregator)(
|
||||
self.estimator, self.errorbar, n_boot=self.n_boot, seed=self.seed,
|
||||
)
|
||||
|
||||
# TODO abstract variable to aggregate over here-ish. Better name?
|
||||
orient = self.orient
|
||||
if orient not in {"x", "y"}:
|
||||
err = f"`orient` must be either 'x' or 'y', not {orient!r}."
|
||||
raise ValueError(err)
|
||||
other = {"x": "y", "y": "x"}[orient]
|
||||
|
||||
# TODO How to handle NA? We don't want NA to propagate through to the
|
||||
# estimate/CI when some values are present, but we would also like
|
||||
# matplotlib to show "gaps" in the line when all values are missing.
|
||||
# This is straightforward absent aggregation, but complicated with it.
|
||||
# If we want to use nas, we need to conditionalize dropna in iter_data.
|
||||
|
||||
# Loop over the semantic subsets and add to the plot
|
||||
grouping_vars = "hue", "size", "style"
|
||||
for sub_vars, sub_data in self.iter_data(grouping_vars, from_comp_data=True):
|
||||
|
||||
if self.sort:
|
||||
sort_vars = ["units", orient, other]
|
||||
sort_cols = [var for var in sort_vars if var in self.variables]
|
||||
sub_data = sub_data.sort_values(sort_cols)
|
||||
|
||||
if (
|
||||
self.estimator is not None
|
||||
and sub_data[orient].value_counts().max() > 1
|
||||
):
|
||||
if "units" in self.variables:
|
||||
# TODO eventually relax this constraint
|
||||
err = "estimator must be None when specifying units"
|
||||
raise ValueError(err)
|
||||
grouped = sub_data.groupby(orient, sort=self.sort)
|
||||
# Could pass as_index=False instead of reset_index,
|
||||
# but that fails on a corner case with older pandas.
|
||||
sub_data = (
|
||||
grouped
|
||||
.apply(agg, other, **groupby_apply_include_groups(False))
|
||||
.reset_index()
|
||||
)
|
||||
else:
|
||||
sub_data[f"{other}min"] = np.nan
|
||||
sub_data[f"{other}max"] = np.nan
|
||||
|
||||
# Apply inverse axis scaling
|
||||
for var in "xy":
|
||||
_, inv = _get_transform_functions(ax, var)
|
||||
for col in sub_data.filter(regex=f"^{var}"):
|
||||
sub_data[col] = inv(sub_data[col])
|
||||
|
||||
# --- Draw the main line(s)
|
||||
|
||||
if "units" in self.variables: # XXX why not add to grouping variables?
|
||||
lines = []
|
||||
for _, unit_data in sub_data.groupby("units"):
|
||||
lines.extend(ax.plot(unit_data["x"], unit_data["y"], **kws))
|
||||
else:
|
||||
lines = ax.plot(sub_data["x"], sub_data["y"], **kws)
|
||||
|
||||
for line in lines:
|
||||
|
||||
if "hue" in sub_vars:
|
||||
line.set_color(self._hue_map(sub_vars["hue"]))
|
||||
|
||||
if "size" in sub_vars:
|
||||
line.set_linewidth(self._size_map(sub_vars["size"]))
|
||||
|
||||
if "style" in sub_vars:
|
||||
attributes = self._style_map(sub_vars["style"])
|
||||
if "dashes" in attributes:
|
||||
line.set_dashes(attributes["dashes"])
|
||||
if "marker" in attributes:
|
||||
line.set_marker(attributes["marker"])
|
||||
|
||||
line_color = line.get_color()
|
||||
line_alpha = line.get_alpha()
|
||||
line_capstyle = line.get_solid_capstyle()
|
||||
|
||||
# --- Draw the confidence intervals
|
||||
|
||||
if self.estimator is not None and self.errorbar is not None:
|
||||
|
||||
# TODO handling of orientation will need to happen here
|
||||
|
||||
if self.err_style == "band":
|
||||
|
||||
func = {"x": ax.fill_between, "y": ax.fill_betweenx}[orient]
|
||||
func(
|
||||
sub_data[orient],
|
||||
sub_data[f"{other}min"], sub_data[f"{other}max"],
|
||||
color=line_color, **err_kws
|
||||
)
|
||||
|
||||
elif self.err_style == "bars":
|
||||
|
||||
error_param = {
|
||||
f"{other}err": (
|
||||
sub_data[other] - sub_data[f"{other}min"],
|
||||
sub_data[f"{other}max"] - sub_data[other],
|
||||
)
|
||||
}
|
||||
ebars = ax.errorbar(
|
||||
sub_data["x"], sub_data["y"], **error_param,
|
||||
linestyle="", color=line_color, alpha=line_alpha,
|
||||
**err_kws
|
||||
)
|
||||
|
||||
# Set the capstyle properly on the error bars
|
||||
for obj in ebars.get_children():
|
||||
if isinstance(obj, mpl.collections.LineCollection):
|
||||
obj.set_capstyle(line_capstyle)
|
||||
|
||||
# Finalize the axes details
|
||||
self._add_axis_labels(ax)
|
||||
if self.legend:
|
||||
legend_artist = partial(mpl.lines.Line2D, xdata=[], ydata=[])
|
||||
attrs = {"hue": "color", "size": "linewidth", "style": None}
|
||||
self.add_legend_data(ax, legend_artist, kws, attrs)
|
||||
handles, _ = ax.get_legend_handles_labels()
|
||||
if handles:
|
||||
legend = ax.legend(title=self.legend_title)
|
||||
adjust_legend_subtitles(legend)
|
||||
|
||||
|
||||
class _ScatterPlotter(_RelationalPlotter):
|
||||
|
||||
_legend_attributes = ["color", "s", "marker"]
|
||||
|
||||
def __init__(self, *, data=None, variables={}, legend=None):
|
||||
|
||||
# TODO this is messy, we want the mapping to be agnostic about
|
||||
# the kind of plot to draw, but for the time being we need to set
|
||||
# this information so the SizeMapping can use it
|
||||
self._default_size_range = (
|
||||
np.r_[.5, 2] * np.square(mpl.rcParams["lines.markersize"])
|
||||
)
|
||||
|
||||
super().__init__(data=data, variables=variables)
|
||||
|
||||
self.legend = legend
|
||||
|
||||
def plot(self, ax, kws):
|
||||
|
||||
# --- Determine the visual attributes of the plot
|
||||
|
||||
data = self.comp_data.dropna()
|
||||
if data.empty:
|
||||
return
|
||||
|
||||
kws = normalize_kwargs(kws, mpl.collections.PathCollection)
|
||||
|
||||
# Define the vectors of x and y positions
|
||||
empty = np.full(len(data), np.nan)
|
||||
x = data.get("x", empty)
|
||||
y = data.get("y", empty)
|
||||
|
||||
# Apply inverse scaling to the coordinate variables
|
||||
_, inv_x = _get_transform_functions(ax, "x")
|
||||
_, inv_y = _get_transform_functions(ax, "y")
|
||||
x, y = inv_x(x), inv_y(y)
|
||||
|
||||
if "style" in self.variables:
|
||||
# Use a representative marker so scatter sets the edgecolor
|
||||
# properly for line art markers. We currently enforce either
|
||||
# all or none line art so this works.
|
||||
example_level = self._style_map.levels[0]
|
||||
example_marker = self._style_map(example_level, "marker")
|
||||
kws.setdefault("marker", example_marker)
|
||||
|
||||
# Conditionally set the marker edgecolor based on whether the marker is "filled"
|
||||
# See https://github.com/matplotlib/matplotlib/issues/17849 for context
|
||||
m = kws.get("marker", mpl.rcParams.get("marker", "o"))
|
||||
if not isinstance(m, mpl.markers.MarkerStyle):
|
||||
# TODO in more recent matplotlib (which?) can pass a MarkerStyle here
|
||||
m = mpl.markers.MarkerStyle(m)
|
||||
if m.is_filled():
|
||||
kws.setdefault("edgecolor", "w")
|
||||
|
||||
# Draw the scatter plot
|
||||
points = ax.scatter(x=x, y=y, **kws)
|
||||
|
||||
# Apply the mapping from semantic variables to artist attributes
|
||||
|
||||
if "hue" in self.variables:
|
||||
points.set_facecolors(self._hue_map(data["hue"]))
|
||||
|
||||
if "size" in self.variables:
|
||||
points.set_sizes(self._size_map(data["size"]))
|
||||
|
||||
if "style" in self.variables:
|
||||
p = [self._style_map(val, "path") for val in data["style"]]
|
||||
points.set_paths(p)
|
||||
|
||||
# Apply dependent default attributes
|
||||
|
||||
if "linewidth" not in kws:
|
||||
sizes = points.get_sizes()
|
||||
linewidth = .08 * np.sqrt(np.percentile(sizes, 10))
|
||||
points.set_linewidths(linewidth)
|
||||
kws["linewidth"] = linewidth
|
||||
|
||||
# Finalize the axes details
|
||||
self._add_axis_labels(ax)
|
||||
if self.legend:
|
||||
attrs = {"hue": "color", "size": "s", "style": None}
|
||||
self.add_legend_data(ax, _scatter_legend_artist, kws, attrs)
|
||||
handles, _ = ax.get_legend_handles_labels()
|
||||
if handles:
|
||||
legend = ax.legend(title=self.legend_title)
|
||||
adjust_legend_subtitles(legend)
|
||||
|
||||
|
||||
def lineplot(
|
||||
data=None, *,
|
||||
x=None, y=None, hue=None, size=None, style=None, units=None, weights=None,
|
||||
palette=None, hue_order=None, hue_norm=None,
|
||||
sizes=None, size_order=None, size_norm=None,
|
||||
dashes=True, markers=None, style_order=None,
|
||||
estimator="mean", errorbar=("ci", 95), n_boot=1000, seed=None,
|
||||
orient="x", sort=True, err_style="band", err_kws=None,
|
||||
legend="auto", ci="deprecated", ax=None, **kwargs
|
||||
):
|
||||
|
||||
# Handle deprecation of ci parameter
|
||||
errorbar = _deprecate_ci(errorbar, ci)
|
||||
|
||||
p = _LinePlotter(
|
||||
data=data,
|
||||
variables=dict(
|
||||
x=x, y=y, hue=hue, size=size, style=style, units=units, weight=weights
|
||||
),
|
||||
estimator=estimator, n_boot=n_boot, seed=seed, errorbar=errorbar,
|
||||
sort=sort, orient=orient, err_style=err_style, err_kws=err_kws,
|
||||
legend=legend,
|
||||
)
|
||||
|
||||
p.map_hue(palette=palette, order=hue_order, norm=hue_norm)
|
||||
p.map_size(sizes=sizes, order=size_order, norm=size_norm)
|
||||
p.map_style(markers=markers, dashes=dashes, order=style_order)
|
||||
|
||||
if ax is None:
|
||||
ax = plt.gca()
|
||||
|
||||
if "style" not in p.variables and not {"ls", "linestyle"} & set(kwargs): # XXX
|
||||
kwargs["dashes"] = "" if dashes is None or isinstance(dashes, bool) else dashes
|
||||
|
||||
if not p.has_xy_data:
|
||||
return ax
|
||||
|
||||
p._attach(ax)
|
||||
|
||||
# Other functions have color as an explicit param,
|
||||
# and we should probably do that here too
|
||||
color = kwargs.pop("color", kwargs.pop("c", None))
|
||||
kwargs["color"] = _default_color(ax.plot, hue, color, kwargs)
|
||||
|
||||
p.plot(ax, kwargs)
|
||||
return ax
|
||||
|
||||
|
||||
lineplot.__doc__ = """\
|
||||
Draw a line plot with possibility of several semantic groupings.
|
||||
|
||||
{narrative.main_api}
|
||||
|
||||
{narrative.relational_semantic}
|
||||
|
||||
By default, the plot aggregates over multiple `y` values at each value of
|
||||
`x` and shows an estimate of the central tendency and a confidence
|
||||
interval for that estimate.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
{params.core.data}
|
||||
{params.core.xy}
|
||||
hue : vector or key in `data`
|
||||
Grouping variable that will produce lines with different colors.
|
||||
Can be either categorical or numeric, although color mapping will
|
||||
behave differently in latter case.
|
||||
size : vector or key in `data`
|
||||
Grouping variable that will produce lines with different widths.
|
||||
Can be either categorical or numeric, although size mapping will
|
||||
behave differently in latter case.
|
||||
style : vector or key in `data`
|
||||
Grouping variable that will produce lines with different dashes
|
||||
and/or markers. Can have a numeric dtype but will always be treated
|
||||
as categorical.
|
||||
{params.rel.units}
|
||||
weights : vector or key in `data`
|
||||
Data values or column used to compute weighted estimation.
|
||||
Note that use of weights currently limits the choice of statistics
|
||||
to a 'mean' estimator and 'ci' errorbar.
|
||||
{params.core.palette}
|
||||
{params.core.hue_order}
|
||||
{params.core.hue_norm}
|
||||
{params.rel.sizes}
|
||||
{params.rel.size_order}
|
||||
{params.rel.size_norm}
|
||||
{params.rel.dashes}
|
||||
{params.rel.markers}
|
||||
{params.rel.style_order}
|
||||
{params.rel.estimator}
|
||||
{params.stat.errorbar}
|
||||
{params.rel.n_boot}
|
||||
{params.rel.seed}
|
||||
orient : "x" or "y"
|
||||
Dimension along which the data are sorted / aggregated. Equivalently,
|
||||
the "independent variable" of the resulting function.
|
||||
sort : boolean
|
||||
If True, the data will be sorted by the x and y variables, otherwise
|
||||
lines will connect points in the order they appear in the dataset.
|
||||
err_style : "band" or "bars"
|
||||
Whether to draw the confidence intervals with translucent error bands
|
||||
or discrete error bars.
|
||||
err_kws : dict of keyword arguments
|
||||
Additional parameters to control the aesthetics of the error bars. The
|
||||
kwargs are passed either to :meth:`matplotlib.axes.Axes.fill_between`
|
||||
or :meth:`matplotlib.axes.Axes.errorbar`, depending on `err_style`.
|
||||
{params.rel.legend}
|
||||
{params.rel.ci}
|
||||
{params.core.ax}
|
||||
kwargs : key, value mappings
|
||||
Other keyword arguments are passed down to
|
||||
:meth:`matplotlib.axes.Axes.plot`.
|
||||
|
||||
Returns
|
||||
-------
|
||||
{returns.ax}
|
||||
|
||||
See Also
|
||||
--------
|
||||
{seealso.scatterplot}
|
||||
{seealso.pointplot}
|
||||
|
||||
Examples
|
||||
--------
|
||||
|
||||
.. include:: ../docstrings/lineplot.rst
|
||||
|
||||
""".format(
|
||||
narrative=_relational_narrative,
|
||||
params=_param_docs,
|
||||
returns=_core_docs["returns"],
|
||||
seealso=_core_docs["seealso"],
|
||||
)
|
||||
|
||||
|
||||
def scatterplot(
|
||||
data=None, *,
|
||||
x=None, y=None, hue=None, size=None, style=None,
|
||||
palette=None, hue_order=None, hue_norm=None,
|
||||
sizes=None, size_order=None, size_norm=None,
|
||||
markers=True, style_order=None, legend="auto", ax=None,
|
||||
**kwargs
|
||||
):
|
||||
|
||||
p = _ScatterPlotter(
|
||||
data=data,
|
||||
variables=dict(x=x, y=y, hue=hue, size=size, style=style),
|
||||
legend=legend
|
||||
)
|
||||
|
||||
p.map_hue(palette=palette, order=hue_order, norm=hue_norm)
|
||||
p.map_size(sizes=sizes, order=size_order, norm=size_norm)
|
||||
p.map_style(markers=markers, order=style_order)
|
||||
|
||||
if ax is None:
|
||||
ax = plt.gca()
|
||||
|
||||
if not p.has_xy_data:
|
||||
return ax
|
||||
|
||||
p._attach(ax)
|
||||
|
||||
color = kwargs.pop("color", None)
|
||||
kwargs["color"] = _default_color(ax.scatter, hue, color, kwargs)
|
||||
|
||||
p.plot(ax, kwargs)
|
||||
|
||||
return ax
|
||||
|
||||
|
||||
scatterplot.__doc__ = """\
|
||||
Draw a scatter plot with possibility of several semantic groupings.
|
||||
|
||||
{narrative.main_api}
|
||||
|
||||
{narrative.relational_semantic}
|
||||
|
||||
Parameters
|
||||
----------
|
||||
{params.core.data}
|
||||
{params.core.xy}
|
||||
hue : vector or key in `data`
|
||||
Grouping variable that will produce points with different colors.
|
||||
Can be either categorical or numeric, although color mapping will
|
||||
behave differently in latter case.
|
||||
size : vector or key in `data`
|
||||
Grouping variable that will produce points with different sizes.
|
||||
Can be either categorical or numeric, although size mapping will
|
||||
behave differently in latter case.
|
||||
style : vector or key in `data`
|
||||
Grouping variable that will produce points with different markers.
|
||||
Can have a numeric dtype but will always be treated as categorical.
|
||||
{params.core.palette}
|
||||
{params.core.hue_order}
|
||||
{params.core.hue_norm}
|
||||
{params.rel.sizes}
|
||||
{params.rel.size_order}
|
||||
{params.rel.size_norm}
|
||||
{params.rel.markers}
|
||||
{params.rel.style_order}
|
||||
{params.rel.legend}
|
||||
{params.core.ax}
|
||||
kwargs : key, value mappings
|
||||
Other keyword arguments are passed down to
|
||||
:meth:`matplotlib.axes.Axes.scatter`.
|
||||
|
||||
Returns
|
||||
-------
|
||||
{returns.ax}
|
||||
|
||||
See Also
|
||||
--------
|
||||
{seealso.lineplot}
|
||||
{seealso.stripplot}
|
||||
{seealso.swarmplot}
|
||||
|
||||
Examples
|
||||
--------
|
||||
|
||||
.. include:: ../docstrings/scatterplot.rst
|
||||
|
||||
""".format(
|
||||
narrative=_relational_narrative,
|
||||
params=_param_docs,
|
||||
returns=_core_docs["returns"],
|
||||
seealso=_core_docs["seealso"],
|
||||
)
|
||||
|
||||
|
||||
def relplot(
|
||||
data=None, *,
|
||||
x=None, y=None, hue=None, size=None, style=None, units=None, weights=None,
|
||||
row=None, col=None, col_wrap=None, row_order=None, col_order=None,
|
||||
palette=None, hue_order=None, hue_norm=None,
|
||||
sizes=None, size_order=None, size_norm=None,
|
||||
markers=None, dashes=None, style_order=None,
|
||||
legend="auto", kind="scatter", height=5, aspect=1, facet_kws=None,
|
||||
**kwargs
|
||||
):
|
||||
|
||||
if kind == "scatter":
|
||||
|
||||
Plotter = _ScatterPlotter
|
||||
func = scatterplot
|
||||
markers = True if markers is None else markers
|
||||
|
||||
elif kind == "line":
|
||||
|
||||
Plotter = _LinePlotter
|
||||
func = lineplot
|
||||
dashes = True if dashes is None else dashes
|
||||
|
||||
else:
|
||||
err = f"Plot kind {kind} not recognized"
|
||||
raise ValueError(err)
|
||||
|
||||
# Check for attempt to plot onto specific axes and warn
|
||||
if "ax" in kwargs:
|
||||
msg = (
|
||||
"relplot is a figure-level function and does not accept "
|
||||
"the `ax` parameter. You may wish to try {}".format(kind + "plot")
|
||||
)
|
||||
warnings.warn(msg, UserWarning)
|
||||
kwargs.pop("ax")
|
||||
|
||||
# Use the full dataset to map the semantics
|
||||
variables = dict(x=x, y=y, hue=hue, size=size, style=style)
|
||||
if kind == "line":
|
||||
variables["units"] = units
|
||||
variables["weight"] = weights
|
||||
else:
|
||||
if units is not None:
|
||||
msg = "The `units` parameter has no effect with kind='scatter'."
|
||||
warnings.warn(msg, stacklevel=2)
|
||||
if weights is not None:
|
||||
msg = "The `weights` parameter has no effect with kind='scatter'."
|
||||
warnings.warn(msg, stacklevel=2)
|
||||
p = Plotter(
|
||||
data=data,
|
||||
variables=variables,
|
||||
legend=legend,
|
||||
)
|
||||
p.map_hue(palette=palette, order=hue_order, norm=hue_norm)
|
||||
p.map_size(sizes=sizes, order=size_order, norm=size_norm)
|
||||
p.map_style(markers=markers, dashes=dashes, order=style_order)
|
||||
|
||||
# Extract the semantic mappings
|
||||
if "hue" in p.variables:
|
||||
palette = p._hue_map.lookup_table
|
||||
hue_order = p._hue_map.levels
|
||||
hue_norm = p._hue_map.norm
|
||||
else:
|
||||
palette = hue_order = hue_norm = None
|
||||
|
||||
if "size" in p.variables:
|
||||
sizes = p._size_map.lookup_table
|
||||
size_order = p._size_map.levels
|
||||
size_norm = p._size_map.norm
|
||||
|
||||
if "style" in p.variables:
|
||||
style_order = p._style_map.levels
|
||||
if markers:
|
||||
markers = {k: p._style_map(k, "marker") for k in style_order}
|
||||
else:
|
||||
markers = None
|
||||
if dashes:
|
||||
dashes = {k: p._style_map(k, "dashes") for k in style_order}
|
||||
else:
|
||||
dashes = None
|
||||
else:
|
||||
markers = dashes = style_order = None
|
||||
|
||||
# Now extract the data that would be used to draw a single plot
|
||||
variables = p.variables
|
||||
plot_data = p.plot_data
|
||||
|
||||
# Define the common plotting parameters
|
||||
plot_kws = dict(
|
||||
palette=palette, hue_order=hue_order, hue_norm=hue_norm,
|
||||
sizes=sizes, size_order=size_order, size_norm=size_norm,
|
||||
markers=markers, dashes=dashes, style_order=style_order,
|
||||
legend=False,
|
||||
)
|
||||
plot_kws.update(kwargs)
|
||||
if kind == "scatter":
|
||||
plot_kws.pop("dashes")
|
||||
|
||||
# Add the grid semantics onto the plotter
|
||||
grid_variables = dict(
|
||||
x=x, y=y, row=row, col=col, hue=hue, size=size, style=style,
|
||||
)
|
||||
if kind == "line":
|
||||
grid_variables.update(units=units, weights=weights)
|
||||
p.assign_variables(data, grid_variables)
|
||||
|
||||
# Define the named variables for plotting on each facet
|
||||
# Rename the variables with a leading underscore to avoid
|
||||
# collisions with faceting variable names
|
||||
plot_variables = {v: f"_{v}" for v in variables}
|
||||
if "weight" in plot_variables:
|
||||
plot_variables["weights"] = plot_variables.pop("weight")
|
||||
plot_kws.update(plot_variables)
|
||||
|
||||
# Pass the row/col variables to FacetGrid with their original
|
||||
# names so that the axes titles render correctly
|
||||
for var in ["row", "col"]:
|
||||
# Handle faceting variables that lack name information
|
||||
if var in p.variables and p.variables[var] is None:
|
||||
p.variables[var] = f"_{var}_"
|
||||
grid_kws = {v: p.variables.get(v) for v in ["row", "col"]}
|
||||
|
||||
# Rename the columns of the plot_data structure appropriately
|
||||
new_cols = plot_variables.copy()
|
||||
new_cols.update(grid_kws)
|
||||
full_data = p.plot_data.rename(columns=new_cols)
|
||||
|
||||
# Set up the FacetGrid object
|
||||
facet_kws = {} if facet_kws is None else facet_kws.copy()
|
||||
g = FacetGrid(
|
||||
data=full_data.dropna(axis=1, how="all"),
|
||||
**grid_kws,
|
||||
col_wrap=col_wrap, row_order=row_order, col_order=col_order,
|
||||
height=height, aspect=aspect, dropna=False,
|
||||
**facet_kws
|
||||
)
|
||||
|
||||
# Draw the plot
|
||||
g.map_dataframe(func, **plot_kws)
|
||||
|
||||
# Label the axes, using the original variables
|
||||
# Pass "" when the variable name is None to overwrite internal variables
|
||||
g.set_axis_labels(variables.get("x") or "", variables.get("y") or "")
|
||||
|
||||
if legend:
|
||||
# Replace the original plot data so the legend uses numeric data with
|
||||
# the correct type, since we force a categorical mapping above.
|
||||
p.plot_data = plot_data
|
||||
|
||||
# Handle the additional non-semantic keyword arguments out here.
|
||||
# We're selective because some kwargs may be seaborn function specific
|
||||
# and not relevant to the matplotlib artists going into the legend.
|
||||
# Ideally, we will have a better solution where we don't need to re-make
|
||||
# the legend out here and will have parity with the axes-level functions.
|
||||
keys = ["c", "color", "alpha", "m", "marker"]
|
||||
if kind == "scatter":
|
||||
legend_artist = _scatter_legend_artist
|
||||
keys += ["s", "facecolor", "fc", "edgecolor", "ec", "linewidth", "lw"]
|
||||
else:
|
||||
legend_artist = partial(mpl.lines.Line2D, xdata=[], ydata=[])
|
||||
keys += [
|
||||
"markersize", "ms",
|
||||
"markeredgewidth", "mew",
|
||||
"markeredgecolor", "mec",
|
||||
"linestyle", "ls",
|
||||
"linewidth", "lw",
|
||||
]
|
||||
|
||||
common_kws = {k: v for k, v in kwargs.items() if k in keys}
|
||||
attrs = {"hue": "color", "style": None}
|
||||
if kind == "scatter":
|
||||
attrs["size"] = "s"
|
||||
elif kind == "line":
|
||||
attrs["size"] = "linewidth"
|
||||
p.add_legend_data(g.axes.flat[0], legend_artist, common_kws, attrs)
|
||||
if p.legend_data:
|
||||
g.add_legend(legend_data=p.legend_data,
|
||||
label_order=p.legend_order,
|
||||
title=p.legend_title,
|
||||
adjust_subtitles=True)
|
||||
|
||||
# Rename the columns of the FacetGrid's `data` attribute
|
||||
# to match the original column names
|
||||
orig_cols = {
|
||||
f"_{k}": f"_{k}_" if v is None else v for k, v in variables.items()
|
||||
}
|
||||
grid_data = g.data.rename(columns=orig_cols)
|
||||
if data is not None and (x is not None or y is not None):
|
||||
if not isinstance(data, pd.DataFrame):
|
||||
data = pd.DataFrame(data)
|
||||
g.data = pd.merge(
|
||||
data,
|
||||
grid_data[grid_data.columns.difference(data.columns)],
|
||||
left_index=True,
|
||||
right_index=True,
|
||||
)
|
||||
else:
|
||||
g.data = grid_data
|
||||
|
||||
return g
|
||||
|
||||
|
||||
relplot.__doc__ = """\
|
||||
Figure-level interface for drawing relational plots onto a FacetGrid.
|
||||
|
||||
This function provides access to several different axes-level functions
|
||||
that show the relationship between two variables with semantic mappings
|
||||
of subsets. The `kind` parameter selects the underlying axes-level
|
||||
function to use:
|
||||
|
||||
- :func:`scatterplot` (with `kind="scatter"`; the default)
|
||||
- :func:`lineplot` (with `kind="line"`)
|
||||
|
||||
Extra keyword arguments are passed to the underlying function, so you
|
||||
should refer to the documentation for each to see kind-specific options.
|
||||
|
||||
{narrative.main_api}
|
||||
|
||||
{narrative.relational_semantic}
|
||||
|
||||
After plotting, the :class:`FacetGrid` with the plot is returned and can
|
||||
be used directly to tweak supporting plot details or add other layers.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
{params.core.data}
|
||||
{params.core.xy}
|
||||
hue : vector or key in `data`
|
||||
Grouping variable that will produce elements with different colors.
|
||||
Can be either categorical or numeric, although color mapping will
|
||||
behave differently in latter case.
|
||||
size : vector or key in `data`
|
||||
Grouping variable that will produce elements with different sizes.
|
||||
Can be either categorical or numeric, although size mapping will
|
||||
behave differently in latter case.
|
||||
style : vector or key in `data`
|
||||
Grouping variable that will produce elements with different styles.
|
||||
Can have a numeric dtype but will always be treated as categorical.
|
||||
{params.rel.units}
|
||||
weights : vector or key in `data`
|
||||
Data values or column used to compute weighted estimation.
|
||||
Note that use of weights currently limits the choice of statistics
|
||||
to a 'mean' estimator and 'ci' errorbar.
|
||||
{params.facets.rowcol}
|
||||
{params.facets.col_wrap}
|
||||
row_order, col_order : lists of strings
|
||||
Order to organize the rows and/or columns of the grid in, otherwise the
|
||||
orders are inferred from the data objects.
|
||||
{params.core.palette}
|
||||
{params.core.hue_order}
|
||||
{params.core.hue_norm}
|
||||
{params.rel.sizes}
|
||||
{params.rel.size_order}
|
||||
{params.rel.size_norm}
|
||||
{params.rel.style_order}
|
||||
{params.rel.dashes}
|
||||
{params.rel.markers}
|
||||
{params.rel.legend}
|
||||
kind : string
|
||||
Kind of plot to draw, corresponding to a seaborn relational plot.
|
||||
Options are `"scatter"` or `"line"`.
|
||||
{params.facets.height}
|
||||
{params.facets.aspect}
|
||||
facet_kws : dict
|
||||
Dictionary of other keyword arguments to pass to :class:`FacetGrid`.
|
||||
kwargs : key, value pairings
|
||||
Other keyword arguments are passed through to the underlying plotting
|
||||
function.
|
||||
|
||||
Returns
|
||||
-------
|
||||
{returns.facetgrid}
|
||||
|
||||
Examples
|
||||
--------
|
||||
|
||||
.. include:: ../docstrings/relplot.rst
|
||||
|
||||
""".format(
|
||||
narrative=_relational_narrative,
|
||||
params=_param_docs,
|
||||
returns=_core_docs["returns"],
|
||||
)
|
||||
897
ftline/lib/python3.12/site-packages/seaborn/utils.py
Normal file
897
ftline/lib/python3.12/site-packages/seaborn/utils.py
Normal file
@@ -0,0 +1,897 @@
|
||||
"""Utility functions, mostly for internal use."""
|
||||
import os
|
||||
import inspect
|
||||
import warnings
|
||||
import colorsys
|
||||
from contextlib import contextmanager
|
||||
from urllib.request import urlopen, urlretrieve
|
||||
from types import ModuleType
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
import matplotlib as mpl
|
||||
from matplotlib.colors import to_rgb
|
||||
import matplotlib.pyplot as plt
|
||||
from matplotlib.cbook import normalize_kwargs
|
||||
|
||||
from seaborn._core.typing import deprecated
|
||||
from seaborn.external.version import Version
|
||||
from seaborn.external.appdirs import user_cache_dir
|
||||
|
||||
__all__ = ["desaturate", "saturate", "set_hls_values", "move_legend",
|
||||
"despine", "get_dataset_names", "get_data_home", "load_dataset"]
|
||||
|
||||
DATASET_SOURCE = "https://raw.githubusercontent.com/mwaskom/seaborn-data/master"
|
||||
DATASET_NAMES_URL = f"{DATASET_SOURCE}/dataset_names.txt"
|
||||
|
||||
|
||||
def ci_to_errsize(cis, heights):
|
||||
"""Convert intervals to error arguments relative to plot heights.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
cis : 2 x n sequence
|
||||
sequence of confidence interval limits
|
||||
heights : n sequence
|
||||
sequence of plot heights
|
||||
|
||||
Returns
|
||||
-------
|
||||
errsize : 2 x n array
|
||||
sequence of error size relative to height values in correct
|
||||
format as argument for plt.bar
|
||||
|
||||
"""
|
||||
cis = np.atleast_2d(cis).reshape(2, -1)
|
||||
heights = np.atleast_1d(heights)
|
||||
errsize = []
|
||||
for i, (low, high) in enumerate(np.transpose(cis)):
|
||||
h = heights[i]
|
||||
elow = h - low
|
||||
ehigh = high - h
|
||||
errsize.append([elow, ehigh])
|
||||
|
||||
errsize = np.asarray(errsize).T
|
||||
return errsize
|
||||
|
||||
|
||||
def _draw_figure(fig):
|
||||
"""Force draw of a matplotlib figure, accounting for back-compat."""
|
||||
# See https://github.com/matplotlib/matplotlib/issues/19197 for context
|
||||
fig.canvas.draw()
|
||||
if fig.stale:
|
||||
try:
|
||||
fig.draw(fig.canvas.get_renderer())
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
|
||||
def _default_color(method, hue, color, kws, saturation=1):
|
||||
"""If needed, get a default color by using the matplotlib property cycle."""
|
||||
|
||||
if hue is not None:
|
||||
# This warning is probably user-friendly, but it's currently triggered
|
||||
# in a FacetGrid context and I don't want to mess with that logic right now
|
||||
# if color is not None:
|
||||
# msg = "`color` is ignored when `hue` is assigned."
|
||||
# warnings.warn(msg)
|
||||
return None
|
||||
|
||||
kws = kws.copy()
|
||||
kws.pop("label", None)
|
||||
|
||||
if color is not None:
|
||||
if saturation < 1:
|
||||
color = desaturate(color, saturation)
|
||||
return color
|
||||
|
||||
elif method.__name__ == "plot":
|
||||
|
||||
color = normalize_kwargs(kws, mpl.lines.Line2D).get("color")
|
||||
scout, = method([], [], scalex=False, scaley=False, color=color)
|
||||
color = scout.get_color()
|
||||
scout.remove()
|
||||
|
||||
elif method.__name__ == "scatter":
|
||||
|
||||
# Matplotlib will raise if the size of x/y don't match s/c,
|
||||
# and the latter might be in the kws dict
|
||||
scout_size = max(
|
||||
np.atleast_1d(kws.get(key, [])).shape[0]
|
||||
for key in ["s", "c", "fc", "facecolor", "facecolors"]
|
||||
)
|
||||
scout_x = scout_y = np.full(scout_size, np.nan)
|
||||
|
||||
scout = method(scout_x, scout_y, **kws)
|
||||
facecolors = scout.get_facecolors()
|
||||
|
||||
if not len(facecolors):
|
||||
# Handle bug in matplotlib <= 3.2 (I think)
|
||||
# This will limit the ability to use non color= kwargs to specify
|
||||
# a color in versions of matplotlib with the bug, but trying to
|
||||
# work out what the user wanted by re-implementing the broken logic
|
||||
# of inspecting the kwargs is probably too brittle.
|
||||
single_color = False
|
||||
else:
|
||||
single_color = np.unique(facecolors, axis=0).shape[0] == 1
|
||||
|
||||
# Allow the user to specify an array of colors through various kwargs
|
||||
if "c" not in kws and single_color:
|
||||
color = to_rgb(facecolors[0])
|
||||
|
||||
scout.remove()
|
||||
|
||||
elif method.__name__ == "bar":
|
||||
|
||||
# bar() needs masked, not empty data, to generate a patch
|
||||
scout, = method([np.nan], [np.nan], **kws)
|
||||
color = to_rgb(scout.get_facecolor())
|
||||
scout.remove()
|
||||
# Axes.bar adds both a patch and a container
|
||||
method.__self__.containers.pop(-1)
|
||||
|
||||
elif method.__name__ == "fill_between":
|
||||
|
||||
kws = normalize_kwargs(kws, mpl.collections.PolyCollection)
|
||||
scout = method([], [], **kws)
|
||||
facecolor = scout.get_facecolor()
|
||||
color = to_rgb(facecolor[0])
|
||||
scout.remove()
|
||||
|
||||
if saturation < 1:
|
||||
color = desaturate(color, saturation)
|
||||
|
||||
return color
|
||||
|
||||
|
||||
def desaturate(color, prop):
|
||||
"""Decrease the saturation channel of a color by some percent.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
color : matplotlib color
|
||||
hex, rgb-tuple, or html color name
|
||||
prop : float
|
||||
saturation channel of color will be multiplied by this value
|
||||
|
||||
Returns
|
||||
-------
|
||||
new_color : rgb tuple
|
||||
desaturated color code in RGB tuple representation
|
||||
|
||||
"""
|
||||
# Check inputs
|
||||
if not 0 <= prop <= 1:
|
||||
raise ValueError("prop must be between 0 and 1")
|
||||
|
||||
# Get rgb tuple rep
|
||||
rgb = to_rgb(color)
|
||||
|
||||
# Short circuit to avoid floating point issues
|
||||
if prop == 1:
|
||||
return rgb
|
||||
|
||||
# Convert to hls
|
||||
h, l, s = colorsys.rgb_to_hls(*rgb)
|
||||
|
||||
# Desaturate the saturation channel
|
||||
s *= prop
|
||||
|
||||
# Convert back to rgb
|
||||
new_color = colorsys.hls_to_rgb(h, l, s)
|
||||
|
||||
return new_color
|
||||
|
||||
|
||||
def saturate(color):
|
||||
"""Return a fully saturated color with the same hue.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
color : matplotlib color
|
||||
hex, rgb-tuple, or html color name
|
||||
|
||||
Returns
|
||||
-------
|
||||
new_color : rgb tuple
|
||||
saturated color code in RGB tuple representation
|
||||
|
||||
"""
|
||||
return set_hls_values(color, s=1)
|
||||
|
||||
|
||||
def set_hls_values(color, h=None, l=None, s=None): # noqa
|
||||
"""Independently manipulate the h, l, or s channels of a color.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
color : matplotlib color
|
||||
hex, rgb-tuple, or html color name
|
||||
h, l, s : floats between 0 and 1, or None
|
||||
new values for each channel in hls space
|
||||
|
||||
Returns
|
||||
-------
|
||||
new_color : rgb tuple
|
||||
new color code in RGB tuple representation
|
||||
|
||||
"""
|
||||
# Get an RGB tuple representation
|
||||
rgb = to_rgb(color)
|
||||
vals = list(colorsys.rgb_to_hls(*rgb))
|
||||
for i, val in enumerate([h, l, s]):
|
||||
if val is not None:
|
||||
vals[i] = val
|
||||
|
||||
rgb = colorsys.hls_to_rgb(*vals)
|
||||
return rgb
|
||||
|
||||
|
||||
def axlabel(xlabel, ylabel, **kwargs):
|
||||
"""Grab current axis and label it.
|
||||
|
||||
DEPRECATED: will be removed in a future version.
|
||||
|
||||
"""
|
||||
msg = "This function is deprecated and will be removed in a future version"
|
||||
warnings.warn(msg, FutureWarning)
|
||||
ax = plt.gca()
|
||||
ax.set_xlabel(xlabel, **kwargs)
|
||||
ax.set_ylabel(ylabel, **kwargs)
|
||||
|
||||
|
||||
def remove_na(vector):
|
||||
"""Helper method for removing null values from data vectors.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
vector : vector object
|
||||
Must implement boolean masking with [] subscript syntax.
|
||||
|
||||
Returns
|
||||
-------
|
||||
clean_clean : same type as ``vector``
|
||||
Vector of data with null values removed. May be a copy or a view.
|
||||
|
||||
"""
|
||||
return vector[pd.notnull(vector)]
|
||||
|
||||
|
||||
def get_color_cycle():
|
||||
"""Return the list of colors in the current matplotlib color cycle
|
||||
|
||||
Parameters
|
||||
----------
|
||||
None
|
||||
|
||||
Returns
|
||||
-------
|
||||
colors : list
|
||||
List of matplotlib colors in the current cycle, or dark gray if
|
||||
the current color cycle is empty.
|
||||
"""
|
||||
cycler = mpl.rcParams['axes.prop_cycle']
|
||||
return cycler.by_key()['color'] if 'color' in cycler.keys else [".15"]
|
||||
|
||||
|
||||
def despine(fig=None, ax=None, top=True, right=True, left=False,
|
||||
bottom=False, offset=None, trim=False):
|
||||
"""Remove the top and right spines from plot(s).
|
||||
|
||||
fig : matplotlib figure, optional
|
||||
Figure to despine all axes of, defaults to the current figure.
|
||||
ax : matplotlib axes, optional
|
||||
Specific axes object to despine. Ignored if fig is provided.
|
||||
top, right, left, bottom : boolean, optional
|
||||
If True, remove that spine.
|
||||
offset : int or dict, optional
|
||||
Absolute distance, in points, spines should be moved away
|
||||
from the axes (negative values move spines inward). A single value
|
||||
applies to all spines; a dict can be used to set offset values per
|
||||
side.
|
||||
trim : bool, optional
|
||||
If True, limit spines to the smallest and largest major tick
|
||||
on each non-despined axis.
|
||||
|
||||
Returns
|
||||
-------
|
||||
None
|
||||
|
||||
"""
|
||||
# Get references to the axes we want
|
||||
if fig is None and ax is None:
|
||||
axes = plt.gcf().axes
|
||||
elif fig is not None:
|
||||
axes = fig.axes
|
||||
elif ax is not None:
|
||||
axes = [ax]
|
||||
|
||||
for ax_i in axes:
|
||||
for side in ["top", "right", "left", "bottom"]:
|
||||
# Toggle the spine objects
|
||||
is_visible = not locals()[side]
|
||||
ax_i.spines[side].set_visible(is_visible)
|
||||
if offset is not None and is_visible:
|
||||
try:
|
||||
val = offset.get(side, 0)
|
||||
except AttributeError:
|
||||
val = offset
|
||||
ax_i.spines[side].set_position(('outward', val))
|
||||
|
||||
# Potentially move the ticks
|
||||
if left and not right:
|
||||
maj_on = any(
|
||||
t.tick1line.get_visible()
|
||||
for t in ax_i.yaxis.majorTicks
|
||||
)
|
||||
min_on = any(
|
||||
t.tick1line.get_visible()
|
||||
for t in ax_i.yaxis.minorTicks
|
||||
)
|
||||
ax_i.yaxis.set_ticks_position("right")
|
||||
for t in ax_i.yaxis.majorTicks:
|
||||
t.tick2line.set_visible(maj_on)
|
||||
for t in ax_i.yaxis.minorTicks:
|
||||
t.tick2line.set_visible(min_on)
|
||||
|
||||
if bottom and not top:
|
||||
maj_on = any(
|
||||
t.tick1line.get_visible()
|
||||
for t in ax_i.xaxis.majorTicks
|
||||
)
|
||||
min_on = any(
|
||||
t.tick1line.get_visible()
|
||||
for t in ax_i.xaxis.minorTicks
|
||||
)
|
||||
ax_i.xaxis.set_ticks_position("top")
|
||||
for t in ax_i.xaxis.majorTicks:
|
||||
t.tick2line.set_visible(maj_on)
|
||||
for t in ax_i.xaxis.minorTicks:
|
||||
t.tick2line.set_visible(min_on)
|
||||
|
||||
if trim:
|
||||
# clip off the parts of the spines that extend past major ticks
|
||||
xticks = np.asarray(ax_i.get_xticks())
|
||||
if xticks.size:
|
||||
firsttick = np.compress(xticks >= min(ax_i.get_xlim()),
|
||||
xticks)[0]
|
||||
lasttick = np.compress(xticks <= max(ax_i.get_xlim()),
|
||||
xticks)[-1]
|
||||
ax_i.spines['bottom'].set_bounds(firsttick, lasttick)
|
||||
ax_i.spines['top'].set_bounds(firsttick, lasttick)
|
||||
newticks = xticks.compress(xticks <= lasttick)
|
||||
newticks = newticks.compress(newticks >= firsttick)
|
||||
ax_i.set_xticks(newticks)
|
||||
|
||||
yticks = np.asarray(ax_i.get_yticks())
|
||||
if yticks.size:
|
||||
firsttick = np.compress(yticks >= min(ax_i.get_ylim()),
|
||||
yticks)[0]
|
||||
lasttick = np.compress(yticks <= max(ax_i.get_ylim()),
|
||||
yticks)[-1]
|
||||
ax_i.spines['left'].set_bounds(firsttick, lasttick)
|
||||
ax_i.spines['right'].set_bounds(firsttick, lasttick)
|
||||
newticks = yticks.compress(yticks <= lasttick)
|
||||
newticks = newticks.compress(newticks >= firsttick)
|
||||
ax_i.set_yticks(newticks)
|
||||
|
||||
|
||||
def move_legend(obj, loc, **kwargs):
|
||||
"""
|
||||
Recreate a plot's legend at a new location.
|
||||
|
||||
The name is a slight misnomer. Matplotlib legends do not expose public
|
||||
control over their position parameters. So this function creates a new legend,
|
||||
copying over the data from the original object, which is then removed.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
obj : the object with the plot
|
||||
This argument can be either a seaborn or matplotlib object:
|
||||
|
||||
- :class:`seaborn.FacetGrid` or :class:`seaborn.PairGrid`
|
||||
- :class:`matplotlib.axes.Axes` or :class:`matplotlib.figure.Figure`
|
||||
|
||||
loc : str or int
|
||||
Location argument, as in :meth:`matplotlib.axes.Axes.legend`.
|
||||
|
||||
kwargs
|
||||
Other keyword arguments are passed to :meth:`matplotlib.axes.Axes.legend`.
|
||||
|
||||
Examples
|
||||
--------
|
||||
|
||||
.. include:: ../docstrings/move_legend.rst
|
||||
|
||||
"""
|
||||
# This is a somewhat hackish solution that will hopefully be obviated by
|
||||
# upstream improvements to matplotlib legends that make them easier to
|
||||
# modify after creation.
|
||||
|
||||
from seaborn.axisgrid import Grid # Avoid circular import
|
||||
|
||||
# Locate the legend object and a method to recreate the legend
|
||||
if isinstance(obj, Grid):
|
||||
old_legend = obj.legend
|
||||
legend_func = obj.figure.legend
|
||||
elif isinstance(obj, mpl.axes.Axes):
|
||||
old_legend = obj.legend_
|
||||
legend_func = obj.legend
|
||||
elif isinstance(obj, mpl.figure.Figure):
|
||||
if obj.legends:
|
||||
old_legend = obj.legends[-1]
|
||||
else:
|
||||
old_legend = None
|
||||
legend_func = obj.legend
|
||||
else:
|
||||
err = "`obj` must be a seaborn Grid or matplotlib Axes or Figure instance."
|
||||
raise TypeError(err)
|
||||
|
||||
if old_legend is None:
|
||||
err = f"{obj} has no legend attached."
|
||||
raise ValueError(err)
|
||||
|
||||
# Extract the components of the legend we need to reuse
|
||||
# Import here to avoid a circular import
|
||||
from seaborn._compat import get_legend_handles
|
||||
handles = get_legend_handles(old_legend)
|
||||
labels = [t.get_text() for t in old_legend.get_texts()]
|
||||
|
||||
# Handle the case where the user is trying to override the labels
|
||||
if (new_labels := kwargs.pop("labels", None)) is not None:
|
||||
if len(new_labels) != len(labels):
|
||||
err = "Length of new labels does not match existing legend."
|
||||
raise ValueError(err)
|
||||
labels = new_labels
|
||||
|
||||
# Extract legend properties that can be passed to the recreation method
|
||||
# (Vexingly, these don't all round-trip)
|
||||
legend_kws = inspect.signature(mpl.legend.Legend).parameters
|
||||
props = {k: v for k, v in old_legend.properties().items() if k in legend_kws}
|
||||
|
||||
# Delegate default bbox_to_anchor rules to matplotlib
|
||||
props.pop("bbox_to_anchor")
|
||||
|
||||
# Try to propagate the existing title and font properties; respect new ones too
|
||||
title = props.pop("title")
|
||||
if "title" in kwargs:
|
||||
title.set_text(kwargs.pop("title"))
|
||||
title_kwargs = {k: v for k, v in kwargs.items() if k.startswith("title_")}
|
||||
for key, val in title_kwargs.items():
|
||||
title.set(**{key[6:]: val})
|
||||
kwargs.pop(key)
|
||||
|
||||
# Try to respect the frame visibility
|
||||
kwargs.setdefault("frameon", old_legend.legendPatch.get_visible())
|
||||
|
||||
# Remove the old legend and create the new one
|
||||
props.update(kwargs)
|
||||
old_legend.remove()
|
||||
new_legend = legend_func(handles, labels, loc=loc, **props)
|
||||
new_legend.set_title(title.get_text(), title.get_fontproperties())
|
||||
|
||||
# Let the Grid object continue to track the correct legend object
|
||||
if isinstance(obj, Grid):
|
||||
obj._legend = new_legend
|
||||
|
||||
|
||||
def _kde_support(data, bw, gridsize, cut, clip):
|
||||
"""Establish support for a kernel density estimate."""
|
||||
support_min = max(data.min() - bw * cut, clip[0])
|
||||
support_max = min(data.max() + bw * cut, clip[1])
|
||||
support = np.linspace(support_min, support_max, gridsize)
|
||||
|
||||
return support
|
||||
|
||||
|
||||
def ci(a, which=95, axis=None):
|
||||
"""Return a percentile range from an array of values."""
|
||||
p = 50 - which / 2, 50 + which / 2
|
||||
return np.nanpercentile(a, p, axis)
|
||||
|
||||
|
||||
def get_dataset_names():
|
||||
"""Report available example datasets, useful for reporting issues.
|
||||
|
||||
Requires an internet connection.
|
||||
|
||||
"""
|
||||
with urlopen(DATASET_NAMES_URL) as resp:
|
||||
txt = resp.read()
|
||||
|
||||
dataset_names = [name.strip() for name in txt.decode().split("\n")]
|
||||
return list(filter(None, dataset_names))
|
||||
|
||||
|
||||
def get_data_home(data_home=None):
|
||||
"""Return a path to the cache directory for example datasets.
|
||||
|
||||
This directory is used by :func:`load_dataset`.
|
||||
|
||||
If the ``data_home`` argument is not provided, it will use a directory
|
||||
specified by the `SEABORN_DATA` environment variable (if it exists)
|
||||
or otherwise default to an OS-appropriate user cache location.
|
||||
|
||||
"""
|
||||
if data_home is None:
|
||||
data_home = os.environ.get("SEABORN_DATA", user_cache_dir("seaborn"))
|
||||
data_home = os.path.expanduser(data_home)
|
||||
if not os.path.exists(data_home):
|
||||
os.makedirs(data_home)
|
||||
return data_home
|
||||
|
||||
|
||||
def load_dataset(name, cache=True, data_home=None, **kws):
|
||||
"""Load an example dataset from the online repository (requires internet).
|
||||
|
||||
This function provides quick access to a small number of example datasets
|
||||
that are useful for documenting seaborn or generating reproducible examples
|
||||
for bug reports. It is not necessary for normal usage.
|
||||
|
||||
Note that some of the datasets have a small amount of preprocessing applied
|
||||
to define a proper ordering for categorical variables.
|
||||
|
||||
Use :func:`get_dataset_names` to see a list of available datasets.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
name : str
|
||||
Name of the dataset (``{name}.csv`` on
|
||||
https://github.com/mwaskom/seaborn-data).
|
||||
cache : boolean, optional
|
||||
If True, try to load from the local cache first, and save to the cache
|
||||
if a download is required.
|
||||
data_home : string, optional
|
||||
The directory in which to cache data; see :func:`get_data_home`.
|
||||
kws : keys and values, optional
|
||||
Additional keyword arguments are passed to passed through to
|
||||
:func:`pandas.read_csv`.
|
||||
|
||||
Returns
|
||||
-------
|
||||
df : :class:`pandas.DataFrame`
|
||||
Tabular data, possibly with some preprocessing applied.
|
||||
|
||||
"""
|
||||
# A common beginner mistake is to assume that one's personal data needs
|
||||
# to be passed through this function to be usable with seaborn.
|
||||
# Let's provide a more helpful error than you would otherwise get.
|
||||
if isinstance(name, pd.DataFrame):
|
||||
err = (
|
||||
"This function accepts only strings (the name of an example dataset). "
|
||||
"You passed a pandas DataFrame. If you have your own dataset, "
|
||||
"it is not necessary to use this function before plotting."
|
||||
)
|
||||
raise TypeError(err)
|
||||
|
||||
url = f"{DATASET_SOURCE}/{name}.csv"
|
||||
|
||||
if cache:
|
||||
cache_path = os.path.join(get_data_home(data_home), os.path.basename(url))
|
||||
if not os.path.exists(cache_path):
|
||||
if name not in get_dataset_names():
|
||||
raise ValueError(f"'{name}' is not one of the example datasets.")
|
||||
urlretrieve(url, cache_path)
|
||||
full_path = cache_path
|
||||
else:
|
||||
full_path = url
|
||||
|
||||
df = pd.read_csv(full_path, **kws)
|
||||
|
||||
if df.iloc[-1].isnull().all():
|
||||
df = df.iloc[:-1]
|
||||
|
||||
# Set some columns as a categorical type with ordered levels
|
||||
|
||||
if name == "tips":
|
||||
df["day"] = pd.Categorical(df["day"], ["Thur", "Fri", "Sat", "Sun"])
|
||||
df["sex"] = pd.Categorical(df["sex"], ["Male", "Female"])
|
||||
df["time"] = pd.Categorical(df["time"], ["Lunch", "Dinner"])
|
||||
df["smoker"] = pd.Categorical(df["smoker"], ["Yes", "No"])
|
||||
|
||||
elif name == "flights":
|
||||
months = df["month"].str[:3]
|
||||
df["month"] = pd.Categorical(months, months.unique())
|
||||
|
||||
elif name == "exercise":
|
||||
df["time"] = pd.Categorical(df["time"], ["1 min", "15 min", "30 min"])
|
||||
df["kind"] = pd.Categorical(df["kind"], ["rest", "walking", "running"])
|
||||
df["diet"] = pd.Categorical(df["diet"], ["no fat", "low fat"])
|
||||
|
||||
elif name == "titanic":
|
||||
df["class"] = pd.Categorical(df["class"], ["First", "Second", "Third"])
|
||||
df["deck"] = pd.Categorical(df["deck"], list("ABCDEFG"))
|
||||
|
||||
elif name == "penguins":
|
||||
df["sex"] = df["sex"].str.title()
|
||||
|
||||
elif name == "diamonds":
|
||||
df["color"] = pd.Categorical(
|
||||
df["color"], ["D", "E", "F", "G", "H", "I", "J"],
|
||||
)
|
||||
df["clarity"] = pd.Categorical(
|
||||
df["clarity"], ["IF", "VVS1", "VVS2", "VS1", "VS2", "SI1", "SI2", "I1"],
|
||||
)
|
||||
df["cut"] = pd.Categorical(
|
||||
df["cut"], ["Ideal", "Premium", "Very Good", "Good", "Fair"],
|
||||
)
|
||||
|
||||
elif name == "taxis":
|
||||
df["pickup"] = pd.to_datetime(df["pickup"])
|
||||
df["dropoff"] = pd.to_datetime(df["dropoff"])
|
||||
|
||||
elif name == "seaice":
|
||||
df["Date"] = pd.to_datetime(df["Date"])
|
||||
|
||||
elif name == "dowjones":
|
||||
df["Date"] = pd.to_datetime(df["Date"])
|
||||
|
||||
return df
|
||||
|
||||
|
||||
def axis_ticklabels_overlap(labels):
|
||||
"""Return a boolean for whether the list of ticklabels have overlaps.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
labels : list of matplotlib ticklabels
|
||||
|
||||
Returns
|
||||
-------
|
||||
overlap : boolean
|
||||
True if any of the labels overlap.
|
||||
|
||||
"""
|
||||
if not labels:
|
||||
return False
|
||||
try:
|
||||
bboxes = [l.get_window_extent() for l in labels]
|
||||
overlaps = [b.count_overlaps(bboxes) for b in bboxes]
|
||||
return max(overlaps) > 1
|
||||
except RuntimeError:
|
||||
# Issue on macos backend raises an error in the above code
|
||||
return False
|
||||
|
||||
|
||||
def axes_ticklabels_overlap(ax):
|
||||
"""Return booleans for whether the x and y ticklabels on an Axes overlap.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
ax : matplotlib Axes
|
||||
|
||||
Returns
|
||||
-------
|
||||
x_overlap, y_overlap : booleans
|
||||
True when the labels on that axis overlap.
|
||||
|
||||
"""
|
||||
return (axis_ticklabels_overlap(ax.get_xticklabels()),
|
||||
axis_ticklabels_overlap(ax.get_yticklabels()))
|
||||
|
||||
|
||||
def locator_to_legend_entries(locator, limits, dtype):
|
||||
"""Return levels and formatted levels for brief numeric legends."""
|
||||
raw_levels = locator.tick_values(*limits).astype(dtype)
|
||||
|
||||
# The locator can return ticks outside the limits, clip them here
|
||||
raw_levels = [l for l in raw_levels if l >= limits[0] and l <= limits[1]]
|
||||
|
||||
class dummy_axis:
|
||||
def get_view_interval(self):
|
||||
return limits
|
||||
|
||||
if isinstance(locator, mpl.ticker.LogLocator):
|
||||
formatter = mpl.ticker.LogFormatter()
|
||||
else:
|
||||
formatter = mpl.ticker.ScalarFormatter()
|
||||
# Avoid having an offset/scientific notation which we don't currently
|
||||
# have any way of representing in the legend
|
||||
formatter.set_useOffset(False)
|
||||
formatter.set_scientific(False)
|
||||
formatter.axis = dummy_axis()
|
||||
|
||||
formatted_levels = formatter.format_ticks(raw_levels)
|
||||
|
||||
return raw_levels, formatted_levels
|
||||
|
||||
|
||||
def relative_luminance(color):
|
||||
"""Calculate the relative luminance of a color according to W3C standards
|
||||
|
||||
Parameters
|
||||
----------
|
||||
color : matplotlib color or sequence of matplotlib colors
|
||||
Hex code, rgb-tuple, or html color name.
|
||||
|
||||
Returns
|
||||
-------
|
||||
luminance : float(s) between 0 and 1
|
||||
|
||||
"""
|
||||
rgb = mpl.colors.colorConverter.to_rgba_array(color)[:, :3]
|
||||
rgb = np.where(rgb <= .03928, rgb / 12.92, ((rgb + .055) / 1.055) ** 2.4)
|
||||
lum = rgb.dot([.2126, .7152, .0722])
|
||||
try:
|
||||
return lum.item()
|
||||
except ValueError:
|
||||
return lum
|
||||
|
||||
|
||||
def to_utf8(obj):
|
||||
"""Return a string representing a Python object.
|
||||
|
||||
Strings (i.e. type ``str``) are returned unchanged.
|
||||
|
||||
Byte strings (i.e. type ``bytes``) are returned as UTF-8-decoded strings.
|
||||
|
||||
For other objects, the method ``__str__()`` is called, and the result is
|
||||
returned as a string.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
obj : object
|
||||
Any Python object
|
||||
|
||||
Returns
|
||||
-------
|
||||
s : str
|
||||
UTF-8-decoded string representation of ``obj``
|
||||
|
||||
"""
|
||||
if isinstance(obj, str):
|
||||
return obj
|
||||
try:
|
||||
return obj.decode(encoding="utf-8")
|
||||
except AttributeError: # obj is not bytes-like
|
||||
return str(obj)
|
||||
|
||||
|
||||
def _check_argument(param, options, value, prefix=False):
|
||||
"""Raise if value for param is not in options."""
|
||||
if prefix and value is not None:
|
||||
failure = not any(value.startswith(p) for p in options if isinstance(p, str))
|
||||
else:
|
||||
failure = value not in options
|
||||
if failure:
|
||||
raise ValueError(
|
||||
f"The value for `{param}` must be one of {options}, "
|
||||
f"but {repr(value)} was passed."
|
||||
)
|
||||
return value
|
||||
|
||||
|
||||
def _assign_default_kwargs(kws, call_func, source_func):
|
||||
"""Assign default kwargs for call_func using values from source_func."""
|
||||
# This exists so that axes-level functions and figure-level functions can
|
||||
# both call a Plotter method while having the default kwargs be defined in
|
||||
# the signature of the axes-level function.
|
||||
# An alternative would be to have a decorator on the method that sets its
|
||||
# defaults based on those defined in the axes-level function.
|
||||
# Then the figure-level function would not need to worry about defaults.
|
||||
# I am not sure which is better.
|
||||
needed = inspect.signature(call_func).parameters
|
||||
defaults = inspect.signature(source_func).parameters
|
||||
|
||||
for param in needed:
|
||||
if param in defaults and param not in kws:
|
||||
kws[param] = defaults[param].default
|
||||
|
||||
return kws
|
||||
|
||||
|
||||
def adjust_legend_subtitles(legend):
|
||||
"""
|
||||
Make invisible-handle "subtitles" entries look more like titles.
|
||||
|
||||
Note: This function is not part of the public API and may be changed or removed.
|
||||
|
||||
"""
|
||||
# Legend title not in rcParams until 3.0
|
||||
font_size = plt.rcParams.get("legend.title_fontsize", None)
|
||||
hpackers = legend.findobj(mpl.offsetbox.VPacker)[0].get_children()
|
||||
for hpack in hpackers:
|
||||
draw_area, text_area = hpack.get_children()
|
||||
handles = draw_area.get_children()
|
||||
if not all(artist.get_visible() for artist in handles):
|
||||
draw_area.set_width(0)
|
||||
for text in text_area.get_children():
|
||||
if font_size is not None:
|
||||
text.set_size(font_size)
|
||||
|
||||
|
||||
def _deprecate_ci(errorbar, ci):
|
||||
"""
|
||||
Warn on usage of ci= and convert to appropriate errorbar= arg.
|
||||
|
||||
ci was deprecated when errorbar was added in 0.12. It should not be removed
|
||||
completely for some time, but it can be moved out of function definitions
|
||||
(and extracted from kwargs) after one cycle.
|
||||
|
||||
"""
|
||||
if ci is not deprecated and ci != "deprecated":
|
||||
if ci is None:
|
||||
errorbar = None
|
||||
elif ci == "sd":
|
||||
errorbar = "sd"
|
||||
else:
|
||||
errorbar = ("ci", ci)
|
||||
msg = (
|
||||
"\n\nThe `ci` parameter is deprecated. "
|
||||
f"Use `errorbar={repr(errorbar)}` for the same effect.\n"
|
||||
)
|
||||
warnings.warn(msg, FutureWarning, stacklevel=3)
|
||||
|
||||
return errorbar
|
||||
|
||||
|
||||
def _get_transform_functions(ax, axis):
|
||||
"""Return the forward and inverse transforms for a given axis."""
|
||||
axis_obj = getattr(ax, f"{axis}axis")
|
||||
transform = axis_obj.get_transform()
|
||||
return transform.transform, transform.inverted().transform
|
||||
|
||||
|
||||
@contextmanager
|
||||
def _disable_autolayout():
|
||||
"""Context manager for preventing rc-controlled auto-layout behavior."""
|
||||
# This is a workaround for an issue in matplotlib, for details see
|
||||
# https://github.com/mwaskom/seaborn/issues/2914
|
||||
# The only affect of this rcParam is to set the default value for
|
||||
# layout= in plt.figure, so we could just do that instead.
|
||||
# But then we would need to own the complexity of the transition
|
||||
# from tight_layout=True -> layout="tight". This seems easier,
|
||||
# but can be removed when (if) that is simpler on the matplotlib side,
|
||||
# or if the layout algorithms are improved to handle figure legends.
|
||||
orig_val = mpl.rcParams["figure.autolayout"]
|
||||
try:
|
||||
mpl.rcParams["figure.autolayout"] = False
|
||||
yield
|
||||
finally:
|
||||
mpl.rcParams["figure.autolayout"] = orig_val
|
||||
|
||||
|
||||
def _version_predates(lib: ModuleType, version: str) -> bool:
|
||||
"""Helper function for checking version compatibility."""
|
||||
return Version(lib.__version__) < Version(version)
|
||||
|
||||
|
||||
def _scatter_legend_artist(**kws):
|
||||
|
||||
kws = normalize_kwargs(kws, mpl.collections.PathCollection)
|
||||
|
||||
edgecolor = kws.pop("edgecolor", None)
|
||||
rc = mpl.rcParams
|
||||
line_kws = {
|
||||
"linestyle": "",
|
||||
"marker": kws.pop("marker", "o"),
|
||||
"markersize": np.sqrt(kws.pop("s", rc["lines.markersize"] ** 2)),
|
||||
"markerfacecolor": kws.pop("facecolor", kws.get("color")),
|
||||
"markeredgewidth": kws.pop("linewidth", 0),
|
||||
**kws,
|
||||
}
|
||||
|
||||
if edgecolor is not None:
|
||||
if edgecolor == "face":
|
||||
line_kws["markeredgecolor"] = line_kws["markerfacecolor"]
|
||||
else:
|
||||
line_kws["markeredgecolor"] = edgecolor
|
||||
|
||||
return mpl.lines.Line2D([], [], **line_kws)
|
||||
|
||||
|
||||
def _get_patch_legend_artist(fill):
|
||||
|
||||
def legend_artist(**kws):
|
||||
|
||||
color = kws.pop("color", None)
|
||||
if color is not None:
|
||||
if fill:
|
||||
kws["facecolor"] = color
|
||||
else:
|
||||
kws["edgecolor"] = color
|
||||
kws["facecolor"] = "none"
|
||||
|
||||
return mpl.patches.Rectangle((0, 0), 0, 0, **kws)
|
||||
|
||||
return legend_artist
|
||||
426
ftline/lib/python3.12/site-packages/seaborn/widgets.py
Normal file
426
ftline/lib/python3.12/site-packages/seaborn/widgets.py
Normal file
@@ -0,0 +1,426 @@
|
||||
import numpy as np
|
||||
import matplotlib.pyplot as plt
|
||||
from matplotlib.colors import LinearSegmentedColormap
|
||||
|
||||
try:
|
||||
from ipywidgets import interact, FloatSlider, IntSlider
|
||||
except ImportError:
|
||||
def interact(f):
|
||||
msg = "Interactive palettes require `ipywidgets`, which is not installed."
|
||||
raise ImportError(msg)
|
||||
|
||||
from .miscplot import palplot
|
||||
from .palettes import (color_palette, dark_palette, light_palette,
|
||||
diverging_palette, cubehelix_palette)
|
||||
|
||||
|
||||
__all__ = ["choose_colorbrewer_palette", "choose_cubehelix_palette",
|
||||
"choose_dark_palette", "choose_light_palette",
|
||||
"choose_diverging_palette"]
|
||||
|
||||
|
||||
def _init_mutable_colormap():
|
||||
"""Create a matplotlib colormap that will be updated by the widgets."""
|
||||
greys = color_palette("Greys", 256)
|
||||
cmap = LinearSegmentedColormap.from_list("interactive", greys)
|
||||
cmap._init()
|
||||
cmap._set_extremes()
|
||||
return cmap
|
||||
|
||||
|
||||
def _update_lut(cmap, colors):
|
||||
"""Change the LUT values in a matplotlib colormap in-place."""
|
||||
cmap._lut[:256] = colors
|
||||
cmap._set_extremes()
|
||||
|
||||
|
||||
def _show_cmap(cmap):
|
||||
"""Show a continuous matplotlib colormap."""
|
||||
from .rcmod import axes_style # Avoid circular import
|
||||
with axes_style("white"):
|
||||
f, ax = plt.subplots(figsize=(8.25, .75))
|
||||
ax.set(xticks=[], yticks=[])
|
||||
x = np.linspace(0, 1, 256)[np.newaxis, :]
|
||||
ax.pcolormesh(x, cmap=cmap)
|
||||
|
||||
|
||||
def choose_colorbrewer_palette(data_type, as_cmap=False):
|
||||
"""Select a palette from the ColorBrewer set.
|
||||
|
||||
These palettes are built into matplotlib and can be used by name in
|
||||
many seaborn functions, or by passing the object returned by this function.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
data_type : {'sequential', 'diverging', 'qualitative'}
|
||||
This describes the kind of data you want to visualize. See the seaborn
|
||||
color palette docs for more information about how to choose this value.
|
||||
Note that you can pass substrings (e.g. 'q' for 'qualitative.
|
||||
|
||||
as_cmap : bool
|
||||
If True, the return value is a matplotlib colormap rather than a
|
||||
list of discrete colors.
|
||||
|
||||
Returns
|
||||
-------
|
||||
pal or cmap : list of colors or matplotlib colormap
|
||||
Object that can be passed to plotting functions.
|
||||
|
||||
See Also
|
||||
--------
|
||||
dark_palette : Create a sequential palette with dark low values.
|
||||
light_palette : Create a sequential palette with bright low values.
|
||||
diverging_palette : Create a diverging palette from selected colors.
|
||||
cubehelix_palette : Create a sequential palette or colormap using the
|
||||
cubehelix system.
|
||||
|
||||
|
||||
"""
|
||||
if data_type.startswith("q") and as_cmap:
|
||||
raise ValueError("Qualitative palettes cannot be colormaps.")
|
||||
|
||||
pal = []
|
||||
if as_cmap:
|
||||
cmap = _init_mutable_colormap()
|
||||
|
||||
if data_type.startswith("s"):
|
||||
opts = ["Greys", "Reds", "Greens", "Blues", "Oranges", "Purples",
|
||||
"BuGn", "BuPu", "GnBu", "OrRd", "PuBu", "PuRd", "RdPu", "YlGn",
|
||||
"PuBuGn", "YlGnBu", "YlOrBr", "YlOrRd"]
|
||||
variants = ["regular", "reverse", "dark"]
|
||||
|
||||
@interact
|
||||
def choose_sequential(name=opts, n=(2, 18),
|
||||
desat=FloatSlider(min=0, max=1, value=1),
|
||||
variant=variants):
|
||||
if variant == "reverse":
|
||||
name += "_r"
|
||||
elif variant == "dark":
|
||||
name += "_d"
|
||||
|
||||
if as_cmap:
|
||||
colors = color_palette(name, 256, desat)
|
||||
_update_lut(cmap, np.c_[colors, np.ones(256)])
|
||||
_show_cmap(cmap)
|
||||
else:
|
||||
pal[:] = color_palette(name, n, desat)
|
||||
palplot(pal)
|
||||
|
||||
elif data_type.startswith("d"):
|
||||
opts = ["RdBu", "RdGy", "PRGn", "PiYG", "BrBG",
|
||||
"RdYlBu", "RdYlGn", "Spectral"]
|
||||
variants = ["regular", "reverse"]
|
||||
|
||||
@interact
|
||||
def choose_diverging(name=opts, n=(2, 16),
|
||||
desat=FloatSlider(min=0, max=1, value=1),
|
||||
variant=variants):
|
||||
if variant == "reverse":
|
||||
name += "_r"
|
||||
if as_cmap:
|
||||
colors = color_palette(name, 256, desat)
|
||||
_update_lut(cmap, np.c_[colors, np.ones(256)])
|
||||
_show_cmap(cmap)
|
||||
else:
|
||||
pal[:] = color_palette(name, n, desat)
|
||||
palplot(pal)
|
||||
|
||||
elif data_type.startswith("q"):
|
||||
opts = ["Set1", "Set2", "Set3", "Paired", "Accent",
|
||||
"Pastel1", "Pastel2", "Dark2"]
|
||||
|
||||
@interact
|
||||
def choose_qualitative(name=opts, n=(2, 16),
|
||||
desat=FloatSlider(min=0, max=1, value=1)):
|
||||
pal[:] = color_palette(name, n, desat)
|
||||
palplot(pal)
|
||||
|
||||
if as_cmap:
|
||||
return cmap
|
||||
return pal
|
||||
|
||||
|
||||
def choose_dark_palette(input="husl", as_cmap=False):
|
||||
"""Launch an interactive widget to create a dark sequential palette.
|
||||
|
||||
This corresponds with the :func:`dark_palette` function. This kind
|
||||
of palette is good for data that range between relatively uninteresting
|
||||
low values and interesting high values.
|
||||
|
||||
Requires IPython 2+ and must be used in the notebook.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
input : {'husl', 'hls', 'rgb'}
|
||||
Color space for defining the seed value. Note that the default is
|
||||
different than the default input for :func:`dark_palette`.
|
||||
as_cmap : bool
|
||||
If True, the return value is a matplotlib colormap rather than a
|
||||
list of discrete colors.
|
||||
|
||||
Returns
|
||||
-------
|
||||
pal or cmap : list of colors or matplotlib colormap
|
||||
Object that can be passed to plotting functions.
|
||||
|
||||
See Also
|
||||
--------
|
||||
dark_palette : Create a sequential palette with dark low values.
|
||||
light_palette : Create a sequential palette with bright low values.
|
||||
cubehelix_palette : Create a sequential palette or colormap using the
|
||||
cubehelix system.
|
||||
|
||||
"""
|
||||
pal = []
|
||||
if as_cmap:
|
||||
cmap = _init_mutable_colormap()
|
||||
|
||||
if input == "rgb":
|
||||
@interact
|
||||
def choose_dark_palette_rgb(r=(0., 1.),
|
||||
g=(0., 1.),
|
||||
b=(0., 1.),
|
||||
n=(3, 17)):
|
||||
color = r, g, b
|
||||
if as_cmap:
|
||||
colors = dark_palette(color, 256, input="rgb")
|
||||
_update_lut(cmap, colors)
|
||||
_show_cmap(cmap)
|
||||
else:
|
||||
pal[:] = dark_palette(color, n, input="rgb")
|
||||
palplot(pal)
|
||||
|
||||
elif input == "hls":
|
||||
@interact
|
||||
def choose_dark_palette_hls(h=(0., 1.),
|
||||
l=(0., 1.), # noqa: E741
|
||||
s=(0., 1.),
|
||||
n=(3, 17)):
|
||||
color = h, l, s
|
||||
if as_cmap:
|
||||
colors = dark_palette(color, 256, input="hls")
|
||||
_update_lut(cmap, colors)
|
||||
_show_cmap(cmap)
|
||||
else:
|
||||
pal[:] = dark_palette(color, n, input="hls")
|
||||
palplot(pal)
|
||||
|
||||
elif input == "husl":
|
||||
@interact
|
||||
def choose_dark_palette_husl(h=(0, 359),
|
||||
s=(0, 99),
|
||||
l=(0, 99), # noqa: E741
|
||||
n=(3, 17)):
|
||||
color = h, s, l
|
||||
if as_cmap:
|
||||
colors = dark_palette(color, 256, input="husl")
|
||||
_update_lut(cmap, colors)
|
||||
_show_cmap(cmap)
|
||||
else:
|
||||
pal[:] = dark_palette(color, n, input="husl")
|
||||
palplot(pal)
|
||||
|
||||
if as_cmap:
|
||||
return cmap
|
||||
return pal
|
||||
|
||||
|
||||
def choose_light_palette(input="husl", as_cmap=False):
|
||||
"""Launch an interactive widget to create a light sequential palette.
|
||||
|
||||
This corresponds with the :func:`light_palette` function. This kind
|
||||
of palette is good for data that range between relatively uninteresting
|
||||
low values and interesting high values.
|
||||
|
||||
Requires IPython 2+ and must be used in the notebook.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
input : {'husl', 'hls', 'rgb'}
|
||||
Color space for defining the seed value. Note that the default is
|
||||
different than the default input for :func:`light_palette`.
|
||||
as_cmap : bool
|
||||
If True, the return value is a matplotlib colormap rather than a
|
||||
list of discrete colors.
|
||||
|
||||
Returns
|
||||
-------
|
||||
pal or cmap : list of colors or matplotlib colormap
|
||||
Object that can be passed to plotting functions.
|
||||
|
||||
See Also
|
||||
--------
|
||||
light_palette : Create a sequential palette with bright low values.
|
||||
dark_palette : Create a sequential palette with dark low values.
|
||||
cubehelix_palette : Create a sequential palette or colormap using the
|
||||
cubehelix system.
|
||||
|
||||
"""
|
||||
pal = []
|
||||
if as_cmap:
|
||||
cmap = _init_mutable_colormap()
|
||||
|
||||
if input == "rgb":
|
||||
@interact
|
||||
def choose_light_palette_rgb(r=(0., 1.),
|
||||
g=(0., 1.),
|
||||
b=(0., 1.),
|
||||
n=(3, 17)):
|
||||
color = r, g, b
|
||||
if as_cmap:
|
||||
colors = light_palette(color, 256, input="rgb")
|
||||
_update_lut(cmap, colors)
|
||||
_show_cmap(cmap)
|
||||
else:
|
||||
pal[:] = light_palette(color, n, input="rgb")
|
||||
palplot(pal)
|
||||
|
||||
elif input == "hls":
|
||||
@interact
|
||||
def choose_light_palette_hls(h=(0., 1.),
|
||||
l=(0., 1.), # noqa: E741
|
||||
s=(0., 1.),
|
||||
n=(3, 17)):
|
||||
color = h, l, s
|
||||
if as_cmap:
|
||||
colors = light_palette(color, 256, input="hls")
|
||||
_update_lut(cmap, colors)
|
||||
_show_cmap(cmap)
|
||||
else:
|
||||
pal[:] = light_palette(color, n, input="hls")
|
||||
palplot(pal)
|
||||
|
||||
elif input == "husl":
|
||||
@interact
|
||||
def choose_light_palette_husl(h=(0, 359),
|
||||
s=(0, 99),
|
||||
l=(0, 99), # noqa: E741
|
||||
n=(3, 17)):
|
||||
color = h, s, l
|
||||
if as_cmap:
|
||||
colors = light_palette(color, 256, input="husl")
|
||||
_update_lut(cmap, colors)
|
||||
_show_cmap(cmap)
|
||||
else:
|
||||
pal[:] = light_palette(color, n, input="husl")
|
||||
palplot(pal)
|
||||
|
||||
if as_cmap:
|
||||
return cmap
|
||||
return pal
|
||||
|
||||
|
||||
def choose_diverging_palette(as_cmap=False):
|
||||
"""Launch an interactive widget to choose a diverging color palette.
|
||||
|
||||
This corresponds with the :func:`diverging_palette` function. This kind
|
||||
of palette is good for data that range between interesting low values
|
||||
and interesting high values with a meaningful midpoint. (For example,
|
||||
change scores relative to some baseline value).
|
||||
|
||||
Requires IPython 2+ and must be used in the notebook.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
as_cmap : bool
|
||||
If True, the return value is a matplotlib colormap rather than a
|
||||
list of discrete colors.
|
||||
|
||||
Returns
|
||||
-------
|
||||
pal or cmap : list of colors or matplotlib colormap
|
||||
Object that can be passed to plotting functions.
|
||||
|
||||
See Also
|
||||
--------
|
||||
diverging_palette : Create a diverging color palette or colormap.
|
||||
choose_colorbrewer_palette : Interactively choose palettes from the
|
||||
colorbrewer set, including diverging palettes.
|
||||
|
||||
"""
|
||||
pal = []
|
||||
if as_cmap:
|
||||
cmap = _init_mutable_colormap()
|
||||
|
||||
@interact
|
||||
def choose_diverging_palette(
|
||||
h_neg=IntSlider(min=0,
|
||||
max=359,
|
||||
value=220),
|
||||
h_pos=IntSlider(min=0,
|
||||
max=359,
|
||||
value=10),
|
||||
s=IntSlider(min=0, max=99, value=74),
|
||||
l=IntSlider(min=0, max=99, value=50), # noqa: E741
|
||||
sep=IntSlider(min=1, max=50, value=10),
|
||||
n=(2, 16),
|
||||
center=["light", "dark"]
|
||||
):
|
||||
if as_cmap:
|
||||
colors = diverging_palette(h_neg, h_pos, s, l, sep, 256, center)
|
||||
_update_lut(cmap, colors)
|
||||
_show_cmap(cmap)
|
||||
else:
|
||||
pal[:] = diverging_palette(h_neg, h_pos, s, l, sep, n, center)
|
||||
palplot(pal)
|
||||
|
||||
if as_cmap:
|
||||
return cmap
|
||||
return pal
|
||||
|
||||
|
||||
def choose_cubehelix_palette(as_cmap=False):
|
||||
"""Launch an interactive widget to create a sequential cubehelix palette.
|
||||
|
||||
This corresponds with the :func:`cubehelix_palette` function. This kind
|
||||
of palette is good for data that range between relatively uninteresting
|
||||
low values and interesting high values. The cubehelix system allows the
|
||||
palette to have more hue variance across the range, which can be helpful
|
||||
for distinguishing a wider range of values.
|
||||
|
||||
Requires IPython 2+ and must be used in the notebook.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
as_cmap : bool
|
||||
If True, the return value is a matplotlib colormap rather than a
|
||||
list of discrete colors.
|
||||
|
||||
Returns
|
||||
-------
|
||||
pal or cmap : list of colors or matplotlib colormap
|
||||
Object that can be passed to plotting functions.
|
||||
|
||||
See Also
|
||||
--------
|
||||
cubehelix_palette : Create a sequential palette or colormap using the
|
||||
cubehelix system.
|
||||
|
||||
"""
|
||||
pal = []
|
||||
if as_cmap:
|
||||
cmap = _init_mutable_colormap()
|
||||
|
||||
@interact
|
||||
def choose_cubehelix(n_colors=IntSlider(min=2, max=16, value=9),
|
||||
start=FloatSlider(min=0, max=3, value=0),
|
||||
rot=FloatSlider(min=-1, max=1, value=.4),
|
||||
gamma=FloatSlider(min=0, max=5, value=1),
|
||||
hue=FloatSlider(min=0, max=1, value=.8),
|
||||
light=FloatSlider(min=0, max=1, value=.85),
|
||||
dark=FloatSlider(min=0, max=1, value=.15),
|
||||
reverse=False):
|
||||
|
||||
if as_cmap:
|
||||
colors = cubehelix_palette(256, start, rot, gamma,
|
||||
hue, light, dark, reverse)
|
||||
_update_lut(cmap, np.c_[colors, np.ones(256)])
|
||||
_show_cmap(cmap)
|
||||
else:
|
||||
pal[:] = cubehelix_palette(n_colors, start, rot, gamma,
|
||||
hue, light, dark, reverse)
|
||||
palplot(pal)
|
||||
|
||||
if as_cmap:
|
||||
return cmap
|
||||
return pal
|
||||
Reference in New Issue
Block a user