├── .github └── workflows │ ├── publish.yml │ └── test.yml ├── .gitignore ├── Justfile ├── LICENSE ├── README.md ├── datasette_plot ├── __init__.py ├── frontend │ ├── components │ │ └── marks.tsx │ └── targets │ │ ├── main.css │ │ └── main.tsx └── static │ ├── main.min.css │ └── main.min.js ├── package-lock.json ├── package.json ├── pyproject.toml ├── tests └── test_plot.py └── tsconfig.json /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish Python Package 2 | 3 | on: 4 | release: 5 | types: [created] 6 | 7 | permissions: 8 | contents: read 9 | 10 | jobs: 11 | test: 12 | runs-on: ubuntu-latest 13 | strategy: 14 | matrix: 15 | python-version: ["3.8", "3.9", "3.10", "3.11"] 16 | steps: 17 | - uses: actions/checkout@v3 18 | - name: Set up Python ${{ matrix.python-version }} 19 | uses: actions/setup-python@v4 20 | with: 21 | python-version: ${{ matrix.python-version }} 22 | cache: pip 23 | cache-dependency-path: pyproject.toml 24 | - name: Install dependencies 25 | run: | 26 | pip install '.[test]' 27 | - name: Run tests 28 | run: | 29 | pytest 30 | deploy: 31 | runs-on: ubuntu-latest 32 | needs: [test] 33 | steps: 34 | - uses: actions/checkout@v3 35 | - name: Set up Python 36 | uses: actions/setup-python@v4 37 | with: 38 | python-version: "3.11" 39 | cache: pip 40 | cache-dependency-path: pyproject.toml 41 | - name: Install dependencies 42 | run: | 43 | pip install setuptools wheel twine build 44 | - name: Publish 45 | env: 46 | TWINE_USERNAME: __token__ 47 | TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }} 48 | run: | 49 | python -m build 50 | twine upload dist/* 51 | 52 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: [push, pull_request] 4 | 5 | permissions: 6 | contents: read 7 | 8 | jobs: 9 | test: 10 | runs-on: ubuntu-latest 11 | strategy: 12 | matrix: 13 | python-version: ["3.8", "3.9", "3.10", "3.11"] 14 | steps: 15 | - uses: actions/checkout@v3 16 | - name: Set up Python ${{ matrix.python-version }} 17 | uses: actions/setup-python@v4 18 | with: 19 | python-version: ${{ matrix.python-version }} 20 | cache: pip 21 | cache-dependency-path: pyproject.toml 22 | - name: Install dependencies 23 | run: | 24 | pip install '.[test]' 25 | - name: Run tests 26 | run: | 27 | pytest 28 | 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .venv 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | venv 6 | .eggs 7 | .pytest_cache 8 | *.egg-info 9 | .DS_Store 10 | .vscode 11 | dist 12 | build 13 | 14 | node_modules/ 15 | 16 | *.csv 17 | *.db 18 | 19 | tests/data 20 | -------------------------------------------------------------------------------- /Justfile: -------------------------------------------------------------------------------- 1 | flags := "" 2 | 3 | js: 4 | ./node_modules/.bin/esbuild \ 5 | --bundle --minify --format=esm --jsx-factory=h --jsx-fragment=Fragment {{flags}} \ 6 | --out-extension:.js=.min.js \ 7 | --out-extension:.css=.min.css \ 8 | datasette_plot/frontend/targets/*.tsx \ 9 | --target=safari12 \ 10 | --outdir=datasette_plot/static 11 | 12 | dev: 13 | DATASETTE_SECRET=abc123 watchexec --signal SIGKILL --restart --clear -e py,ts,js,html,css,sql -- \ 14 | python3 -m datasette -p 8999 \ 15 | --setting max_returned_rows 10000 \ 16 | --root test.db 17 | 18 | test: 19 | pytest 20 | 21 | format: 22 | black . 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # datasette-plot 2 | 3 | [![PyPI](https://img.shields.io/pypi/v/datasette-plot.svg)](https://pypi.org/project/datasette-plot/) 4 | [![Changelog](https://img.shields.io/github/v/release/datasette/datasette-plot?include_prereleases&label=changelog)](https://github.com/datasette/datasette-plot/releases) 5 | [![Tests](https://github.com/datasette/datasette-plot/workflows/Test/badge.svg)](https://github.com/datasette/datasette-plot/actions?query=workflow%3ATest) 6 | [![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](https://github.com/datasette/datasette-plot/blob/main/LICENSE) 7 | 8 | A Datasette plugin for making data visualizations with [Observable Plot](https://observablehq.com/plot/). 9 | 10 | 11 | 12 | ## Installation 13 | 14 | Install this plugin in the same environment as Datasette. 15 | 16 | ```bash 17 | datasette install datasette-plot 18 | ``` 19 | 20 | ## Usage 21 | 22 | Once installed, table pages and SQL query pages will have a new "Show plot" button that will open the `datasette-plot`. 23 | 24 | 25 | 26 | Currently, `datasette-plot` only supports a few [visualization marks](https://observablehq.com/plot/features/marks) from Plot, including dot, line, area, bar, and more. 27 | 28 | 29 | 30 | Use the `Link to this plot` URL to share visualization with others. The link will have your plot options encoded in the URL. 31 | 32 | ## Development 33 | 34 | To set up this plugin locally, first checkout the code. Then create a new virtual environment: 35 | 36 | ```bash 37 | cd datasette-plot 38 | python3 -m venv venv 39 | source venv/bin/activate 40 | ``` 41 | 42 | Now install the dependencies and test dependencies: 43 | 44 | ```bash 45 | pip install -e '.[test]' 46 | ``` 47 | 48 | To run the tests: 49 | 50 | ```bash 51 | pytest 52 | ``` 53 | -------------------------------------------------------------------------------- /datasette_plot/__init__.py: -------------------------------------------------------------------------------- 1 | from datasette import hookimpl 2 | import json 3 | 4 | 5 | @hookimpl 6 | def extra_body_script( 7 | template, database, table, columns, view_name, request, datasette 8 | ): 9 | url = datasette.urls.path("/-/static-plugins/datasette-plot/main.min.js") 10 | return f"import({json.dumps(url)}).then(d => d.main())" 11 | 12 | 13 | @hookimpl 14 | def extra_css_urls(template, database, table, columns, view_name, request, datasette): 15 | return [datasette.urls.path("/-/static-plugins/datasette-plot/main.min.css")] 16 | -------------------------------------------------------------------------------- /datasette_plot/frontend/components/marks.tsx: -------------------------------------------------------------------------------- 1 | import { h } from "preact"; 2 | import { useEffect, useId, useState } from "preact/hooks"; 3 | import { 4 | MarkOptions, 5 | DotOptions, 6 | AreaYOptions, 7 | BarYOptions, 8 | LineYOptions, 9 | LinearRegressionYOptions, 10 | lineY, 11 | barY, 12 | } from "@observablehq/plot"; 13 | 14 | function ChanelValueSelector(props: { 15 | required: boolean; 16 | title: string; 17 | columns: string[]; 18 | value: string | undefined; 19 | setValue: (v: string | undefined) => void; 20 | }) { 21 | const { title, columns, value, setValue, required } = props; 22 | const [show, setShow] = useState( 23 | props.required || value !== undefined 24 | ); 25 | 26 | useEffect(() => { 27 | if (!show && !value) setValue(undefined); 28 | }, [show, value]); 29 | 30 | return ( 31 |
32 |
setShow((d) => !d) : null} 35 | > 36 |
37 |
{title}
38 |
39 | {(show || required) && ( 40 | 50 | )} 51 |
52 |
53 |
{ 55 | if (!required) setShow((d) => !d); 56 | // hiding this should remove any value 57 | if (show) { 58 | setValue(undefined); 59 | } 60 | }} 61 | style={{ 62 | marginLeft: ".5rem", 63 | color: required ? "rgba(0,0,0,0)" : "", 64 | }} 65 | > 66 | {required ? "-" : show ? "-" : "+"} 67 |
68 |
69 |
70 | ); 71 | } 72 | 73 | function DotEditor(props: { 74 | columns: string[]; 75 | onUpdate: (options: DotOptions) => void; 76 | options: DotOptions; 77 | }) { 78 | const [x, setX] = useState(props.options.x as string); 79 | const [y, setY] = useState(props.options.y as string); 80 | const [fill, setFill] = useState( 81 | props.options.fill as string | undefined 82 | ); 83 | const [stroke, setStroke] = useState( 84 | props.options.stroke as string | undefined 85 | ); 86 | useEffect(() => { 87 | props.onUpdate({ x, y, fill, stroke }); 88 | }, [x, y, fill, stroke]); 89 | 90 | const id = useId(); 91 | 92 | return ( 93 |
94 | 101 | 108 | 115 | 122 |
123 | ); 124 | } 125 | function AreaEditor(props: { 126 | columns: string[]; 127 | onUpdate: (options: AreaYOptions) => void; 128 | options: AreaYOptions; 129 | }) { 130 | const [x, setX] = useState(props.options.x as string); 131 | const [y, setY] = useState(props.options.y as string); 132 | const [fill, setFill] = useState(props.options.fill as string); 133 | useEffect(() => { 134 | props.onUpdate({ x, y, fill, tip: true }); 135 | }, [x, y, fill]); 136 | return ( 137 |
138 | 145 | 152 | 159 |
160 | ); 161 | } 162 | function BarEditor(props: { 163 | columns: string[]; 164 | onUpdate: (options: BarYOptions) => void; 165 | options: BarYOptions; 166 | }) { 167 | const [x, setX] = useState(props.options.x as string); 168 | const [y, setY] = useState(props.options.y as string); 169 | const [fill, setFill] = useState(props.options.fill as string); 170 | useEffect(() => { 171 | props.onUpdate({ x, y, fill, tip: true }); 172 | }, [x, y, fill]); 173 | return ( 174 |
175 | 182 | 189 | 196 |
197 | ); 198 | } 199 | function LineEditor(props: { 200 | columns: string[]; 201 | onUpdate: (options: DotOptions) => void; 202 | options: LineYOptions; 203 | }) { 204 | const [x, setX] = useState(props.options.x as string); 205 | const [y, setY] = useState(props.options.y as string); 206 | const [stroke, setStroke] = useState(props.options.stroke as string); 207 | useEffect(() => { 208 | props.onUpdate({ x, y, tip: true }); 209 | }, [x, y]); 210 | return ( 211 |
212 | 219 | 226 | 233 |
234 | ); 235 | } 236 | function LinearRegressionYEditor(props: { 237 | columns: string[]; 238 | onUpdate: (options: LinearRegressionYOptions) => void; 239 | options: LinearRegressionYOptions; 240 | }) { 241 | const [x, setX] = useState(props.options.x as string); 242 | const [y, setY] = useState(props.options.y as string); 243 | const [stroke, setStroke] = useState(props.options.stroke as string); 244 | useEffect(() => { 245 | props.onUpdate({ x, y, stroke, tip: true }); 246 | }, [x, y, stroke]); 247 | return ( 248 |
249 | 256 | 263 | 270 |
271 | ); 272 | } 273 | 274 | export const enum Mark { 275 | Dot = "dot", 276 | LinearRegressionY = "linear-regression-y", 277 | BarY = "bar-y", 278 | AreaY = "area-y", 279 | LineY = "line-y", 280 | } 281 | 282 | export function MarkEditor(props: { 283 | columns: string[]; 284 | initalMark: Mark; 285 | initionalOptions: MarkOptions; 286 | onUpdate: (m: Mark, o: MarkOptions) => void; 287 | onDelete: () => void; 288 | }) { 289 | let [mark, setMark] = useState(props.initalMark); 290 | let [options, setOptions] = useState(props.initionalOptions); 291 | 292 | useEffect(() => { 293 | props.onUpdate(mark, options); 294 | }, [mark, options]); 295 | 296 | function render() { 297 | switch (mark) { 298 | case Mark.Dot: 299 | return ( 300 | 305 | ); 306 | case Mark.LinearRegressionY: 307 | return ( 308 | 313 | ); 314 | case Mark.AreaY: 315 | return ( 316 | 321 | ); 322 | case Mark.BarY: 323 | return ( 324 | 329 | ); 330 | case Mark.LineY: 331 | return ( 332 | 337 | ); 338 | } 339 | } 340 | return ( 341 |
342 |
343 |
344 | 358 |
359 |
360 | 363 |
364 |
365 |
{render()}
366 |
367 | ); 368 | } 369 | -------------------------------------------------------------------------------- /datasette_plot/frontend/targets/main.css: -------------------------------------------------------------------------------- 1 | .datasette-plot { 2 | background: white; 3 | max-width: 800px; 4 | 5 | & .mark-editor { 6 | border: 1px solid black; 7 | max-width: 320px; 8 | padding: 0.5rem; 9 | margin-bottom: 0.5rem; 10 | 11 | & .mark-editor-header { 12 | display: flex; 13 | justify-content: space-between; 14 | } 15 | 16 | & .delete-mark { 17 | background: none; 18 | border: none; 19 | text-decoration: underline; 20 | cursor: pointer; 21 | } 22 | } 23 | 24 | & .channel-value-selector { 25 | &:not(:last-child) { 26 | border-bottom: 1px solid #c5c5c5; 27 | } 28 | & .title-bar { 29 | display: flex; 30 | justify-content: space-between; 31 | &.toggleable { 32 | cursor: pointer; 33 | } 34 | & .dp-title { 35 | font-weight: 600; 36 | } 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /datasette_plot/frontend/targets/main.tsx: -------------------------------------------------------------------------------- 1 | import "./main.css"; 2 | 3 | import { h, render } from "preact"; 4 | import { useEffect, useMemo, useRef, useState } from "preact/hooks"; 5 | import { 6 | dot, 7 | plot, 8 | linearRegressionY, 9 | MarkOptions, 10 | lineY, 11 | barY, 12 | areaY, 13 | } from "@observablehq/plot"; 14 | 15 | import { MarkEditor, Mark } from "../components/marks"; 16 | 17 | function interestingColumns(columns: string[], sample: { [key: string]: any }) { 18 | let x, y; 19 | for (const column of columns) { 20 | if (column === "rowid" || column === "id") continue; 21 | if (typeof sample[column] === "number") { 22 | if (x === undefined) { 23 | x = column; 24 | } else if (y === undefined) { 25 | y = column; 26 | break; 27 | } 28 | } 29 | } 30 | if (x === undefined) columns[0]; 31 | if (y === undefined) columns[1]; 32 | return [x, y]; 33 | } 34 | 35 | interface Row { 36 | [key: string]: string | null | number | Date; 37 | } 38 | 39 | function PlotEditor(props: { 40 | data: any; 41 | columns: string[]; 42 | initialMarks?: { mark: Mark; options: MarkOptions }[]; 43 | }) { 44 | const init = useMemo(() => { 45 | const [x, y] = interestingColumns(props.columns, props.data[0]); 46 | return { mark: Mark.Dot, options: { x, y } } as { 47 | mark: Mark; 48 | options: MarkOptions; 49 | }; 50 | }, []); 51 | const [marks, setMarks] = useState<{ mark: Mark; options: MarkOptions }[]>( 52 | props.initialMarks ?? [init] 53 | ); 54 | 55 | function onAdddMark() { 56 | let x, y; 57 | if (marks.length) { 58 | // @ts-ignore TODO: x/y dont exist in MarkOptions 59 | x = marks[marks.length - 1].options.x; 60 | // @ts-ignore TODO: x/y dont exist in MarkOptions 61 | y = marks[marks.length - 1].options.y; 62 | } else { 63 | [x, y] = interestingColumns(props.columns, props.data[0]); 64 | } 65 | 66 | setMarks([ 67 | ...marks, 68 | { 69 | mark: Mark.Dot, 70 | // @ts-ignore TODO: x/y dont exist in MarkOptions 71 | options: { x, y }, 72 | }, 73 | ]); 74 | } 75 | 76 | return ( 77 |
78 |
79 | 80 |
81 | Marks 82 | {marks.map((mark, idx) => ( 83 | { 88 | setMarks(marks.map((d, i) => (i === idx ? { mark, options } : d))); 89 | }} 90 | onDelete={() => { 91 | setMarks(marks.filter((d, i) => i !== idx)); 92 | }} 93 | /> 94 | ))} 95 | 96 |
97 | ); 98 | } 99 | function Preview(props: { 100 | data: any; 101 | marks: { mark: Mark; options: MarkOptions }[]; 102 | }) { 103 | const target = useRef(null); 104 | useEffect(() => { 105 | if (!target.current) return; 106 | const p = plot({ 107 | width: 800, 108 | color: { legend: true }, 109 | marks: props.marks.map((m) => { 110 | switch (m.mark) { 111 | case Mark.Dot: 112 | return dot(props.data, m.options); 113 | case Mark.LinearRegressionY: 114 | return linearRegressionY(props.data, m.options); 115 | case Mark.LineY: 116 | return lineY(props.data, m.options); 117 | case Mark.BarY: 118 | return barY(props.data, m.options); 119 | case Mark.AreaY: 120 | return areaY(props.data, m.options); 121 | } 122 | }), 123 | }); 124 | 125 | target.current.appendChild(p); 126 | return () => target.current.removeChild(p); 127 | }, [target, props.data, props.marks]); 128 | 129 | const url = (() => { 130 | const baseUrl = new URL(window.location.href); 131 | baseUrl.searchParams.delete("_plot-mark"); 132 | for (const mark of props.marks) { 133 | baseUrl.searchParams.append("_plot-mark", JSON.stringify(mark)); 134 | } 135 | return baseUrl.toString(); 136 | })(); 137 | 138 | return ( 139 |
140 |
141 | Link to this plot 142 |
143 | ); 144 | } 145 | 146 | function App(props: { 147 | rows: any[]; 148 | next: string | null; 149 | columns: string[]; 150 | initialMarks?: { mark: Mark; options: MarkOptions }[]; 151 | }) { 152 | const { rows, next, columns, initialMarks } = props; 153 | const [show, setShow] = useState(props.initialMarks !== null); 154 | if (!show) { 155 | return ( 156 |
157 | 158 |
159 | ); 160 | } 161 | 162 | return ( 163 |
164 | 165 | 166 | {next !== null ? ( 167 |
Warning: not all table rows returned, only {rows.length} rows
168 | ) : null} 169 |
170 | ); 171 | } 172 | 173 | interface DatasetteJsonResponse { 174 | rows: { [key: string]: null | string | number }[]; 175 | ok: boolean; 176 | next: string | null; 177 | truncated: boolean; 178 | } 179 | 180 | export async function main() { 181 | const dataUrl = new URL( 182 | window.location.origin + 183 | window.location.pathname + 184 | ".json" + 185 | window.location.search 186 | ); 187 | if (!dataUrl.searchParams.has("_size")) { 188 | dataUrl.searchParams.set("_size", "max"); 189 | } 190 | 191 | const data = (await fetch(dataUrl).then((r) => 192 | r.json() 193 | )) as DatasetteJsonResponse; 194 | const columns = Object.keys(data.rows[0]); 195 | 196 | // for now, any column named "date" should be converted to JS dates. 197 | const rows: Row[] = data.rows.slice(); 198 | for (const column of columns) { 199 | if (column.toLowerCase() == "date") { 200 | for (const row of rows) { 201 | row[column] = new Date(row[column]); 202 | } 203 | } 204 | } 205 | 206 | const target = 207 | document.querySelector("form.sql") || 208 | document.querySelector("form.filters"); 209 | const root = target.insertAdjacentElement( 210 | "afterend", 211 | document.createElement("div") 212 | ); 213 | 214 | const url = new URL(window.location.href); 215 | const initialMarks = url.searchParams.has("_plot-mark") 216 | ? url.searchParams.getAll("_plot-mark").map((d) => JSON.parse(d)) 217 | : null; 218 | render( 219 | , 225 | root 226 | ); 227 | } 228 | 229 | document.addEventListener("DOMContentLoaded", main); 230 | -------------------------------------------------------------------------------- /datasette_plot/static/main.min.css: -------------------------------------------------------------------------------- 1 | .datasette-plot{background:white;max-width:800px}.datasette-plot .mark-editor{border:1px solid black;max-width:320px;padding:.5rem;margin-bottom:.5rem}.datasette-plot .mark-editor .mark-editor-header{display:flex;justify-content:space-between}.datasette-plot .mark-editor .delete-mark{background:none;border:none;text-decoration:underline;cursor:pointer}.datasette-plot .channel-value-selector:not(:last-child){border-bottom:1px solid #c5c5c5}.datasette-plot .channel-value-selector .title-bar{display:flex;justify-content:space-between}.datasette-plot .channel-value-selector .title-bar.toggleable{cursor:pointer}.datasette-plot .channel-value-selector .title-bar .dp-title{font-weight:600} 2 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "datasette-plot", 3 | "version": "1.0.0", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "datasette-plot", 9 | "version": "1.0.0", 10 | "license": "ISC", 11 | "dependencies": { 12 | "@observablehq/plot": "^0.6.12", 13 | "esbuild": "^0.19.4", 14 | "preact": "^10.18.1" 15 | } 16 | }, 17 | "node_modules/@esbuild/android-arm": { 18 | "version": "0.19.4", 19 | "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.19.4.tgz", 20 | "integrity": "sha512-uBIbiYMeSsy2U0XQoOGVVcpIktjLMEKa7ryz2RLr7L/vTnANNEsPVAh4xOv7ondGz6ac1zVb0F8Jx20rQikffQ==", 21 | "cpu": [ 22 | "arm" 23 | ], 24 | "optional": true, 25 | "os": [ 26 | "android" 27 | ], 28 | "engines": { 29 | "node": ">=12" 30 | } 31 | }, 32 | "node_modules/@esbuild/android-arm64": { 33 | "version": "0.19.4", 34 | "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.19.4.tgz", 35 | "integrity": "sha512-mRsi2vJsk4Bx/AFsNBqOH2fqedxn5L/moT58xgg51DjX1la64Z3Npicut2VbhvDFO26qjWtPMsVxCd80YTFVeg==", 36 | "cpu": [ 37 | "arm64" 38 | ], 39 | "optional": true, 40 | "os": [ 41 | "android" 42 | ], 43 | "engines": { 44 | "node": ">=12" 45 | } 46 | }, 47 | "node_modules/@esbuild/android-x64": { 48 | "version": "0.19.4", 49 | "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.19.4.tgz", 50 | "integrity": "sha512-4iPufZ1TMOD3oBlGFqHXBpa3KFT46aLl6Vy7gwed0ZSYgHaZ/mihbYb4t7Z9etjkC9Al3ZYIoOaHrU60gcMy7g==", 51 | "cpu": [ 52 | "x64" 53 | ], 54 | "optional": true, 55 | "os": [ 56 | "android" 57 | ], 58 | "engines": { 59 | "node": ">=12" 60 | } 61 | }, 62 | "node_modules/@esbuild/darwin-arm64": { 63 | "version": "0.19.4", 64 | "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.19.4.tgz", 65 | "integrity": "sha512-Lviw8EzxsVQKpbS+rSt6/6zjn9ashUZ7Tbuvc2YENgRl0yZTktGlachZ9KMJUsVjZEGFVu336kl5lBgDN6PmpA==", 66 | "cpu": [ 67 | "arm64" 68 | ], 69 | "optional": true, 70 | "os": [ 71 | "darwin" 72 | ], 73 | "engines": { 74 | "node": ">=12" 75 | } 76 | }, 77 | "node_modules/@esbuild/darwin-x64": { 78 | "version": "0.19.4", 79 | "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.19.4.tgz", 80 | "integrity": "sha512-YHbSFlLgDwglFn0lAO3Zsdrife9jcQXQhgRp77YiTDja23FrC2uwnhXMNkAucthsf+Psr7sTwYEryxz6FPAVqw==", 81 | "cpu": [ 82 | "x64" 83 | ], 84 | "optional": true, 85 | "os": [ 86 | "darwin" 87 | ], 88 | "engines": { 89 | "node": ">=12" 90 | } 91 | }, 92 | "node_modules/@esbuild/freebsd-arm64": { 93 | "version": "0.19.4", 94 | "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.4.tgz", 95 | "integrity": "sha512-vz59ijyrTG22Hshaj620e5yhs2dU1WJy723ofc+KUgxVCM6zxQESmWdMuVmUzxtGqtj5heHyB44PjV/HKsEmuQ==", 96 | "cpu": [ 97 | "arm64" 98 | ], 99 | "optional": true, 100 | "os": [ 101 | "freebsd" 102 | ], 103 | "engines": { 104 | "node": ">=12" 105 | } 106 | }, 107 | "node_modules/@esbuild/freebsd-x64": { 108 | "version": "0.19.4", 109 | "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.19.4.tgz", 110 | "integrity": "sha512-3sRbQ6W5kAiVQRBWREGJNd1YE7OgzS0AmOGjDmX/qZZecq8NFlQsQH0IfXjjmD0XtUYqr64e0EKNFjMUlPL3Cw==", 111 | "cpu": [ 112 | "x64" 113 | ], 114 | "optional": true, 115 | "os": [ 116 | "freebsd" 117 | ], 118 | "engines": { 119 | "node": ">=12" 120 | } 121 | }, 122 | "node_modules/@esbuild/linux-arm": { 123 | "version": "0.19.4", 124 | "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.19.4.tgz", 125 | "integrity": "sha512-z/4ArqOo9EImzTi4b6Vq+pthLnepFzJ92BnofU1jgNlcVb+UqynVFdoXMCFreTK7FdhqAzH0vmdwW5373Hm9pg==", 126 | "cpu": [ 127 | "arm" 128 | ], 129 | "optional": true, 130 | "os": [ 131 | "linux" 132 | ], 133 | "engines": { 134 | "node": ">=12" 135 | } 136 | }, 137 | "node_modules/@esbuild/linux-arm64": { 138 | "version": "0.19.4", 139 | "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.19.4.tgz", 140 | "integrity": "sha512-ZWmWORaPbsPwmyu7eIEATFlaqm0QGt+joRE9sKcnVUG3oBbr/KYdNE2TnkzdQwX6EDRdg/x8Q4EZQTXoClUqqA==", 141 | "cpu": [ 142 | "arm64" 143 | ], 144 | "optional": true, 145 | "os": [ 146 | "linux" 147 | ], 148 | "engines": { 149 | "node": ">=12" 150 | } 151 | }, 152 | "node_modules/@esbuild/linux-ia32": { 153 | "version": "0.19.4", 154 | "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.19.4.tgz", 155 | "integrity": "sha512-EGc4vYM7i1GRUIMqRZNCTzJh25MHePYsnQfKDexD8uPTCm9mK56NIL04LUfX2aaJ+C9vyEp2fJ7jbqFEYgO9lQ==", 156 | "cpu": [ 157 | "ia32" 158 | ], 159 | "optional": true, 160 | "os": [ 161 | "linux" 162 | ], 163 | "engines": { 164 | "node": ">=12" 165 | } 166 | }, 167 | "node_modules/@esbuild/linux-loong64": { 168 | "version": "0.19.4", 169 | "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.19.4.tgz", 170 | "integrity": "sha512-WVhIKO26kmm8lPmNrUikxSpXcgd6HDog0cx12BUfA2PkmURHSgx9G6vA19lrlQOMw+UjMZ+l3PpbtzffCxFDRg==", 171 | "cpu": [ 172 | "loong64" 173 | ], 174 | "optional": true, 175 | "os": [ 176 | "linux" 177 | ], 178 | "engines": { 179 | "node": ">=12" 180 | } 181 | }, 182 | "node_modules/@esbuild/linux-mips64el": { 183 | "version": "0.19.4", 184 | "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.19.4.tgz", 185 | "integrity": "sha512-keYY+Hlj5w86hNp5JJPuZNbvW4jql7c1eXdBUHIJGTeN/+0QFutU3GrS+c27L+NTmzi73yhtojHk+lr2+502Mw==", 186 | "cpu": [ 187 | "mips64el" 188 | ], 189 | "optional": true, 190 | "os": [ 191 | "linux" 192 | ], 193 | "engines": { 194 | "node": ">=12" 195 | } 196 | }, 197 | "node_modules/@esbuild/linux-ppc64": { 198 | "version": "0.19.4", 199 | "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.19.4.tgz", 200 | "integrity": "sha512-tQ92n0WMXyEsCH4m32S21fND8VxNiVazUbU4IUGVXQpWiaAxOBvtOtbEt3cXIV3GEBydYsY8pyeRMJx9kn3rvw==", 201 | "cpu": [ 202 | "ppc64" 203 | ], 204 | "optional": true, 205 | "os": [ 206 | "linux" 207 | ], 208 | "engines": { 209 | "node": ">=12" 210 | } 211 | }, 212 | "node_modules/@esbuild/linux-riscv64": { 213 | "version": "0.19.4", 214 | "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.19.4.tgz", 215 | "integrity": "sha512-tRRBey6fG9tqGH6V75xH3lFPpj9E8BH+N+zjSUCnFOX93kEzqS0WdyJHkta/mmJHn7MBaa++9P4ARiU4ykjhig==", 216 | "cpu": [ 217 | "riscv64" 218 | ], 219 | "optional": true, 220 | "os": [ 221 | "linux" 222 | ], 223 | "engines": { 224 | "node": ">=12" 225 | } 226 | }, 227 | "node_modules/@esbuild/linux-s390x": { 228 | "version": "0.19.4", 229 | "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.19.4.tgz", 230 | "integrity": "sha512-152aLpQqKZYhThiJ+uAM4PcuLCAOxDsCekIbnGzPKVBRUDlgaaAfaUl5NYkB1hgY6WN4sPkejxKlANgVcGl9Qg==", 231 | "cpu": [ 232 | "s390x" 233 | ], 234 | "optional": true, 235 | "os": [ 236 | "linux" 237 | ], 238 | "engines": { 239 | "node": ">=12" 240 | } 241 | }, 242 | "node_modules/@esbuild/linux-x64": { 243 | "version": "0.19.4", 244 | "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.19.4.tgz", 245 | "integrity": "sha512-Mi4aNA3rz1BNFtB7aGadMD0MavmzuuXNTaYL6/uiYIs08U7YMPETpgNn5oue3ICr+inKwItOwSsJDYkrE9ekVg==", 246 | "cpu": [ 247 | "x64" 248 | ], 249 | "optional": true, 250 | "os": [ 251 | "linux" 252 | ], 253 | "engines": { 254 | "node": ">=12" 255 | } 256 | }, 257 | "node_modules/@esbuild/netbsd-x64": { 258 | "version": "0.19.4", 259 | "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.19.4.tgz", 260 | "integrity": "sha512-9+Wxx1i5N/CYo505CTT7T+ix4lVzEdz0uCoYGxM5JDVlP2YdDC1Bdz+Khv6IbqmisT0Si928eAxbmGkcbiuM/A==", 261 | "cpu": [ 262 | "x64" 263 | ], 264 | "optional": true, 265 | "os": [ 266 | "netbsd" 267 | ], 268 | "engines": { 269 | "node": ">=12" 270 | } 271 | }, 272 | "node_modules/@esbuild/openbsd-x64": { 273 | "version": "0.19.4", 274 | "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.19.4.tgz", 275 | "integrity": "sha512-MFsHleM5/rWRW9EivFssop+OulYVUoVcqkyOkjiynKBCGBj9Lihl7kh9IzrreDyXa4sNkquei5/DTP4uCk25xw==", 276 | "cpu": [ 277 | "x64" 278 | ], 279 | "optional": true, 280 | "os": [ 281 | "openbsd" 282 | ], 283 | "engines": { 284 | "node": ">=12" 285 | } 286 | }, 287 | "node_modules/@esbuild/sunos-x64": { 288 | "version": "0.19.4", 289 | "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.19.4.tgz", 290 | "integrity": "sha512-6Xq8SpK46yLvrGxjp6HftkDwPP49puU4OF0hEL4dTxqCbfx09LyrbUj/D7tmIRMj5D5FCUPksBbxyQhp8tmHzw==", 291 | "cpu": [ 292 | "x64" 293 | ], 294 | "optional": true, 295 | "os": [ 296 | "sunos" 297 | ], 298 | "engines": { 299 | "node": ">=12" 300 | } 301 | }, 302 | "node_modules/@esbuild/win32-arm64": { 303 | "version": "0.19.4", 304 | "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.19.4.tgz", 305 | "integrity": "sha512-PkIl7Jq4mP6ke7QKwyg4fD4Xvn8PXisagV/+HntWoDEdmerB2LTukRZg728Yd1Fj+LuEX75t/hKXE2Ppk8Hh1w==", 306 | "cpu": [ 307 | "arm64" 308 | ], 309 | "optional": true, 310 | "os": [ 311 | "win32" 312 | ], 313 | "engines": { 314 | "node": ">=12" 315 | } 316 | }, 317 | "node_modules/@esbuild/win32-ia32": { 318 | "version": "0.19.4", 319 | "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.19.4.tgz", 320 | "integrity": "sha512-ga676Hnvw7/ycdKB53qPusvsKdwrWzEyJ+AtItHGoARszIqvjffTwaaW3b2L6l90i7MO9i+dlAW415INuRhSGg==", 321 | "cpu": [ 322 | "ia32" 323 | ], 324 | "optional": true, 325 | "os": [ 326 | "win32" 327 | ], 328 | "engines": { 329 | "node": ">=12" 330 | } 331 | }, 332 | "node_modules/@esbuild/win32-x64": { 333 | "version": "0.19.4", 334 | "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.19.4.tgz", 335 | "integrity": "sha512-HP0GDNla1T3ZL8Ko/SHAS2GgtjOg+VmWnnYLhuTksr++EnduYB0f3Y2LzHsUwb2iQ13JGoY6G3R8h6Du/WG6uA==", 336 | "cpu": [ 337 | "x64" 338 | ], 339 | "optional": true, 340 | "os": [ 341 | "win32" 342 | ], 343 | "engines": { 344 | "node": ">=12" 345 | } 346 | }, 347 | "node_modules/@observablehq/plot": { 348 | "version": "0.6.12", 349 | "resolved": "https://registry.npmjs.org/@observablehq/plot/-/plot-0.6.12.tgz", 350 | "integrity": "sha512-tM2yVFfGqisYd2lKDi9rfkO31W+sqBuAOBfrP2zFeHgjGY2BRbN+4khapLGdNMNX8x81r0HrQlMnQmD9gIo6IQ==", 351 | "dependencies": { 352 | "d3": "^7.8.0", 353 | "interval-tree-1d": "^1.0.0", 354 | "isoformat": "^0.2.0" 355 | }, 356 | "engines": { 357 | "node": ">=12" 358 | } 359 | }, 360 | "node_modules/binary-search-bounds": { 361 | "version": "2.0.5", 362 | "resolved": "https://registry.npmjs.org/binary-search-bounds/-/binary-search-bounds-2.0.5.tgz", 363 | "integrity": "sha512-H0ea4Fd3lS1+sTEB2TgcLoK21lLhwEJzlQv3IN47pJS976Gx4zoWe0ak3q+uYh60ppQxg9F16Ri4tS1sfD4+jA==" 364 | }, 365 | "node_modules/commander": { 366 | "version": "7.2.0", 367 | "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", 368 | "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", 369 | "engines": { 370 | "node": ">= 10" 371 | } 372 | }, 373 | "node_modules/d3": { 374 | "version": "7.8.5", 375 | "resolved": "https://registry.npmjs.org/d3/-/d3-7.8.5.tgz", 376 | "integrity": "sha512-JgoahDG51ncUfJu6wX/1vWQEqOflgXyl4MaHqlcSruTez7yhaRKR9i8VjjcQGeS2en/jnFivXuaIMnseMMt0XA==", 377 | "dependencies": { 378 | "d3-array": "3", 379 | "d3-axis": "3", 380 | "d3-brush": "3", 381 | "d3-chord": "3", 382 | "d3-color": "3", 383 | "d3-contour": "4", 384 | "d3-delaunay": "6", 385 | "d3-dispatch": "3", 386 | "d3-drag": "3", 387 | "d3-dsv": "3", 388 | "d3-ease": "3", 389 | "d3-fetch": "3", 390 | "d3-force": "3", 391 | "d3-format": "3", 392 | "d3-geo": "3", 393 | "d3-hierarchy": "3", 394 | "d3-interpolate": "3", 395 | "d3-path": "3", 396 | "d3-polygon": "3", 397 | "d3-quadtree": "3", 398 | "d3-random": "3", 399 | "d3-scale": "4", 400 | "d3-scale-chromatic": "3", 401 | "d3-selection": "3", 402 | "d3-shape": "3", 403 | "d3-time": "3", 404 | "d3-time-format": "4", 405 | "d3-timer": "3", 406 | "d3-transition": "3", 407 | "d3-zoom": "3" 408 | }, 409 | "engines": { 410 | "node": ">=12" 411 | } 412 | }, 413 | "node_modules/d3-array": { 414 | "version": "3.2.4", 415 | "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", 416 | "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", 417 | "dependencies": { 418 | "internmap": "1 - 2" 419 | }, 420 | "engines": { 421 | "node": ">=12" 422 | } 423 | }, 424 | "node_modules/d3-axis": { 425 | "version": "3.0.0", 426 | "resolved": "https://registry.npmjs.org/d3-axis/-/d3-axis-3.0.0.tgz", 427 | "integrity": "sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==", 428 | "engines": { 429 | "node": ">=12" 430 | } 431 | }, 432 | "node_modules/d3-brush": { 433 | "version": "3.0.0", 434 | "resolved": "https://registry.npmjs.org/d3-brush/-/d3-brush-3.0.0.tgz", 435 | "integrity": "sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==", 436 | "dependencies": { 437 | "d3-dispatch": "1 - 3", 438 | "d3-drag": "2 - 3", 439 | "d3-interpolate": "1 - 3", 440 | "d3-selection": "3", 441 | "d3-transition": "3" 442 | }, 443 | "engines": { 444 | "node": ">=12" 445 | } 446 | }, 447 | "node_modules/d3-chord": { 448 | "version": "3.0.1", 449 | "resolved": "https://registry.npmjs.org/d3-chord/-/d3-chord-3.0.1.tgz", 450 | "integrity": "sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==", 451 | "dependencies": { 452 | "d3-path": "1 - 3" 453 | }, 454 | "engines": { 455 | "node": ">=12" 456 | } 457 | }, 458 | "node_modules/d3-color": { 459 | "version": "3.1.0", 460 | "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", 461 | "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", 462 | "engines": { 463 | "node": ">=12" 464 | } 465 | }, 466 | "node_modules/d3-contour": { 467 | "version": "4.0.2", 468 | "resolved": "https://registry.npmjs.org/d3-contour/-/d3-contour-4.0.2.tgz", 469 | "integrity": "sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==", 470 | "dependencies": { 471 | "d3-array": "^3.2.0" 472 | }, 473 | "engines": { 474 | "node": ">=12" 475 | } 476 | }, 477 | "node_modules/d3-delaunay": { 478 | "version": "6.0.4", 479 | "resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-6.0.4.tgz", 480 | "integrity": "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==", 481 | "dependencies": { 482 | "delaunator": "5" 483 | }, 484 | "engines": { 485 | "node": ">=12" 486 | } 487 | }, 488 | "node_modules/d3-dispatch": { 489 | "version": "3.0.1", 490 | "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", 491 | "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==", 492 | "engines": { 493 | "node": ">=12" 494 | } 495 | }, 496 | "node_modules/d3-drag": { 497 | "version": "3.0.0", 498 | "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz", 499 | "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", 500 | "dependencies": { 501 | "d3-dispatch": "1 - 3", 502 | "d3-selection": "3" 503 | }, 504 | "engines": { 505 | "node": ">=12" 506 | } 507 | }, 508 | "node_modules/d3-dsv": { 509 | "version": "3.0.1", 510 | "resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-3.0.1.tgz", 511 | "integrity": "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==", 512 | "dependencies": { 513 | "commander": "7", 514 | "iconv-lite": "0.6", 515 | "rw": "1" 516 | }, 517 | "bin": { 518 | "csv2json": "bin/dsv2json.js", 519 | "csv2tsv": "bin/dsv2dsv.js", 520 | "dsv2dsv": "bin/dsv2dsv.js", 521 | "dsv2json": "bin/dsv2json.js", 522 | "json2csv": "bin/json2dsv.js", 523 | "json2dsv": "bin/json2dsv.js", 524 | "json2tsv": "bin/json2dsv.js", 525 | "tsv2csv": "bin/dsv2dsv.js", 526 | "tsv2json": "bin/dsv2json.js" 527 | }, 528 | "engines": { 529 | "node": ">=12" 530 | } 531 | }, 532 | "node_modules/d3-ease": { 533 | "version": "3.0.1", 534 | "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", 535 | "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", 536 | "engines": { 537 | "node": ">=12" 538 | } 539 | }, 540 | "node_modules/d3-fetch": { 541 | "version": "3.0.1", 542 | "resolved": "https://registry.npmjs.org/d3-fetch/-/d3-fetch-3.0.1.tgz", 543 | "integrity": "sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==", 544 | "dependencies": { 545 | "d3-dsv": "1 - 3" 546 | }, 547 | "engines": { 548 | "node": ">=12" 549 | } 550 | }, 551 | "node_modules/d3-force": { 552 | "version": "3.0.0", 553 | "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-3.0.0.tgz", 554 | "integrity": "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==", 555 | "dependencies": { 556 | "d3-dispatch": "1 - 3", 557 | "d3-quadtree": "1 - 3", 558 | "d3-timer": "1 - 3" 559 | }, 560 | "engines": { 561 | "node": ">=12" 562 | } 563 | }, 564 | "node_modules/d3-format": { 565 | "version": "3.1.0", 566 | "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz", 567 | "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==", 568 | "engines": { 569 | "node": ">=12" 570 | } 571 | }, 572 | "node_modules/d3-geo": { 573 | "version": "3.1.0", 574 | "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-3.1.0.tgz", 575 | "integrity": "sha512-JEo5HxXDdDYXCaWdwLRt79y7giK8SbhZJbFWXqbRTolCHFI5jRqteLzCsq51NKbUoX0PjBVSohxrx+NoOUujYA==", 576 | "dependencies": { 577 | "d3-array": "2.5.0 - 3" 578 | }, 579 | "engines": { 580 | "node": ">=12" 581 | } 582 | }, 583 | "node_modules/d3-hierarchy": { 584 | "version": "3.1.2", 585 | "resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz", 586 | "integrity": "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==", 587 | "engines": { 588 | "node": ">=12" 589 | } 590 | }, 591 | "node_modules/d3-interpolate": { 592 | "version": "3.0.1", 593 | "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", 594 | "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", 595 | "dependencies": { 596 | "d3-color": "1 - 3" 597 | }, 598 | "engines": { 599 | "node": ">=12" 600 | } 601 | }, 602 | "node_modules/d3-path": { 603 | "version": "3.1.0", 604 | "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", 605 | "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", 606 | "engines": { 607 | "node": ">=12" 608 | } 609 | }, 610 | "node_modules/d3-polygon": { 611 | "version": "3.0.1", 612 | "resolved": "https://registry.npmjs.org/d3-polygon/-/d3-polygon-3.0.1.tgz", 613 | "integrity": "sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==", 614 | "engines": { 615 | "node": ">=12" 616 | } 617 | }, 618 | "node_modules/d3-quadtree": { 619 | "version": "3.0.1", 620 | "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-3.0.1.tgz", 621 | "integrity": "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==", 622 | "engines": { 623 | "node": ">=12" 624 | } 625 | }, 626 | "node_modules/d3-random": { 627 | "version": "3.0.1", 628 | "resolved": "https://registry.npmjs.org/d3-random/-/d3-random-3.0.1.tgz", 629 | "integrity": "sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==", 630 | "engines": { 631 | "node": ">=12" 632 | } 633 | }, 634 | "node_modules/d3-scale": { 635 | "version": "4.0.2", 636 | "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", 637 | "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", 638 | "dependencies": { 639 | "d3-array": "2.10.0 - 3", 640 | "d3-format": "1 - 3", 641 | "d3-interpolate": "1.2.0 - 3", 642 | "d3-time": "2.1.1 - 3", 643 | "d3-time-format": "2 - 4" 644 | }, 645 | "engines": { 646 | "node": ">=12" 647 | } 648 | }, 649 | "node_modules/d3-scale-chromatic": { 650 | "version": "3.0.0", 651 | "resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-3.0.0.tgz", 652 | "integrity": "sha512-Lx9thtxAKrO2Pq6OO2Ua474opeziKr279P/TKZsMAhYyNDD3EnCffdbgeSYN5O7m2ByQsxtuP2CSDczNUIZ22g==", 653 | "dependencies": { 654 | "d3-color": "1 - 3", 655 | "d3-interpolate": "1 - 3" 656 | }, 657 | "engines": { 658 | "node": ">=12" 659 | } 660 | }, 661 | "node_modules/d3-selection": { 662 | "version": "3.0.0", 663 | "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", 664 | "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", 665 | "engines": { 666 | "node": ">=12" 667 | } 668 | }, 669 | "node_modules/d3-shape": { 670 | "version": "3.2.0", 671 | "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", 672 | "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", 673 | "dependencies": { 674 | "d3-path": "^3.1.0" 675 | }, 676 | "engines": { 677 | "node": ">=12" 678 | } 679 | }, 680 | "node_modules/d3-time": { 681 | "version": "3.1.0", 682 | "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", 683 | "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", 684 | "dependencies": { 685 | "d3-array": "2 - 3" 686 | }, 687 | "engines": { 688 | "node": ">=12" 689 | } 690 | }, 691 | "node_modules/d3-time-format": { 692 | "version": "4.1.0", 693 | "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", 694 | "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", 695 | "dependencies": { 696 | "d3-time": "1 - 3" 697 | }, 698 | "engines": { 699 | "node": ">=12" 700 | } 701 | }, 702 | "node_modules/d3-timer": { 703 | "version": "3.0.1", 704 | "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", 705 | "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", 706 | "engines": { 707 | "node": ">=12" 708 | } 709 | }, 710 | "node_modules/d3-transition": { 711 | "version": "3.0.1", 712 | "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz", 713 | "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", 714 | "dependencies": { 715 | "d3-color": "1 - 3", 716 | "d3-dispatch": "1 - 3", 717 | "d3-ease": "1 - 3", 718 | "d3-interpolate": "1 - 3", 719 | "d3-timer": "1 - 3" 720 | }, 721 | "engines": { 722 | "node": ">=12" 723 | }, 724 | "peerDependencies": { 725 | "d3-selection": "2 - 3" 726 | } 727 | }, 728 | "node_modules/d3-zoom": { 729 | "version": "3.0.0", 730 | "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz", 731 | "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==", 732 | "dependencies": { 733 | "d3-dispatch": "1 - 3", 734 | "d3-drag": "2 - 3", 735 | "d3-interpolate": "1 - 3", 736 | "d3-selection": "2 - 3", 737 | "d3-transition": "2 - 3" 738 | }, 739 | "engines": { 740 | "node": ">=12" 741 | } 742 | }, 743 | "node_modules/delaunator": { 744 | "version": "5.0.0", 745 | "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.0.0.tgz", 746 | "integrity": "sha512-AyLvtyJdbv/U1GkiS6gUUzclRoAY4Gs75qkMygJJhU75LW4DNuSF2RMzpxs9jw9Oz1BobHjTdkG3zdP55VxAqw==", 747 | "dependencies": { 748 | "robust-predicates": "^3.0.0" 749 | } 750 | }, 751 | "node_modules/esbuild": { 752 | "version": "0.19.4", 753 | "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.4.tgz", 754 | "integrity": "sha512-x7jL0tbRRpv4QUyuDMjONtWFciygUxWaUM1kMX2zWxI0X2YWOt7MSA0g4UdeSiHM8fcYVzpQhKYOycZwxTdZkA==", 755 | "hasInstallScript": true, 756 | "bin": { 757 | "esbuild": "bin/esbuild" 758 | }, 759 | "engines": { 760 | "node": ">=12" 761 | }, 762 | "optionalDependencies": { 763 | "@esbuild/android-arm": "0.19.4", 764 | "@esbuild/android-arm64": "0.19.4", 765 | "@esbuild/android-x64": "0.19.4", 766 | "@esbuild/darwin-arm64": "0.19.4", 767 | "@esbuild/darwin-x64": "0.19.4", 768 | "@esbuild/freebsd-arm64": "0.19.4", 769 | "@esbuild/freebsd-x64": "0.19.4", 770 | "@esbuild/linux-arm": "0.19.4", 771 | "@esbuild/linux-arm64": "0.19.4", 772 | "@esbuild/linux-ia32": "0.19.4", 773 | "@esbuild/linux-loong64": "0.19.4", 774 | "@esbuild/linux-mips64el": "0.19.4", 775 | "@esbuild/linux-ppc64": "0.19.4", 776 | "@esbuild/linux-riscv64": "0.19.4", 777 | "@esbuild/linux-s390x": "0.19.4", 778 | "@esbuild/linux-x64": "0.19.4", 779 | "@esbuild/netbsd-x64": "0.19.4", 780 | "@esbuild/openbsd-x64": "0.19.4", 781 | "@esbuild/sunos-x64": "0.19.4", 782 | "@esbuild/win32-arm64": "0.19.4", 783 | "@esbuild/win32-ia32": "0.19.4", 784 | "@esbuild/win32-x64": "0.19.4" 785 | } 786 | }, 787 | "node_modules/iconv-lite": { 788 | "version": "0.6.3", 789 | "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", 790 | "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", 791 | "dependencies": { 792 | "safer-buffer": ">= 2.1.2 < 3.0.0" 793 | }, 794 | "engines": { 795 | "node": ">=0.10.0" 796 | } 797 | }, 798 | "node_modules/internmap": { 799 | "version": "2.0.3", 800 | "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", 801 | "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", 802 | "engines": { 803 | "node": ">=12" 804 | } 805 | }, 806 | "node_modules/interval-tree-1d": { 807 | "version": "1.0.4", 808 | "resolved": "https://registry.npmjs.org/interval-tree-1d/-/interval-tree-1d-1.0.4.tgz", 809 | "integrity": "sha512-wY8QJH+6wNI0uh4pDQzMvl+478Qh7Rl4qLmqiluxALlNvl+I+o5x38Pw3/z7mDPTPS1dQalZJXsmbvxx5gclhQ==", 810 | "dependencies": { 811 | "binary-search-bounds": "^2.0.0" 812 | } 813 | }, 814 | "node_modules/isoformat": { 815 | "version": "0.2.1", 816 | "resolved": "https://registry.npmjs.org/isoformat/-/isoformat-0.2.1.tgz", 817 | "integrity": "sha512-tFLRAygk9NqrRPhJSnNGh7g7oaVWDwR0wKh/GM2LgmPa50Eg4UfyaCO4I8k6EqJHl1/uh2RAD6g06n5ygEnrjQ==" 818 | }, 819 | "node_modules/preact": { 820 | "version": "10.18.1", 821 | "resolved": "https://registry.npmjs.org/preact/-/preact-10.18.1.tgz", 822 | "integrity": "sha512-mKUD7RRkQQM6s7Rkmi7IFkoEHjuFqRQUaXamO61E6Nn7vqF/bo7EZCmSyrUnp2UWHw0O7XjZ2eeXis+m7tf4lg==", 823 | "funding": { 824 | "type": "opencollective", 825 | "url": "https://opencollective.com/preact" 826 | } 827 | }, 828 | "node_modules/robust-predicates": { 829 | "version": "3.0.2", 830 | "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.2.tgz", 831 | "integrity": "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==" 832 | }, 833 | "node_modules/rw": { 834 | "version": "1.3.3", 835 | "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz", 836 | "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==" 837 | }, 838 | "node_modules/safer-buffer": { 839 | "version": "2.1.2", 840 | "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", 841 | "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" 842 | } 843 | } 844 | } 845 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "datasette-plot", 3 | "version": "1.0.0", 4 | "description": "[![PyPI](https://img.shields.io/pypi/v/datasette-plot.svg)](https://pypi.org/project/datasette-plot/) [![Changelog](https://img.shields.io/github/v/release/datasette/datasette-plot?include_prereleases&label=changelog)](https://github.com/datasette/datasette-plot/releases) [![Tests](https://github.com/datasette/datasette-plot/workflows/Test/badge.svg)](https://github.com/datasette/datasette-plot/actions?query=workflow%3ATest) [![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](https://github.com/datasette/datasette-plot/blob/main/LICENSE)", 5 | "main": "index.js", 6 | "directories": { 7 | "test": "tests" 8 | }, 9 | "scripts": { 10 | "test": "echo \"Error: no test specified\" && exit 1" 11 | }, 12 | "keywords": [], 13 | "author": "", 14 | "license": "ISC", 15 | "dependencies": { 16 | "@observablehq/plot": "^0.6.12", 17 | "esbuild": "^0.19.4", 18 | "preact": "^10.18.1" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "datasette-plot" 3 | version = "0.1.0" 4 | description = "Observable Plot Datasette plugin" 5 | readme = "README.md" 6 | authors = [{name = "Alex Garcia"}] 7 | license = {text = "Apache-2.0"} 8 | classifiers=[ 9 | "Framework :: Datasette", 10 | "License :: OSI Approved :: Apache Software License" 11 | ] 12 | requires-python = ">=3.8" 13 | dependencies = [ 14 | "datasette>=1.0a6" 15 | ] 16 | 17 | [project.urls] 18 | Homepage = "https://github.com/datasette/datasette-plot" 19 | Changelog = "https://github.com/datasette/datasette-plot/releases" 20 | Issues = "https://github.com/datasette/datasette-plot/issues" 21 | CI = "https://github.com/datasette/datasette-plot/actions" 22 | 23 | [project.entry-points.datasette] 24 | plot = "datasette_plot" 25 | 26 | [project.optional-dependencies] 27 | test = ["pytest", "pytest-asyncio", "ruff"] 28 | 29 | [tool.pytest.ini_options] 30 | asyncio_mode = "strict" 31 | 32 | 33 | [tool.setuptools.packages.find] 34 | namespaces = false 35 | 36 | [tool.setuptools.package-data] 37 | datasette_plot = ["static/*", "templates/*"] 38 | -------------------------------------------------------------------------------- /tests/test_plot.py: -------------------------------------------------------------------------------- 1 | from datasette.app import Datasette 2 | import pytest 3 | 4 | 5 | @pytest.mark.asyncio 6 | async def test_plugin_is_installed(): 7 | datasette = Datasette(memory=True) 8 | response = await datasette.client.get("/-/plugins.json") 9 | assert response.status_code == 200 10 | installed_plugins = {p["name"] for p in response.json()} 11 | assert "datasette-plot" in installed_plugins 12 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2019", 4 | "moduleResolution": "Node", 5 | "jsx": "react", 6 | "jsxFactory": "h", 7 | "jsxFragmentFactory": "Fragment" 8 | } 9 | } 10 | --------------------------------------------------------------------------------