diff --git a/demo/__init__.py b/demo/__init__.py
index 3f58bf24..07d569d7 100644
--- a/demo/__init__.py
+++ b/demo/__init__.py
@@ -11,6 +11,7 @@
from httpx import AsyncClient
from .auth import router as auth_router
+from .charts import router as charts_router
from .components_list import router as components_router
from .forms import router as forms_router
from .main import router as main_router
@@ -38,6 +39,7 @@ async def lifespan(app_: FastAPI):
app.include_router(table_router, prefix='/api/table')
app.include_router(forms_router, prefix='/api/forms')
app.include_router(auth_router, prefix='/api/auth')
+app.include_router(charts_router, prefix='/api/charts')
app.include_router(main_router, prefix='/api')
diff --git a/demo/charts.py b/demo/charts.py
new file mode 100644
index 00000000..8b6c7e4d
--- /dev/null
+++ b/demo/charts.py
@@ -0,0 +1,79 @@
+from __future__ import annotations as _annotations
+
+from fastapi import APIRouter
+from fastui import AnyComponent, FastUI
+from fastui import components as c
+from fastui.components import RechartsLineChart
+from fastui.events import PageEvent
+from pydantic import BaseModel
+
+from .shared import demo_page
+
+router = APIRouter()
+
+
+@router.get('/{kind}', response_model=FastUI, response_model_exclude_none=True)
+def charts_view(kind: str) -> list[AnyComponent]:
+ return demo_page(
+ c.LinkList(
+ links=[
+ c.Link(
+ components=[c.Text(text='Recharts Line Chart')],
+ on_click=PageEvent(
+ name='change-chart',
+ push_path='/charts/recharts-line-chart',
+ context={'kind': 'recharts-line-chart'},
+ ),
+ active='/charts/recharts-line-chart',
+ ),
+ ],
+ mode='tabs',
+ class_name='+ mb-4',
+ ),
+ c.ServerLoad(
+ path='/charts/content/{kind}',
+ load_trigger=PageEvent(name='change-chart'),
+ components=charts_content_view(kind),
+ ),
+ title='Charts',
+ )
+
+
+class Data(BaseModel):
+ name: str
+ uv: int
+ pv: int
+ amt: int
+
+
+data_list = [
+ Data(name='Page A', uv=4000, pv=2400, amt=2400),
+ Data(name='Page B', uv=3000, pv=1398, amt=2210),
+ Data(name='Page C', uv=2000, pv=9800, amt=2290),
+ Data(name='Page D', uv=2780, pv=3908, amt=2000),
+ Data(name='Page E', uv=1890, pv=4800, amt=2181),
+ Data(name='Page F', uv=2390, pv=3800, amt=2500),
+ Data(name='Page G', uv=3490, pv=4300, amt=2100),
+]
+
+
+@router.get('/content/{kind}', response_model=FastUI, response_model_exclude_none=True)
+def charts_content_view(kind: str) -> list[AnyComponent]:
+ match kind:
+ case 'recharts-line-chart':
+ return [
+ c.Heading(text='Line chart', level=2),
+ c.Paragraph(text='Line chart with Recharts.'),
+ RechartsLineChart(
+ title='Recharts Line Chart',
+ width='100%',
+ height=300,
+ data=data_list,
+ x_key='name',
+ y_keys=['pv', 'uv', 'amt'],
+ y_keys_names=['Page Views', 'Unique Views', 'Amount'],
+ colors=['#8884d8', '#82ca9d', '#ffc658'],
+ ),
+ ]
+ case _:
+ return [c.Text(text='Unknown chart kind')]
diff --git a/demo/main.py b/demo/main.py
index cdba7e22..554937eb 100644
--- a/demo/main.py
+++ b/demo/main.py
@@ -37,6 +37,7 @@ def api_index() -> list[AnyComponent]:
* `Table` — See [cities table](/table/cities) and [users table](/table/users)
* `Pagination` — See the bottom of the [cities table](/table/cities)
* `ModelForm` — See [forms](/forms/login)
+* `Charts` — See [charts](/charts/recharts-line-chart)
Authentication is supported via:
* token based authentication — see [here](/auth/login/password) for an example of password authentication
diff --git a/demo/shared.py b/demo/shared.py
index 70b44de4..732afdec 100644
--- a/demo/shared.py
+++ b/demo/shared.py
@@ -32,6 +32,11 @@ def demo_page(*components: AnyComponent, title: str | None = None) -> list[AnyCo
on_click=GoToEvent(url='/forms/login'),
active='startswith:/forms',
),
+ c.Link(
+ components=[c.Text(text='Charts')],
+ on_click=GoToEvent(url='/charts/recharts-line-chart'),
+ active='startswith:/charts',
+ ),
],
),
c.Page(
diff --git a/demo/tests.py b/demo/tests.py
index cda30982..ed6a16c5 100644
--- a/demo/tests.py
+++ b/demo/tests.py
@@ -32,7 +32,7 @@ def test_api_root(client: TestClient):
{
'title': 'FastUI Demo',
'titleEvent': {'url': '/', 'type': 'go-to'},
- 'startLinks': IsList(length=4),
+ 'startLinks': IsList(length=5),
'endLinks': [],
'type': 'Navbar',
},
diff --git a/package-lock.json b/package-lock.json
index 1c67808a..462f79f0 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -8,6 +8,9 @@
"workspaces": [
"src/*"
],
+ "dependencies": {
+ "recharts": "^2.12.0"
+ },
"devDependencies": {
"@types/node": "^20.9.1",
"@types/react": "^18.2.15",
@@ -1317,6 +1320,60 @@
"integrity": "sha512-myfUej5naTBWnqOCc/MdVOLVjXUXtIA+NpDrDBKJtLLg2shUjBu3cZmB/85RyitKc55+lUUyl7oRfLOvkr2hsw==",
"dev": true
},
+ "node_modules/@types/d3-array": {
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.1.tgz",
+ "integrity": "sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg=="
+ },
+ "node_modules/@types/d3-color": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz",
+ "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A=="
+ },
+ "node_modules/@types/d3-ease": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz",
+ "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA=="
+ },
+ "node_modules/@types/d3-interpolate": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz",
+ "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==",
+ "dependencies": {
+ "@types/d3-color": "*"
+ }
+ },
+ "node_modules/@types/d3-path": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.0.tgz",
+ "integrity": "sha512-P2dlU/q51fkOc/Gfl3Ul9kicV7l+ra934qBFXCFhrZMOL6du1TM0pm1ThYvENukyOn5h9v+yMJ9Fn5JK4QozrQ=="
+ },
+ "node_modules/@types/d3-scale": {
+ "version": "4.0.8",
+ "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.8.tgz",
+ "integrity": "sha512-gkK1VVTr5iNiYJ7vWDI+yUFFlszhNMtVeneJ6lUTKPjprsvLLI9/tgEGiXJOnlINJA8FyA88gfnQsHbybVZrYQ==",
+ "dependencies": {
+ "@types/d3-time": "*"
+ }
+ },
+ "node_modules/@types/d3-shape": {
+ "version": "3.1.6",
+ "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.6.tgz",
+ "integrity": "sha512-5KKk5aKGu2I+O6SONMYSNflgiP0WfZIQvVUMan50wHsLG1G94JlxEVnCpQARfTtzytuY0p/9PXXZb3I7giofIA==",
+ "dependencies": {
+ "@types/d3-path": "*"
+ }
+ },
+ "node_modules/@types/d3-time": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.3.tgz",
+ "integrity": "sha512-2p6olUZ4w3s+07q3Tm2dbiMZy5pCDfYwtLXXHUnVzXgQlZ/OyPtUz6OL382BkOuGlLXqfT+wqv8Fw2v8/0geBw=="
+ },
+ "node_modules/@types/d3-timer": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz",
+ "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw=="
+ },
"node_modules/@types/debug": {
"version": "4.1.12",
"resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz",
@@ -2143,6 +2200,14 @@
"node": ">=0.10"
}
},
+ "node_modules/clsx": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.0.tgz",
+ "integrity": "sha512-m3iNNWpd9rl3jvvcBnu70ylMdrXt8Vlq4HYadnU5fwcOtvkSQWPmj7amUcDT2qYI7risszBjI5AUIUox9D16pg==",
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
@@ -2225,6 +2290,116 @@
"type": "^1.0.1"
}
},
+ "node_modules/d3-array": {
+ "version": "3.2.4",
+ "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz",
+ "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==",
+ "dependencies": {
+ "internmap": "1 - 2"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-color": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz",
+ "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-ease": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz",
+ "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-format": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz",
+ "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-interpolate": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz",
+ "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==",
+ "dependencies": {
+ "d3-color": "1 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-path": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz",
+ "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-scale": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz",
+ "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==",
+ "dependencies": {
+ "d3-array": "2.10.0 - 3",
+ "d3-format": "1 - 3",
+ "d3-interpolate": "1.2.0 - 3",
+ "d3-time": "2.1.1 - 3",
+ "d3-time-format": "2 - 4"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-shape": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz",
+ "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==",
+ "dependencies": {
+ "d3-path": "^3.1.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-time": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz",
+ "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==",
+ "dependencies": {
+ "d3-array": "2 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-time-format": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz",
+ "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==",
+ "dependencies": {
+ "d3-time": "1 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-timer": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz",
+ "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==",
+ "engines": {
+ "node": ">=12"
+ }
+ },
"node_modules/debug": {
"version": "4.3.4",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
@@ -2241,6 +2416,11 @@
}
}
},
+ "node_modules/decimal.js-light": {
+ "version": "2.5.1",
+ "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz",
+ "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg=="
+ },
"node_modules/decode-named-character-reference": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.0.2.tgz",
@@ -3029,6 +3209,11 @@
"es5-ext": "~0.10.14"
}
},
+ "node_modules/eventemitter3": {
+ "version": "4.0.7",
+ "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz",
+ "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw=="
+ },
"node_modules/ext": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/ext/-/ext-1.7.0.tgz",
@@ -3055,6 +3240,14 @@
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
"dev": true
},
+ "node_modules/fast-equals": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.0.1.tgz",
+ "integrity": "sha512-WF1Wi8PwwSY7/6Kx0vKXtw8RwuSGoM1bvDaJbu7MxDlR1vovZjIAKrnzyrThgAjm6JDTu0fVgWXDlMGspodfoQ==",
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
"node_modules/fast-glob": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz",
@@ -3704,6 +3897,14 @@
"node": ">= 0.4"
}
},
+ "node_modules/internmap": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz",
+ "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==",
+ "engines": {
+ "node": ">=12"
+ }
+ },
"node_modules/invariant": {
"version": "2.2.4",
"resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz",
@@ -4292,8 +4493,7 @@
"node_modules/lodash": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
- "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
- "dev": true
+ "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
},
"node_modules/lodash.merge": {
"version": "4.6.2",
@@ -5748,6 +5948,20 @@
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0"
}
},
+ "node_modules/react-smooth": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/react-smooth/-/react-smooth-4.0.0.tgz",
+ "integrity": "sha512-2NMXOBY1uVUQx1jBeENGA497HK20y6CPGYL1ZnJLeoQ8rrc3UfmOM82sRxtzpcoCkUMy4CS0RGylfuVhuFjBgg==",
+ "dependencies": {
+ "fast-equals": "^5.0.1",
+ "prop-types": "^15.8.1",
+ "react-transition-group": "^4.4.5"
+ },
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0",
+ "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0"
+ }
+ },
"node_modules/react-syntax-highlighter": {
"version": "15.5.0",
"resolved": "https://registry.npmjs.org/react-syntax-highlighter/-/react-syntax-highlighter-15.5.0.tgz",
@@ -5789,6 +6003,36 @@
"node": ">=8.10.0"
}
},
+ "node_modules/recharts": {
+ "version": "2.12.0",
+ "resolved": "https://registry.npmjs.org/recharts/-/recharts-2.12.0.tgz",
+ "integrity": "sha512-rVNcdNQ5b7+40Ue7mcEKZJyEv+3SUk2bDEVvOyXPDXXVE7TU3lrvnJUgAvO36hSzhRP2DnAamKXvHLFIFOU0Ww==",
+ "dependencies": {
+ "clsx": "^2.0.0",
+ "eventemitter3": "^4.0.1",
+ "lodash": "^4.17.19",
+ "react-is": "^16.10.2",
+ "react-smooth": "^4.0.0",
+ "recharts-scale": "^0.4.4",
+ "tiny-invariant": "^1.3.1",
+ "victory-vendor": "^36.6.8"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "peerDependencies": {
+ "react": "^16.0.0 || ^17.0.0 || ^18.0.0",
+ "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0"
+ }
+ },
+ "node_modules/recharts-scale": {
+ "version": "0.4.5",
+ "resolved": "https://registry.npmjs.org/recharts-scale/-/recharts-scale-0.4.5.tgz",
+ "integrity": "sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w==",
+ "dependencies": {
+ "decimal.js-light": "^2.4.1"
+ }
+ },
"node_modules/reflect.getprototypeof": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.4.tgz",
@@ -6366,6 +6610,11 @@
"next-tick": "1"
}
},
+ "node_modules/tiny-invariant": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.1.tgz",
+ "integrity": "sha512-AD5ih2NlSssTCwsMznbvwMZpJ1cbhkGd2uueNxzv2jDlEeZdU04JQfRnggJQ8DrcVBGjAsCKwFBbDlVNtEMlzw=="
+ },
"node_modules/to-fast-properties": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz",
@@ -6706,6 +6955,27 @@
"url": "https://opencollective.com/unified"
}
},
+ "node_modules/victory-vendor": {
+ "version": "36.9.1",
+ "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-36.9.1.tgz",
+ "integrity": "sha512-+pZIP+U3pEJdDCeFmsXwHzV7vNHQC/eIbHklfe2ZCZqayYRH7lQbHcVgsJ0XOOv27hWs4jH4MONgXxHMObTMSA==",
+ "dependencies": {
+ "@types/d3-array": "^3.0.3",
+ "@types/d3-ease": "^3.0.0",
+ "@types/d3-interpolate": "^3.0.1",
+ "@types/d3-scale": "^4.0.2",
+ "@types/d3-shape": "^3.1.0",
+ "@types/d3-time": "^3.0.0",
+ "@types/d3-timer": "^3.0.0",
+ "d3-array": "^3.1.6",
+ "d3-ease": "^3.0.1",
+ "d3-interpolate": "^3.0.1",
+ "d3-scale": "^4.0.2",
+ "d3-shape": "^3.1.0",
+ "d3-time": "^3.0.0",
+ "d3-timer": "^3.0.1"
+ }
+ },
"node_modules/vite": {
"version": "5.0.12",
"resolved": "https://registry.npmjs.org/vite/-/vite-5.0.12.tgz",
@@ -6954,7 +7224,7 @@
},
"src/npm-fastui": {
"name": "@pydantic/fastui",
- "version": "0.0.21",
+ "version": "0.0.22",
"license": "MIT",
"dependencies": {
"@microsoft/fetch-event-source": "^2.0.1",
@@ -6971,7 +7241,7 @@
},
"src/npm-fastui-bootstrap": {
"name": "@pydantic/fastui-bootstrap",
- "version": "0.0.21",
+ "version": "0.0.22",
"license": "MIT",
"dependencies": {
"bootstrap": "^5.3.2",
@@ -6981,12 +7251,12 @@
"sass": "^1.69.5"
},
"peerDependencies": {
- "@pydantic/fastui": "0.0.21"
+ "@pydantic/fastui": "0.0.22"
}
},
"src/npm-fastui-prebuilt": {
"name": "@pydantic/fastui-prebuilt",
- "version": "0.0.21",
+ "version": "0.0.22",
"license": "MIT",
"devDependencies": {
"@vitejs/plugin-react-swc": "^3.3.2",
diff --git a/package.json b/package.json
index 98639d0a..0690910e 100644
--- a/package.json
+++ b/package.json
@@ -39,5 +39,8 @@
"json-schema-to-typescript": "^13.1.1",
"prettier": "^3.0.3",
"typescript": "^5.0.2"
+ },
+ "dependencies": {
+ "recharts": "^2.12.0"
}
}
diff --git a/src/npm-fastui/src/components/LineChart.tsx b/src/npm-fastui/src/components/LineChart.tsx
new file mode 100644
index 00000000..26817345
--- /dev/null
+++ b/src/npm-fastui/src/components/LineChart.tsx
@@ -0,0 +1,36 @@
+import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from 'recharts'
+
+import type { RechartsLineChart as FastUILineChart } from '../models'
+
+export const LineChartComp = (props: FastUILineChart) => {
+ const { width, height, data, xKey, yKeys, yKeysNames, colors, tooltip } = props
+
+ return (
+
+
+
+
+
+ {tooltip && }
+
+ {yKeys.map((yKey, i) => (
+
+ ))}
+
+
+ )
+}
diff --git a/src/npm-fastui/src/components/index.tsx b/src/npm-fastui/src/components/index.tsx
index d0398a75..952e77a0 100644
--- a/src/npm-fastui/src/components/index.tsx
+++ b/src/npm-fastui/src/components/index.tsx
@@ -41,6 +41,7 @@ import { FireEventComp } from './FireEvent'
import { ErrorComp } from './error'
import { SpinnerComp } from './spinner'
import { CustomComp } from './Custom'
+import { LineChartComp } from './LineChart'
// TODO some better way to export components
export {
@@ -166,6 +167,8 @@ export const AnyComp: FC = (props) => {
return
case 'Custom':
return
+ case 'RechartsLineChart':
+ return
default:
unreachable('Unexpected component type', type, props)
return
diff --git a/src/npm-fastui/src/models.d.ts b/src/npm-fastui/src/models.d.ts
index a0ca91cd..7204b7a9 100644
--- a/src/npm-fastui/src/models.d.ts
+++ b/src/npm-fastui/src/models.d.ts
@@ -40,6 +40,7 @@ export type FastProps =
| FormFieldSelect
| FormFieldSelectSearch
| ModelForm
+ | RechartsLineChart
export type ClassName =
| string
| ClassName[]
@@ -460,3 +461,17 @@ export interface ModelForm {
| FormFieldSelectSearch
)[]
}
+export interface RechartsLineChart {
+ title: string
+ width?: number | string
+ height: number | string
+ data: BaseModel[]
+ className?: ClassName
+ type: 'RechartsLineChart'
+ xKey: string
+ yKeys: string[]
+ yKeysNames?: string[]
+ colors: string[]
+ tooltip?: boolean
+}
+export interface BaseModel {}
diff --git a/src/python-fastui/fastui/components/__init__.py b/src/python-fastui/fastui/components/__init__.py
index f2d3f423..948b8dfd 100644
--- a/src/python-fastui/fastui/components/__init__.py
+++ b/src/python-fastui/fastui/components/__init__.py
@@ -14,6 +14,7 @@
from .. import class_name as _class_name
from .. import events
from .. import types as _types
+from .charts import RechartsLineChart
from .display import Details, Display
from .forms import (
Form,
@@ -67,6 +68,8 @@
'FormFieldInput',
'FormFieldSelect',
'FormFieldSelectSearch',
+ # then charts
+ 'RechartsLineChart',
)
@@ -343,6 +346,7 @@ class Custom(_p.BaseModel, extra='forbid'):
Form,
FormField,
ModelForm,
+ RechartsLineChart,
],
_p.Field(discriminator='type'),
]
diff --git a/src/python-fastui/fastui/components/charts.py b/src/python-fastui/fastui/components/charts.py
new file mode 100644
index 00000000..80669344
--- /dev/null
+++ b/src/python-fastui/fastui/components/charts.py
@@ -0,0 +1,37 @@
+import typing as _t
+from abc import ABC
+
+import pydantic as _p
+
+from .. import class_name as _class_name
+
+if _t.TYPE_CHECKING:
+ pass
+
+
+DataPoint = _t.TypeVar('DataPoint', bound=_p.BaseModel)
+
+
+class BaseChart(_p.BaseModel, ABC, defer_build=True):
+ title: str
+ width: _t.Union[int, str] = '100%'
+ height: _t.Union[int, str]
+ data: _t.List[DataPoint] # type: ignore
+ class_name: _class_name.ClassNameField = None
+
+
+class RechartsLineChart(BaseChart):
+ type: _t.Literal['RechartsLineChart'] = 'RechartsLineChart'
+ x_key: str = _p.Field(..., serialization_alias='xKey')
+ y_keys: _t.List[str] = _p.Field(..., serialization_alias='yKeys')
+ y_keys_names: _t.Union[_t.List[str], None] = _p.Field(None, serialization_alias='yKeysNames')
+ colors: _t.List[str]
+ tooltip: bool = True
+
+ @_p.model_validator(mode='after')
+ def check_length_of_y_keys_colors_and_y_keys_names(self):
+ if len(self.y_keys) != len(self.colors):
+ raise _p.ValidationError('Length of y_keys and colors must be the same')
+ if self.y_keys_names and len(self.y_keys) != len(self.y_keys_names):
+ raise _p.ValidationError('Length of y_keys and y_keys_names must be the same')
+ return self