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.
- {pyretailscience-0.1.2 → pyretailscience-0.2.0}/PKG-INFO +1 -1
- {pyretailscience-0.1.2 → pyretailscience-0.2.0}/pyproject.toml +1 -1
- pyretailscience-0.2.0/pyretailscience/customer.py +351 -0
- {pyretailscience-0.1.2 → pyretailscience-0.2.0}/pyretailscience/data/simulation.py +3 -2
- pyretailscience-0.2.0/pyretailscience/style/graph_utils.py +37 -0
- pyretailscience-0.2.0/pyretailscience/style/tailwind.py +386 -0
- {pyretailscience-0.1.2 → pyretailscience-0.2.0}/LICENSE +0 -0
- {pyretailscience-0.1.2 → pyretailscience-0.2.0}/README.md +0 -0
- {pyretailscience-0.1.2 → pyretailscience-0.2.0}/pyretailscience/__init__.py +0 -0
- {pyretailscience-0.1.2 → pyretailscience-0.2.0}/pyretailscience/data/__init__.py +0 -0
- {pyretailscience-0.1.2 → pyretailscience-0.2.0}/pyretailscience/data/cli.py +0 -0
- {pyretailscience-0.1.2 → pyretailscience-0.2.0}/pyretailscience/data/contracts.py +0 -0
|
@@ -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) ->
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|