chartout 0.0.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.
- chartout-0.0.0/PKG-INFO +38 -0
- chartout-0.0.0/README.md +4 -0
- chartout-0.0.0/__init__.py +2 -0
- chartout-0.0.0/cart.py +84 -0
- chartout-0.0.0/chartout.egg-info/PKG-INFO +38 -0
- chartout-0.0.0/chartout.egg-info/SOURCES.txt +14 -0
- chartout-0.0.0/chartout.egg-info/dependency_links.txt +1 -0
- chartout-0.0.0/chartout.egg-info/top_level.txt +1 -0
- chartout-0.0.0/develop.py +120 -0
- chartout-0.0.0/models.py +54 -0
- chartout-0.0.0/py.typed +0 -0
- chartout-0.0.0/setup.cfg +4 -0
- chartout-0.0.0/setup.py +47 -0
- chartout-0.0.0/store.py +135 -0
- chartout-0.0.0/support.py +124 -0
- chartout-0.0.0/texture.py +172 -0
chartout-0.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: chartout
|
|
3
|
+
Version: 0.0.0
|
|
4
|
+
Summary: Chartout - Print Your Insights, Anytime, Anywhere
|
|
5
|
+
Home-page: https://chartout.io
|
|
6
|
+
Download-URL: https://download_url.com
|
|
7
|
+
Author: mattijn
|
|
8
|
+
Author-email: info@hoekinsights.nl
|
|
9
|
+
License: GPL-3.0 license
|
|
10
|
+
Project-URL: Source, https://github.com/hoekinsights/chartout
|
|
11
|
+
Keywords: python3 python reserve reserver reserved
|
|
12
|
+
Classifier: Development Status :: 1 - Planning
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.6
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.7
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Requires-Python: >=3.6
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
Dynamic: author
|
|
23
|
+
Dynamic: author-email
|
|
24
|
+
Dynamic: classifier
|
|
25
|
+
Dynamic: description
|
|
26
|
+
Dynamic: description-content-type
|
|
27
|
+
Dynamic: download-url
|
|
28
|
+
Dynamic: home-page
|
|
29
|
+
Dynamic: keywords
|
|
30
|
+
Dynamic: license
|
|
31
|
+
Dynamic: project-url
|
|
32
|
+
Dynamic: requires-python
|
|
33
|
+
Dynamic: summary
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
## Overview
|
|
37
|
+
chartout is a Python library for doing awesome things.
|
|
38
|
+
This name has been reserved using [Reserver](https://github.com/openscilab/reserver).
|
chartout-0.0.0/README.md
ADDED
chartout-0.0.0/cart.py
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
from dataclasses import dataclass, asdict
|
|
2
|
+
from typing import Optional, List
|
|
3
|
+
from .models import CartItem
|
|
4
|
+
from .support import viz_to_cart_item, VizLike
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass
|
|
8
|
+
class Cart:
|
|
9
|
+
"""A class to represent a shopping cart containing CartItems.
|
|
10
|
+
|
|
11
|
+
Attributes:
|
|
12
|
+
items (List[CartItem]): A list of items in the cart, which can be
|
|
13
|
+
CartItem instances.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
items: List[CartItem]
|
|
17
|
+
|
|
18
|
+
def __init__(self, items: Optional[List[CartItem]] = None):
|
|
19
|
+
"""Initialize the Cart with an optional list of items.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
items (Optional[List[CartItem]]): A list of dictionaries conforming to
|
|
23
|
+
CartItem to initialize the cart with.
|
|
24
|
+
"""
|
|
25
|
+
self.items: List[CartItem] = []
|
|
26
|
+
if items is not None:
|
|
27
|
+
self.add(items)
|
|
28
|
+
|
|
29
|
+
def add(self, item: CartItem | List[CartItem] | VizLike) -> None:
|
|
30
|
+
"""Add a CartItem, a list of CartItems, or a VizLike to the cart.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
item (CartItem | List[CartItem] | VizLike): A CartItem, a list of CartItems, or a VizLike to be added to the cart.
|
|
34
|
+
|
|
35
|
+
Raises:
|
|
36
|
+
ValueError: If an item in the list is not a valid CartItem or VizLike.
|
|
37
|
+
"""
|
|
38
|
+
if isinstance(item, VizLike):
|
|
39
|
+
# Convert VizLike to CartItem
|
|
40
|
+
item = viz_to_cart_item(item)
|
|
41
|
+
self.items.append(item)
|
|
42
|
+
elif isinstance(item, list):
|
|
43
|
+
for i in item:
|
|
44
|
+
if isinstance(i, dict):
|
|
45
|
+
i = CartItem(**i)
|
|
46
|
+
self.items.append(i)
|
|
47
|
+
else:
|
|
48
|
+
if isinstance(item, dict):
|
|
49
|
+
item = CartItem(**item)
|
|
50
|
+
self.items.append(item)
|
|
51
|
+
|
|
52
|
+
def remove(self, *, index: int) -> None:
|
|
53
|
+
"""Remove a CartItem from the cart by its index.
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
index (int): The index of the item to be removed from the cart.
|
|
57
|
+
|
|
58
|
+
Raises:
|
|
59
|
+
IndexError: If the index is out of range.
|
|
60
|
+
"""
|
|
61
|
+
if index < 0 or index >= len(self.items):
|
|
62
|
+
raise IndexError("Index out of range.")
|
|
63
|
+
del self.items[index]
|
|
64
|
+
|
|
65
|
+
def __repr__(self) -> str:
|
|
66
|
+
"""Return a string representation of the Cart.
|
|
67
|
+
|
|
68
|
+
Returns:
|
|
69
|
+
str: A string representation of the Cart, including item IDs, names, quantities, and texture information.
|
|
70
|
+
"""
|
|
71
|
+
if not self.items:
|
|
72
|
+
return "Cart(empty)"
|
|
73
|
+
|
|
74
|
+
items_repr = "\n".join(
|
|
75
|
+
f" - ID: {item.id}\n"
|
|
76
|
+
f" Name: {item.name or 'Unnamed'}\n"
|
|
77
|
+
f" Quantity: {item.quantity}\n"
|
|
78
|
+
f" Textures: [{', '.join(texture.id for texture in item.textures)}]"
|
|
79
|
+
for item in self.items
|
|
80
|
+
)
|
|
81
|
+
return f"Cart:\n{items_repr}"
|
|
82
|
+
|
|
83
|
+
def to_dict(self):
|
|
84
|
+
return asdict(self)
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: chartout
|
|
3
|
+
Version: 0.0.0
|
|
4
|
+
Summary: Chartout - Print Your Insights, Anytime, Anywhere
|
|
5
|
+
Home-page: https://chartout.io
|
|
6
|
+
Download-URL: https://download_url.com
|
|
7
|
+
Author: mattijn
|
|
8
|
+
Author-email: info@hoekinsights.nl
|
|
9
|
+
License: GPL-3.0 license
|
|
10
|
+
Project-URL: Source, https://github.com/hoekinsights/chartout
|
|
11
|
+
Keywords: python3 python reserve reserver reserved
|
|
12
|
+
Classifier: Development Status :: 1 - Planning
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.6
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.7
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Requires-Python: >=3.6
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
Dynamic: author
|
|
23
|
+
Dynamic: author-email
|
|
24
|
+
Dynamic: classifier
|
|
25
|
+
Dynamic: description
|
|
26
|
+
Dynamic: description-content-type
|
|
27
|
+
Dynamic: download-url
|
|
28
|
+
Dynamic: home-page
|
|
29
|
+
Dynamic: keywords
|
|
30
|
+
Dynamic: license
|
|
31
|
+
Dynamic: project-url
|
|
32
|
+
Dynamic: requires-python
|
|
33
|
+
Dynamic: summary
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
## Overview
|
|
37
|
+
chartout is a Python library for doing awesome things.
|
|
38
|
+
This name has been reserved using [Reserver](https://github.com/openscilab/reserver).
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
__init__.py
|
|
3
|
+
cart.py
|
|
4
|
+
develop.py
|
|
5
|
+
models.py
|
|
6
|
+
py.typed
|
|
7
|
+
setup.py
|
|
8
|
+
store.py
|
|
9
|
+
support.py
|
|
10
|
+
texture.py
|
|
11
|
+
chartout.egg-info/PKG-INFO
|
|
12
|
+
chartout.egg-info/SOURCES.txt
|
|
13
|
+
chartout.egg-info/dependency_links.txt
|
|
14
|
+
chartout.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
def altair_heatmap():
|
|
2
|
+
import altair as alt
|
|
3
|
+
import numpy as np
|
|
4
|
+
import pandas as pd
|
|
5
|
+
|
|
6
|
+
# Compute x^2 + y^2 across a 2D grid
|
|
7
|
+
x, y = np.meshgrid(range(-5, 5), range(-5, 5))
|
|
8
|
+
z = x**2 + y**2
|
|
9
|
+
|
|
10
|
+
# Convert this grid to columnar data expected by Altair
|
|
11
|
+
source = pd.DataFrame({"x": x.ravel(), "y": y.ravel(), "z": z.ravel()})
|
|
12
|
+
|
|
13
|
+
chart = alt.Chart(source).mark_rect().encode(x="x:O", y="y:O", color="z:Q")
|
|
14
|
+
return chart
|
|
15
|
+
|
|
16
|
+
def altair_comet():
|
|
17
|
+
import altair as alt
|
|
18
|
+
import vega_datasets
|
|
19
|
+
|
|
20
|
+
chart = alt.Chart(
|
|
21
|
+
vega_datasets.data.barley.url,
|
|
22
|
+
title='Barley Yield comparison between 1932 and 1931'
|
|
23
|
+
).mark_trail().encode(
|
|
24
|
+
alt.X('year:O').title(None),
|
|
25
|
+
alt.Y('variety:N').title('Variety'),
|
|
26
|
+
alt.Size('yield:Q')
|
|
27
|
+
.scale(range=[0, 12])
|
|
28
|
+
.legend(values=[20, 60])
|
|
29
|
+
.title('Barley Yield (bushels/acre)'),
|
|
30
|
+
alt.Color('delta:Q')
|
|
31
|
+
.scale(domainMid=0)
|
|
32
|
+
.title('Yield Delta (%)'),
|
|
33
|
+
alt.Tooltip(['year:O', 'yield:Q']),
|
|
34
|
+
alt.Column('site:N').title('Site')
|
|
35
|
+
).transform_pivot(
|
|
36
|
+
"year",
|
|
37
|
+
value="yield",
|
|
38
|
+
groupby=["variety", "site"]
|
|
39
|
+
).transform_fold(
|
|
40
|
+
["1931", "1932"],
|
|
41
|
+
as_=["year", "yield"]
|
|
42
|
+
).transform_calculate(
|
|
43
|
+
calculate="datum['1932'] - datum['1931']",
|
|
44
|
+
as_="delta"
|
|
45
|
+
).configure_legend(
|
|
46
|
+
orient='bottom',
|
|
47
|
+
direction='horizontal'
|
|
48
|
+
).configure_view(
|
|
49
|
+
stroke=None
|
|
50
|
+
)
|
|
51
|
+
return chart
|
|
52
|
+
|
|
53
|
+
def altair_map_selection():
|
|
54
|
+
import altair as alt
|
|
55
|
+
from vega_datasets import data
|
|
56
|
+
import geopandas as gpd
|
|
57
|
+
|
|
58
|
+
# load data
|
|
59
|
+
gdf_quakies = gpd.read_file(data.earthquakes.url, driver="GeoJSON")
|
|
60
|
+
gdf_world = gpd.read_file(data.world_110m.url, driver="TopoJSON")
|
|
61
|
+
|
|
62
|
+
# defintion for interactive brush
|
|
63
|
+
brush = alt.selection_interval(
|
|
64
|
+
encodings=["longitude"], empty=False, value={"longitude": [-50, -110]}
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
# world disk
|
|
68
|
+
sphere = alt.Chart(alt.sphere()).mark_geoshape(
|
|
69
|
+
fill="transparent", stroke="lightgray", strokeWidth=1
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
# countries as shapes
|
|
73
|
+
world = alt.Chart(gdf_world).mark_geoshape(
|
|
74
|
+
fill="lightgray", stroke="white", strokeWidth=0.1
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
# earthquakes as dots on map
|
|
78
|
+
quakes = (
|
|
79
|
+
alt.Chart(gdf_quakies)
|
|
80
|
+
.transform_calculate(
|
|
81
|
+
lon="datum.geometry.coordinates[0]",
|
|
82
|
+
lat="datum.geometry.coordinates[1]",
|
|
83
|
+
)
|
|
84
|
+
.mark_circle(opacity=0.35, tooltip=True)
|
|
85
|
+
.encode(
|
|
86
|
+
longitude="lon:Q",
|
|
87
|
+
latitude="lat:Q",
|
|
88
|
+
color=alt.when(brush)
|
|
89
|
+
.then(alt.value("goldenrod"))
|
|
90
|
+
.otherwise(alt.value("steelblue")),
|
|
91
|
+
size=alt.Size("mag:Q").scale(
|
|
92
|
+
type="pow", range=[1, 1000], domain=[0, 7], exponent=4
|
|
93
|
+
),
|
|
94
|
+
)
|
|
95
|
+
.add_params(brush)
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
# combine layers for the map
|
|
99
|
+
left_map = alt.layer(sphere, world, quakes).project(type="mercator")
|
|
100
|
+
|
|
101
|
+
# histogram of binned earthquakes
|
|
102
|
+
bars = (
|
|
103
|
+
alt.Chart(gdf_quakies)
|
|
104
|
+
.mark_bar()
|
|
105
|
+
.encode(
|
|
106
|
+
x=alt.X("mag:Q").bin(extent=[0, 7]),
|
|
107
|
+
y="count(mag):Q",
|
|
108
|
+
color=alt.value("steelblue"),
|
|
109
|
+
)
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
# filtered earthquakes
|
|
113
|
+
bars_overlay = bars.encode(color=alt.value("goldenrod")).transform_filter(brush)
|
|
114
|
+
|
|
115
|
+
# combine layers for histogram
|
|
116
|
+
right_bars = alt.layer(bars, bars_overlay)
|
|
117
|
+
|
|
118
|
+
# vertical concatenate map and bars
|
|
119
|
+
chart = left_map | right_bars
|
|
120
|
+
return chart
|
chartout-0.0.0/models.py
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
from dataclasses import dataclass, field, asdict
|
|
2
|
+
from typing import List, Union, Optional, Dict, Any
|
|
3
|
+
|
|
4
|
+
# Data Classes
|
|
5
|
+
@dataclass
|
|
6
|
+
class Texture:
|
|
7
|
+
id: str
|
|
8
|
+
content: Union[str, Any] # 'Any' can be used for alt.Chart or other types
|
|
9
|
+
|
|
10
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
11
|
+
"""Convert the Texture instance to a dictionary."""
|
|
12
|
+
return asdict(self)
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class StoreItem:
|
|
16
|
+
id: str
|
|
17
|
+
name: Optional[str] = None
|
|
18
|
+
textures: List[Texture] = field(default_factory=list)
|
|
19
|
+
|
|
20
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
21
|
+
"""Convert the StoreItem instance to a dictionary."""
|
|
22
|
+
return asdict(self)
|
|
23
|
+
|
|
24
|
+
@dataclass
|
|
25
|
+
class CartItem(StoreItem):
|
|
26
|
+
quantity: int = 1
|
|
27
|
+
|
|
28
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
29
|
+
"""Convert the CartItem instance to a dictionary."""
|
|
30
|
+
return asdict(self)
|
|
31
|
+
|
|
32
|
+
@dataclass
|
|
33
|
+
class ActiveItem(StoreItem):
|
|
34
|
+
textures: List[Texture] = field(default_factory=list)
|
|
35
|
+
|
|
36
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
37
|
+
"""Convert the ActiveItem instance to a dictionary."""
|
|
38
|
+
return asdict(self)
|
|
39
|
+
|
|
40
|
+
@dataclass
|
|
41
|
+
class ActiveTexture:
|
|
42
|
+
texture: bytes # Equivalent to Uint8Array in TypeScript
|
|
43
|
+
|
|
44
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
45
|
+
"""Convert the ActiveTexture instance to a dictionary."""
|
|
46
|
+
return asdict(self)
|
|
47
|
+
|
|
48
|
+
@dataclass
|
|
49
|
+
class InitViz:
|
|
50
|
+
images: Dict[int, bytes] = field(default_factory=dict)
|
|
51
|
+
|
|
52
|
+
def to_dict(self) -> Dict[int, bytes]:
|
|
53
|
+
"""Convert the InitViz instance to a dictionary."""
|
|
54
|
+
return self.images
|
chartout-0.0.0/py.typed
ADDED
|
File without changes
|
chartout-0.0.0/setup.cfg
ADDED
chartout-0.0.0/setup.py
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
|
|
2
|
+
import sys
|
|
3
|
+
|
|
4
|
+
try:
|
|
5
|
+
from setuptools import setup
|
|
6
|
+
except ImportError:
|
|
7
|
+
from distutils.core import setup
|
|
8
|
+
|
|
9
|
+
# invalid email
|
|
10
|
+
# download url
|
|
11
|
+
# url
|
|
12
|
+
# project urls
|
|
13
|
+
|
|
14
|
+
setup(
|
|
15
|
+
name ="chartout",
|
|
16
|
+
packages=[".",],
|
|
17
|
+
version='0.0.0',
|
|
18
|
+
description="Chartout - Print Your Insights, Anytime, Anywhere",
|
|
19
|
+
long_description="""
|
|
20
|
+
## Overview
|
|
21
|
+
chartout is a Python library for doing awesome things.
|
|
22
|
+
This name has been reserved using [Reserver](https://github.com/openscilab/reserver).
|
|
23
|
+
""",
|
|
24
|
+
long_description_content_type='text/markdown',
|
|
25
|
+
author="mattijn",
|
|
26
|
+
author_email="info@hoekinsights.nl",
|
|
27
|
+
url="https://chartout.io",
|
|
28
|
+
download_url="https://download_url.com",
|
|
29
|
+
keywords="python3 python reserve reserver reserved",
|
|
30
|
+
project_urls={
|
|
31
|
+
'Source':"https://github.com/hoekinsights/chartout",
|
|
32
|
+
},
|
|
33
|
+
install_requires="",
|
|
34
|
+
python_requires='>=3.6',
|
|
35
|
+
classifiers=[
|
|
36
|
+
'Development Status :: 1 - Planning',
|
|
37
|
+
'Programming Language :: Python :: 3.6',
|
|
38
|
+
'Programming Language :: Python :: 3.7',
|
|
39
|
+
'Programming Language :: Python :: 3.8',
|
|
40
|
+
'Programming Language :: Python :: 3.9',
|
|
41
|
+
'Programming Language :: Python :: 3.10',
|
|
42
|
+
'Programming Language :: Python :: 3.11',
|
|
43
|
+
'Programming Language :: Python :: 3.12',
|
|
44
|
+
],
|
|
45
|
+
license="GPL-3.0 license",
|
|
46
|
+
)
|
|
47
|
+
|
chartout-0.0.0/store.py
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import pathlib
|
|
3
|
+
import urllib.request
|
|
4
|
+
from typing import Any, Dict, Optional, Union
|
|
5
|
+
|
|
6
|
+
import anywidget
|
|
7
|
+
import traitlets
|
|
8
|
+
from traitlets import TraitType
|
|
9
|
+
|
|
10
|
+
from .cart import Cart
|
|
11
|
+
from .models import ActiveItem, CartItem, InitViz
|
|
12
|
+
from .support import (
|
|
13
|
+
VizLike,
|
|
14
|
+
is_viz_like,
|
|
15
|
+
viz_to_active_item,
|
|
16
|
+
viz_to_init_viz,
|
|
17
|
+
cart_item_to_active_item,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
# Helper Classes
|
|
22
|
+
class MemoryViewTrait(TraitType):
|
|
23
|
+
"""A trait type that accepts memoryview objects and converts them to bytes."""
|
|
24
|
+
|
|
25
|
+
def validate(self, obj, value):
|
|
26
|
+
if isinstance(value, memoryview):
|
|
27
|
+
return bytes(value)
|
|
28
|
+
elif isinstance(value, (bytes, bytearray)):
|
|
29
|
+
return value
|
|
30
|
+
else:
|
|
31
|
+
self.error(obj, value)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
# Main Classes
|
|
35
|
+
class Store(anywidget.AnyWidget):
|
|
36
|
+
"""A class representing a store widget for managing cart items.
|
|
37
|
+
|
|
38
|
+
This class provides functionality to manage the state of a shopping cart,
|
|
39
|
+
including adding, removing, and serializing cart items. It uses traitlets
|
|
40
|
+
for automatic syncing with the front-end and supports JSON serialization
|
|
41
|
+
for easy data interchange.
|
|
42
|
+
|
|
43
|
+
Attributes:
|
|
44
|
+
cart (Cart): An instance of the Cart class containing items currently in the cart.
|
|
45
|
+
active (Dict[str, Any]): A dictionary representing the active item state.
|
|
46
|
+
init (Dict[str, Any]): A read-only dictionary representing the initial
|
|
47
|
+
state of the store.
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
# Paths for JavaScript and CSS
|
|
51
|
+
_esm = pathlib.Path("../chartout-app/bundle/Widget.js")
|
|
52
|
+
# _css = pathlib.Path("../chartout-app/bundle/styles.css")
|
|
53
|
+
|
|
54
|
+
cart = traitlets.List(
|
|
55
|
+
trait=traitlets.Dict(
|
|
56
|
+
key_trait=traitlets.Unicode(), value_trait=traitlets.Any()
|
|
57
|
+
),
|
|
58
|
+
allow_none=True,
|
|
59
|
+
default_value=None,
|
|
60
|
+
).tag(sync=True)
|
|
61
|
+
|
|
62
|
+
active_item = traitlets.Dict(
|
|
63
|
+
key_trait=traitlets.Unicode(), value_trait=traitlets.Any(), allow_none=True
|
|
64
|
+
).tag(sync=True)
|
|
65
|
+
|
|
66
|
+
active_texture = traitlets.Dict(
|
|
67
|
+
key_trait=traitlets.Unicode(),
|
|
68
|
+
value_trait=MemoryViewTrait(),
|
|
69
|
+
allow_none=True,
|
|
70
|
+
default_value=None,
|
|
71
|
+
read_only=False,
|
|
72
|
+
).tag(sync=True)
|
|
73
|
+
|
|
74
|
+
init_viz = traitlets.Dict(
|
|
75
|
+
key_trait=traitlets.Int(), value_trait=traitlets.Bytes(), allow_none=True
|
|
76
|
+
).tag(sync=True)
|
|
77
|
+
|
|
78
|
+
def __init__(self, item: Optional[Union[Cart, VizLike]] = None, **kwargs):
|
|
79
|
+
"""Initialize the Store with an optional cart or a valid Altair chart."""
|
|
80
|
+
super().__init__(**kwargs)
|
|
81
|
+
self.active_texture = None
|
|
82
|
+
if isinstance(item, Cart):
|
|
83
|
+
self.cart = item.to_dict()["items"]
|
|
84
|
+
self.init_viz = {}
|
|
85
|
+
self.active_item = (
|
|
86
|
+
cart_item_to_active_item(self.cart[0]) if len(self.cart) > 0 else None
|
|
87
|
+
)
|
|
88
|
+
elif is_viz_like(item):
|
|
89
|
+
self.cart = []
|
|
90
|
+
init_viz_obj = viz_to_init_viz(item)
|
|
91
|
+
self.init_viz = init_viz_obj.to_dict()
|
|
92
|
+
self.active_item = viz_to_active_item(init_viz_obj).to_dict()
|
|
93
|
+
elif item is not None:
|
|
94
|
+
raise TypeError("item must be of type Cart or VizLike.")
|
|
95
|
+
else:
|
|
96
|
+
self.cart = []
|
|
97
|
+
self.init_viz = {}
|
|
98
|
+
self.active_item = None
|
|
99
|
+
|
|
100
|
+
def to_json(self):
|
|
101
|
+
"""Serialize the widget's state to a JSON-compatible dictionary."""
|
|
102
|
+
return {
|
|
103
|
+
"cart": self.cart,
|
|
104
|
+
"active_item": self.active_item,
|
|
105
|
+
"init_viz": self.init_viz,
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
def from_json(self, data: Dict[str, Any]):
|
|
109
|
+
"""Deserialize JSON data into the Store."""
|
|
110
|
+
if "cart" in data:
|
|
111
|
+
self.cart = Cart() # Initialize a new Cart instance
|
|
112
|
+
self.cart.items = [CartItem(**item) for item in data["cart"]]
|
|
113
|
+
if "active" in data:
|
|
114
|
+
self.active_item = ActiveItem(**data["active"])
|
|
115
|
+
if "init_viz" in data:
|
|
116
|
+
self.init_viz = InitViz(images=data["init_viz"]).to_dict()
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
# Functions
|
|
120
|
+
def customizables(debug: bool = False) -> Any:
|
|
121
|
+
"""Retrieve a JSON object from the Chartout API for customizables based on category."""
|
|
122
|
+
if debug:
|
|
123
|
+
url = "http://127.0.0.1:8000/v1/products/"
|
|
124
|
+
else:
|
|
125
|
+
url = "https://api.chartout.io/v1/products/"
|
|
126
|
+
try:
|
|
127
|
+
with urllib.request.urlopen(url) as response:
|
|
128
|
+
if response.status != 200:
|
|
129
|
+
raise Exception(f"Error fetching data: {response.status}")
|
|
130
|
+
data = response.read()
|
|
131
|
+
return json.loads(data)
|
|
132
|
+
except urllib.error.URLError as e:
|
|
133
|
+
raise ConnectionError(f"Failed to connect to {url}: {e.reason}")
|
|
134
|
+
except json.JSONDecodeError as e:
|
|
135
|
+
raise ValueError(f"Failed to parse JSON response: {e.msg}")
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
import sys
|
|
3
|
+
import io
|
|
4
|
+
from typing import TYPE_CHECKING, Any, TypeVar, Optional, Dict
|
|
5
|
+
import uuid
|
|
6
|
+
import hashlib
|
|
7
|
+
import os
|
|
8
|
+
import time
|
|
9
|
+
|
|
10
|
+
# Conditional imports for type checking
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
if sys.version_info >= (3, 10):
|
|
13
|
+
from typing import TypeGuard
|
|
14
|
+
else:
|
|
15
|
+
from typing_extensions import TypeGuard
|
|
16
|
+
import altair as alt
|
|
17
|
+
|
|
18
|
+
# Import models
|
|
19
|
+
from .models import ActiveItem, Texture, InitViz, CartItem
|
|
20
|
+
|
|
21
|
+
# Define a new type variable for VizLike
|
|
22
|
+
VizLike = TypeVar("VizLike", bound=Any)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
# Helper Functions
|
|
26
|
+
def get_altair() -> Any:
|
|
27
|
+
"""Get altair module (if already imported - else return None)."""
|
|
28
|
+
return sys.modules.get("altair", None)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def is_altair_chart(chart: Any) -> TypeGuard[alt.typing.ChartType]:
|
|
32
|
+
"""Check whether `chart` is an Altair Chart without importing altair."""
|
|
33
|
+
return (alt := get_altair()) is not None and alt.typing.is_chart_type(chart)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def chart_to_png(chart: Any) -> bytes:
|
|
37
|
+
"""Convert an Altair chart to PNG byte data."""
|
|
38
|
+
if is_altair_chart(chart):
|
|
39
|
+
byte_stream = io.BytesIO()
|
|
40
|
+
chart.save(byte_stream, format="png", scale_factor=2, ppi=300)
|
|
41
|
+
byte_stream.seek(0)
|
|
42
|
+
return byte_stream.getvalue()
|
|
43
|
+
else:
|
|
44
|
+
msg = f"The provided DataViz object is not supported. Got: {type(chart)}"
|
|
45
|
+
raise TypeError(msg)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
# Main Functions
|
|
49
|
+
def is_viz_like(viz: VizLike) -> TypeGuard[VizLike]:
|
|
50
|
+
"""Check whether `viz` is a valid Altair chart."""
|
|
51
|
+
return is_altair_chart(viz)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def viz_to_active_item(init_viz: InitViz) -> ActiveItem:
|
|
55
|
+
"""Convert an InitViz item to an ActiveItem."""
|
|
56
|
+
# Assuming the first image in the InitViz is used for the ActiveItem
|
|
57
|
+
first_image_index = next(iter(init_viz.images))
|
|
58
|
+
png_data = init_viz.images[first_image_index]
|
|
59
|
+
|
|
60
|
+
return ActiveItem(
|
|
61
|
+
name="Canvas",
|
|
62
|
+
id="canvas_10x10",
|
|
63
|
+
textures=[
|
|
64
|
+
Texture(
|
|
65
|
+
id="canvas_10x10_texture",
|
|
66
|
+
content=png_data
|
|
67
|
+
)
|
|
68
|
+
]
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def viz_to_init_viz(viz: VizLike) -> InitViz:
|
|
73
|
+
"""Convert a VizLike item to an InitViz."""
|
|
74
|
+
images = {0: chart_to_png(viz)}
|
|
75
|
+
return InitViz(images=images)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def cart_item_to_active_item(cart_item: Dict[str, Any]) -> ActiveItem:
|
|
79
|
+
"""Convert a CartItem to an ActiveItem."""
|
|
80
|
+
return ActiveItem(
|
|
81
|
+
id=cart_item['id'],
|
|
82
|
+
name=cart_item.get('name'),
|
|
83
|
+
textures=cart_item['textures']
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def viz_to_cart_item(viz: VizLike) -> CartItem:
|
|
88
|
+
"""Convert a VizLike item to a CartItem."""
|
|
89
|
+
# Convert the VizLike object to PNG bytes
|
|
90
|
+
png_data = chart_to_png(viz)
|
|
91
|
+
|
|
92
|
+
# Create a Texture instance for the CartItem
|
|
93
|
+
texture = Texture(
|
|
94
|
+
id="my_canvas_texture",
|
|
95
|
+
content=png_data
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
# Create and return a CartItem instance
|
|
99
|
+
return CartItem(
|
|
100
|
+
id="my_canvas_id",
|
|
101
|
+
name="VizLike Item",
|
|
102
|
+
textures=[texture],
|
|
103
|
+
quantity=1
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def generate_external_id() -> str:
|
|
108
|
+
"""Generate a unique external ID for order creation."""
|
|
109
|
+
# Generate a UUID
|
|
110
|
+
unique_id = uuid.uuid4().hex
|
|
111
|
+
|
|
112
|
+
# Get machine-specific information (e.g., hostname)
|
|
113
|
+
machine_info = os.uname().nodename
|
|
114
|
+
|
|
115
|
+
# Hash the machine information to keep it consistent and anonymized
|
|
116
|
+
machine_hash = hashlib.sha256(machine_info.encode()).hexdigest()
|
|
117
|
+
|
|
118
|
+
# Get the current timestamp
|
|
119
|
+
timestamp = int(time.time())
|
|
120
|
+
|
|
121
|
+
# Combine all parts to form the external_id
|
|
122
|
+
external_id = f"{unique_id}-{machine_hash[:8]}-{timestamp}"
|
|
123
|
+
|
|
124
|
+
return external_id
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
from typing import Any, Dict, Union, List
|
|
2
|
+
import io
|
|
3
|
+
from PIL import Image, ImageDraw
|
|
4
|
+
|
|
5
|
+
# Import from other modules
|
|
6
|
+
from .store import customizables
|
|
7
|
+
from .support import is_viz_like, chart_to_png
|
|
8
|
+
|
|
9
|
+
# Helper Functions
|
|
10
|
+
def process_image_for_source_size(img, source_size, user_modifications=None):
|
|
11
|
+
"""Resize image to fit within source_size while maintaining aspect ratio and alignment."""
|
|
12
|
+
orig_width, orig_height = img.size
|
|
13
|
+
aspect_ratio = orig_width / orig_height
|
|
14
|
+
|
|
15
|
+
# Determine new dimensions to fit within source_size while maintaining aspect ratio
|
|
16
|
+
if source_size["width"] / aspect_ratio <= source_size["height"]:
|
|
17
|
+
fit_width = source_size["width"]
|
|
18
|
+
fit_height = int(fit_width / aspect_ratio)
|
|
19
|
+
else:
|
|
20
|
+
fit_height = source_size["height"]
|
|
21
|
+
fit_width = int(fit_height * aspect_ratio)
|
|
22
|
+
|
|
23
|
+
# Resize the image to fit within the source size
|
|
24
|
+
resized_img = img.resize((fit_width, fit_height))
|
|
25
|
+
|
|
26
|
+
# Apply scale modification if provided
|
|
27
|
+
scale = user_modifications.get("scale", 1.0) if user_modifications else 1.0
|
|
28
|
+
scaled_width = int(fit_width * scale)
|
|
29
|
+
scaled_height = int(fit_height * scale)
|
|
30
|
+
|
|
31
|
+
# Resize the image based on the scale
|
|
32
|
+
scaled_img = resized_img.resize((scaled_width, scaled_height))
|
|
33
|
+
|
|
34
|
+
# Create a new canvas for the source size
|
|
35
|
+
source_canvas = Image.new(
|
|
36
|
+
"RGB", (source_size["width"], source_size["height"]), (255, 255, 255)
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
# Determine alignment
|
|
40
|
+
alignment = user_modifications.get(
|
|
41
|
+
"alignment",
|
|
42
|
+
source_size.get("alignment", {"horizontal": "center", "vertical": "middle"}),
|
|
43
|
+
)
|
|
44
|
+
dx = user_modifications.get("dx", 0)
|
|
45
|
+
dy = user_modifications.get("dy", 0)
|
|
46
|
+
|
|
47
|
+
if alignment["horizontal"] == "left":
|
|
48
|
+
x_pos = 0 + dx
|
|
49
|
+
elif alignment["horizontal"] == "right":
|
|
50
|
+
x_pos = source_size["width"] - scaled_width + dx
|
|
51
|
+
else: # center
|
|
52
|
+
x_pos = (source_size["width"] - scaled_width) // 2 + dx
|
|
53
|
+
|
|
54
|
+
if alignment["vertical"] == "top":
|
|
55
|
+
y_pos = 0 + dy
|
|
56
|
+
elif alignment["vertical"] == "bottom":
|
|
57
|
+
y_pos = source_size["height"] - scaled_height + dy
|
|
58
|
+
else: # middle
|
|
59
|
+
y_pos = (source_size["height"] - scaled_height) // 2 + dy
|
|
60
|
+
|
|
61
|
+
# Paste the scaled image onto the source canvas, clipping if necessary
|
|
62
|
+
source_canvas.paste(scaled_img, (x_pos, y_pos))
|
|
63
|
+
return source_canvas, {
|
|
64
|
+
"scaled_width": scaled_width,
|
|
65
|
+
"scaled_height": scaled_height,
|
|
66
|
+
"x_pos": x_pos,
|
|
67
|
+
"y_pos": y_pos,
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
def position_image_on_canvas(resized_img, canvas_position, user_modifications):
|
|
71
|
+
"""Position the resized image on the canvas using user modifications."""
|
|
72
|
+
# Create a tile canvas
|
|
73
|
+
tile_canvas = Image.new(
|
|
74
|
+
"RGB", (canvas_position["width"], canvas_position["height"]), (255, 255, 255)
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
final_img = resized_img.resize(
|
|
78
|
+
(canvas_position["width"], canvas_position["height"])
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
tile_canvas.paste(final_img, (0, 0))
|
|
82
|
+
|
|
83
|
+
return tile_canvas
|
|
84
|
+
|
|
85
|
+
# Main Functions
|
|
86
|
+
def create_tiled_image(variant):
|
|
87
|
+
"""Create a tiled image from a variant."""
|
|
88
|
+
# Create a blank canvas
|
|
89
|
+
canvas_size = variant["canvas_size"]
|
|
90
|
+
canvas = Image.new("RGB", (canvas_size, canvas_size), (255, 255, 255))
|
|
91
|
+
|
|
92
|
+
for texture in variant["textures"]:
|
|
93
|
+
if texture["type"] == "image":
|
|
94
|
+
try:
|
|
95
|
+
# Check if content is a byte stream
|
|
96
|
+
if isinstance(texture["content"], bytes):
|
|
97
|
+
img = Image.open(io.BytesIO(texture["content"]))
|
|
98
|
+
else:
|
|
99
|
+
# Assume content is a file path
|
|
100
|
+
img = Image.open(texture["content"])
|
|
101
|
+
|
|
102
|
+
# Ensure the image is loaded
|
|
103
|
+
img.load()
|
|
104
|
+
except Exception as e:
|
|
105
|
+
print(f"Error loading image for texture ID {texture['id']}: {e}")
|
|
106
|
+
continue
|
|
107
|
+
|
|
108
|
+
# Process image for source size
|
|
109
|
+
resized_img = process_image_for_source_size(img, texture["source_size"])
|
|
110
|
+
|
|
111
|
+
# Apply user modifications and position on canvas
|
|
112
|
+
user_modifications = texture.get("user_modifications", {})
|
|
113
|
+
tile_canvas = position_image_on_canvas(
|
|
114
|
+
resized_img, texture["canvas_position"], user_modifications
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
# Calculate position on the main canvas
|
|
118
|
+
x = texture["canvas_position"]["x"]
|
|
119
|
+
y = texture["canvas_position"]["y"]
|
|
120
|
+
|
|
121
|
+
# Paste the processed image onto the main canvas
|
|
122
|
+
canvas.paste(tile_canvas, (x, y))
|
|
123
|
+
|
|
124
|
+
elif texture["type"] == "color":
|
|
125
|
+
# Create a colored rectangle
|
|
126
|
+
color = texture["content"]
|
|
127
|
+
x = texture["canvas_position"]["x"]
|
|
128
|
+
y = texture["canvas_position"]["y"]
|
|
129
|
+
width = texture["canvas_position"]["width"]
|
|
130
|
+
height = texture["canvas_position"]["height"]
|
|
131
|
+
draw = ImageDraw.Draw(canvas)
|
|
132
|
+
draw.rectangle([x, y, x + width, y + height], fill=color)
|
|
133
|
+
|
|
134
|
+
# Flip the image
|
|
135
|
+
canvas = canvas.transpose(Image.FLIP_TOP_BOTTOM)
|
|
136
|
+
# Save or display the image
|
|
137
|
+
output = io.BytesIO()
|
|
138
|
+
canvas.save(output, format="PNG")
|
|
139
|
+
output.seek(0)
|
|
140
|
+
return output.getvalue()
|
|
141
|
+
|
|
142
|
+
def variant_to_texture(id_variant: str, textures: List[Dict[str, Any]]) -> bytes:
|
|
143
|
+
"""Create texture data image using the given variant ID and textures."""
|
|
144
|
+
# Retrieve product configurations from the API
|
|
145
|
+
products_json = customizables(debug=True)
|
|
146
|
+
my_variant = next(
|
|
147
|
+
(v for v in products_json["variants"] if v["id"] == id_variant), None
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
if not my_variant:
|
|
151
|
+
raise ValueError(f"Variant with ID {id_variant} not found in variants.")
|
|
152
|
+
|
|
153
|
+
# Update the variant's textures with the provided textures
|
|
154
|
+
for texture in textures:
|
|
155
|
+
for variant_texture in my_variant["textures"]:
|
|
156
|
+
if variant_texture["id"] == texture["id"]:
|
|
157
|
+
if variant_texture["type"] == "image" and is_viz_like(
|
|
158
|
+
texture["content"]
|
|
159
|
+
):
|
|
160
|
+
# Convert chart to PNG if content is a chart
|
|
161
|
+
variant_texture["content"] = chart_to_png(texture["content"])
|
|
162
|
+
else:
|
|
163
|
+
# Directly assign content for non-chart textures
|
|
164
|
+
variant_texture["content"] = texture["content"]
|
|
165
|
+
# Update other properties if needed
|
|
166
|
+
variant_texture["user_modifications"] = texture.get(
|
|
167
|
+
"user_modifications", variant_texture.get("user_modifications", {})
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
# Create the tiled image using the updated variant
|
|
171
|
+
texture_data = create_tiled_image(my_variant)
|
|
172
|
+
return texture_data
|