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