pyretailscience 0.1.2__tar.gz → 0.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: pyretailscience
3
- Version: 0.1.2
3
+ Version: 0.2.0
4
4
  Summary: Retail Data Science Tools
5
5
  License: Elastic-2.0
6
6
  Author: Murray Vanwyk
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "pyretailscience"
3
- version = "0.1.2"
3
+ version = "0.2.0"
4
4
  description = "Retail Data Science Tools"
5
5
  authors = ["Murray Vanwyk <2493311+mvanwyk@users.noreply.github.com>"]
6
6
  readme = "README.md"
@@ -0,0 +1,351 @@
1
+ import matplotlib.pyplot as plt
2
+ import matplotlib.ticker as mtick
3
+ import pandas as pd
4
+ from matplotlib.axes import Axes, SubplotBase
5
+
6
+ from pyretailscience.data.contracts import TransactionItemLevelContract
7
+ from pyretailscience.style.graph_utils import human_format, standard_graph_styles
8
+ from pyretailscience.style.tailwind import COLORS
9
+ import operator
10
+
11
+
12
+ class PurchasesPerCustomer:
13
+ """A class to plot the distribution of the number of purchases per customer.
14
+
15
+ Attributes:
16
+ cust_purchases_s (pd.Series): The number of purchases per customer.
17
+
18
+ Args:
19
+ df (pd.DataFrame): A dataframe with the transaction data. The dataframe must comply with the
20
+ TransactionItemLevelContract.
21
+ """
22
+
23
+ def __init__(self, df: pd.DataFrame) -> None:
24
+ if TransactionItemLevelContract(df).validate() is False:
25
+ raise ValueError("The dataframe does not comply with the TransactionItemLevelContract")
26
+
27
+ self.cust_purchases_s = df.groupby("customer_id").size()
28
+
29
+ def plot(
30
+ self,
31
+ bins: int = 10,
32
+ cumlative: bool = False,
33
+ ax: Axes | None = None,
34
+ draw_percentile_line: bool = False,
35
+ percentile_line: float = 0.5,
36
+ source_text: str = None,
37
+ **kwargs: dict[str, any],
38
+ ) -> SubplotBase:
39
+ """Plot the distribution of the number of purchases per customer.
40
+
41
+ Args:
42
+ bins (int, optional): The number of bins to plot. Defaults to 10.
43
+ cumlative (bool, optional): Whether to plot the cumulative distribution. Defaults to False.
44
+ ax (Axes, optional): The Matplotlib axes to plot the graph on. Defaults to None.
45
+ draw_percentile_line (bool, optional): Whether to draw a line at the percentile specified with
46
+ the `percentile_line` paramter. Defaults to False.
47
+ percentile_line (float, optional): The percentile to draw a line at. Defaults to 0.8.
48
+
49
+ Returns:
50
+ SubplotBase: The Matplotlib axes of the plot
51
+ """
52
+
53
+ density = False
54
+ if cumlative:
55
+ density = True
56
+
57
+ ax = self.cust_purchases_s.hist(
58
+ bins=bins,
59
+ cumulative=cumlative,
60
+ ax=ax,
61
+ density=density,
62
+ color=COLORS["green"][500],
63
+ **kwargs,
64
+ )
65
+ ax.set_xlabel("Number of purchases")
66
+ ax.xaxis.set_major_formatter(lambda x, pos: human_format(x, pos, decimals=0))
67
+
68
+ ax = standard_graph_styles(ax)
69
+
70
+ if cumlative:
71
+ plt.title("Number of Purchases Cumulative Distribution")
72
+ plt.ylabel("Percentage of customers")
73
+ ax.yaxis.set_major_formatter(mtick.PercentFormatter(xmax=1, decimals=0))
74
+
75
+ else:
76
+ plt.title("Number of Purchases Distribution")
77
+ plt.ylabel("Number of customers")
78
+ ax.yaxis.set_major_formatter(lambda x, pos: human_format(x, pos, decimals=0))
79
+
80
+ if draw_percentile_line:
81
+ if percentile_line > 1 or percentile_line < 0:
82
+ raise ValueError("Percentile line must be between 0 and 1")
83
+ ax.axvline(
84
+ x=self.purchases_percentile(percentile_line),
85
+ color=COLORS["red"][500],
86
+ linestyle="--",
87
+ lw=2,
88
+ label=f"{percentile_line:.1%} of customers",
89
+ )
90
+ ax.legend(frameon=False)
91
+
92
+ if source_text:
93
+ ax.annotate(
94
+ source_text,
95
+ xy=(-0.1, -0.2),
96
+ xycoords="axes fraction",
97
+ ha="left",
98
+ va="center",
99
+ fontsize=10,
100
+ )
101
+
102
+ return ax
103
+
104
+ def purchases_percentile(self, percentile: float = 0.5) -> float:
105
+ """Get the number of purchases at a given percentile.
106
+
107
+ Args:
108
+ percentile (float): The percentile to get the number of purchases at.
109
+
110
+ Returns:
111
+ float: The number of purchases at the given percentile.
112
+ """
113
+ return self.cust_purchases_s.quantile(percentile)
114
+
115
+ def find_purchase_percentile(self, number_of_purchases: int, comparison: str = "less_than_equal_to") -> float:
116
+ """Find the percentile of the number of purchases.
117
+
118
+ Args:
119
+ number_of_purchases (int): The number of purchases to find the percentile of.
120
+
121
+ Returns:
122
+ float: The percentile of the number of purchases.
123
+ """
124
+ ops = {
125
+ "less_than": operator.lt,
126
+ "less_than_equal_to": operator.le,
127
+ "equal_to": operator.eq,
128
+ "not_equal_to": operator.ne,
129
+ "greater_than": operator.gt,
130
+ "greater_than_equal_to": operator.ge,
131
+ }
132
+
133
+ if comparison not in ops:
134
+ raise ValueError(
135
+ "Comparison must be one of 'less_than', 'less_than_equal_to', 'equal_to', 'not_equal_to', "
136
+ "'greater_than', 'greater_than_equal_to'"
137
+ )
138
+
139
+ return len(self.cust_purchases_s[ops[comparison](self.cust_purchases_s, number_of_purchases)]) / len(
140
+ self.cust_purchases_s
141
+ )
142
+
143
+
144
+ class DaysBetweenPurchases:
145
+ """A class to plot the distribution of the average number of days between purchases per customer.
146
+
147
+ Attributes:
148
+ purchase_dist_s (pd.Series): The average number of days between purchases per customer.
149
+
150
+ Args:
151
+ df (pd.DataFrame): A dataframe with the transaction data. The dataframe must comply with the
152
+ TransactionItemLevelContract.
153
+
154
+ Raises:
155
+ ValueError: If the dataframe does not comply with the TransactionItemLevelContract
156
+ """
157
+
158
+ def __init__(self, df: pd.DataFrame) -> None:
159
+ if TransactionItemLevelContract(df).validate() is False:
160
+ raise ValueError("The dataframe does not comply with the TransactionItemLevelContract")
161
+
162
+ purchase_dist_df = df[["customer_id", "transaction_datetime"]].copy()
163
+ purchase_dist_df["transaction_datetime"] = df["transaction_datetime"].dt.floor("D")
164
+ purchase_dist_df = purchase_dist_df.drop_duplicates().sort_values(["customer_id", "transaction_datetime"])
165
+ purchase_dist_df["diff"] = purchase_dist_df.groupby("customer_id")["transaction_datetime"].transform(
166
+ lambda x: x.diff()
167
+ )
168
+ purchase_dist_df = purchase_dist_df[~purchase_dist_df["diff"].isnull()]
169
+ purchase_dist_df["diff"] = purchase_dist_df["diff"].dt.days
170
+ self.purchase_dist_s = purchase_dist_df.groupby("customer_id")["diff"].mean()
171
+
172
+ def plot(
173
+ self,
174
+ bins: int = 10,
175
+ cumlative: bool = False,
176
+ ax: Axes | None = None,
177
+ draw_percentile_line: bool = False,
178
+ percentile_line: float = 0.5,
179
+ source_text: str = None,
180
+ **kwargs: dict[str, any],
181
+ ) -> SubplotBase:
182
+ """Plot the distribution of the average number of days between purchases per customer.
183
+
184
+ Args:
185
+ bins (int, optional): The number of bins to plot. Defaults to 10.
186
+ cumlative (bool, optional): Whether to plot the cumulative distribution. Defaults to False.
187
+ ax (Axes, optional): The Matplotlib axes to plot the graph on. Defaults to None.
188
+ draw_percentile_line (bool, optional): Whether to draw a line at the percentile specified with
189
+ the `percentile_line` paramter. Defaults to False.
190
+ percentile_line (float, optional): The percentile to draw a line at. Defaults to 0.8.
191
+
192
+ Returns:
193
+ SubplotBase: The Matplotlib axes of the plot
194
+ """
195
+ density = False
196
+ if cumlative:
197
+ density = True
198
+
199
+ ax = self.purchase_dist_s.hist(
200
+ bins=bins,
201
+ cumulative=cumlative,
202
+ ax=ax,
203
+ density=density,
204
+ color=COLORS["green"][500],
205
+ **kwargs,
206
+ )
207
+ plt.xlabel("Average Number of Days Between Purchases")
208
+ ax.xaxis.set_major_formatter(lambda x, pos: human_format(x, pos, decimals=0))
209
+
210
+ ax = standard_graph_styles(ax)
211
+
212
+ if cumlative:
213
+ plt.title("Average Days Between Purchases Cumulative Distribution")
214
+ plt.ylabel("Percentage of Customers")
215
+ ax.yaxis.set_major_formatter(mtick.PercentFormatter(xmax=1, decimals=0))
216
+
217
+ else:
218
+ plt.title("Average Days Between Purchases Distribution")
219
+ plt.ylabel("Number of Customers")
220
+ ax.yaxis.set_major_formatter(lambda x, pos: human_format(x, pos, decimals=0))
221
+
222
+ if draw_percentile_line:
223
+ if percentile_line > 1 or percentile_line < 0:
224
+ raise ValueError("Percentile line must be between 0 and 1")
225
+ ax.axvline(
226
+ x=self.purchases_percentile(percentile_line),
227
+ color=COLORS["red"][500],
228
+ linestyle="--",
229
+ lw=2,
230
+ label=f"{percentile_line:.1%} of customers",
231
+ ymax=0.96,
232
+ )
233
+ ax.legend(frameon=False)
234
+
235
+ if source_text:
236
+ ax.annotate(
237
+ source_text,
238
+ xy=(-0.1, -0.2),
239
+ xycoords="axes fraction",
240
+ ha="left",
241
+ va="center",
242
+ fontsize=10,
243
+ )
244
+
245
+ return ax
246
+
247
+ def purchases_percentile(self, percentile: float = 0.5) -> float:
248
+ """Get the average number of days between purchases at a given percentile.
249
+
250
+ Args:
251
+ percentile (float): The percentile to get the average number of days between purchases at.
252
+
253
+ Returns:
254
+ float: The average number of days between purchases at the given percentile.
255
+ """
256
+ return self.purchase_dist_s.quantile(percentile)
257
+
258
+
259
+ class TransactionChurn:
260
+ """A class to plot the churn rate by number of purchases.
261
+
262
+ Attributes:
263
+ purchase_dist_df (pd.DataFrame): The churn rate by number of purchases.
264
+ n_unique_customers (int): The number of unique customers in the dataframe.
265
+
266
+ Args:
267
+ df (pd.DataFrame): A dataframe with the transaction data. The dataframe must comply with the
268
+ TransactionItemLevelContract.
269
+ churn_period (float): The number of days to consider a customer churned.
270
+ """
271
+
272
+ def __init__(self, df: pd.DataFrame, churn_period: float) -> None:
273
+ if TransactionItemLevelContract(df).validate() is False:
274
+ raise ValueError("The dataframe does not comply with the TransactionItemLevelContract")
275
+
276
+ purchase_dist_df = df[["customer_id", "transaction_datetime"]].copy()
277
+ # Truncate the transaction_datetime to the day
278
+ purchase_dist_df["transaction_datetime"] = df["transaction_datetime"].dt.floor("D")
279
+ purchase_dist_df = purchase_dist_df.drop_duplicates()
280
+ purchase_dist_df = purchase_dist_df.sort_values(["customer_id", "transaction_datetime"])
281
+ purchase_dist_df["transaction_number"] = purchase_dist_df.groupby("customer_id").cumcount() + 1
282
+
283
+ purchase_dist_df["last_transaction"] = (
284
+ purchase_dist_df.groupby("customer_id")["transaction_datetime"].shift(-1).isna()
285
+ )
286
+ purchase_dist_df["transaction_before_churn_window"] = purchase_dist_df["transaction_datetime"] < (
287
+ purchase_dist_df["transaction_datetime"].max() - pd.Timedelta(days=churn_period)
288
+ )
289
+ purchase_dist_df["churned"] = (
290
+ purchase_dist_df["last_transaction"] & purchase_dist_df["transaction_before_churn_window"]
291
+ )
292
+
293
+ purchase_dist_df = (
294
+ purchase_dist_df[purchase_dist_df["transaction_before_churn_window"]]
295
+ .groupby(["transaction_number"])["churned"]
296
+ .value_counts()
297
+ .unstack()
298
+ )
299
+ purchase_dist_df.columns = ["retained", "churned"]
300
+ purchase_dist_df["churned_pct"] = purchase_dist_df["churned"].div(purchase_dist_df.sum(axis=1))
301
+ self.purchase_dist_df = purchase_dist_df
302
+
303
+ self.n_unique_customers = df["customer_id"].nunique()
304
+
305
+ def plot(
306
+ self,
307
+ cumlative: bool = False,
308
+ ax: Axes | None = None,
309
+ source_text: str = None,
310
+ **kwargs: dict[str, any],
311
+ ) -> SubplotBase:
312
+ """Plot the churn rate by number of purchases.
313
+
314
+ Args:
315
+ cumlative (bool, optional): Whether to plot the cumulative distribution. Defaults to False.
316
+ ax (Axes, optional): The Matplotlib axes to plot the graph on. Defaults to None.
317
+
318
+ Returns:
319
+ SubplotBase: The Matplotlib axes of the plot
320
+ """
321
+ if cumlative:
322
+ cumulative_churn_rate_s = self.purchase_dist_df["churned"].cumsum().div(self.n_unique_customers)
323
+ ax = cumulative_churn_rate_s.plot.area(color=COLORS["green"][500])
324
+ ax.set_xlim(self.purchase_dist_df.index.min(), self.purchase_dist_df.index.max())
325
+ else:
326
+ ax = self.purchase_dist_df["churned_pct"].plot.bar(
327
+ rot=0,
328
+ color=COLORS["green"][500],
329
+ width=0.8,
330
+ **kwargs,
331
+ )
332
+
333
+ standard_graph_styles(ax)
334
+
335
+ # Format y axis as a percent
336
+ ax.yaxis.set_major_formatter(mtick.PercentFormatter(xmax=1.0))
337
+ ax.set_xlabel("Number of Purchases")
338
+ ax.set_ylabel("% Churned")
339
+ ax.set_title("Churn Rate by Number of Purchases")
340
+
341
+ if source_text:
342
+ ax.annotate(
343
+ source_text,
344
+ xy=(-0.1, -0.2),
345
+ xycoords="axes fraction",
346
+ ha="left",
347
+ va="center",
348
+ fontsize=10,
349
+ )
350
+
351
+ return ax
@@ -1,8 +1,9 @@
1
+ from __future__ import annotations
2
+
1
3
  import logging
2
4
  import uuid
3
5
  from dataclasses import dataclass
4
6
  from datetime import date, datetime, time, timedelta
5
- from typing import Self
6
7
 
7
8
  import numpy as np
8
9
  import pandas as pd
@@ -318,7 +319,7 @@ class Simulation:
318
319
  self.transactions = []
319
320
 
320
321
  @classmethod
321
- def from_config_file(cls, seed: int, config_file: str) -> Self:
322
+ def from_config_file(cls, seed: int, config_file: str) -> Simulation:
322
323
  """Create a Simulation from a config file.
323
324
 
324
325
  Args:
@@ -0,0 +1,37 @@
1
+ from matplotlib.axes import Axes
2
+
3
+
4
+ def human_format(num, pos=None, decimals=0, prefix="") -> str:
5
+ """Format a number in a human readable format for Matplotlib.
6
+
7
+ Args:
8
+ num (float): The number to format.
9
+ pos (int, optional): The position. Defaults to None. Only used for Matplotlib compatibility.
10
+ decimals (int, optional): The number of decimals. Defaults to 0.
11
+ prefix (str, optional): The prefix of the returned string, eg '$'. Defaults to "".
12
+
13
+ Returns:
14
+ str: The formatted number.
15
+ """
16
+ magnitude = 0
17
+ while abs(num) >= 1000:
18
+ magnitude += 1
19
+ num /= 1000.0
20
+
21
+ # Add more suffixes if you need them
22
+ return f"{prefix}%.{decimals}f%s" % (num, ["", "K", "M", "G", "T", "P"][magnitude])
23
+
24
+
25
+ def standard_graph_styles(ax: Axes) -> Axes:
26
+ """Apply standard styles to a Matplotlib graph.
27
+
28
+ Args:
29
+ ax (Axes): The graph to apply the styles to.
30
+
31
+ Returns:
32
+ Axes: The graph with the styles applied.
33
+ """
34
+ ax.spines[["top", "right"]].set_visible(False)
35
+ ax.grid(which="major", axis="x", color="#DAD8D7", alpha=0.5, zorder=1)
36
+ ax.grid(which="major", axis="y", color="#DAD8D7", alpha=0.5, zorder=1)
37
+ return ax
@@ -0,0 +1,386 @@
1
+ from matplotlib.colors import ListedColormap
2
+ from matplotlib.colors import LinearSegmentedColormap
3
+
4
+ # Colors from Tailwind CSS
5
+ # https://raw.githubusercontent.com/tailwindlabs/tailwindcss/a1e74f055b13a7ef5775bdd72a77a4d397565016/src/public/colors.js
6
+ COLORS = {
7
+ "slate": {
8
+ 50: "#f8fafc",
9
+ 100: "#f1f5f9",
10
+ 200: "#e2e8f0",
11
+ 300: "#cbd5e1",
12
+ 400: "#94a3b8",
13
+ 500: "#64748b",
14
+ 600: "#475569",
15
+ 700: "#334155",
16
+ 800: "#1e293b",
17
+ 900: "#0f172a",
18
+ 950: "#020617",
19
+ },
20
+ "gray": {
21
+ 50: "#f9fafb",
22
+ 100: "#f3f4f6",
23
+ 200: "#e5e7eb",
24
+ 300: "#d1d5db",
25
+ 400: "#9ca3af",
26
+ 500: "#6b7280",
27
+ 600: "#4b5563",
28
+ 700: "#374151",
29
+ 800: "#1f2937",
30
+ 900: "#111827",
31
+ 950: "#030712",
32
+ },
33
+ "zinc": {
34
+ 50: "#fafafa",
35
+ 100: "#f4f4f5",
36
+ 200: "#e4e4e7",
37
+ 300: "#d4d4d8",
38
+ 400: "#a1a1aa",
39
+ 500: "#71717a",
40
+ 600: "#52525b",
41
+ 700: "#3f3f46",
42
+ 800: "#27272a",
43
+ 900: "#18181b",
44
+ 950: "#09090b",
45
+ },
46
+ "neutral": {
47
+ 50: "#fafafa",
48
+ 100: "#f5f5f5",
49
+ 200: "#e5e5e5",
50
+ 300: "#d4d4d4",
51
+ 400: "#a3a3a3",
52
+ 500: "#737373",
53
+ 600: "#525252",
54
+ 700: "#404040",
55
+ 800: "#262626",
56
+ 900: "#171717",
57
+ 950: "#0a0a0a",
58
+ },
59
+ "stone": {
60
+ 50: "#fafaf9",
61
+ 100: "#f5f5f4",
62
+ 200: "#e7e5e4",
63
+ 300: "#d6d3d1",
64
+ 400: "#a8a29e",
65
+ 500: "#78716c",
66
+ 600: "#57534e",
67
+ 700: "#44403c",
68
+ 800: "#292524",
69
+ 900: "#1c1917",
70
+ 950: "#0c0a09",
71
+ },
72
+ "red": {
73
+ 50: "#fef2f2",
74
+ 100: "#fee2e2",
75
+ 200: "#fecaca",
76
+ 300: "#fca5a5",
77
+ 400: "#f87171",
78
+ 500: "#ef4444",
79
+ 600: "#dc2626",
80
+ 700: "#b91c1c",
81
+ 800: "#991b1b",
82
+ 900: "#7f1d1d",
83
+ 950: "#450a0a",
84
+ },
85
+ "orange": {
86
+ 50: "#fff7ed",
87
+ 100: "#ffedd5",
88
+ 200: "#fed7aa",
89
+ 300: "#fdba74",
90
+ 400: "#fb923c",
91
+ 500: "#f97316",
92
+ 600: "#ea580c",
93
+ 700: "#c2410c",
94
+ 800: "#9a3412",
95
+ 900: "#7c2d12",
96
+ 950: "#431407",
97
+ },
98
+ "amber": {
99
+ 50: "#fffbeb",
100
+ 100: "#fef3c7",
101
+ 200: "#fde68a",
102
+ 300: "#fcd34d",
103
+ 400: "#fbbf24",
104
+ 500: "#f59e0b",
105
+ 600: "#d97706",
106
+ 700: "#b45309",
107
+ 800: "#92400e",
108
+ 900: "#78350f",
109
+ 950: "#451a03",
110
+ },
111
+ "yellow": {
112
+ 50: "#fefce8",
113
+ 100: "#fef9c3",
114
+ 200: "#fef08a",
115
+ 300: "#fde047",
116
+ 400: "#facc15",
117
+ 500: "#eab308",
118
+ 600: "#ca8a04",
119
+ 700: "#a16207",
120
+ 800: "#854d0e",
121
+ 900: "#713f12",
122
+ 950: "#422006",
123
+ },
124
+ "lime": {
125
+ 50: "#f7fee7",
126
+ 100: "#ecfccb",
127
+ 200: "#d9f99d",
128
+ 300: "#bef264",
129
+ 400: "#a3e635",
130
+ 500: "#84cc16",
131
+ 600: "#65a30d",
132
+ 700: "#4d7c0f",
133
+ 800: "#3f6212",
134
+ 900: "#365314",
135
+ 950: "#1a2e05",
136
+ },
137
+ "green": {
138
+ 50: "#f0fdf4",
139
+ 100: "#dcfce7",
140
+ 200: "#bbf7d0",
141
+ 300: "#86efac",
142
+ 400: "#4ade80",
143
+ 500: "#22c55e",
144
+ 600: "#16a34a",
145
+ 700: "#15803d",
146
+ 800: "#166534",
147
+ 900: "#14532d",
148
+ 950: "#052e16",
149
+ },
150
+ "emerald": {
151
+ 50: "#ecfdf5",
152
+ 100: "#d1fae5",
153
+ 200: "#a7f3d0",
154
+ 300: "#6ee7b7",
155
+ 400: "#34d399",
156
+ 500: "#10b981",
157
+ 600: "#059669",
158
+ 700: "#047857",
159
+ 800: "#065f46",
160
+ 900: "#064e3b",
161
+ 950: "#022c22",
162
+ },
163
+ "teal": {
164
+ 50: "#f0fdfa",
165
+ 100: "#ccfbf1",
166
+ 200: "#99f6e4",
167
+ 300: "#5eead4",
168
+ 400: "#2dd4bf",
169
+ 500: "#14b8a6",
170
+ 600: "#0d9488",
171
+ 700: "#0f766e",
172
+ 800: "#115e59",
173
+ 900: "#134e4a",
174
+ 950: "#042f2e",
175
+ },
176
+ "cyan": {
177
+ 50: "#ecfeff",
178
+ 100: "#cffafe",
179
+ 200: "#a5f3fc",
180
+ 300: "#67e8f9",
181
+ 400: "#22d3ee",
182
+ 500: "#06b6d4",
183
+ 600: "#0891b2",
184
+ 700: "#0e7490",
185
+ 800: "#155e75",
186
+ 900: "#164e63",
187
+ 950: "#083344",
188
+ },
189
+ "sky": {
190
+ 50: "#f0f9ff",
191
+ 100: "#e0f2fe",
192
+ 200: "#bae6fd",
193
+ 300: "#7dd3fc",
194
+ 400: "#38bdf8",
195
+ 500: "#0ea5e9",
196
+ 600: "#0284c7",
197
+ 700: "#0369a1",
198
+ 800: "#075985",
199
+ 900: "#0c4a6e",
200
+ 950: "#082f49",
201
+ },
202
+ "blue": {
203
+ 50: "#eff6ff",
204
+ 100: "#dbeafe",
205
+ 200: "#bfdbfe",
206
+ 300: "#93c5fd",
207
+ 400: "#60a5fa",
208
+ 500: "#3b82f6",
209
+ 600: "#2563eb",
210
+ 700: "#1d4ed8",
211
+ 800: "#1e40af",
212
+ 900: "#1e3a8a",
213
+ 950: "#172554",
214
+ },
215
+ "indigo": {
216
+ 50: "#eef2ff",
217
+ 100: "#e0e7ff",
218
+ 200: "#c7d2fe",
219
+ 300: "#a5b4fc",
220
+ 400: "#818cf8",
221
+ 500: "#6366f1",
222
+ 600: "#4f46e5",
223
+ 700: "#4338ca",
224
+ 800: "#3730a3",
225
+ 900: "#312e81",
226
+ 950: "#1e1b4b",
227
+ },
228
+ "violet": {
229
+ 50: "#f5f3ff",
230
+ 100: "#ede9fe",
231
+ 200: "#ddd6fe",
232
+ 300: "#c4b5fd",
233
+ 400: "#a78bfa",
234
+ 500: "#8b5cf6",
235
+ 600: "#7c3aed",
236
+ 700: "#6d28d9",
237
+ 800: "#5b21b6",
238
+ 900: "#4c1d95",
239
+ 950: "#2e1065",
240
+ },
241
+ "purple": {
242
+ 50: "#faf5ff",
243
+ 100: "#f3e8ff",
244
+ 200: "#e9d5ff",
245
+ 300: "#d8b4fe",
246
+ 400: "#c084fc",
247
+ 500: "#a855f7",
248
+ 600: "#9333ea",
249
+ 700: "#7e22ce",
250
+ 800: "#6b21a8",
251
+ 900: "#581c87",
252
+ 950: "#3b0764",
253
+ },
254
+ "fuchsia": {
255
+ 50: "#fdf4ff",
256
+ 100: "#fae8ff",
257
+ 200: "#f5d0fe",
258
+ 300: "#f0abfc",
259
+ 400: "#e879f9",
260
+ 500: "#d946ef",
261
+ 600: "#c026d3",
262
+ 700: "#a21caf",
263
+ 800: "#86198f",
264
+ 900: "#701a75",
265
+ 950: "#4a044e",
266
+ },
267
+ "pink": {
268
+ 50: "#fdf2f8",
269
+ 100: "#fce7f3",
270
+ 200: "#fbcfe8",
271
+ 300: "#f9a8d4",
272
+ 400: "#f472b6",
273
+ 500: "#ec4899",
274
+ 600: "#db2777",
275
+ 700: "#be185d",
276
+ 800: "#9d174d",
277
+ 900: "#831843",
278
+ 950: "#500724",
279
+ },
280
+ "rose": {
281
+ 50: "#fff1f2",
282
+ 100: "#ffe4e6",
283
+ 200: "#fecdd3",
284
+ 300: "#fda4af",
285
+ 400: "#fb7185",
286
+ 500: "#f43f5e",
287
+ 600: "#e11d48",
288
+ 700: "#be123c",
289
+ 800: "#9f1239",
290
+ 900: "#881337",
291
+ 950: "#4c0519",
292
+ },
293
+ }
294
+
295
+
296
+ def get_color_list(name):
297
+ if name not in COLORS:
298
+ raise ValueError(f"Color name {name} not found. Valid colors are: {', '.join(COLORS.keys())}")
299
+ return [COLORS[name][key] for key in COLORS[name].keys()]
300
+
301
+
302
+ def get_listed_cmap(name):
303
+ return ListedColormap(get_color_list(name))
304
+
305
+
306
+ def get_linear_cmap(name):
307
+ return LinearSegmentedColormap.from_list(f"{name}_linear_colormap", get_color_list(name))
308
+
309
+
310
+ def get_base_cmap() -> ListedColormap:
311
+ color_order = [
312
+ "red",
313
+ "orange",
314
+ "yellow",
315
+ "green",
316
+ "teal",
317
+ "sky",
318
+ "indigo",
319
+ "purple",
320
+ "pink",
321
+ "slate",
322
+ "amber",
323
+ "lime",
324
+ "emerald",
325
+ "cyan",
326
+ "blue",
327
+ "violet",
328
+ "fuchsia",
329
+ "rose",
330
+ ]
331
+ color_numbers = [500, 300, 700]
332
+ colors = []
333
+ for color_number in color_numbers:
334
+ for color in color_order:
335
+ colors.append(COLORS[color][color_number])
336
+
337
+ return ListedColormap(colors)
338
+
339
+
340
+ slate_cmap = get_listed_cmap("slate")
341
+ gray_cmap = get_listed_cmap("gray")
342
+ zinc_cmap = get_listed_cmap("zinc")
343
+ neutral_cmap = get_listed_cmap("neutral")
344
+ stone_cmap = get_listed_cmap("stone")
345
+ red_cmap = get_listed_cmap("red")
346
+ orange_cmap = get_listed_cmap("orange")
347
+ amber_cmap = get_listed_cmap("amber")
348
+ yellow_cmap = get_listed_cmap("yellow")
349
+ lime_cmap = get_listed_cmap("lime")
350
+ green_cmap = get_listed_cmap("green")
351
+ emerald_cmap = get_listed_cmap("emerald")
352
+ teal_cmap = get_listed_cmap("teal")
353
+ cyan_cmap = get_listed_cmap("cyan")
354
+ sky_cmap = get_listed_cmap("sky")
355
+ blue_cmap = get_listed_cmap("blue")
356
+ indigo_cmap = get_listed_cmap("indigo")
357
+ violet_cmap = get_listed_cmap("violet")
358
+ purple_cmap = get_listed_cmap("purple")
359
+ fuchsia_cmap = get_listed_cmap("fuchsia")
360
+ pink_cmap = get_listed_cmap("pink")
361
+
362
+
363
+ slate_linear_cmap = get_linear_cmap("slate")
364
+ gray_linear_cmap = get_linear_cmap("gray")
365
+ zinc_linear_cmap = get_linear_cmap("zinc")
366
+ neutral_linear_cmap = get_linear_cmap("neutral")
367
+ stone_linear_cmap = get_linear_cmap("stone")
368
+ red_linear_cmap = get_linear_cmap("red")
369
+ orange_linear_cmap = get_linear_cmap("orange")
370
+ amber_linear_cmap = get_linear_cmap("amber")
371
+ yellow_linear_cmap = get_linear_cmap("yellow")
372
+ lime_linear_cmap = get_linear_cmap("lime")
373
+ green_linear_cmap = get_linear_cmap("green")
374
+ emerald_linear_cmap = get_linear_cmap("emerald")
375
+ teal_linear_cmap = get_linear_cmap("teal")
376
+ cyan_linear_cmap = get_linear_cmap("cyan")
377
+ sky_linear_cmap = get_linear_cmap("sky")
378
+ blue_linear_cmap = get_linear_cmap("blue")
379
+ indigo_linear_cmap = get_linear_cmap("indigo")
380
+ violet_linear_cmap = get_linear_cmap("violet")
381
+ purple_linear_cmap = get_linear_cmap("purple")
382
+ fuchsia_linear_cmap = get_linear_cmap("fuchsia")
383
+ pink_linear_cmap = get_linear_cmap("pink")
384
+
385
+
386
+ base_cmap = get_base_cmap()
File without changes