diff --git a/bored-charts/README.md b/bored-charts/README.md index 5273b50..a079767 100644 --- a/bored-charts/README.md +++ b/bored-charts/README.md @@ -22,19 +22,18 @@ pip install bored-charts uvicorn from pathlib import Path import plotly.express as px +import plotly.graph_objects as go from boredcharts import BCRouter, boredcharts -from boredcharts.jinja import to_html -from fastapi.responses import HTMLResponse pages = Path(__file__).parent.absolute() / "pages" figure_router = BCRouter() -@figure_router.chart("usa_population") -async def usa_population() -> HTMLResponse: - df = px.data.gapminder().query("country=='United States'") +@figure_router.chart("population") +async def population(country: str) -> go.Figure: + df = px.data.gapminder().query(f"country=='{country}'") fig = px.bar(df, x="year", y="pop") - return HTMLResponse(to_html(fig)) + return fig app = boredcharts( @@ -52,7 +51,7 @@ pages/populations.md: USA's population has been growing linearly for the last 70 years: -{{ figure("usa_population") }} +{{ figure("population", country="United States") }} ``` ### Run your app diff --git a/bored-charts/boredcharts/__init__.py b/bored-charts/boredcharts/__init__.py index 38e66d8..eeb5b29 100644 --- a/bored-charts/boredcharts/__init__.py +++ b/bored-charts/boredcharts/__init__.py @@ -1,4 +1,4 @@ -__version__ = "0.2.0" +__version__ = "0.3.0" from boredcharts.router import BCRouter from boredcharts.webapp import boredcharts diff --git a/bored-charts/boredcharts/router.py b/bored-charts/boredcharts/router.py index 266e08f..695abfb 100644 --- a/bored-charts/boredcharts/router.py +++ b/bored-charts/boredcharts/router.py @@ -1,16 +1,74 @@ from collections.abc import Callable +from typing import Any +import plotly.graph_objects as go from fastapi import APIRouter +from fastapi.responses import HTMLResponse from fastapi.types import DecoratedCallable +from pydantic import GetCoreSchemaHandler +from pydantic_core import CoreSchema, core_schema + +from boredcharts.jinja import to_html + + +def validate_figure(fig: Any) -> go.Figure: + assert isinstance(fig, go.Figure) + return fig + + +class HTMLFigure(go.Figure): # type: ignore[misc] + """A Plotly Figure that Pydantic can understand and serialize. + + This allows us to return a Plotly Figure from a FastAPI route. + """ + + @classmethod + def __get_pydantic_core_schema__( + cls, _source_type: Any, _handler: GetCoreSchemaHandler + ) -> CoreSchema: + return core_schema.json_or_python_schema( + json_schema=core_schema.any_schema(), + python_schema=core_schema.union_schema( + [ + core_schema.is_instance_schema(go.Figure), + core_schema.any_schema(), + ] + ), + serialization=core_schema.plain_serializer_function_ser_schema( + lambda instance: to_html(instance) + ), + ) class BCRouter(APIRouter): + """A FastAPI APIRouter that is specifically designed for creating chart routes. + + Usage: + + ```py + from boredcharts import BCRouter + import plotly.graph_objects as go + + router = BCRouter() + + @router.chart("my_chart") + async def my_chart() -> go.Figure: + return go.Figure() + ``` + """ + def chart( self, name: str, ) -> Callable[[DecoratedCallable], DecoratedCallable]: + """ + Creates a GET route for a chart, just a shorter form of the FastAPI get decorator, + your function still has to return a HTMLResponse + """ path = f"/figure/{name}" - return self.api_route( + return self.get( path=path, name=name, + response_model=HTMLFigure, + response_class=HTMLResponse, ) diff --git a/examples/full/bcexample/figures.py b/examples/full/bcexample/figures.py index 66d7e5c..a5dc63d 100644 --- a/examples/full/bcexample/figures.py +++ b/examples/full/bcexample/figures.py @@ -3,13 +3,12 @@ import numpy as np import plotly.express as px from boredcharts import BCRouter -from boredcharts.jinja import to_html -from fastapi.responses import HTMLResponse from plotly.graph_objects import Figure router = BCRouter() +@router.chart("population") async def example(report_name: str, country: str) -> Figure: df = px.data.gapminder().query(f"country=='{country}'") fig = px.bar(df, x="year", y="pop") @@ -30,17 +29,12 @@ async def example(report_name: str, country: str) -> Figure: return fig -# TODO: pass functions into framework, auto generate these routes -@router.chart("example_simple_usa") -async def fig_example_simple(report_name: str) -> HTMLResponse: - return HTMLResponse(to_html(await example(report_name, "United States"))) - - -@router.chart("example_params") -async def fig_example(report_name: str, country: str) -> HTMLResponse: - return HTMLResponse(to_html(await example(report_name, country))) +@router.chart("usa_population") +async def fig_example_simple(report_name: str) -> Figure: + return await example(report_name, "United States") +@router.chart("elasticity_vs_profit") async def elasticity_vs_profit( report_name: str, margin: float | None = None ) -> mplfig.Figure: @@ -96,11 +90,3 @@ async def elasticity_vs_profit( ax.grid(True) return fig - - -# TODO: pass functions into framework, auto generate these routes -@router.chart("elasticity_vs_profit") -async def fig_elasticity_vs_profit( - report_name: str, margin: float | None = None -) -> HTMLResponse: - return HTMLResponse(to_html(await elasticity_vs_profit(report_name, margin))) diff --git a/examples/full/bcexample/pages/example.md b/examples/full/bcexample/pages/example.md index 1bc8531..9f88919 100644 --- a/examples/full/bcexample/pages/example.md +++ b/examples/full/bcexample/pages/example.md @@ -11,21 +11,21 @@ The USA's population has been growing linearly:
 {%- raw %}
-{{ figure("example_simple_usa") }}
+{{ figure("usa_population") }}
 {% endraw -%}
 
-{{ figure("example_simple_usa") }} +{{ figure("usa_population") }} South Africa's growth is a bit weirder looking according to this chart:
 {%- raw %}
-{{ figure("example_params", country="South Africa") }}
+{{ figure("population", country="South Africa") }}
 {% endraw -%}
 
-{{ figure("example_params", country="South Africa") }} +{{ figure("population", country="South Africa") }} We can put two charts side by side: @@ -33,8 +33,8 @@ We can put two charts side by side: {%- raw %} {{ row( - figure("example_params", country="United Kingdom"), - figure("example_params", country="France"), + figure("population", country="United Kingdom"), + figure("population", country="France"), ) }} {% endraw -%} @@ -42,8 +42,8 @@ We can put two charts side by side: {{ row( - figure("example_params", country="United Kingdom"), - figure("example_params", country="France"), + figure("population", country="United Kingdom"), + figure("population", country="France"), ) }} @@ -53,8 +53,8 @@ And we can add custom tailwind classes to the figures: {%- raw %} {{ row( - figure("example_params", country="Canada", class="h-[300px] min-w-[300px]"), - figure("example_params", country="Australia", class="h-[300px] min-w-[300px]"), + figure("population", country="Canada", class="h-[300px] min-w-[300px]"), + figure("population", country="Australia", class="h-[300px] min-w-[300px]"), ) }} {% endraw -%} @@ -62,8 +62,8 @@ And we can add custom tailwind classes to the figures: {{ row( - figure("example_params", country="Canada", class="h-[300px] min-w-[300px]"), - figure("example_params", country="Australia", class="h-[300px] min-w-[300px]"), + figure("population", country="Canada", class="h-[300px] min-w-[300px]"), + figure("population", country="Australia", class="h-[300px] min-w-[300px]"), ) }} @@ -73,15 +73,15 @@ We can also dip into html when we need to
 {%- raw %}
 <div class="flex flex-wrap">
-  {{ figure("example_params", country="United Kingdom") }}
-  {{ figure("example_params", country="France") }}
+  {{ figure("population", country="United Kingdom") }}
+  {{ figure("population", country="France") }}
 </div>
 {% endraw -%}
 
- {{ figure("example_params", country="United Kingdom") }} - {{ figure("example_params", country="France") }} + {{ figure("population", country="United Kingdom") }} + {{ figure("population", country="France") }}
Or a matplotlib char diff --git a/examples/minimal/main.py b/examples/minimal/main.py index 13773bc..3cadfdb 100644 --- a/examples/minimal/main.py +++ b/examples/minimal/main.py @@ -1,19 +1,18 @@ from pathlib import Path import plotly.express as px +import plotly.graph_objects as go from boredcharts import BCRouter, boredcharts -from boredcharts.jinja import to_html -from fastapi.responses import HTMLResponse pages = Path(__file__).parent.absolute() / "pages" figure_router = BCRouter() -@figure_router.chart("usa_population") -async def usa_population() -> HTMLResponse: - df = px.data.gapminder().query("country=='United States'") +@figure_router.chart("population") +async def population(country: str) -> go.Figure: + df = px.data.gapminder().query(f"country=='{country}'") fig = px.bar(df, x="year", y="pop") - return HTMLResponse(to_html(fig)) + return fig app = boredcharts( diff --git a/examples/minimal/pages/populations.md b/examples/minimal/pages/populations.md index 60a1ca5..278a2e0 100644 --- a/examples/minimal/pages/populations.md +++ b/examples/minimal/pages/populations.md @@ -2,4 +2,4 @@ USA's population has been growing linearly for the last 70 years: -{{ figure("usa_population") }} +{{ figure("population", country="United States") }}