From 1de63bd21933d760101d90ff88469e3a5b9e1490 Mon Sep 17 00:00:00 2001 From: Stefan Kuethe Date: Wed, 14 Feb 2024 21:10:35 +0100 Subject: [PATCH] Add express chart --- docs/examples/playground/app1.py | 26 +++++++++++++++++ docs/shinylive.md | 1 + shinyecharts/_core.py | 7 ++++- shinyecharts/chart.py | 31 ++++++++++----------- shinyecharts/chartcontext.py | 3 ++ shinyecharts/express.py | 48 ++++++++++++++++++++++++++++++++ tests/test_express.py | 17 +++++++++++ 7 files changed, 115 insertions(+), 18 deletions(-) create mode 100644 docs/examples/playground/app1.py create mode 100644 docs/shinylive.md create mode 100644 shinyecharts/chartcontext.py create mode 100644 tests/test_express.py diff --git a/docs/examples/playground/app1.py b/docs/examples/playground/app1.py new file mode 100644 index 0000000..cd5b120 --- /dev/null +++ b/docs/examples/playground/app1.py @@ -0,0 +1,26 @@ +from pandas import DataFrame +from shiny.express import ui +from shinyecharts.express import Chart, InitOptions +from shinyecharts.renderer import ChartRenderer + +init_options = InitOptions(width=600, height=400, renderer="canvas") + +data = DataFrame( + [[0, 1, 2, 3], [1, 4, 5, 6], [2, -2, 4, 9]], + columns=["a", "b", "c", "d"], +) + + +@ChartRenderer +def render_lines(): + return Chart().data(data).encode("a", "b").encode("a", "c", color="pink") + + +@ChartRenderer +def render_scatter(): + return Chart().data(data).encode("a", "b", type="scatter") + + +@ChartRenderer +def render_bar(): + return Chart().data(data).encode("a", "b", type="bar").encode("a", "c", type="bar") diff --git a/docs/shinylive.md b/docs/shinylive.md new file mode 100644 index 0000000..d7ae857 --- /dev/null +++ b/docs/shinylive.md @@ -0,0 +1 @@ +https://shinylive.io/py/editor/#code=NobwRAdghgtgpmAXGKAHVA6VBPMAaMAYwHsIAXOcpMAMwCdiYACVKCAEygGcmBLGVMTpkmAEShkoAMTqw4AHQiKAxEwCyAVy4ioAGwDuUbDwBGcPgKEV2eJsTIALOHX28u5gILo+PCPaY0xBocivSMTFwOvBDYGHAAHqh0cFw8-ILCTBq8oQzMkdHYcIQOUMJplpkAwqXCtgCSELxkAPKoZLykXLnhBTHFtWRcGMTtnRAVGSIAMtEKEGH5Uf0lZUMYyRzOzhZTTDVrAEqU7Nt0iorRzQD6ox1dTAC8TI3NbfcTABSu7I6PAGwABkBticvAA5g4yI8ACzA2ybU7JOiPeRENgAN24aIAlBcOBIoE8xISZHJPoomFSmMBgCCmABGWwAJlsAGYALq2YBMpgw2wAVls-y5NNZTAAtOL+UwAJwcrmU6kkXQaGATR7ANFQNG2NEmXVMNGEQ1o9hoxUQPFKG26OY8Z4UiDUpizCBwT7xVEoQ3Yb0G-BMMjEYi6DqoR7sXiEMifMh0CHg5zeqDxNy42y6OBJjiR6OxnHWl0YKDsdjXdwJlIUoi4pVUktlivOXjVs11q34xQAAQOwmOW2RilONCYiOc104kncBcQ9bHcDIGjozr7sauZFuYy6tinUEjhJxGBnW4+nzt7q41rAAF88OBoPBqMkAI7ZZLwcjDMjxMj4IikBQVDIH0RSrOUt4ckAA diff --git a/shinyecharts/_core.py b/shinyecharts/_core.py index cc62b23..283bbdf 100644 --- a/shinyecharts/_core.py +++ b/shinyecharts/_core.py @@ -5,11 +5,13 @@ from pandas import DataFrame +# ------------------------- +# Utils +# ------------------------- def snake_to_camel_case(snake_str: str) -> str: return snake_str[0].lower() + snake_str.title()[1:].replace("_", "") -# TODO: Do not check for DataFrame instance here def df_to_dataset(df: DataFrame = None) -> dict: return {"dataset": {"source": [df.columns.to_list()] + df.to_numpy().tolist()}} @@ -29,6 +31,9 @@ def to_dict(self): ) +# ------------------------- +# Option +# ------------------------- class BaseOption(object): CHART_TYPE: str = None diff --git a/shinyecharts/chart.py b/shinyecharts/chart.py index 03f183b..84a8fdd 100644 --- a/shinyecharts/chart.py +++ b/shinyecharts/chart.py @@ -20,41 +20,38 @@ class InitOptions(object): class Chart(object): """Chart""" + _theme: str = None + _option: dict = dict() + _data: DataFrame = None + def __init__( self, init_options: InitOptions = InitOptions(), - theme: str | None = "dark", + theme: str = None, data: DataFrame = None, ) -> None: - self.init_options = init_options - self.theme = theme - self.data = data - - # TODO: Move to class attributes - self.option = dict() - - # TODO: Remove - def set_pie_data(self, data: DataFrame) -> Chart: - pass + self._init_options = init_options + self._theme = theme + self._data = data # Set option attributes def attr(self, **kwargs) -> Chart: pass # TODO: Set data here - def set_data(self, data: DataFrame) -> Chart: + def set_data(self, data: DataFrame | dict) -> Chart: pass def set_option(self, option: dict | ChartOption | BaseOption) -> Chart: - self.option = option if isinstance(option, dict) else option.to_dict() + self._option = option if isinstance(option, dict) else option.to_dict() return self def to_dict(self) -> dict: dataset = ( - df_to_dataset(self.data) if isinstance(self.data, DataFrame) else dict() + df_to_dataset(self._data) if isinstance(self._data, DataFrame) else dict() ) return { - "initOptions": asdict(self.init_options), - "option": dataset | self.option, - "theme": self.theme, + "initOptions": asdict(self._init_options), + "option": dataset | self._option, + "theme": self._theme, } diff --git a/shinyecharts/chartcontext.py b/shinyecharts/chartcontext.py new file mode 100644 index 0000000..84ae88f --- /dev/null +++ b/shinyecharts/chartcontext.py @@ -0,0 +1,3 @@ +class ChartContext(object): + def __init__(self, id): + self.id = id diff --git a/shinyecharts/express.py b/shinyecharts/express.py index dd768f4..67b61c4 100644 --- a/shinyecharts/express.py +++ b/shinyecharts/express.py @@ -1,6 +1,54 @@ from __future__ import annotations +from pandas import DataFrame +from .chart import Chart as BaseChart +from .chart import InitOptions + + +class Chart(BaseChart): + def __init__(self, options: InitOptions = InitOptions()): + super().__init__(options) + self._option = dict(xAxis=dict(), yAxis=dict()) + self.legend() + + def _update_option(self, **kwargs): + self._option.update(**kwargs) + + def dark(self) -> Chart: + self._theme = "dark" + return self + + def data(self, df: DataFrame) -> Chart: + self._data = df + return self + + def x_axis(self, type: str = "value") -> Chart: + self._update_option(xAxis=dict(type=type)) + return self + + def y_axis(self): + return self + + def encode( + self, x: str | int = "x", y: str | int = "y", type: str = "line", **kwargs + ) -> Chart: + if type == "bar": + self.x_axis("category") + + series = dict(name=y, type=type, encode=dict(x=x, y=y)) | kwargs + if "series" in self._option.keys(): + self._option["series"].append(series) + else: + self._update_option(series=[series]) + return self + + def legend(self, show=True, **kwargs) -> Chart: + self._option.update(legend=dict(show=True) | kwargs) + return self + + +# ------------------------- class PieChart(object): pass diff --git a/tests/test_express.py b/tests/test_express.py new file mode 100644 index 0000000..a0f58c1 --- /dev/null +++ b/tests/test_express.py @@ -0,0 +1,17 @@ +from pandas import DataFrame +from shinyecharts.express import Chart + + +def test_chart(): + # Prepare + data = DataFrame([[1, 2], [2, 4]], columns=["x", "y"]) + + # Act + chart = Chart().dark().data(data).encode("x", "y") + print(chart._option) + print(chart.to_dict()) + + chart_dict = chart.to_dict() + + # Assert + assert chart_dict["theme"] == "dark"