python-plotto 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 The plotto authors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,46 @@
1
+ Metadata-Version: 2.4
2
+ Name: python-plotto
3
+ Version: 0.1.0
4
+ Summary: Grammar-of-graphics plots for notebooks, web pages and slides with one renderer.
5
+ License-Expression: MIT
6
+ Project-URL: Homepage, https://theoradusz.github.io/plotto/
7
+ Project-URL: Source, https://github.com/theoradusz/plotto
8
+ Keywords: plot,chart,grammar of graphics,ggplot,notebook,visualization
9
+ Classifier: Development Status :: 4 - Beta
10
+ Classifier: Framework :: Jupyter
11
+ Classifier: Intended Audience :: Science/Research
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Topic :: Scientific/Engineering :: Visualization
14
+ Requires-Python: >=3.12
15
+ Description-Content-Type: text/markdown
16
+ License-File: LICENSE
17
+ Requires-Dist: pandas>=2
18
+ Requires-Dist: numpy
19
+ Provides-Extra: test
20
+ Requires-Dist: pytest; extra == "test"
21
+ Requires-Dist: ruff; extra == "test"
22
+ Requires-Dist: black; extra == "test"
23
+ Requires-Dist: nbconvert; extra == "test"
24
+ Requires-Dist: ipykernel; extra == "test"
25
+ Dynamic: license-file
26
+
27
+ # python-plotto
28
+
29
+ Grammar-of-graphics plots for notebooks, web pages and slides with one renderer.
30
+
31
+ ```
32
+ pip install python-plotto
33
+ ```
34
+
35
+ ```python
36
+ from plotto import plot
37
+
38
+ p = plot(df, x="time", y="value", color="sensor").line().facet("indicator", free="y")
39
+ p # in a notebook: drawn at the cell width, with copy and download buttons
40
+ p.interactive() # zoom, pan, hover, legend toggle
41
+ p.save("fig.png") # needs Node.js 20 or later
42
+ ```
43
+
44
+ - Gallery, grammar tutorial and reference: https://theoradusz.github.io/plotto/
45
+ - Source and full documentation: https://github.com/theoradusz/plotto
46
+ - The JavaScript package for web pages and React: `npm install plotto`
@@ -0,0 +1,20 @@
1
+ # python-plotto
2
+
3
+ Grammar-of-graphics plots for notebooks, web pages and slides with one renderer.
4
+
5
+ ```
6
+ pip install python-plotto
7
+ ```
8
+
9
+ ```python
10
+ from plotto import plot
11
+
12
+ p = plot(df, x="time", y="value", color="sensor").line().facet("indicator", free="y")
13
+ p # in a notebook: drawn at the cell width, with copy and download buttons
14
+ p.interactive() # zoom, pan, hover, legend toggle
15
+ p.save("fig.png") # needs Node.js 20 or later
16
+ ```
17
+
18
+ - Gallery, grammar tutorial and reference: https://theoradusz.github.io/plotto/
19
+ - Source and full documentation: https://github.com/theoradusz/plotto
20
+ - The JavaScript package for web pages and React: `npm install plotto`
@@ -0,0 +1,10 @@
1
+ """plotto: grammar-of-graphics plots for notebooks, web pages and slides with one renderer.
2
+
3
+ https://github.com/theoradusz/plotto
4
+ """
5
+
6
+ __version__ = "0.1.0"
7
+
8
+ from .plot import Plot, plot # noqa: E402
9
+
10
+ __all__ = ["Plot", "plot", "__version__"]
@@ -0,0 +1,51 @@
1
+ {
2
+ "options": {
3
+ "size": "number",
4
+ "alpha": "number",
5
+ "dash": ["solid", "dashed", "dotted", "dashdot"],
6
+ "jitter": "number",
7
+ "summary": ["mean", "median", "min", "max", "sum"],
8
+ "slope": "number",
9
+ "intercept": "number",
10
+ "position": ["stack", "dodge", "overlay"],
11
+ "bins": "number",
12
+ "density": "boolean",
13
+ "method": ["lm", "loess"],
14
+ "span": "number",
15
+ "band": "boolean"
16
+ },
17
+ "layers": {
18
+ "line": { "channels": ["x", "y", "color", "group", "tooltip"], "options": ["size", "alpha", "dash", "summary"], "requires": ["x", "y"] },
19
+ "path": { "channels": ["x", "y", "color", "group", "tooltip"], "options": ["size", "alpha", "dash"], "requires": ["x", "y"] },
20
+ "step": { "channels": ["x", "y", "color", "group", "tooltip"], "options": ["size", "alpha", "dash"], "requires": ["x", "y"] },
21
+ "point": { "channels": ["x", "y", "color", "tooltip"], "options": ["size", "alpha", "jitter", "summary"], "requires": ["x", "y"] },
22
+ "bar": { "channels": ["x", "y", "color", "tooltip"], "options": ["alpha", "position"], "requires": ["x", "y"] },
23
+ "hist": { "channels": ["x", "color"], "options": ["alpha", "bins", "density", "position"], "requires": ["x"] },
24
+ "density": { "channels": ["x", "color"], "options": ["size", "alpha", "dash"], "requires": ["x"] },
25
+ "area": { "channels": ["x", "y", "color", "group"], "options": ["alpha", "position"], "requires": ["x", "y"] },
26
+ "ribbon": { "channels": ["x", "ymin", "ymax", "color", "group"], "options": ["alpha"], "requires": ["x", "ymin", "ymax"] },
27
+ "smooth": { "channels": ["x", "y", "color"], "options": ["size", "alpha", "dash", "method", "span", "band"], "requires": ["x", "y"] },
28
+ "hline": { "channels": ["y", "color"], "options": ["size", "alpha", "dash"], "requires": ["y"], "inherit": false },
29
+ "vline": { "channels": ["x", "color"], "options": ["size", "alpha", "dash"], "requires": ["x"], "inherit": false },
30
+ "abline": { "channels": ["color"], "options": ["slope", "intercept", "size", "alpha", "dash"], "requires": [], "inherit": false },
31
+ "segment": { "channels": ["x", "y", "xend", "yend", "color"], "options": ["size", "alpha", "dash"], "requires": ["x", "y", "xend", "yend"] },
32
+ "rect": { "channels": ["xmin", "xmax", "ymin", "ymax", "color", "tooltip"], "options": ["alpha"], "requires": ["xmin", "xmax", "ymin", "ymax"] },
33
+ "band": { "channels": ["xmin", "xmax", "ymin", "ymax", "color"], "options": ["alpha"], "requires": [], "inherit": false },
34
+ "tile": { "channels": ["x", "y", "color", "tooltip"], "options": ["alpha"], "requires": ["x", "y", "color"] },
35
+ "text": { "channels": ["x", "y", "label", "color"], "options": ["size", "alpha"], "requires": ["x", "y", "label"] },
36
+ "errorbar": { "channels": ["x", "ymin", "ymax", "color"], "options": ["size", "alpha"], "requires": ["x", "ymin", "ymax"] },
37
+ "box": { "channels": ["x", "y", "color"], "options": ["alpha"], "requires": ["x", "y"] }
38
+ },
39
+ "scales": {
40
+ "x": ["type", "limits", "format"],
41
+ "y": ["type", "limits", "format"],
42
+ "color": ["values", "palette", "scheme", "limits", "mid", "clamp"]
43
+ },
44
+ "scaleTypes": ["linear", "log", "time", "category"],
45
+ "facet": ["free", "ncol"],
46
+ "grid": ["free"],
47
+ "free": ["x", "y", "both"],
48
+ "labels": ["title", "subtitle", "x", "y", "color", "caption"],
49
+ "theme": ["format", "width", "height", "size", "legend"],
50
+ "legend": ["right", "bottom", "none"]
51
+ }
@@ -0,0 +1,34 @@
1
+ var N={options:{size:"number",alpha:"number",dash:["solid","dashed","dotted","dashdot"],jitter:"number",summary:["mean","median","min","max","sum"],slope:"number",intercept:"number",position:["stack","dodge","overlay"],bins:"number",density:"boolean",method:["lm","loess"],span:"number",band:"boolean"},layers:{line:{channels:["x","y","color","group","tooltip"],options:["size","alpha","dash","summary"],requires:["x","y"]},path:{channels:["x","y","color","group","tooltip"],options:["size","alpha","dash"],requires:["x","y"]},step:{channels:["x","y","color","group","tooltip"],options:["size","alpha","dash"],requires:["x","y"]},point:{channels:["x","y","color","tooltip"],options:["size","alpha","jitter","summary"],requires:["x","y"]},bar:{channels:["x","y","color","tooltip"],options:["alpha","position"],requires:["x","y"]},hist:{channels:["x","color"],options:["alpha","bins","density","position"],requires:["x"]},density:{channels:["x","color"],options:["size","alpha","dash"],requires:["x"]},area:{channels:["x","y","color","group"],options:["alpha","position"],requires:["x","y"]},ribbon:{channels:["x","ymin","ymax","color","group"],options:["alpha"],requires:["x","ymin","ymax"]},smooth:{channels:["x","y","color"],options:["size","alpha","dash","method","span","band"],requires:["x","y"]},hline:{channels:["y","color"],options:["size","alpha","dash"],requires:["y"],inherit:!1},vline:{channels:["x","color"],options:["size","alpha","dash"],requires:["x"],inherit:!1},abline:{channels:["color"],options:["slope","intercept","size","alpha","dash"],requires:[],inherit:!1},segment:{channels:["x","y","xend","yend","color"],options:["size","alpha","dash"],requires:["x","y","xend","yend"]},rect:{channels:["xmin","xmax","ymin","ymax","color","tooltip"],options:["alpha"],requires:["xmin","xmax","ymin","ymax"]},band:{channels:["xmin","xmax","ymin","ymax","color"],options:["alpha"],requires:[],inherit:!1},tile:{channels:["x","y","color","tooltip"],options:["alpha"],requires:["x","y","color"]},text:{channels:["x","y","label","color"],options:["size","alpha"],requires:["x","y","label"]},errorbar:{channels:["x","ymin","ymax","color"],options:["size","alpha"],requires:["x","ymin","ymax"]},box:{channels:["x","y","color"],options:["alpha"],requires:["x","y"]}},scales:{x:["type","limits","format"],y:["type","limits","format"],color:["values","palette","scheme","limits","mid","clamp"]},scaleTypes:["linear","log","time","category"],facet:["free","ncol"],grid:["free"],free:["x","y","both"],labels:["title","subtitle","x","y","color","caption"],theme:["format","width","height","size","legend"],legend:["right","bottom","none"]};var O={styles:{pastel:{font:"Helvetica, Arial, 'Liberation Sans', sans-serif",metrics:"arial",background:"#FFFEFB",ink:"#3A3631",muted:"#7A736A",grid:"#ECE8E1",gridMinor:"#F6F3EE",tick:"#D6D0C6",strip:"#F4F0E9",accent:"#3E8A99",palette:"pastel",scheme:"ai",colors:{blue:"#7AA6DA",orange:"#EF977F",green:"#7DB89A",purple:"#A79BD6",pink:"#E48FAE",yellow:"#DDB459",teal:"#63B7C1",red:"#CF7468",brown:"#BB9C84",gray:"#97A1AD"},weight:1.25},wota:{font:"'M PLUS 1p', sans-serif",metrics:"mplus",ink:"#333333",muted:"#666666",grid:"#D9D9D9",gridMinor:"#EFEFEF",tick:"#BBBBBB",strip:"#EEF1FA",accent:"#5C6BC0",palette:"wota",scheme:"sakura",colors:{pink:"#E4007F",blue:"#5C6BC0",teal:"#00A3A3",orange:"#F5A623",purple:"#8E44AD",gray:"#7F7F7F",green:"#3BA55C",red:"#D7263D",yellow:"#F2C14E",brown:"#8D6E63"},weight:1,background:"#FFFFFF"},paper:{ink:"#000000",muted:"#333333",grid:"#E6E6E6",gridMinor:"#F5F5F5",tick:"#8C8C8C",strip:"#EBEBEB",accent:"#0072B2",palette:["blue","orange","green","pink","yellow","teal","red","brown","purple","gray"],scheme:"viridis",colors:{blue:"#0072B2",orange:"#E69F00",green:"#009E73",pink:"#CC79A7",yellow:"#F0E442",teal:"#56B4E9",red:"#D55E00",brown:"#8C6D31",purple:"#6A3D9A",gray:"#7F7F7F"},weight:1,background:"#FFFFFF"},dark:{background:"#16181D",ink:"#E8EAED",muted:"#A6ABB3",grid:"#2E323A",gridMinor:"#22252B",tick:"#4F5560",strip:"#252933",accent:"#8AB4F8",palette:["blue","orange","green","purple","pink","yellow","teal","red","brown","gray"],scheme:"yuhi",colors:{blue:"#8AB4F8",orange:"#FBA94C",green:"#81C995",purple:"#C58AF9",pink:"#FF8BCB",yellow:"#FDD663",teal:"#78D9D3",red:"#F28B82",brown:"#C8A27A",gray:"#9AA0A6"},weight:1.15}},palettes:{pastel:["#7AA6DA","#EF977F","#7DB89A","#A79BD6","#E48FAE","#DDB459","#63B7C1","#CF7468","#BB9C84","#97A1AD"],wota:["#E4007F","#5C6BC0","#00A3A3","#F5A623","#8E44AD","#7F7F7F","#3BA55C","#D7263D","#F2C14E","#8D6E63"],sakura:["#E16B8C","#5DAC81","#8B81C3","#FFB11B","#7DB9DE","#F596AA","#B9887D","#91AD70"],aizome:["#0F2540","#51A8DD","#66BAB7","#656765","#0D5661","#A5DEE4","#7B90D2"],momiji:["#CB4042","#FFB11B","#6C6A2D","#6D2E5B","#ED784A","#734338","#C99833"],tableau:["#4E79A7","#F28E2B","#E15759","#76B7B2","#59A14F","#EDC948","#B07AA1","#FF9DA7","#9C755F","#BAB0AC"]},schemes:{ai:["#F4F8F8","#A5DEE4","#33A6B8","#0D5661","#0F2540"],sakura:["#FEF4F4","#FEDFE1","#F596AA","#D05A6E","#8E354A","#4A1D30"],matcha:["#F8F6E3","#D7E3B5","#91AD70","#5DAC81","#1B813E","#0B3B2A"],yuhi:["#FFF3D6","#FFB11B","#ED784A","#CB4042","#6D2E5B","#26113A"],sumi:["#FCFAF2","#BDC0BA","#828282","#434343","#1C1C1C"],aiaka:["#0F2540","#33A6B8","#A5DEE4","#F7F6F0","#F4A7A0","#CB4042","#5C1A1B"]},formats:{screen:{width:900,height:450,size:12,line:1.5,point:2.5,reflow:!0},report:{width:640,height:400,size:10,line:1.2,point:2,reflow:!1},slide:{width:1280,height:720,size:20,line:2.5,point:4,reflow:!1}}};var le=N.layers,se=[...new Set(Object.values(le).flatMap(t=>t.channels))];function ae(t,e={}){let n=[ce(t,"plot()")];for(let[s,o]of Object.entries(e))se.includes(s)||Z("plot()","channel",s,se),qt(n[0],o,"plot()");return new Ht({data:n,mapping:e,layers:[],facet:null,scales:{},labels:{},theme:{style:"pastel"}})}var Ht=class t{#e;constructor(e){this.#e=e}#n(e){return new t({...this.#e,...e})}#t(e,n={}){let s=le[e],o=`${e}()`,{data:r,...i}=n,l=this.#e.data,a=0;r!==void 0&&(l=[...l,ce(r,o)],a=l.length-1);let c=l[a].columns,u={type:e,mapping:{},set:{}};a&&(u.data=a);for(let[p,d]of Object.entries(i))s.channels.includes(p)&&typeof d=="string"&&d in c?u.mapping[p]=d:s.channels.includes(p)||s.options.includes(p)?u.set[p]=xn(p,d,o,c):Z(o,"option",p,["data",...s.channels,...s.options]);return this.#n({data:l,layers:[...this.#e.layers,u]})}line(e){return this.#t("line",e)}step(e){return this.#t("step",e)}bar(e){return this.#t("bar",e)}hist(e){return this.#t("hist",e)}density(e){return this.#t("density",e)}area(e){return this.#t("area",e)}ribbon(e){return this.#t("ribbon",e)}smooth(e){return this.#t("smooth",e)}path(e){return this.#t("path",e)}point(e){return this.#t("point",e)}hline(e){return this.#t("hline",e)}vline(e){return this.#t("vline",e)}abline(e){return this.#t("abline",e)}segment(e){return this.#t("segment",e)}rect(e){return this.#t("rect",e)}band(e){return this.#t("band",e)}tile(e){return this.#t("tile",e)}text(e){return this.#t("text",e)}errorbar(e){return this.#t("errorbar",e)}box(e){return this.#t("box",e)}facet(e,n={}){return qt(this.#e.data[0],e,"facet()"),kt(n,N.facet,"facet()"),ie(n.free,"facet()"),this.#n({facet:{wrap:e,free:n.free??null,ncol:n.ncol??null}})}grid({rows:e=null,cols:n=null,...s}={}){if(e===null&&n===null)throw new Error("grid(): set rows, cols or both");for(let o of[e,n])o!==null&&qt(this.#e.data[0],o,"grid()");return kt(s,N.grid,"grid()"),ie(s.free,"grid()"),this.#n({facet:{rows:e,cols:n,free:s.free??null}})}x(e){return this.#o("x",e)}y(e){return this.#o("y",e)}color(e){return this.#o("color",e)}#o(e,n={}){let s=`${e}()`;if(kt(n,N.scales[e],s),n.type!==void 0&&!N.scaleTypes.includes(n.type)&&Z(s,"type",n.type,N.scaleTypes),n.palette!==void 0&&!Array.isArray(n.palette)&&!(n.palette in O.palettes)&&Z(s,"palette",n.palette,Object.keys(O.palettes)),n.mid!==void 0&&typeof n.mid!="number")throw new Error(`${s}: mid must be a number`);let o={...n};if(o.limits){if(!Array.isArray(o.limits))throw new Error(`${s}: limits must be a list, for example [0, 10] or [0, null]`);o.limits=o.limits.map(r=>r==null?null:Rt(r))}return this.#n({scales:{...this.#e.scales,[e]:o}})}labels(e={}){return kt(e,N.labels,"labels()"),this.#n({labels:{...this.#e.labels,...e}})}theme(e="pastel",n={}){return kt(n,N.theme,"theme()"),e in O.styles||Z("theme()","style",e,Object.keys(O.styles)),n.format!==void 0&&!(n.format in O.formats)&&Z("theme()","format",n.format,Object.keys(O.formats)),n.legend!==void 0&&!N.legend.includes(n.legend)&&Z("theme()","legend",n.legend,N.legend),this.#n({theme:{style:e,...n}})}spec(){let e=this.#e,n=e.data.map(()=>new Set);for(let o of Object.values(e.mapping))n.forEach((r,i)=>o in e.data[i].columns&&r.add(o));for(let o of e.layers)for(let r of Object.values(o.mapping))n[o.data??0].add(r);for(let o of[e.facet?.wrap,e.facet?.rows,e.facet?.cols])o&&n.forEach((r,i)=>o in e.data[i].columns&&r.add(o));return{version:1,data:e.data.map((o,r)=>({columns:Object.fromEntries([...n[r]].map(i=>[i,o.columns[i]]))})),mapping:e.mapping,layers:e.layers,facet:e.facet,scales:e.scales,labels:e.labels,theme:e.theme}}};function ce(t,e){if(!Array.isArray(t))throw new Error(`${e}: data must be an array of row objects`);let n=[...new Set(t.flatMap(Object.keys))],s={};for(let o of n){let r=t.map(c=>c[o]??null),i=r.find(c=>c!==null&&!Number.isNaN(c)),l=i instanceof Date?"time":typeof i=="number"?"number":"string",a=r.map(c=>c===null||Number.isNaN(c)?null:l==="string"?String(c):Rt(c));s[o]={type:l,values:a}}return{columns:s}}function Rt(t){return t instanceof Date?t.getTime()-t.getTimezoneOffset()*6e4:typeof t=="number"&&!Number.isInteger(t)?Number(t.toPrecision(6)):t}function xn(t,e,n,s){let o=N.options[t];if(t==="tooltip"||t==="group")throw new Error(`${n}: ${t} must be a column. Columns: ${Object.keys(s).join(", ")}`);if(Array.isArray(o)&&!o.includes(e)&&Z(n,t,e,o),o==="number"&&typeof e!="number")throw new Error(`${n}: ${t} must be a number`);if(o==="boolean"&&typeof e!="boolean")throw new Error(`${n}: ${t} must be true or false`);if(t==="color"||t==="label")return e;if(typeof e=="string"&&!o)throw new Error(`${n}: "${e}" is not a column. Columns: ${Object.keys(s).join(", ")}`);return Rt(e)}function qt(t,e,n){if(!(e in t.columns))throw new Error(`${n}: "${e}" is not a column. Columns: ${Object.keys(t.columns).join(", ")}`)}function kt(t,e,n){for(let s of Object.keys(t))e.includes(s)||Z(n,"option",s,e)}function ie(t,e){t!=null&&!N.free.includes(t)&&Z(e,"free",t,N.free)}function Z(t,e,n,s){throw new Error(`${t}: unknown ${e} "${n}". Valid: ${s.join(", ")}`)}import*as V from"https://cdn.jsdelivr.net/npm/@observablehq/plot@0.6.17/+esm";import*as Bt from"https://cdn.jsdelivr.net/npm/d3@7.9.0/+esm";import*as $ from"https://cdn.jsdelivr.net/npm/d3@7.9.0/+esm";import*as H from"https://cdn.jsdelivr.net/npm/d3@7.9.0/+esm";var yn=Object.keys(O.styles),gn=Object.keys(O.formats),bn=Object.keys(O.palettes),wn=Object.keys(O.schemes),ue=["ink","muted","grid","tick","strip","accent","background"];function pe({style:t="pastel",format:e="screen",legend:n="right",...s}={}){if(!O.styles[t])throw new Error(`theme(): unknown style "${t}". Valid: ${yn.join(", ")}`);if(!O.formats[e])throw new Error(`theme(): unknown format "${e}". Valid: ${gn.join(", ")}`);let o=O.styles.pastel,r=O.styles[t],i=O.formats[e],l=s.width!=null;for(let c of Object.keys(s))s[c]==null&&delete s[c];let a={...o,...r,colors:{...o.colors,...r.colors},...i,legend:n,...s,style:t,format:e};return a.reflow=i.reflow&&!l,a.line=i.line*(r.weight??o.weight??1),a.point=i.point*Math.sqrt(r.weight??o.weight??1),s.width!=null&&s.height==null&&(a.height=Math.round(s.width*i.height/i.width)),a.fixedHeight=s.height!=null,a.palette=It(a,a.palette),a}function It(t,e){let n=Array.isArray(e)?e:O.palettes[e];if(!n)throw new Error(`color(): unknown palette "${e}". Valid: ${bn.join(", ")}`);return n.map(s=>ht(t,s)??s)}function de(t,e){let n=o=>H.piecewise(H.interpolateLab,o.map(r=>ht(t,r)??r));if(Array.isArray(e))return n(e);if(O.schemes[e])return n(O.schemes[e]);let s=typeof e=="string"&&H[`interpolate${e[0].toUpperCase()}${e.slice(1)}`];if(!s)throw new Error(`color(): unknown scheme "${e}". plotto schemes: ${wn.join(", ")}; or a d3 scheme such as viridis, magma, blues`);return s}function ht(t,e){if(typeof e=="string")return t.colors[e]?t.colors[e]:ue.includes(e)?t[e]:H.color(e)?e:void 0}function me(t){return[...Object.keys(t.colors),...ue]}function Yt(t){let e=o=>{let r=[o.r,o.g,o.b].map(i=>(i/=255,i<=.03928?i/12.92:((i+.055)/1.055)**2.4));return .2126*r[0]+.7152*r[1]+.0722*r[2]},n=H.rgb(t);if(e(n)>.42)return{bg:n.formatHex(),fg:"#1F2328"};let s=H.hcl(n);for(let o=0;o<20&&1.05/(e(H.rgb(s))+.05)<4;o++)s=H.hcl(s.h,s.c,s.l-4);return{bg:H.rgb(s).formatHex(),fg:"#FFFFFF"}}function he(t){return Yt(t).fg}function fe(t){let e=t.size;return{base:e,tick:e-1,title:Math.round(e*1.35),subtitle:e,strip:e-1,legend:e-1,axisTitle:e,hover:Math.max(11,e-1)}}var Et={x:["x","xmin","xmax","xend"],y:["y","ymin","ymax","yend"]},xe=.05,Wt=864e5;function Vt(t,e){let n=t.scales[e]?.type;if(n)return n;for(let s of t.layers)for(let o of Et[e]){let r=s.channels[o]?.column;if(!r)continue;let i=t.tables[s.table].columns[r].type;return i==="time"?"time":i==="string"?"category":"linear"}return"linear"}function ge(t,e,n,s){let o=e.channels[n];if(!o)return;if("value"in o)return s.map(()=>o.value);let r=t.tables[e.table].columns[o.column].values;return s.map(i=>r[i])}function be(t,e,n,s){let o=[];for(let i of s)for(let l of i.views)for(let a of Et[e]){let c=l.cols[a];if(c)for(let u of c)u!=null&&(n==="category"||Number.isFinite(u))&&o.push(u)}if(n==="category")return vn(t,e,o);let r=$.extent(o.filter(i=>n!=="log"||i>0));return r[0]===void 0?n==="log"?[1,10]:[0,1]:we(r,n)}function vn(t,e,n){let s=t.layers.map(r=>r.channels[e]?.column&&t.tables[r.table].columns[r.channels[e].column].levels).find(Boolean),o=new Set(n.map(String));return s?s.filter(r=>o.has(r)):[...o].sort(Ct)}function Ct(t,e){return String(t).localeCompare(String(e),void 0,{numeric:!0})}function we([t,e],n){if(n==="log"){let[o,r]=we([Math.log10(t),Math.log10(e)],"linear");return[10**o,10**r]}let s=e>t?(e-t)*xe:n==="time"?36e5:Math.abs(t)*xe||1;return[t-s,e+s]}function ft(t,e,n,s){if(t==="category")return{major:e,minor:[],format:String};let[o,r]=e;if(t==="time"){let l=$.scaleUtc().domain([new Date(o),new Date(r)]).ticks(n),a=s??kn(l,r-o);return{major:l,minor:ye(l,e,c=>+c).map(c=>new Date(c)),format:$.utcFormat(a)}}if(t==="log"){let l=$.range(Math.ceil(Math.log10(o)),Math.floor(Math.log10(r))+1).map(u=>10**u),a=l.length>=2?l:$.scaleLog().domain(e).ticks(n).filter(u=>[1,2,5].includes(Math.round(u/10**Math.floor(Math.log10(u)+1e-9)))),c=l.flatMap(u=>$.range(2,10).map(p=>u/10*p)).filter(u=>u>o&&u<r);return{major:a,minor:c,format:$.format(s??"~g")}}let i=$.ticks(o,r,n);return{major:i,minor:ye(i,e,Number),format:s?$.format(s):$.tickFormat(o,r,n)}}function ye(t,[e,n],s){if(t.length<2)return[];let o=s(t[1])-s(t[0]);return[s(t[0])-o/2,...t.map(i=>s(i)+o/2)].filter(i=>i>e&&i<n)}function kn(t,e){let n=t.length>=2?t[1]-t[0]:e;return n>=28*Wt?"%Y-%m":n>=Wt?"%m-%d":n>=6e4?e>Wt?"%m-%d %H:%M":"%H:%M":"%H:%M:%S"}var En=$.format(",.4~r"),ve={number:t=>t==null?"\u2013":Math.abs(t)>=1e15||t!==0&&Math.abs(t)<1e-4?$.format(".3~e")(t):En(t),time:$.utcFormat("%m-%d %H:%M"),string:t=>t==null?"\u2013":String(t)};function ke(t){let e=t.theme,n=t.scales.color??{},s=t.layers.filter(f=>f.channels.color?.column);if(!s.length)return{kind:"none"};let o=s[0].channels.color.column,r=s.find(f=>f.channels.color.column!==o);if(r)throw new Error(`plotto: color maps "${o}" and "${r.channels.color.column}". Map color to one column only`);let i=s.map(f=>t.tables[f.table].columns[o]),l=i[0].type,a=i.flatMap(f=>f.values).filter(f=>f!==null),c=f=>ht(e,f)??f;if(l==="string"){let f=i.find(S=>S.levels)?.levels,g=new Set(a),h=n.limits??(f?f.filter(S=>g.has(S)):[...g].sort(Ct)),y=n.palette?It(e,n.palette):e.palette,k=n.values,M=Array.isArray(k)?(S,B)=>c(k[B%k.length]):k?(S,B)=>c(k[S]??y[B%y.length]):(S,B)=>y[B%y.length],C=new Map(h.map((S,B)=>[S,M(S,B)]));return{kind:"ordinal",column:o,domain:h,apply:S=>C.get(S)??e.muted}}let[u,p]=Mn(n.limits,$.extent(a)),d=de(e,n.values??n.scheme??e.scheme),m=n.mid,v=f=>m===void 0?(f-u)/(p-u||1):f<m?.5*((f-u)/(m-u||1)):.5+.5*((f-m)/(p-m||1)),x=f=>f>=u&&f<=p,b=f=>f===null?e.muted:n.clamp||x(f)?d(Math.max(0,Math.min(1,v(f)))):e.muted;return{kind:"continuous",column:o,domain:[u,p],type:l,interpolate:d,apply:b,ramp:f=>b(u+f*(p-u))}}function Mn(t,e){return t?[t[0]??e[0],t[1]??e[1]]:e}function Ee(t){let e=t.facet;if(!e)return{kind:"none",nrow:1,ncol:1,panels:[Gt(t,0,0,{},{})]};if(e.kind==="wrap"){let r=Xt(t,e.by),i=e.ncol??An(r.length),l=r.map((a,c)=>Gt(t,Math.floor(c/i),c%i,{[e.by]:a},{top:a}));return{kind:"wrap",nrow:Math.ceil(r.length/i),ncol:i,panels:l}}let n=e.rows?Xt(t,e.rows):[null],s=e.cols?Xt(t,e.cols):[null],o=[];return n.forEach((r,i)=>s.forEach((l,a)=>{let c={};e.rows&&(c[e.rows]=r),e.cols&&(c[e.cols]=l),o.push(Gt(t,i,a,c,{top:i===0?l:null,right:a===s.length-1?r:null}))})),{kind:"grid",nrow:n.length,ncol:s.length,panels:o}}function An(t){return t<=3?t:t<=6?Math.ceil(t/2):t<=12?Math.ceil(t/3):Math.ceil(Math.sqrt(t))}function Xt(t,e){let n=t.tables[0].columns[e];return n.levels?n.levels.filter(s=>n.values.includes(s)):[...new Set(n.values.filter(s=>s!==null).map(String))].sort(Ct)}function Gt(t,e,n,s,o){let r=t.layers.map(i=>{let l=t.tables[i.table];if(!Object.values(i.channels).some(u=>u.column))return[0];let a=Object.entries(s).filter(([u])=>u in l.columns),c=Array.from({length:l.n},(u,p)=>p);return a.length?c.filter(u=>a.every(([p,d])=>String(l.columns[p].values[u])===d)):c});return{row:e,col:n,strips:o,rows:r}}import*as E from"https://cdn.jsdelivr.net/npm/@observablehq/plot@0.6.17/+esm";import*as Y from"https://cdn.jsdelivr.net/npm/d3@7.9.0/+esm";var Fn={solid:void 0,dashed:"6,4",dotted:"1.5,3",dashdot:"6,3,1.5,3"},D="frame",J=(t,e)=>({stroke:t.colors(e),strokeWidth:t.options.size??t.theme.line,strokeOpacity:t.options.alpha??1,strokeDasharray:Fn[t.options.dash],strokeLinejoin:"round",strokeLinecap:"round"}),ot=(t,e,n=1)=>({fill:t.colors(e),fillOpacity:t.options.alpha??n}),W=t=>t.theme.palette[0],Sn=t=>e=>[E.line(e.index,{x:e.v("x"),y:e.v("y"),z:e.groups(),curve:t,...J(e,W(e)),clip:D})],Me={line:t=>[E.line(t.index,{x:t.v("x"),y:t.v("y"),z:t.groups(),sort:{channel:"x"},...J(t,W(t)),clip:D})],path:Sn("linear"),step:t=>[E.line(t.index,{x:t.v("x"),y:t.v("y"),z:t.groups(),sort:{channel:"x"},curve:"step-after",...J(t,W(t)),clip:D})],point:t=>{let e={x:Cn(t),y:t.v("y"),r:t.options.size??t.theme.point,...ot(t,W(t)),clip:D};return t.options.jitter&&t.kind.x==="category"&&(e.render=Dn(t.options.jitter)),[E.dot(t.index,e)]},bar:t=>{let e={...ot(t,W(t),Ut(t,1)),clip:D};if(t.kind.x!=="category")return[E.rect(t.index,{x1:t.v("xmin"),x2:t.v("xmax"),y1:t.v("ymin"),y2:t.v("ymax"),...e})];let n=t.v("slots")[0]??1;if(n===1)return[E.barY(t.index,{x:t.v("x"),y1:t.v("ymin"),y2:t.v("ymax"),...e})];let s=t.bandwidth.x/n,o=(r,i)=>Array.isArray(r)?i.map(l=>r[l]):r;return Y.range(n).map(r=>{let i=t.index.filter(a=>t.v("slot")[a]===r),l={x:o(t.v("x"),i),y1:o(t.v("ymin"),i),y2:o(t.v("ymax"),i),fill:o(e.fill,i)};return E.barY(Y.range(i.length),{...e,...l,insetLeft:r*s+.5,insetRight:(n-1-r)*s+.5})})},hist:t=>[E.rect(t.index,{x1:t.v("xmin"),x2:t.v("xmax"),y1:t.v("ymin"),y2:t.v("ymax"),insetLeft:.5,insetRight:.5,...ot(t,W(t),Ut(t)),clip:D})],density:t=>[E.areaY(t.index,{x:t.v("x"),y:t.v("y"),z:t.groups(),fill:t.colors(W(t)),fillOpacity:.15,clip:D}),E.line(t.index,{x:t.v("x"),y:t.v("y"),z:t.groups(),...J(t,W(t)),clip:D})],area:t=>[E.areaY(t.index,{x:t.v("x"),y1:t.v("ymin"),y2:t.v("ymax"),z:t.groups(),...ot(t,W(t),Ut(t,.8)),clip:D})],ribbon:t=>[E.areaY(t.index,{x:t.v("x"),y1:t.v("ymin"),y2:t.v("ymax"),z:t.groups(),...ot(t,W(t),.2),clip:D})],smooth:t=>{let e=[];return t.v("ymin").some(n=>n!==null)&&e.push(E.areaY(t.index,{x:t.v("x"),y1:t.v("ymin"),y2:t.v("ymax"),z:t.groups(),fill:t.colors(t.theme.muted),fillOpacity:.15,clip:D})),e.push(E.line(t.index,{x:t.v("x"),y:t.v("y"),z:t.groups(),...J(t,t.theme.accent),clip:D})),e},hline:t=>[E.ruleY(t.index,{y:t.v("y"),...J(t,t.theme.muted),strokeWidth:t.options.size??1,clip:D})],vline:t=>[E.ruleX(t.index,{x:t.v("x"),...J(t,t.theme.muted),strokeWidth:t.options.size??1,clip:D})],abline:t=>{let[e,n]=t.domain.x.map(Number),{slope:s=1,intercept:o=0}=t.options;return[E.line([0,1],{x:[e,n],y:[o+s*e,o+s*n],...J(t,t.theme.muted),strokeWidth:t.options.size??1,clip:D})]},segment:t=>[E.link(t.index,{x1:t.v("x"),y1:t.v("y"),x2:t.v("xend"),y2:t.v("yend"),...J(t,t.theme.ink),clip:D})],rect:t=>[E.rect(t.index,{x1:t.v("xmin"),x2:t.v("xmax"),y1:t.v("ymin"),y2:t.v("ymax"),...ot(t,W(t)),clip:D})],band:t=>{let e=(s,o,r)=>t.v(s)??t.index.map(()=>t.domain[o][r]),n={x1:e("xmin","x",0),x2:e("xmax","x",1),y1:e("ymin","y",0),y2:e("ymax","y",1)};return[E.rect(t.index,{...n,...ot(t,t.theme.muted,.12),clip:D})]},tile:t=>{let e=ot(t,W(t)),[n,s]=[t.kind.x==="category",t.kind.y==="category"];if(n&&s)return[E.cell(t.index,{x:t.v("x"),y:t.v("y"),...e,inset:.5,clip:D})];let[o,r]=n?[]:Dt(t.v("x")),[i,l]=s?[]:Dt(t.v("y"));return n?[E.barY(t.index,{x:t.v("x"),y1:i,y2:l,...e,clip:D})]:s?[E.barX(t.index,{y:t.v("y"),x1:o,x2:r,...e,insetTop:.5,insetBottom:.5,clip:D})]:[E.rect(t.index,{x1:o,x2:r,y1:i,y2:l,...e,clip:D})]},text:t=>[E.text(t.index,{x:t.v("x"),y:t.v("y"),text:t.v("label"),fontSize:t.options.size??t.fonts.tick,...ot(t,t.theme.ink),clip:D})],errorbar:t=>{let e={...J(t,t.theme.ink),strokeWidth:t.options.size??1.2},n=E.ruleX(t.index,{x:t.v("x"),y1:t.v("ymin"),y2:t.v("ymax"),...e,clip:D});if(t.kind.x==="category"){let i=t.bandwidth.x*.35,l=a=>E.tickY(t.index,{x:t.v("x"),y:t.v(a),...e,insetLeft:i,insetRight:i,clip:D});return[n,l("ymin"),l("ymax")]}let[s,o]=Dt(t.v("x"),.25),r=i=>E.link(t.index,{x1:s,x2:o,y1:t.v(i),y2:t.v(i),...e,clip:D});return[n,r("ymin"),r("ymax")]},box:t=>[E.boxY(t.index,{x:t.v("x"),y:t.v("y"),fill:t.colors(t.theme.strip),fillOpacity:t.options.alpha??1,stroke:t.theme.ink,strokeWidth:1,r:t.theme.point})]};function Ut(t,e=.9){return t.options.position==="overlay"?.45:e}function Dt(t,e=.5){let n=t.map(Number),s=[...new Set(n)].sort((i,l)=>i-l),o=Y.min(Y.pairs(s),([i,l])=>l-i)??1,r=t[0]instanceof Date?i=>new Date(i):i=>i;return[n.map(i=>r(i-o*e)),n.map(i=>r(i+o*e))]}function Cn(t){let e=t.v("x");if(!t.options.jitter||t.kind.x==="category")return e;let n=Y.randomUniform.source(Y.randomLcg(42))(-1,1),[s]=Dt(e,t.options.jitter);return e.map((o,r)=>+o+(+o-+s[r])*n()).map(o=>e[0]instanceof Date?new Date(o):o)}function Dn(t){return(e,n,s,o,r,i)=>{let l=Y.randomUniform.source(Y.randomLcg(42))(-1,1),a=n.scales.x?.bandwidth??20,c=Float64Array.from(s.x,u=>u+l()*a*t*.5);return i(e,n,{...s,x:c},o,r)}}import*as yt from"https://cdn.jsdelivr.net/npm/d3@7.9.0/+esm";var zn="http://www.w3.org/2000/svg";function L(t,e,n={}){let s=t.createElementNS(zn,e);for(let[o,r]of Object.entries(n))r!=null&&s.setAttribute(o,String(r));return s}function lt(t,e,n,s="text"){let o=L(t,s,n);return o.textContent=e,o}var Ae={arial:{regular:[.278,.278,.355,.556,.556,.889,.667,.191,.333,.333,.389,.584,.278,.333,.278,.278,.556,.556,.556,.556,.556,.556,.556,.556,.556,.556,.278,.278,.584,.584,.584,.556,1.015,.667,.667,.722,.722,.667,.611,.778,.722,.278,.5,.667,.556,.833,.722,.778,.667,.778,.722,.667,.611,.722,.667,.944,.667,.667,.611,.278,.278,.278,.469,.556,.333,.556,.556,.5,.556,.556,.278,.556,.556,.222,.222,.5,.222,.833,.556,.556,.556,.556,.333,.5,.278,.556,.5,.722,.5,.5,.5,.334,.26,.334,.584],bold:[.278,.333,.474,.556,.556,.889,.722,.238,.333,.333,.389,.584,.278,.333,.278,.278,.556,.556,.556,.556,.556,.556,.556,.556,.556,.556,.333,.333,.584,.584,.584,.611,.975,.722,.722,.722,.722,.667,.611,.778,.722,.278,.556,.722,.611,.833,.722,.778,.667,.778,.722,.667,.611,.722,.667,.944,.667,.667,.611,.333,.278,.333,.584,.556,.333,.556,.611,.556,.611,.556,.333,.611,.611,.278,.278,.556,.278,.889,.611,.611,.611,.611,.389,.556,.333,.611,.556,.778,.556,.556,.5,.389,.28,.389,.584]},mplus:{regular:[.29,.363,.4,.678,.605,.847,.712,.237,.385,.385,.527,.733,.304,.457,.283,.507,.62,.62,.62,.62,.62,.62,.62,.62,.62,.62,.363,.384,.667,.753,.667,.611,.823,.693,.598,.695,.682,.587,.572,.745,.71,.317,.527,.633,.585,.857,.713,.793,.6,.785,.62,.573,.653,.683,.693,.953,.65,.67,.637,.47,.507,.47,.653,.583,.278,.548,.578,.522,.578,.552,.522,.573,.573,.307,.347,.562,.307,.805,.568,.57,.573,.573,.453,.497,.512,.558,.557,.81,.553,.557,.547,.513,.343,.513,.69],bold:[.304,.425,.478,.692,.625,.901,.753,.291,.426,.426,.536,.735,.351,.471,.338,.546,.64,.64,.64,.64,.64,.64,.64,.64,.64,.64,.418,.431,.681,.755,.681,.628,.818,.715,.62,.703,.696,.601,.586,.756,.735,.351,.558,.678,.6,.878,.735,.798,.628,.798,.651,.608,.668,.705,.715,.971,.678,.698,.651,.501,.546,.501,.695,.609,.336,.576,.606,.54,.606,.57,.543,.601,.605,.335,.378,.603,.335,.843,.6,.585,.601,.601,.485,.525,.533,.59,.581,.831,.585,.585,.568,.535,.381,.535,.705]}};var Bn=11904;function ut(t,e,n=!1,s="arial"){let o=Ae[s][n?"bold":"regular"],r=0;for(let i of String(t)){let l=i.codePointAt(0);l>=32&&l<127?r+=o[l-32]:r+=l>=Bn?1:.6}return r*e}function xt(t,e,n,s,o){if(t=String(t),ut(t,e,s,o)<=n)return t;let r=[...t];for(;r.length>1&&ut(r.join("")+"\u2026",e,s,o)>n;)r.pop();return r.join("")+"\u2026"}function _t(t,e,n,s,o,r=2){let i=String(t).split(/\s+/),l=[],a="";for(let u of i){let p=a?`${a} ${u}`:u;a&&ut(p,e,s,o)>n?(l.push(a),a=u):a=p}if(l.push(a),l.length<=r)return l;let c=l.slice(0,r);return c[r-1]=xt(l.slice(r-1).join(" "),e,n,s,o),c}var Mt=18,At=6,Q={thick:12,long:150,slices:48},$n=["line","path","step","density","smooth","hline","vline","abline","segment","errorbar"],Ln=["bar","hist","area","ribbon","rect","tile","box"];function Fe(t,e,n,{horizontal:s,maxWidth:o,maxHeight:r}){if(e.kind==="none"||t.theme.legend==="none")return null;let i=t.theme.metrics,l=(g,h,y)=>ut(g,h,y,i),a=t.labels.color===null?null:t.labels.color??e.column,c=a?n.legend*1.8:0,u=a?l(a,n.legend+1,!0):0;if(e.kind==="continuous"){let g=ft(e.type==="time"?"time":"linear",e.domain,4),h=g.major.map(g.format),y=yt.max(h,S=>l(S,n.legend))??0,k=Math.min(Q.long,s?o:r-c-4),[M,C]=s?[k,Q.thick+n.legend*1.6]:[Q.thick+At+y,k];return{kind:"ramp",title:a,titleH:c,color:e,long:k,ticks:g.major,labels:h,width:Math.max(u,M),height:c+C,horizontal:s}}let p=t.layers.filter(g=>g.channels.color?.column),d=p.some(g=>Ln.includes(g.type))?"square":p.some(g=>$n.includes(g.type))?"line":"point",m=Math.round(n.legend*1.75),v=Math.max(80,(s?o:220)-Mt-At),x=e.domain.map(g=>{let h=xt(g,n.legend,v,!1,i);return{value:g,label:h,color:e.apply(g),w:Mt+At+l(h,n.legend)}});if(!s){let g=Math.max(1,Math.floor((r-c)/m)),h=yt.range(Math.ceil(x.length/g)).map(M=>x.slice(M*g,(M+1)*g)),y=0;for(let M of h)M.forEach((C,S)=>Object.assign(C,{x:y,y:c+S*m})),y+=(yt.max(M,C=>C.w)??0)+n.legend;let k=Math.min(x.length,g);return{kind:"swatches",title:a,titleH:c,key:d,rowH:m,items:x,width:Math.max(u,y-n.legend),height:c+k*m,horizontal:s}}let b=0,w=c;for(let g of x)b>0&&b+g.w>o&&([b,w]=[0,w+m]),Object.assign(g,{x:b,y:w}),b+=g.w+n.legend*1.5;let f=Math.max(u,yt.max(x,g=>g.x+g.w)??0);return{kind:"swatches",title:a,titleH:c,key:d,rowH:m,items:x,width:f,height:w+m,horizontal:s}}function Se(t,e,n,s,o=new Set){let r=L(t,"g",{class:"pt-legend","font-size":s.legend});return e.title&&r.append(lt(t,e.title,{y:s.legend,"font-weight":"bold","font-size":s.legend+1,fill:n.ink})),e.kind==="ramp"?Pn(t,r,e,n,s):e.items.forEach((i,l)=>r.append(On(t,e,i,l,n,o.has(i.value)))),r}function On(t,e,n,s,o,r){let i=e.rowH/2,l=L(t,"g",{class:"pt-key","data-index":s,transform:`translate(${n.x},${n.y})`,opacity:r?.35:1}),a=L(t,"rect",{width:n.w,height:e.rowH,fill:"transparent"}),c=e.key==="line"?L(t,"line",{x1:1,x2:Mt-1,y1:i,y2:i,stroke:n.color,"stroke-width":Math.max(2.5,o.line+1)}):e.key==="point"?L(t,"circle",{cx:Mt/2,cy:i,r:Math.max(3.5,o.point+1),fill:n.color}):L(t,"rect",{x:3,y:i-6,width:12,height:12,fill:n.color}),u=lt(t,n.label,{x:Mt+At,y:i,dy:"0.35em",fill:o.muted,"text-decoration":r?"line-through":null});return l.append(a,c,u),l}function Pn(t,e,n,s,o){let{color:r,horizontal:i,long:l}=n,[a,c]=r.domain.map(Number),u=d=>(Number(d)-a)/(c-a||1),p=n.titleH;for(let d=0;d<Q.slices;d++){let m=d/Q.slices,v=l/Q.slices+.5,x=r.ramp(i?m:1-m),b=i?{x:m*l,y:p,width:v,height:Q.thick}:{x:0,y:p+m*l,width:Q.thick,height:v};e.append(L(t,"rect",{...b,fill:x}))}n.ticks.forEach((d,m)=>{let v=u(d)*l,x=i?{x:v,y:p+Q.thick+o.legend*1.2,"text-anchor":"middle"}:{x:Q.thick+At,y:p+l-v,dy:"0.35em"};e.append(lt(t,n.labels[m],{...x,fill:s.muted}))})}import*as U from"https://cdn.jsdelivr.net/npm/d3@7.9.0/+esm";var pt={size:4,padding:3},Tn=40,jn=.22;function Ce({ns:t,grid:e,kind:n,home:s,legend:o,fonts:r,W:i,H:l}){let a=t.theme,c=a.metrics,u=(F,I,st)=>ut(F,I,st,c),p=r.base,d=Math.round(p*.9),m=Math.round(p*.8),{title:v,subtitle:x,caption:b,x:w,y:f}=t.labels,g=i-2*d-Math.min(180,i*jn),h=v?_t(v,r.title,g,!0,c):[],y=x?_t(x,r.subtitle,g,!1,c):[],k=h.length*r.title*1.25+y.length*r.subtitle*1.4,M=d+k+(k?p*.7:0),C=w?r.axisTitle*1.7:0,S=f?r.axisTitle*1.7:0,B=b?r.tick*1.8:0,j=o&&!o.horizontal?o.width+p*1.5:0,q=o?.horizontal?o.height+p*.8:0,z={x:d+S,y:M,w:i-2*d-S-j,h:l-M-d-C-q-B},tt=e.kind==="none"?0:Math.round(r.strip*2),et=e.kind==="wrap"?tt:0,R=e.kind==="grid"&&t.facet.cols?tt+2:0,T=e.kind==="grid"&&t.facet.rows?tt+2:0,ee=t.facet?.free??{x:!1,y:!1},cn=e.panels.length,ne=F=>ee.y||F.col===0,Lt=(F,I)=>ee.x||(e.kind==="wrap"?I+e.ncol>=cn:F.row===e.nrow-1),X=z.w/e.ncol,K=z.h/e.nrow,at,nt,Ot,wt;for(let F=0;F<2;F++){Ot={x:Math.max(2,X/(n.x==="time"?110:75)),y:Math.max(2,K/45)},at=Array(e.ncol).fill(0),nt=Array(e.nrow).fill(0),wt=!1;let I=0;e.panels.forEach((G,it)=>{if(ne(G)){let ct=ft(n.y,s[it].y,Ot.y,t.scales.y?.format),mt=U.max(ct.major,Nt=>u(ct.format(Nt),r.tick))??0;at[G.col]=Math.max(at[G.col],Math.ceil(mt+pt.size+pt.padding+3))}if(n.x==="category"&&Lt(G,it)){let ct=s[it].x,mt=U.max(ct,Nt=>u(Nt,r.tick))??0;I=Math.max(I,mt),mt>X/(ct.length+.2)*.95&&(wt=!0)}});let st=wt?Math.min(I,K*.6)*Math.sin(Tn*Math.PI/180)+r.tick:r.tick*1.2;e.panels.forEach((G,it)=>{Lt(G,it)&&(nt[G.row]=Math.max(nt[G.row],Math.ceil(st+pt.size+pt.padding+3)))}),X=(z.w-U.sum(at)-m*(e.ncol-1)-T)/e.ncol,K=(z.h-U.sum(nt)-e.nrow*et-R-m*(e.nrow-1))/e.nrow}X=Math.max(20,X),K=Math.max(20,K);let un=U.cumsum([z.x,...at.slice(0,-1).map(F=>F+X+m)]),pn=U.cumsum([z.y+R,...nt.slice(0,-1).map(F=>et+K+F+m)]),St=e.panels.map((F,I)=>({x:un[F.col],y:pn[F.row]+et,w:at[F.col]+X,h:K+nt[F.row],ml:at[F.col],mb:nt[F.row],innerW:X,innerH:K,count:Ot,rotateX:wt,showX:Lt(F,I),showY:ne(F)})),Pt=[];e.panels.forEach((F,I)=>{let st=St[I],G=st.x+st.ml,it=(ct,mt)=>xt(ct,r.strip,mt-8,!0,c);F.strips.top!=null&&Pt.push({x:G,y:st.y-et-R,w:X,h:tt,text:it(F.strips.top,X),full:F.strips.top}),F.strips.right!=null&&Pt.push({x:G+X+2,y:st.y,w:tt,h:K,text:it(F.strips.right,K),full:F.strips.right,vertical:!0})});let Tt=St[0],oe=St.find((F,I)=>e.panels[I].col===e.ncol-1)??Tt,vt=Tt.x+Tt.ml,re=oe.x+oe.w,dn=z.y+z.h,dt=[],jt=d;for(let F of h)dt.push({text:F,attrs:{x:vt,y:jt+=r.title*1.05,"font-size":r.title,"font-weight":"bold",fill:a.ink,"text-anchor":"start"}}),jt+=r.title*.2;for(let F of y)dt.push({text:F,attrs:{x:vt,y:jt+=r.subtitle*1.3,"font-size":r.subtitle,fill:a.muted,"text-anchor":"start"}});if(w&&dt.push({text:w,attrs:{x:(vt+re)/2,y:dn+r.axisTitle*1.35,"text-anchor":"middle","font-size":r.axisTitle,fill:a.muted}}),f){let F=z.y+(z.h-U.sum(nt))/2;dt.push({text:f,attrs:{transform:`translate(${d+r.axisTitle*.85},${F}) rotate(-90)`,"text-anchor":"middle","font-size":r.axisTitle,fill:a.muted}})}b&&dt.push({text:xt(b,r.tick,i-2*d,!1,c),attrs:{x:vt,y:l-d-(q||0)+r.tick*.2,"font-size":r.tick,fill:a.muted,"text-anchor":"start"}});let mn=o?o.horizontal?{x:Math.max(d,(vt+re-o.width)/2),y:l-d-o.height}:{x:i-d-o.width,y:z.y+Math.max(0,(z.h-U.sum(nt)-o.height)/2)}:null;return{panels:St,strips:Pt,texts:dt,legend:mn,area:z,rotateX:wt}}function De(t,e={}){if(typeof t?.spec=="function"&&(t=t.spec()),t?.version!==1)throw new Error("plotto: unknown spec version");let n=pe({...t.theme,...e}),s=t.data.map(Nn),o=t.layers.map(l=>Hn(l,t.mapping??{},s,n));if(!o.length)throw new Error("plotto: the plot has no layers. Add one, for example .line() or .point()");let r=qn(t.facet),i={...Rn(o),...t.labels};return{tables:s,layers:o,facet:r,scales:t.scales??{},labels:i,theme:n}}function Nn({columns:t}){let e=Object.keys(t),n=e.length?t[e[0]].values.length:1;return{columns:t,n}}function Hn(t,e,n,s){let o=N.layers[t.type];if(!o)throw new Error(`plotto: unknown layer type "${t.type}". Valid: ${Object.keys(N.layers).join(", ")}`);let r=t.data??0,i=n[r].columns,l={};for(let u of o.channels)t.mapping?.[u]!==void 0?l[u]={column:t.mapping[u]}:t.set?.[u]!==void 0?l[u]={value:t.set[u]}:o.inherit!==!1&&e[u]in i&&(l[u]={column:e[u]});let a=o.requires.filter(u=>!l[u]);if(a.length)throw new Error(`${t.type}(): set ${a.join(" and ")}, as a column name or a value`);if(l.color&&"value"in l.color){let u=ht(s,l.color.value);if(!u)throw new Error(`${t.type}(): "${l.color.value}" is not a column or a color. Theme colors: ${me(s).join(", ")}`);l.color={value:u}}let c={};for(let u of o.options)t.set?.[u]!==void 0&&(c[u]=t.set[u]);return{type:t.type,table:r,channels:l,options:c}}function qn(t){if(!t)return null;let e={x:["x","both"].includes(t.free),y:["y","both"].includes(t.free)};return t.wrap?{kind:"wrap",by:t.wrap,ncol:t.ncol??null,free:e}:{kind:"grid",rows:t.rows??null,cols:t.cols??null,free:e}}function Rn(t){let e=o=>{for(let r of t)for(let i of o)if(r.channels[i]?.column)return r.channels[i].column},n=t.find(o=>o.type==="hist"||o.type==="density"),s=e(["y","ymin","yend"])??(n?n.type==="density"||n.options.density?"density":"count":void 0);return{x:e(["x","xmin","xend"]),y:s,color:e(["color"])}}import*as A from"https://cdn.jsdelivr.net/npm/d3@7.9.0/+esm";var zt=80;function Ft(t,e,n){let s=new Map;for(let o=0;o<n;o++){let r=e.map(l=>t[l]?.[o]??null),i=JSON.stringify(r);s.has(i)||s.set(i,{key:r,idx:[]}),s.get(i).idx.push(o)}return[...s.values()]}function ze(t,e,n){let s={mean:A.mean,median:A.median,min:A.min,max:A.max,sum:A.sum}[n],o={x:[],y:[],color:[],group:[]};for(let{key:r,idx:i}of Ft(t,["x","color","group"],e))r[0]!==null&&(o.x.push(r[0]),o.y.push(s(i,l=>t.y[l])),o.color.push(r[1]),o.group.push(r[2]));return o}function Be(t,e,n,s){let o=l=>t.color?s.indexOf(t.color[l]):0,r=t.color?s.filter(l=>t.color.includes(l)):[null],i={...t,ymin:new Array(e),ymax:new Array(e),value:t.y.slice(),slot:new Array(e),slots:new Array(e)};for(let{idx:l}of Ft(t,n==="dodge"?["x","color"]:["x"],e)){l.sort((u,p)=>o(u)-o(p));let a=0,c=0;l.forEach((u,p)=>{let d=t.y[u]??0;if(n!=="overlay"){let m=d>=0?a:c;[i.ymin[u],i.ymax[u]]=[m,m+d],d>=0?a+=d:c+=d}else[i.ymin[u],i.ymax[u]]=[0,d];i.slot[u]=n==="dodge"&&t.color?r.indexOf(t.color[u]):0,i.slots[u]=n==="dodge"?r.length:1})}return i.y=i.ymax,i}function $e(t,e){let n=[...new Set(t.x.filter(l=>l!==null))].sort((l,a)=>l-a),o=(A.min(A.pairs(n),([l,a])=>a-l)??1)*.9,r=[],i=[];for(let l=0;l<e;l++){let a=o/t.slots[l];r.push(t.x[l]-o/2+t.slot[l]*a),i.push(t.x[l]-o/2+(t.slot[l]+1)*a)}return{...t,xmin:r,xmax:i}}function Le(t,e){let n=t.filter(i=>i!==null&&Number.isFinite(i)),[s,o]=A.extent(n);if(s===void 0)return[0,1];if(s===o)return[s-.5,o+.5];let r=A.ticks(...A.nice(s,o,e),e);return r[0]>s&&r.unshift(r[0]-(r[1]-r[0])),r.at(-1)<o&&r.push(r.at(-1)+(r[1]-r[0])),r}function Oe(t,e,n,s){let o={x:[],xmin:[],xmax:[],y:[],color:[]},r=A.bin().domain([n[0],n.at(-1)]).thresholds(n.slice(1,-1));for(let{key:i,idx:l}of Ft(t,["color"],e)){let a=l.map(c=>t.x[c]).filter(c=>c!==null&&Number.isFinite(c));for(let c of r(a)){let u=c.length;o.x.push((c.x0+c.x1)/2),o.xmin.push(c.x0),o.xmax.push(c.x1),o.y.push(s?u/(a.length*(c.x1-c.x0)||1):u),o.color.push(i[0])}}return o}function Pe(t,e){let n={x:[],y:[],color:[],group:[]};for(let{key:s,idx:o}of Ft(t,["color"],e)){let r=o.map(p=>t.x[p]).filter(p=>p!==null&&Number.isFinite(p));if(r.length<2)continue;let i=A.deviation(r),l=A.quantile(r,.75)-A.quantile(r,.25),a=.9*Math.min(i,l/1.34||i)*r.length**-.2||1,[c,u]=A.extent(r);for(let p of A.range(zt).map(d=>c-3*a+(u-c+6*a)*d/(zt-1))){let d=A.sum(r,m=>Math.exp(-.5*((p-m)/a)**2))/(r.length*a*Math.sqrt(2*Math.PI));n.x.push(p),n.y.push(d),n.color.push(s[0]),n.group.push(s[0])}}return n}function Te(t,e,{method:n="lm",span:s=.75,band:o=!0}){let r={x:[],y:[],ymin:[],ymax:[],color:[],group:[]};for(let{key:i,idx:l}of Ft(t,["color"],e)){let a=l.map(m=>[t.x[m],t.y[m]]).filter(([m,v])=>m!==null&&v!==null&&Number.isFinite(m)&&Number.isFinite(v));if(a.length<3)continue;a.sort((m,v)=>m[0]-v[0]);let[c,u]=[a[0][0],a.at(-1)[0]],p=A.range(zt).map(m=>c+(u-c)*m/(zt-1));(n==="loess"?Yn(a,p,s):In(a,p)).forEach(({x:m,y:v,se:x})=>{let b=Wn(a.length-2);r.x.push(m),r.y.push(v),r.ymin.push(o&&x!==void 0?v-b*x:null),r.ymax.push(o&&x!==void 0?v+b*x:null),r.color.push(i[0]),r.group.push(i[0])})}return r}function In(t,e){let n=t.length,s=A.mean(t,c=>c[0]),o=A.mean(t,c=>c[1]),r=A.sum(t,c=>(c[0]-s)**2)||1,i=A.sum(t,c=>(c[0]-s)*(c[1]-o))/r,l=o-i*s,a=Math.sqrt(A.sum(t,c=>(c[1]-l-i*c[0])**2)/Math.max(1,n-2));return e.map(c=>({x:c,y:l+i*c,se:a*Math.sqrt(1/n+(c-s)**2/r)}))}function Yn(t,e,n){let s=Math.max(3,Math.ceil(n*t.length));return e.map(o=>{let r=t.map(([x,b])=>[Math.abs(x-o),x,b]).sort((x,b)=>x[0]-b[0]).slice(0,s),i=r.at(-1)[0]||1,[l,a,c,u,p]=[0,0,0,0,0];for(let[x,b,w]of r){let f=(1-(x/i)**3)**3;l+=f,a+=f*b,c+=f*w,u+=f*b*b,p+=f*b*w}let d=l*u-a*a,m=Math.abs(d)<1e-12?0:(l*p-a*c)/d,v=(c-m*a)/l;return{x:o,y:v+m*o}})}function Wn(t){let e=[12.71,4.3,3.18,2.78,2.57,2.45,2.36,2.31,2.26,2.23,2.2,2.18,2.16,2.14,2.13,2.12,2.11,2.1,2.09,2.09];return t<1?NaN:t<=e.length?e[t-1]:t<=30?2.05:t<=60?2:1.96}function je(t){return t.layers.map(e=>e.type!=="hist"||!e.channels.x.column?null:Le(t.tables[e.table].columns[e.channels.x.column].values,e.options.bins??30))}function Kt(t,e,n,s){let{ns:o,kind:r,colorLevels:i,edges:l}=t;if(s.size&&e.channels.color?.column){let b=o.tables[e.table].columns[e.channels.color.column].values;n=n.filter(w=>!s.has(b[w]))}let a={};for(let b of Object.keys(e.channels))a[b]=ge(o,e,b,n);let c=n.length,u=e.options,p=(b,w,f)=>Be(b,w,f,i),d=b=>b.x?.length??0,m=a,v=n;if(u.summary&&(e.type==="line"||e.type==="point"))m=ze(a,c,u.summary);else if(e.type==="bar")m=p(a,c,u.position??"stack"),r.x!=="category"&&(m=$e(m,c));else if(e.type==="area")m=p(a,c,u.position??"stack");else if(e.type==="hist"){let b=Oe(a,c,l[o.layers.indexOf(e)],u.density);m=p(b,d(b),u.position??"stack")}else e.type==="density"?m=Pe(a,c):e.type==="smooth"&&(m=Te(a,c,u));m!==a&&(v=null);let x={cols:m,n:m===a?c:d(m),rows:v};return x.hover=Vn(o,e,x),x}function Vn(t,e,{cols:n,rows:s}){let o=t.labels.x||e.channels.x?.column||"x",r=t.labels.y||e.channels.y?.column||"y",i=(c,u,p,d)=>({label:c,axis:u,values:p,values2:d}),l=i(o,"x",n.x),a=e.channels.tooltip?.column&&s?[i(e.channels.tooltip.column,null,n.tooltip)]:[];switch(e.type){case"line":case"path":case"step":case"point":return{hx:n.x,hy:n.y,fields:[l,i(r,"y",n.y),...a]};case"bar":case"area":return{hx:n.x,hy:n.ymax,fields:[l,i(r,"y",n.value),...a]};case"hist":return{hx:n.x,hy:n.ymax,fields:[i(o,"x",n.xmin,n.xmax),i(e.options.density?"density":"count",null,n.value)]};case"density":return{hx:n.x,hy:n.y,fields:[l,i("density",null,n.y)]};case"smooth":{let c=n.ymin.some(u=>u!==null)?[i("95% CI","y",n.ymin,n.ymax)]:[];return{hx:n.x,hy:n.y,fields:[l,i(r,"y",n.y),...c]}}case"ribbon":return{hx:n.x,hy:n.ymax,fields:[l,i(r,"y",n.ymin,n.ymax)]};case"errorbar":return{hx:n.x,hy:n.ymax,fields:[l,i(r,"y",n.ymin,n.ymax)]};case"tile":{let c=e.channels.color?.column?[i(t.labels.color??"value",null,n.color)]:[];return{hx:n.x,hy:n.y,fields:[l,i(r,"y",n.y),...c,...a],tile:!0}}case"box":return{hx:n.x,hy:n.y,fields:[l,i(r,"y",n.y)]};default:return null}}var Xn=560,Gn=240;function Zt(t,{document:e=globalThis.document,width:n,domains:s,hidden:o=new Set}={}){let r=De(t),i=r.theme,l=Math.round(n??i.width),c=n&&i.reflow&&!i.fixedHeight?Math.max(Gn,Math.round(l*i.height/i.width)):i.height;i.legend==="right"&&l<Xn&&(i.legend="bottom");let u=fe(i),p=Ee(r),d={x:Vt(r,"x"),y:Vt(r,"y")},m=ke(r),v={ns:r,kind:d,colorLevels:m.domain??[],edges:je(r)};for(let y of p.panels)y.views=r.layers.map((k,M)=>Kt(v,k,y.rows[M],new Set));let x=Kn(r,p,d),b=i.legend==="bottom",w=Fe(r,m,u,{horizontal:b,maxWidth:l*.9,maxHeight:c*.8}),f=Ce({ns:r,grid:p,kind:d,home:x,legend:w,fonts:u,W:l,H:c}),g=L(e,"svg",{xmlns:"http://www.w3.org/2000/svg",width:l,height:c,viewBox:`0 0 ${l} ${c}`,"font-family":i.font,"font-size":u.base,style:`color:${i.ink}`});g.append(L(e,"rect",{width:l,height:c,fill:i.background}));for(let y of f.texts)g.append(lt(e,y.text,y.attrs));for(let y of f.strips)g.append(Jn(e,y,i,u));let h={spec:t,ns:r,doc:e,svg:g,theme:i,fonts:u,grid:p,kind:d,color:m,ctx:v,legend:w,layout:f,home:x,W:l,H:c};return h.domains=s??x,h.hidden=o,h.panelLayer=g.appendChild(L(e,"g",{class:"pt-panels"})),w&&(h.legendG=g.appendChild(Re(h))),rt(h),h}function qe(t,e){return Zt(t,e).svg.outerHTML}function rt(t){let{ns:e,grid:n,ctx:s,hidden:o}=t;if(o.size||t.filtered){for(let r of n.panels)r.views=e.layers.map((i,l)=>Kt(s,i,r.rows[l],o));t.filtered=o.size>0}t.panelLayer.replaceChildren(),t.panels=n.panels.map((r,i)=>Un(t,r,i)),t.legendG&&t.legendG.replaceWith(t.legendG=Re(t))}function Re(t){let e=Se(t.doc,t.legend,t.theme,t.fonts,t.hidden);return e.setAttribute("transform",`translate(${t.layout.legend.x},${t.layout.legend.y})`),e}function Un(t,e,n){let{ns:s,doc:o,theme:r,fonts:i,kind:l,color:a}=t,c=t.layout.panels[n],u={x:Ne(l.x,t.domains[n].x),y:Ne(l.y,t.domains[n].y)},p={x:l.x==="category"?c.innerW/(u.x.length+.2)*.8:0},d=s.layers.map((y,k)=>{let M=e.views[k],C=_n({view:M,layer:y,kind:l,color:a,theme:r,fonts:i,domain:u,bandwidth:p});return{layer:y,view:M,c:C,marks:M.n?Me[y.type](C):[]}}),m=y=>{let k=ft(l[y],t.domains[n][y],c.count[y],s.scales[y]?.format),M=C=>l[y]==="category"||+C>=+u[y][0]&&+C<=+u[y][1];return{major:k.major.filter(M),minor:k.minor,format:k.format}},v=m("x"),x=m("y"),b=y=>({stroke:y?r.gridMinor:r.grid,strokeOpacity:1,strokeWidth:y?.6:.9}),w={tickSize:pt.size,tickPadding:pt.padding,fontSize:i.tick,stroke:r.tick,fill:r.muted,label:null},f=[];if(l.x!=="category"&&f.push(V.gridX(v.minor,b(!0)),V.gridX(v.major,b(!1))),l.y!=="category"&&f.push(V.gridY(x.minor,b(!0)),V.gridY(x.major,b(!1))),f.push(...d.flatMap(y=>y.marks)),c.showX){let y=c.rotateX?{tickRotate:-40,textAnchor:"end"}:{};f.push(V.axisX(v.major,{...w,...y,anchor:"bottom",tickFormat:v.format}))}c.showY&&f.push(V.axisY(x.major,{...w,anchor:"left",tickFormat:x.format}));let g=V.plot({document:o,width:c.w,height:c.h,marginLeft:c.ml,marginRight:0,marginTop:0,marginBottom:c.mb,x:He(l.x,u.x),y:He(l.y,u.y),color:{type:"identity"},marks:f}),h=L(o,"g",{class:"pt-panel","data-panel":n,transform:`translate(${c.x},${c.y})`,fill:"currentColor","text-anchor":"middle"});for(let y of[...g.childNodes])y.nodeName.toLowerCase()!=="style"&&h.append(y);return t.panelLayer.append(h),{g:h,plot:g,box:c,layers:d}}function _n({view:t,layer:e,kind:n,color:s,theme:o,fonts:r,domain:i,bandwidth:l}){let a=d=>Et.x.includes(d)?"x":Et.y.includes(d)?"y":null,c={},u=d=>{if(d in c)return c[d];let m=t.cols[d];return m&&n[a(d)]==="time"&&(m=m.map(v=>v===null?null:new Date(v))),c[d]=m},p=e.channels.color;return{index:Bt.range(t.n),v:u,options:e.options,colors:d=>p?"value"in p?p.value:u("color").map(s.apply):d,groups:()=>u("group")??(p?.column?u("color"):void 0),kind:n,theme:o,fonts:r,domain:i,bandwidth:l}}function Ne(t,e){return t==="time"?e.map(n=>new Date(n)):e}function He(t,e){return{type:{linear:"linear",log:"log",time:"utc",category:"band"}[t],domain:e,axis:null,...t==="category"?{padding:.2,round:!1}:{}}}function Kn(t,e,n){let s=t.facet?.free??{x:!1,y:!1},o=(i,l,a)=>s[i]?e.kind==="grid"?i==="x"?`c${l.col}`:`r${l.row}`:`p${a}`:"all",r=e.panels.map(()=>({}));for(let i of["x","y"]){let l=Bt.group(e.panels.map((a,c)=>[a,c]),([a,c])=>o(i,a,c));for(let a of l.values()){let c=Zn(t,i,n[i],a.map(([u])=>u));for(let[,u]of a)r[u][i]=c}}return r}function Zn(t,e,n,s){let o=t.scales[e]?.limits;if(o&&(n==="category"||o.every(i=>i!==null)))return o;let r=be(t,e,n,s);return o?[o[0]??r[0],o[1]??r[1]]:r}function Jn(t,e,n,s){let o=L(t,"g",{class:"pt-strip"});o.append(L(t,"rect",{x:e.x,y:e.y,width:e.w,height:e.h,fill:n.strip}));let r=e.vertical?{transform:`translate(${e.x+e.w/2},${e.y+e.h/2}) rotate(90)`,dy:"0.35em"}:{x:e.x+e.w/2,y:e.y+e.h/2,dy:"0.35em"},i=lt(t,e.text,{...r,"text-anchor":"middle","font-weight":"bold","font-size":s.strip,fill:n.ink});return e.text!==String(e.full)&&i.append(lt(t,e.full,{},"title")),o.append(i),o}async function Ie(t){let e=Ve(t);try{return await navigator.clipboard.write([new ClipboardItem({"image/png":e.then(Qn)})]),"Copied"}catch{return Jt((await e).toDataURL("image/png"),"plot.png"),"Downloaded"}}async function Ye(t){Jt((await Ve(t)).toDataURL("image/png"),"plot.png")}function We(t){Jt(Xe(t),"plot.svg")}async function Ve(t){let e=new Image;e.src=Xe(t),await e.decode();let{width:n,height:s}=t.viewBox.baseVal,o=document.createElement("canvas");return o.width=n*2,o.height=s*2,o.getContext("2d").drawImage(e,0,0,o.width,o.height),o}function Xe(t){let e=t.cloneNode(!0);e.querySelector(".pt-overlay")?.remove();let{width:n,height:s}=t.viewBox.baseVal;e.setAttribute("width",n),e.setAttribute("height",s);let o=new TextEncoder().encode(new XMLSerializer().serializeToString(e)),r="";for(let i of o)r+=String.fromCharCode(i);return`data:image/svg+xml;base64,${btoa(r)}`}var Qn=t=>new Promise(e=>t.toBlob(e,"image/png"));function Jt(t,e){let n=document.createElement("a");n.href=t,n.download=e,n.click()}import*as P from"https://cdn.jsdelivr.net/npm/d3@7.9.0/+esm";var _e=40,to=10,eo=6,Ke=4,no=6,oo=["line","step","area","density","smooth","ribbon"],ro=["bar","hist","tile","box"],so=["point","path","errorbar"];function Ze(t,e,n,s,o){let r=t.panels[e],i=r.plot.scale("x"),l=r.plot.scale("y"),u={state:t,sx:i,sy:l,px:x=>i.apply(x)+(i.bandwidth??0)/2,py:x=>l.apply(x)+(l.bandwidth??0)/2},p=uo(u,r,n);if(p)return{...p,panel:e};let d=lo(u,r,n,s,o?.panel===e?o:null);if(d&&d.d<=to)return{...Ge(u,d),panel:e};let m=ao(u,r,n,s,o?.panel===e?o:null);if(m)return{...m,panel:e};let v=po(u,r,n,s);return v?{...v,panel:e}:d?{...Ge(u,d),panel:e}:null}function bt(t,e,n,s,o){let r=e.channels.color;return r?"value"in r?r.value:t.color.apply(n.cols.color[o]):e.type==="smooth"?t.theme.accent:t.theme.palette[0]}function _(t,e,n){return n==null||Number.isNaN(n)?"\u2013":e&&t.kind[e]==="time"?io(t,e)(n):e&&t.kind[e]==="category"?String(n):typeof n=="number"?ve.number(n):String(n)}function io(t,e){if(t.timeFormats??={},t.timeFormats[e])return t.timeFormats[e];let n=t.grid.panels.flatMap(r=>r.views.flatMap(i=>i.cols[e]??[])).filter(r=>r!==null),s=r=>n.every(i=>r(new Date(i))),o=s(r=>r.getUTCDate()===1&&r.getUTCHours()===0&&r.getUTCMinutes()===0)?"%Y-%m":s(r=>r.getUTCHours()===0&&r.getUTCMinutes()===0)?"%Y-%m-%d":s(r=>r.getUTCSeconds()===0)?"%m-%d %H:%M":"%m-%d %H:%M:%S";return t.timeFormats[e]=P.utcFormat(o)}function gt(t,e,n){return e.values2?`${_(t,e.axis,e.values[n])} \u2013 ${_(t,e.axis,e.values2[n])}`:_(t,e.axis,e.values[n])}function lo({px:t,py:e},n,s,o,r){let i=null,l=null;for(let{layer:a,view:c,c:u}of n.layers){if(!so.includes(a.type)||!c.hover||!c.n)continue;let{hx:p,hy:d}=c.hover;for(let m=0;m<c.n;m++){if(p[m]===null||d[m]===null)continue;let v=Math.hypot(t(p[m])-s,e(d[m])-o);if(v>_e)continue;let x={layer:a,view:c,c:u,k:m,d:v,x:t(p[m]),y:e(d[m])};(!i||v<i.d)&&(i=x),r?.layer===a&&r.k===m&&(l=x)}}return l&&i&&i.d>l.d-Ke?l:i}function Ge({state:t},{layer:e,view:n,c:s,k:o,x:r,y:i,d:l}){let a=bt(t,e,n,s,o),c=e.channels.color?.column?_(t,null,n.cols.color[o]):null,u=n.hover.fields.map(p=>({label:p.label,value:gt(t,p,o)})).filter(p=>p.value!==c);return{layer:e,k:o,d:l,color:a,header:c,rows:u,marks:[{x:r,y:i,color:a}],anchor:{x:r,y:i}}}function ao({state:t,px:e,py:n},s,o,r,i){let l=[];for(let{layer:h,view:y,c:k}of s.layers){if(!oo.includes(h.type)||!y.hover||!y.n)continue;let{hx:M,hy:C}=y.hover,S=k.groups()??[],B=new Map;for(let j=0;j<y.n;j++){if(M[j]===null||C[j]===null)continue;let q=Math.abs(e(M[j])-o),z=S[j]??"";q<=_e&&(!B.has(z)||q<B.get(z).dx)&&B.set(z,{layer:h,view:y,c:k,k:j,dx:q,series:z})}l.push(...B.values())}if(!l.length)return null;let a=P.least(l,h=>h.dx),c=e(a.view.hover.hx[a.k]),u=l.filter(h=>Math.abs(e(h.view.hover.hx[h.k])-c)<=eo);for(let h of u)h.y=n(h.view.hover.hy[h.k]);let p=P.least(u,h=>Math.abs(h.y-r)),d=i?.series!==void 0&&u.find(h=>h.layer===i.layer&&h.series===i.series);d&&Math.abs(d.y-r)<Math.abs(p.y-r)+Ke&&(p=d);let m=h=>h.layer.channels.color?.column?_(t,null,h.view.cols.color[h.k]):h.series!==""?String(h.series):h.view.hover.fields[1]?.label??h.layer.type,v=h=>bt(t,h.layer,h.view,h.c,h.k),x=a.view.hover.fields[0],b=_(t,"x",a.view.hover.hx[a.k]),w={crosshair:c,anchor:{x:c,y:r},layer:p.layer,series:p.series};if(u.length===1||u.length>no){let h=[{label:x?.label??"x",value:b},{label:p.view.hover.fields[1]?.label??"y",value:Ue(t,p.layer,p.view,p.k)}],y=u.length-1;return{...w,color:v(p),header:m(p),rows:h,note:y?`${y} other series at this x`:null,marks:[{x:c,y:p.y,color:v(p)}]}}let f=co(t);u.sort((h,y)=>f(h)-f(y)||h.y-y.y);let g=u.map(h=>({swatch:v(h),label:m(h),value:Ue(t,h.layer,h.view,h.k),strong:h===p}));return{...w,color:t.theme.ink,header:x?`${x.label} ${b}`:b,rows:g,marks:u.map(h=>({x:c,y:h.y,color:v(h)}))}}function co(t){let{color:e}=t;return n=>{let s=n.layer.channels.color?.column?n.view.cols.color[n.k]:null;return s===null?t.ns.layers.indexOf(n.layer):e.kind==="ordinal"?e.domain.indexOf(s):typeof s=="number"?s:0}}function Ue(t,e,n,s){let o=n.hover.fields;return e.type==="smooth"&&o[2]?`${gt(t,o[1],s)} (${gt(t,o[2],s)})`:gt(t,o[1],s)}function uo(t,e,n){for(let{layer:s,view:o,c:r}of e.layers)if(s.type==="box"&&o.n)return ho(t,s,o,r,n);return null}function po(t,e,n,s){let{state:o,sx:r,sy:i}=t;for(let{layer:l,view:a,c}of e.layers){if(!ro.includes(l.type)||l.type==="box"||!a.n)continue;let u=l.type==="tile",p=null;for(let x=0;x<a.n;x++){let[b,w]=fo(l,a,x,r),f=Math.abs((b+w)/2-n);if(u?n<b-Qt(r,w-b)||n>w+Qt(r,w-b):f>w-b)continue;let[g,h]=xo(l,a,x,i),[y,k]=[Math.min(g,h),Math.max(g,h)],M=u?Qt(i,k-y):0,C=s>=y-M&&s<=k+M;if(u&&!C)continue;let S=Math.round(f)*1e6+(C?0:1e5+y);(!p||S<p.score)&&(p={k:x,score:S,rect:{x:b,y,width:w-b,height:k-y}})}if(!p)continue;let{k:d,rect:m}=p,v=bt(o,l,a,c,d);if(u){let x=o.ns.labels.color??l.channels.color?.column??"value",b=l.channels.color?.column?_(o,null,a.cols.color[d]):null,w=a.hover.fields.filter(f=>f.label!==x).map(f=>({label:f.label,value:gt(o,f,d)}));return{color:v,header:b!==null?`${x} ${b}`:null,rows:w,marks:[{rect:m,color:o.theme.ink}],anchor:{x:m.x+m.width,y:s}}}return mo(o,l,a,c,d,m)}return null}function Qt(t,e){return t.bandwidth!==void 0?(t.step-t.bandwidth)/2||e*.125:.5}function mo(t,e,n,s,o,r){let i=e.type==="hist"?d=>n.cols.xmin[d]===n.cols.xmin[o]:d=>String(n.cols.x[d])===String(n.cols.x[o]),l=P.range(n.n).filter(i),a=!!e.channels.color?.column,c=l.sort((d,m)=>a&&t.color.kind==="ordinal"?t.color.domain.indexOf(n.cols.color[d])-t.color.domain.indexOf(n.cols.color[m]):n.cols.ymax[m]-n.cols.ymax[d]).map(d=>({swatch:bt(t,e,n,s,d),label:a?_(t,null,n.cols.color[d]):n.hover.fields[1].label,value:_(t,null,n.cols.value[d]),strong:d===o&&l.length>1}));(e.options.position??"stack")==="stack"&&l.length>1&&c.push({label:"total",value:_(t,null,P.sum(l,d=>n.cols.value[d])),strong:!1,total:!0});let p=n.hover.fields[0];return{color:bt(t,e,n,s,o),header:`${p.label} ${gt(t,p,o)}`,rows:c,marks:[{rect:r,color:t.theme.ink}],anchor:{x:r.x+r.width,y:r.y}}}function ho({state:t,sx:e},n,s,o,r){if(e.bandwidth===void 0)return null;let i=P.range(s.n).find(h=>{let y=e.apply(s.cols.x[h]);return r>=y&&r<=y+e.bandwidth});if(i===void 0)return null;let l=String(s.cols.x[i]),a=P.range(s.n).filter(h=>String(s.cols.x[h])===l).map(h=>s.cols.y[h]).filter(h=>h!==null).sort(P.ascending),[c,u,p]=[.25,.5,.75].map(h=>P.quantile(a,h)),d=p-c,m=P.min(a.filter(h=>h>=c-1.5*d)),v=P.max(a.filter(h=>h<=p+1.5*d)),x=a.filter(h=>h<m||h>v).length,b=h=>_(t,"y",h),w=[{label:"max (whisker)",value:b(v)},{label:"q3",value:b(p)},{label:"median",value:b(u),strong:!0},{label:"q1",value:b(c)},{label:"min (whisker)",value:b(m)},{label:"n",value:String(a.length)}];x&&w.push({label:"outliers",value:String(x)});let f=e.apply(s.cols.x[i]);return{color:n.channels.color?.column?bt(t,n,s,o,i):t.theme.ink,header:`${s.hover.fields[0].label} ${l}`,rows:w,marks:[],anchor:{x:f+e.bandwidth,y:null}}}function fo(t,e,n,s){if(e.cols.xmin&&t.type!=="tile")return[s.apply(e.cols.xmin[n]),s.apply(e.cols.xmax[n])];if(s.bandwidth!==void 0){let i=e.cols.slots?.[n]??1,l=s.bandwidth/i,a=s.apply(e.cols.x[n])+(e.cols.slot?.[n]??0)*l;return[a,a+l]}let o=Je(e.cols.x,s),r=s.apply(e.cols.x[n]);return[r-o,r+o]}function xo(t,e,n,s){if(t.type==="tile"){if(s.bandwidth!==void 0)return[s.apply(e.cols.y[n]),s.apply(e.cols.y[n])+s.bandwidth];let o=Je(e.cols.y,s),r=s.apply(e.cols.y[n]);return[r-o,r+o]}return[s.apply(e.cols.ymin[n]),s.apply(e.cols.ymax[n])]}function Je(t,e){let n=[...new Set(t.map(Number))].sort((o,r)=>o-r),s=1/0;for(let o=1;o<n.length;o++)s=Math.min(s,n[o]-n[o-1]);return Number.isFinite(s)?Math.abs(e.apply(n[0]+s)-e.apply(n[0]))/2:6}var yo={zoom:'<path d="M4 9V4h5M15 4h5v5M20 15v5h-5M9 20H4v-5"/><rect x="9" y="9" width="6" height="6"/>',pan:'<path d="M12 3v18M3 12h18M12 3l-2.5 2.5M12 3l2.5 2.5M12 21l-2.5-2.5M12 21l2.5-2.5M3 12l2.5-2.5M3 12l2.5 2.5M21 12l-2.5-2.5M21 12l-2.5 2.5"/>',zoomIn:'<path d="M12 5v14M5 12h14"/>',zoomOut:'<path d="M5 12h14"/>',home:'<path d="M4 11l8-7 8 7M6 9.5V20h12V9.5"/>',copy:'<rect x="8.5" y="8.5" width="11" height="11"/><path d="M15.5 8.5V4.5h-11v11h4"/>',download:'<path d="M12 4v11M7.5 10.5L12 15l4.5-4.5M5 19.5h14"/>',check:'<path d="M5 12.5l4.5 4.5L19 7.5"/>'},go=`
2
+ .pt-root{position:relative;--pt-size:24px}
3
+ .pt-root svg{display:block;user-select:none;-webkit-user-select:none}
4
+ .pt-root .pt-key{cursor:pointer}
5
+ .pt-root:focus{outline:none}
6
+ .pt-bar{position:absolute;top:6px;right:6px;display:flex;align-items:stretch;z-index:3;background:var(--pt-bg);
7
+ border:1px solid var(--pt-line);opacity:0;transition:opacity .12s ease}
8
+ .pt-root:hover .pt-bar,.pt-root:focus-within .pt-bar,.pt-bar.pt-open{opacity:1}
9
+ @media (hover:none){.pt-bar{opacity:1}}
10
+ .pt-btn{all:unset;box-sizing:border-box;display:grid;place-items:center;width:var(--pt-size);height:var(--pt-size);
11
+ color:var(--pt-muted);cursor:pointer}
12
+ .pt-btn svg{width:58%;height:58%;fill:none;stroke:currentColor;stroke-width:1.75;stroke-linecap:square;stroke-linejoin:miter}
13
+ .pt-btn:hover{color:var(--pt-ink);background:var(--pt-soft)}
14
+ .pt-btn:focus-visible{outline:2px solid var(--pt-accent);outline-offset:-2px}
15
+ .pt-btn.pt-on{color:var(--pt-on-accent);background:var(--pt-accent)}
16
+ .pt-sep{width:1px;background:var(--pt-line)}
17
+ .pt-menu{position:absolute;top:100%;right:-1px;display:none;flex-direction:column;min-width:72px;margin-top:-1px;
18
+ background:var(--pt-bg);border:1px solid var(--pt-line)}
19
+ .pt-menu.pt-open{display:flex}
20
+ .pt-menu button{all:unset;padding:5px 12px;font:600 11px/1.4 system-ui,sans-serif;letter-spacing:.04em;color:var(--pt-ink);cursor:pointer}
21
+ .pt-menu button:hover{color:var(--pt-on-accent);background:var(--pt-accent)}
22
+ .pt-tip{position:absolute;display:none;pointer-events:none;z-index:4;min-width:110px;max-width:280px;background:var(--pt-bg);
23
+ color:var(--pt-ink);border:1px solid var(--pt-c);line-height:1.3;font-variant-numeric:tabular-nums}
24
+ .pt-tip.pt-pinned{border-width:2px;box-shadow:3px 3px 0 color-mix(in srgb,var(--pt-c) 35%,transparent)}
25
+ .pt-tip .pt-head{padding:3px 7px;background:var(--pt-c);color:var(--pt-on-c);font-weight:700;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
26
+ .pt-tip .pt-rows{display:grid;grid-template-columns:auto minmax(0,1fr) auto;column-gap:7px;row-gap:0;padding:4px 7px 5px}
27
+ .pt-tip .pt-sw{width:8px;height:8px;align-self:center}
28
+ .pt-tip .pt-k{color:var(--pt-muted);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
29
+ .pt-tip .pt-v{text-align:right;white-space:nowrap}
30
+ .pt-tip .pt-b{color:var(--pt-ink);font-weight:700}
31
+ .pt-tip .pt-t{border-top:1px solid var(--pt-line);padding-top:2px;margin-top:1px}
32
+ .pt-tip .pt-more{grid-column:1/-1;color:var(--pt-muted);font-size:.9em}
33
+ .pt-error{padding:8px 10px;border-left:3px solid #d93025;background:#fdecea;color:#5f2120;font:12px/1.4 ui-monospace,monospace;white-space:pre-wrap}
34
+ `;function tn(t){if(t.getElementById("plotto-style"))return;let e=t.createElement("style");e.id="plotto-style",e.textContent=go,t.head.append(e)}function en(t,e){let n={bg:e.background,ink:e.ink,muted:e.muted,line:e.grid,soft:e.strip,accent:e.accent,"on-accent":he(e.accent)};for(let[s,o]of Object.entries(n))t.style.setProperty(`--pt-${s}`,o)}function nn(t,e){let n=t.createElement("div");n.className="pt-bar";let s={},o=null;e.forEach((i,l)=>{l&&n.append(Object.assign(t.createElement("div"),{className:"pt-sep"}));for(let a of i){let c=t.createElement("button");c.className="pt-btn",c.type="button",c.title=a.title,c.setAttribute("aria-label",a.title),c.innerHTML=Qe(a.name),c.addEventListener("pointerdown",u=>u.stopPropagation()),c.addEventListener("click",u=>(u.stopPropagation(),a.menu?r():a.action(c))),n.append(c),s[a.name]=c,a.menu&&(o=bo(t,n,a.menu))}});function r(){let i=!o.classList.contains("pt-open");o.classList.toggle("pt-open",i),n.classList.toggle("pt-open",i)}return o&&t.addEventListener("click",()=>o.classList.contains("pt-open")&&r()),{el:n,button:i=>s[i],setSize:i=>n.parentElement?.style.setProperty("--pt-size",`${Math.round(Math.min(30,Math.max(22,i/34)))}px`),flash(i,l){let a=s[i],[c,u]=[a.innerHTML,a.title];a.innerHTML=Qe("check"),a.title=l,a.classList.add("pt-on"),setTimeout(()=>(a.innerHTML=c,a.title=u,a.classList.remove("pt-on")),1100)}}}function bo(t,e,n){let s=t.createElement("div");s.className="pt-menu";for(let[o,r]of n){let i=t.createElement("button");i.type="button",i.textContent=o,i.addEventListener("click",l=>(l.stopPropagation(),s.classList.remove("pt-open"),e.classList.remove("pt-open"),r())),s.append(i)}return e.append(s),s}function Qe(t){return`<svg viewBox="0 0 24 24" aria-hidden="true">${yo[t]}</svg>`}var $t=5,te=320,on=.75,wo=300;function sn(t,e,{interactive:n=!1,width:s}={}){let o=e.ownerDocument;tn(o);let r=o.createElement("div");r.className="pt-root";let i=Object.assign(o.createElement("div"),{className:"pt-tip"}),l={state:null,mode:"zoom"},a=null,c=m=>{let v=l.state&&{domains:l.state.domains===l.state.home?void 0:l.state.domains,hidden:l.state.hidden},x=Zt(t,{document:o,width:m,...v}),b=l.state?.svg;x.overlay=x.svg.appendChild(L(o,"g",{class:"pt-overlay","pointer-events":"none"})),x.theme.reflow||(x.svg.setAttribute("width","100%"),x.svg.removeAttribute("height"),r.style.maxWidth=`${x.W}px`),b?b.replaceWith(x.svg):r.prepend(x.svg),l.state=x,en(r,x.theme),p.setSize(x.theme.reflow?x.W:Math.min(x.W,e.clientWidth||x.W))},u=[[{name:"copy",title:"Copy as PNG",action:m=>Ie(l.state.svg).then(v=>p.flash("copy",v))},{name:"download",title:"Download",menu:[["PNG",()=>Ye(l.state.svg)],["SVG",()=>We(l.state.svg)]]}]];n&&u.unshift([{name:"zoom",title:"Zoom: drag a box",action:()=>d("zoom")},{name:"pan",title:"Pan: drag the view",action:()=>d("pan")}],[{name:"zoomIn",title:"Zoom in",action:()=>rn(l,on)},{name:"zoomOut",title:"Zoom out",action:()=>rn(l,1/on)},{name:"home",title:"Reset view (double-click)",action:()=>ln(l)}]);let p=nn(o,u);r.append(p.el,i);let d=m=>{l.mode=m,p.button("zoom").classList.toggle("pt-on",m==="zoom"),p.button("pan").classList.toggle("pt-on",m==="pan"),r.style.cursor=m==="pan"?"grab":""};try{let m=s??(e.clientWidth?Math.max(te,e.clientWidth):void 0);c(m),e.replaceChildren(r)}catch(m){let v=Object.assign(o.createElement("div"),{className:"pt-error",textContent:`plotto: ${m.message}`});throw e.prepend(v),m}if(n&&(d("zoom"),vo(l,r,i)),l.state.theme.reflow&&s===void 0&&typeof ResizeObserver<"u"){let m;a=new ResizeObserver(()=>{clearTimeout(m),m=setTimeout(()=>{let v=Math.max(te,Math.round(e.clientWidth));v&&v!==l.state.W&&c(v)},100)}),a.observe(e)}return l.redraw=()=>rt(l.state),l.resize=m=>c(Math.max(te,m)),l.destroy=()=>a?.disconnect(),l}function vo(t,e,n){let s=null,o=null,r=()=>t.state.svg,i=w=>{let f=r().getBoundingClientRect(),g=t.state.W/f.width;return[(w.clientX-f.left)*g,(w.clientY-f.top)*g,g]},l=(w,f)=>t.state.layout.panels.findIndex(g=>w>=g.x+g.ml&&w<=g.x+g.ml+g.innerW&&f>=g.y&&f<=g.y+g.innerH),a=!1;e.addEventListener("pointerdown",w=>{if(w.button!==0||!r().contains(w.target))return;let[f,g]=i(w),h=l(f,g);h<0||(w.preventDefault(),e.focus({preventScroll:!0}),s={i:h,x:f,y:g,domains:t.state.domains,moving:!1,id:w.pointerId})}),e.addEventListener("pointermove",w=>{let[f,g,h]=i(w);if(s&&!s.moving&&Math.hypot(f-s.x,g-s.y)>=$t&&(s.moving=!0,e.setPointerCapture(s.id),u(),b(),t.mode==="pan"&&(e.style.cursor="grabbing")),!s?.moving){!a&&!o&&(o=requestAnimationFrame(()=>(o=null,x(f,g,h))));return}if(t.mode==="pan")return Ao(t,s,f,g);let y=t.state.layout.panels[s.i],k=R=>Math.max(y.x+y.ml,Math.min(y.x+y.ml+y.innerW,R)),M=R=>Math.max(y.y,Math.min(y.y+y.innerH,R)),[C,S]=[s.x,k(f)].sort((R,T)=>R-T),[B,j]=[s.y,M(g)].sort((R,T)=>R-T),q=j-B<$t||t.state.kind.y==="category",z=S-C<$t||t.state.kind.x==="category";if(q&&z)return t.state.overlay.replaceChildren();let tt={x:z?y.x+y.ml:C,y:q?y.y:B,width:z?y.innerW:S-C,height:q?y.innerH:j-B},et=t.state.theme.accent;t.state.overlay.replaceChildren(L(t.state.doc,"rect",{...tt,fill:et,"fill-opacity":.1,stroke:et,"stroke-opacity":.7,"stroke-width":1})),s.box={x0:C,x1:S,y0:B,y1:j}}),e.addEventListener("pointerup",w=>{if(s?.moving&&t.mode==="zoom"&&s.box)Mo(t,s.i,s.box);else if(s&&!s.moving){let[f,g,h]=i(w);a?(u(),x(f,g,h)):(x(f,g,h),n.style.display==="block"&&c())}t.mode==="pan"&&(e.style.cursor="grab"),a||t.state.overlay.replaceChildren(),s=null}),e.addEventListener("pointerleave",()=>!s&&!a&&b()),e.addEventListener("keydown",w=>w.key==="Escape"&&(u(),b())),e.tabIndex=-1,t.clearHover=()=>(u(),b());function c(){a=!0,n.classList.add("pt-pinned")}function u(){a=!1,n.classList.remove("pt-pinned")}let p=w=>t.state.doc.elementFromPoint(w.clientX,w.clientY),d=w=>p(w)?.closest?.(".pt-key");e.addEventListener("dblclick",w=>{let[f,g]=i(w);l(f,g)<0||(u(),b(),ln(t))});let m={value:void 0,time:0,before:null};e.addEventListener("click",w=>{let f=d(w);if(!f)return;let g=t.state.legend.items[Number(f.dataset.index)].value;t.clearHover();let h=performance.now();if(g===m.value&&h-m.time<wo)return t.state.hidden=m.before,m={value:void 0,time:0,before:null},Eo(t,g);m={value:g,time:h,before:t.state.hidden};let y=new Set(t.state.hidden);y.has(g)?y.delete(g):y.add(g),t.state.hidden=y,rt(t.state)});let v=null;function x(w,f,g){let h=t.state,y=l(w,f),k=y<0?null:h.layout.panels[y],M=k?Ze(h,y,w-k.x,f-k.y,v):null;if(v=M,!M)return b();let C=h.doc,S=[];if(M.crosshair!==void 0){let T=k.x+M.crosshair;S.push(L(C,"line",{x1:T,x2:T,y1:k.y,y2:k.y+k.innerH,stroke:h.theme.muted,"stroke-opacity":.55,"stroke-width":1}))}for(let T of M.marks)T.rect?S.push(L(C,"rect",{x:k.x+T.rect.x,y:k.y+T.rect.y,width:T.rect.width,height:T.rect.height,fill:"none",stroke:T.color,"stroke-width":1.5})):S.push(L(C,"circle",{cx:k.x+T.x,cy:k.y+T.y,r:h.theme.point+1.5,fill:T.color,stroke:h.theme.background,"stroke-width":1.5}));h.overlay.replaceChildren(...S);let B=Yt(M.color);n.style.setProperty("--pt-c",B.bg),n.style.setProperty("--pt-on-c",B.fg),n.style.font=`${h.fonts.hover}px ${h.theme.font}`,n.replaceChildren(...ko(C,M)),n.style.display="block";let j=(k.x+M.anchor.x)/g,q=f/g,z=e.clientWidth,tt=e.clientHeight,et=j+16+n.offsetWidth>z?j-16-n.offsetWidth:j+16,R=Math.min(Math.max(0,q-18),Math.max(0,tt-n.offsetHeight));n.style.left=`${Math.max(0,et)}px`,n.style.top=`${R}px`}function b(){n.style.display="none",s||t.state.overlay.replaceChildren()}}function ko(t,e){let n=(r,i,l)=>Object.assign(t.createElement(r),{className:i,textContent:l??""}),s=[];e.header&&s.push(n("div","pt-head",e.header));let o=n("div","pt-rows");for(let r of e.rows){let i=n("span","pt-sw");r.swatch&&(i.style.background=r.swatch);let l=[i,n("span","pt-k",r.label),n("span","pt-v",r.value)];r.strong&&l.forEach(a=>a.classList.add("pt-b")),r.total&&l.forEach(a=>a.classList.add("pt-t")),o.append(...l)}return e.note&&o.append(n("span","pt-more",e.note)),s.push(o),s}function ln(t){t.clearHover?.(),t.state.domains=t.state.home,rt(t.state)}function Eo(t,e){let n=t.state.legend.items.map(o=>o.value),s=t.state.hidden.size===n.length-1&&!t.state.hidden.has(e);t.state.hidden=s?new Set:new Set(n.filter(o=>o!==e)),rt(t.state)}function Mo(t,e,{x0:n,x1:s,y0:o,y1:r}){let i=t.state,l=i.layout.panels[e],a=i.panels[e].plot,c=i.domains.map(p=>({...p})),u=(p,d,m)=>{if(i.kind[p]==="category"||m-d<$t)return;let v=a.scale(p),x=p==="x"?l.x:l.y,b=[v.invert(d-x),v.invert(m-x)].map(Number).sort((w,f)=>w-f);an(i,p,e).forEach(w=>c[w][p]=b)};u("x",n,s),u("y",o,r),i.domains=c,rt(i)}function rn(t,e){t.clearHover?.();let n=t.state;n.domains=n.domains.map(s=>{let o={...s};for(let r of["x","y"]){let i=n.kind[r];if(i==="category")continue;let l=i==="log"?Math.log10:d=>d,a=i==="log"?d=>10**d:d=>d,[c,u]=s[r].map(d=>l(+d)),p=(c+u)/2;o[r]=[a(p-(u-c)/2*e),a(p+(u-c)/2*e)]}return o}),rt(n)}function Ao(t,e,n,s){let o=t.state,r=o.layout.panels[e.i],i=o.panels[e.i].plot,l=e.domains.map(a=>({...a}));for(let[a,c,u,p]of[["x",e.x,n,r.x],["y",e.y,s,r.y]]){if(o.kind[a]==="category")continue;let d=i.scale(a),m=o.kind[a]==="log",v=w=>m?Math.log10(w):w,x=v(+d.invert(c-p))-v(+d.invert(u-p)),b=e.domains[e.i][a].map(w=>m?10**(v(w)+x):+w+x);an(o,a,e.i).forEach(w=>l[w][a]=b)}o.domains=l,o.frame||(o.frame=requestAnimationFrame(()=>(o.frame=null,rt(o))))}function an(t,e,n){let s=t.ns.facet?.free?.[e],o=t.grid.panels[n];return t.grid.panels.flatMap((r,i)=>s?t.grid.kind!=="grid"?i===n?[i]:[]:(e==="x"?r.col===o.col:r.row===o.row)?[i]:[]:[i])}globalThis.plotto={plot:ae,draw:sn,toSVG:qe};
@@ -0,0 +1,150 @@
1
+ {
2
+ "styles": {
3
+ "pastel": {
4
+ "font": "Helvetica, Arial, 'Liberation Sans', sans-serif",
5
+ "metrics": "arial",
6
+ "background": "#FFFEFB",
7
+ "ink": "#3A3631",
8
+ "muted": "#7A736A",
9
+ "grid": "#ECE8E1",
10
+ "gridMinor": "#F6F3EE",
11
+ "tick": "#D6D0C6",
12
+ "strip": "#F4F0E9",
13
+ "accent": "#3E8A99",
14
+ "palette": "pastel",
15
+ "scheme": "ai",
16
+ "colors": {
17
+ "blue": "#7AA6DA",
18
+ "orange": "#EF977F",
19
+ "green": "#7DB89A",
20
+ "purple": "#A79BD6",
21
+ "pink": "#E48FAE",
22
+ "yellow": "#DDB459",
23
+ "teal": "#63B7C1",
24
+ "red": "#CF7468",
25
+ "brown": "#BB9C84",
26
+ "gray": "#97A1AD"
27
+ },
28
+ "weight": 1.25
29
+ },
30
+ "wota": {
31
+ "font": "'M PLUS 1p', sans-serif",
32
+ "metrics": "mplus",
33
+ "ink": "#333333",
34
+ "muted": "#666666",
35
+ "grid": "#D9D9D9",
36
+ "gridMinor": "#EFEFEF",
37
+ "tick": "#BBBBBB",
38
+ "strip": "#EEF1FA",
39
+ "accent": "#5C6BC0",
40
+ "palette": "wota",
41
+ "scheme": "sakura",
42
+ "colors": {
43
+ "pink": "#E4007F",
44
+ "blue": "#5C6BC0",
45
+ "teal": "#00A3A3",
46
+ "orange": "#F5A623",
47
+ "purple": "#8E44AD",
48
+ "gray": "#7F7F7F",
49
+ "green": "#3BA55C",
50
+ "red": "#D7263D",
51
+ "yellow": "#F2C14E",
52
+ "brown": "#8D6E63"
53
+ },
54
+ "weight": 1,
55
+ "background": "#FFFFFF"
56
+ },
57
+ "paper": {
58
+ "ink": "#000000",
59
+ "muted": "#333333",
60
+ "grid": "#E6E6E6",
61
+ "gridMinor": "#F5F5F5",
62
+ "tick": "#8C8C8C",
63
+ "strip": "#EBEBEB",
64
+ "accent": "#0072B2",
65
+ "palette": ["blue", "orange", "green", "pink", "yellow", "teal", "red", "brown", "purple", "gray"],
66
+ "scheme": "viridis",
67
+ "colors": {
68
+ "blue": "#0072B2",
69
+ "orange": "#E69F00",
70
+ "green": "#009E73",
71
+ "pink": "#CC79A7",
72
+ "yellow": "#F0E442",
73
+ "teal": "#56B4E9",
74
+ "red": "#D55E00",
75
+ "brown": "#8C6D31",
76
+ "purple": "#6A3D9A",
77
+ "gray": "#7F7F7F"
78
+ },
79
+ "weight": 1,
80
+ "background": "#FFFFFF"
81
+ },
82
+ "dark": {
83
+ "background": "#16181D",
84
+ "ink": "#E8EAED",
85
+ "muted": "#A6ABB3",
86
+ "grid": "#2E323A",
87
+ "gridMinor": "#22252B",
88
+ "tick": "#4F5560",
89
+ "strip": "#252933",
90
+ "accent": "#8AB4F8",
91
+ "palette": ["blue", "orange", "green", "purple", "pink", "yellow", "teal", "red", "brown", "gray"],
92
+ "scheme": "yuhi",
93
+ "colors": {
94
+ "blue": "#8AB4F8",
95
+ "orange": "#FBA94C",
96
+ "green": "#81C995",
97
+ "purple": "#C58AF9",
98
+ "pink": "#FF8BCB",
99
+ "yellow": "#FDD663",
100
+ "teal": "#78D9D3",
101
+ "red": "#F28B82",
102
+ "brown": "#C8A27A",
103
+ "gray": "#9AA0A6"
104
+ },
105
+ "weight": 1.15
106
+ }
107
+ },
108
+ "palettes": {
109
+ "pastel": ["#7AA6DA", "#EF977F", "#7DB89A", "#A79BD6", "#E48FAE", "#DDB459", "#63B7C1", "#CF7468", "#BB9C84", "#97A1AD"],
110
+ "wota": ["#E4007F", "#5C6BC0", "#00A3A3", "#F5A623", "#8E44AD", "#7F7F7F", "#3BA55C", "#D7263D", "#F2C14E", "#8D6E63"],
111
+ "sakura": ["#E16B8C", "#5DAC81", "#8B81C3", "#FFB11B", "#7DB9DE", "#F596AA", "#B9887D", "#91AD70"],
112
+ "aizome": ["#0F2540", "#51A8DD", "#66BAB7", "#656765", "#0D5661", "#A5DEE4", "#7B90D2"],
113
+ "momiji": ["#CB4042", "#FFB11B", "#6C6A2D", "#6D2E5B", "#ED784A", "#734338", "#C99833"],
114
+ "tableau": ["#4E79A7", "#F28E2B", "#E15759", "#76B7B2", "#59A14F", "#EDC948", "#B07AA1", "#FF9DA7", "#9C755F", "#BAB0AC"]
115
+ },
116
+ "schemes": {
117
+ "ai": ["#F4F8F8", "#A5DEE4", "#33A6B8", "#0D5661", "#0F2540"],
118
+ "sakura": ["#FEF4F4", "#FEDFE1", "#F596AA", "#D05A6E", "#8E354A", "#4A1D30"],
119
+ "matcha": ["#F8F6E3", "#D7E3B5", "#91AD70", "#5DAC81", "#1B813E", "#0B3B2A"],
120
+ "yuhi": ["#FFF3D6", "#FFB11B", "#ED784A", "#CB4042", "#6D2E5B", "#26113A"],
121
+ "sumi": ["#FCFAF2", "#BDC0BA", "#828282", "#434343", "#1C1C1C"],
122
+ "aiaka": ["#0F2540", "#33A6B8", "#A5DEE4", "#F7F6F0", "#F4A7A0", "#CB4042", "#5C1A1B"]
123
+ },
124
+ "formats": {
125
+ "screen": {
126
+ "width": 900,
127
+ "height": 450,
128
+ "size": 12,
129
+ "line": 1.5,
130
+ "point": 2.5,
131
+ "reflow": true
132
+ },
133
+ "report": {
134
+ "width": 640,
135
+ "height": 400,
136
+ "size": 10,
137
+ "line": 1.2,
138
+ "point": 2,
139
+ "reflow": false
140
+ },
141
+ "slide": {
142
+ "width": 1280,
143
+ "height": 720,
144
+ "size": 20,
145
+ "line": 2.5,
146
+ "point": 4,
147
+ "reflow": false
148
+ }
149
+ }
150
+ }
@@ -0,0 +1,19 @@
1
+ """Where the package finds its files: python/plotto/_assets/ in an installed package (npm run build:python makes it),
2
+ or the repo root in a checkout."""
3
+
4
+ from pathlib import Path
5
+
6
+ HERE = Path(__file__).resolve().parent
7
+ ASSETS = HERE / "_assets"
8
+ REPO = HERE.parents[1]
9
+
10
+
11
+ def path(name):
12
+ """A shared file (grammar.json, theme.json): the repo file in a checkout (always current), else the packaged copy."""
13
+ repo = REPO / name
14
+ return repo if repo.exists() else ASSETS / name
15
+
16
+
17
+ def checkout():
18
+ """The repo root when the package runs from a checkout with its JS dependencies, else None."""
19
+ return REPO if (REPO / "src" / "render.mjs").exists() and (REPO / "node_modules").exists() else None
@@ -0,0 +1,49 @@
1
+ """DataFrame to columnar spec data. The rules are the same as toTable() and toValue() in src/builder.js."""
2
+
3
+ import math
4
+ from datetime import date, datetime
5
+
6
+ import numpy as np
7
+ import pandas as pd
8
+
9
+
10
+ def to_frame(data, where):
11
+ if isinstance(data, pd.DataFrame):
12
+ return data
13
+ if isinstance(data, list):
14
+ return pd.DataFrame(data)
15
+ raise TypeError(f"{where}: data must be a pandas DataFrame or a list of row dicts")
16
+
17
+
18
+ def to_column(series: pd.Series) -> dict:
19
+ """One spec column: {"type": "number" | "time" | "string", "values": [...], "levels": [...]}."""
20
+ if isinstance(series.dtype, pd.CategoricalDtype):
21
+ levels = [str(c) for c in series.cat.categories]
22
+ return {"type": "string", "values": [None if pd.isna(v) else str(v) for v in series], "levels": levels}
23
+ if pd.api.types.is_datetime64_any_dtype(series):
24
+ if series.dt.tz is not None:
25
+ series = series.dt.tz_localize(None) # wall-clock time of the data's time zone
26
+ ms = series.astype("datetime64[ms]").astype("int64")
27
+ return {"type": "time", "values": [None if na else int(v) for v, na in zip(ms, series.isna(), strict=True)]}
28
+ if pd.api.types.is_bool_dtype(series):
29
+ return {"type": "string", "values": ["true" if v else "false" for v in series]}
30
+ if pd.api.types.is_numeric_dtype(series):
31
+ return {"type": "number", "values": [to_value(v) for v in series.to_numpy()]}
32
+ return {"type": "string", "values": [None if v is None or (isinstance(v, float) and math.isnan(v)) else str(v) for v in series]}
33
+
34
+
35
+ def to_value(v):
36
+ """Wall-clock epoch ms for times; 6 significant digits for floats; None for NaN."""
37
+ if isinstance(v, (pd.Timestamp, datetime, np.datetime64, date)):
38
+ ts = pd.Timestamp(v)
39
+ if ts.tzinfo is not None:
40
+ ts = ts.tz_localize(None)
41
+ return int(ts.value // 1_000_000)
42
+ if isinstance(v, (np.integer, int)) and not isinstance(v, bool):
43
+ return int(v)
44
+ if isinstance(v, (np.floating, float)):
45
+ v = float(v)
46
+ if math.isnan(v):
47
+ return None
48
+ return v if v.is_integer() else float(f"{v:.6g}")
49
+ return v
@@ -0,0 +1,104 @@
1
+ """Outputs: static SVG, PNG and interactive HTML.
2
+
3
+ The notebook HTML uses the browser bundle (plotto.js) and needs no Node. The static SVG and PNG come from one Node
4
+ process (plotto-render --serve) that starts on the first use and stays for the life of the kernel:
5
+ - in a checkout of the repo (with node_modules), node src/render.mjs;
6
+ - else the npm package plotto of the same version, through npx (downloaded once, then cached).
7
+ Without Node, a notebook plot is live only (no static copy for GitHub), and save() raises an error.
8
+ """
9
+
10
+ import atexit
11
+ import base64
12
+ import json
13
+ import os
14
+ import re
15
+ import shutil
16
+ import subprocess
17
+ import threading
18
+ import uuid
19
+ from pathlib import Path
20
+
21
+ from . import __version__, assets
22
+
23
+ # The npm package that npx runs outside a checkout. PLOTTO_NPM overrides it (for example a local .tgz, for tests).
24
+ NPM_PACKAGE = os.environ.get("PLOTTO_NPM", f"plotto@{__version__}")
25
+ _lock = threading.Lock()
26
+ _process = None
27
+ _bundle = None
28
+
29
+
30
+ def _request(op, **params):
31
+ global _process
32
+ with _lock:
33
+ if _process is None or _process.poll() is not None:
34
+ _process = _start()
35
+ _process.stdin.write(json.dumps({"op": op, **params}, allow_nan=False) + "\n")
36
+ _process.stdin.flush()
37
+ reply = json.loads(_process.stdout.readline() or '{"error": "the render process stopped"}')
38
+ if "error" in reply:
39
+ raise RuntimeError(f"plotto render: {reply['error']}")
40
+ return reply
41
+
42
+
43
+ class NodeMissing(RuntimeError):
44
+ pass
45
+
46
+
47
+ def _start():
48
+ node = shutil.which("node")
49
+ if node is None:
50
+ raise NodeMissing("plotto needs Node.js 20 or later on the PATH for static SVG and PNG files (https://nodejs.org)")
51
+ repo = assets.checkout()
52
+ if repo is not None:
53
+ command, cwd = [node, str(repo / "src" / "render.mjs"), "--serve"], repo
54
+ else:
55
+ npx = shutil.which("npx") or str(Path(node).with_name("npx"))
56
+ command, cwd = [npx, "--yes", "--package", NPM_PACKAGE, "plotto-render", "--serve"], None
57
+ process = subprocess.Popen(command, cwd=cwd, stdin=subprocess.PIPE, stdout=subprocess.PIPE, text=True)
58
+ atexit.register(process.kill)
59
+ return process
60
+
61
+
62
+ def svg(spec) -> str:
63
+ return _request("svg", spec=spec)["svg"]
64
+
65
+
66
+ def png(spec, scale=2) -> bytes:
67
+ return base64.b64decode(_request("png", spec=spec, scale=scale)["png"])
68
+
69
+
70
+ def save(spec, path, scale=2):
71
+ path = Path(path)
72
+ if path.suffix == ".svg":
73
+ path.write_text(svg(spec))
74
+ elif path.suffix == ".png":
75
+ path.write_bytes(png(spec, scale))
76
+ else:
77
+ raise ValueError(f"save(): use a .svg or .png file name, not {path.name}")
78
+
79
+
80
+ def html(spec, interactive=False) -> str:
81
+ """Notebook output: the static SVG, then a script that draws the same plot at the cell width, with a toolbar.
82
+
83
+ interactive adds zoom, pan, hover and the legend toggle. Without the script (GitHub, nbviewer) or without
84
+ network access (the script loads d3 and Observable Plot from the CDN), the static SVG stays.
85
+ """
86
+ global _bundle
87
+ if _bundle is None:
88
+ # A checkout bundles the current src/; an installed package uses its packaged bundle.
89
+ packaged = assets.ASSETS / "plotto.js"
90
+ _bundle = _request("bundle")["js"] if assets.checkout() or not packaged.exists() else packaged.read_text()
91
+ key = f"pt-{uuid.uuid4().hex[:12]}"
92
+ try:
93
+ static = re.sub(r'width="\d+(\.\d+)?" height="\d+(\.\d+)?"', 'width="100%"', svg(spec), count=1)
94
+ except RuntimeError:
95
+ static = "" # no Node, or the renderer could not start: the live plot only
96
+ data = json.dumps(spec, allow_nan=False, separators=(",", ":")).replace("</", "<\\/")
97
+ script = _bundle.replace("</script", "<\\/script")
98
+ return (
99
+ f'<div id="{key}">{static}</div>'
100
+ f'<script type="application/json" id="{key}-spec">{data}</script>'
101
+ f'<script type="module">{script}\n'
102
+ f'plotto.draw(JSON.parse(document.getElementById("{key}-spec").textContent), '
103
+ f'document.getElementById("{key}"), {{interactive: {"true" if interactive else "false"}}});</script>'
104
+ )
@@ -0,0 +1,267 @@
1
+ """Chainable plot builder: the same methods, arguments and spec as src/builder.js.
2
+
3
+ Each method returns a new Plot and never changes the old one. A string that names a column is a mapping;
4
+ any other value is a constant.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import json
10
+
11
+ from . import assets, output
12
+ from .data import to_column, to_frame, to_value
13
+
14
+ GRAMMAR = json.loads(assets.path("grammar.json").read_text())
15
+ THEMES = json.loads(assets.path("theme.json").read_text())
16
+ LAYERS = GRAMMAR["layers"]
17
+ PLOT_CHANNELS = list(dict.fromkeys(ch for layer in LAYERS.values() for ch in layer["channels"]))
18
+
19
+
20
+ def plot(data, **mapping) -> Plot:
21
+ """Start a plot: plot(df, x="time", y="W", color="device")."""
22
+ frame = to_frame(data, "plot()")
23
+ for ch, col in mapping.items():
24
+ if ch not in PLOT_CHANNELS:
25
+ _fail("plot()", "channel", ch, PLOT_CHANNELS)
26
+ _need_column(frame, col, "plot()")
27
+ state = {"data": [frame], "mapping": mapping, "layers": [], "facet": None, "scales": {}, "labels": {}, "theme": {"style": "pastel"}}
28
+ return Plot(state)
29
+
30
+
31
+ class Plot:
32
+ def __init__(self, state, interactive=False):
33
+ self._s = state
34
+ self._interactive = interactive
35
+
36
+ def _with(self, **changes) -> Plot:
37
+ return Plot({**self._s, **changes}, self._interactive)
38
+
39
+ def _layer(self, type_, data=None, **options) -> Plot:
40
+ definition = LAYERS[type_]
41
+ where = f"{type_}()"
42
+ tables = self._s["data"]
43
+ index = 0
44
+ if data is not None:
45
+ tables = [*tables, to_frame(data, where)]
46
+ index = len(tables) - 1
47
+ columns = tables[index].columns
48
+ layer = {"type": type_, "mapping": {}, "set": {}}
49
+ if index:
50
+ layer["data"] = index
51
+ for key, value in options.items():
52
+ if key in definition["channels"] and isinstance(value, str) and value in columns:
53
+ layer["mapping"][key] = value
54
+ elif key in definition["channels"] or key in definition["options"]:
55
+ layer["set"][key] = _constant(key, value, where, columns)
56
+ else:
57
+ _fail(where, "option", key, ["data", *definition["channels"], *definition["options"]])
58
+ return self._with(data=tables, layers=[*self._s["layers"], layer])
59
+
60
+ def line(self, **options):
61
+ return self._layer("line", **options)
62
+
63
+ def step(self, **options):
64
+ return self._layer("step", **options)
65
+
66
+ def bar(self, **options):
67
+ return self._layer("bar", **options)
68
+
69
+ def hist(self, **options):
70
+ return self._layer("hist", **options)
71
+
72
+ def density(self, **options):
73
+ return self._layer("density", **options)
74
+
75
+ def area(self, **options):
76
+ return self._layer("area", **options)
77
+
78
+ def ribbon(self, **options):
79
+ return self._layer("ribbon", **options)
80
+
81
+ def smooth(self, **options):
82
+ return self._layer("smooth", **options)
83
+
84
+ def path(self, **options):
85
+ return self._layer("path", **options)
86
+
87
+ def point(self, **options):
88
+ return self._layer("point", **options)
89
+
90
+ def hline(self, **options):
91
+ return self._layer("hline", **options)
92
+
93
+ def vline(self, **options):
94
+ return self._layer("vline", **options)
95
+
96
+ def abline(self, **options):
97
+ return self._layer("abline", **options)
98
+
99
+ def segment(self, **options):
100
+ return self._layer("segment", **options)
101
+
102
+ def rect(self, **options):
103
+ return self._layer("rect", **options)
104
+
105
+ def band(self, **options):
106
+ return self._layer("band", **options)
107
+
108
+ def tile(self, **options):
109
+ return self._layer("tile", **options)
110
+
111
+ def text(self, **options):
112
+ return self._layer("text", **options)
113
+
114
+ def errorbar(self, **options):
115
+ return self._layer("errorbar", **options)
116
+
117
+ def box(self, **options):
118
+ return self._layer("box", **options)
119
+
120
+ def facet(self, by, **options) -> Plot:
121
+ _need_column(self._s["data"][0], by, "facet()")
122
+ _check_keys(options, GRAMMAR["facet"], "facet()")
123
+ _check_free(options.get("free"), "facet()")
124
+ return self._with(facet={"wrap": by, "free": options.get("free"), "ncol": options.get("ncol")})
125
+
126
+ def grid(self, rows=None, cols=None, **options) -> Plot:
127
+ if rows is None and cols is None:
128
+ raise ValueError("grid(): set rows, cols or both")
129
+ for col in (rows, cols):
130
+ if col is not None:
131
+ _need_column(self._s["data"][0], col, "grid()")
132
+ _check_keys(options, GRAMMAR["grid"], "grid()")
133
+ _check_free(options.get("free"), "grid()")
134
+ return self._with(facet={"rows": rows, "cols": cols, "free": options.get("free")})
135
+
136
+ def x(self, **options):
137
+ return self._scale("x", options)
138
+
139
+ def y(self, **options):
140
+ return self._scale("y", options)
141
+
142
+ def color(self, **options):
143
+ return self._scale("color", options)
144
+
145
+ def _scale(self, name, options) -> Plot:
146
+ where = f"{name}()"
147
+ _check_keys(options, GRAMMAR["scales"][name], where)
148
+ if "type" in options and options["type"] not in GRAMMAR["scaleTypes"]:
149
+ _fail(where, "type", options["type"], GRAMMAR["scaleTypes"])
150
+ palette = options.get("palette")
151
+ if palette is not None and not isinstance(palette, (list, tuple)) and palette not in THEMES["palettes"]:
152
+ _fail(where, "palette", palette, list(THEMES["palettes"]))
153
+ if "mid" in options and not isinstance(options["mid"], (int, float)):
154
+ raise ValueError(f"{where}: mid must be a number")
155
+ scale = dict(options)
156
+ if scale.get("limits") is not None:
157
+ if not isinstance(scale["limits"], (list, tuple)):
158
+ raise ValueError(f"{where}: limits must be a list, for example [0, 10] or [0, None]")
159
+ scale["limits"] = [None if v is None else to_value(v) for v in scale["limits"]]
160
+ return self._with(scales={**self._s["scales"], name: scale})
161
+
162
+ def labels(self, **options) -> Plot:
163
+ _check_keys(options, GRAMMAR["labels"], "labels()")
164
+ return self._with(labels={**self._s["labels"], **options})
165
+
166
+ def theme(self, style="pastel", **options) -> Plot:
167
+ """The look (style) and the output size (format): theme("wota", format="slide")."""
168
+ _check_keys(options, GRAMMAR["theme"], "theme()")
169
+ if style not in THEMES["styles"]:
170
+ _fail("theme()", "style", style, list(THEMES["styles"]))
171
+ if "format" in options and options["format"] not in THEMES["formats"]:
172
+ _fail("theme()", "format", options["format"], list(THEMES["formats"]))
173
+ if "legend" in options and options["legend"] not in GRAMMAR["legend"]:
174
+ _fail("theme()", "legend", options["legend"], GRAMMAR["legend"])
175
+ return self._with(theme={"style": style, **options})
176
+
177
+ def interactive(self) -> Plot:
178
+ """The same plot with zoom, pan, hover, legend toggle and copy in the notebook."""
179
+ return Plot(self._s, interactive=True)
180
+
181
+ def spec(self) -> dict:
182
+ """The JSON spec (SPEC.md). Each table keeps only the columns that a mapping or a facet uses."""
183
+ s = self._s
184
+ used = [[] for _ in s["data"]]
185
+
186
+ def use(i, col):
187
+ if col in s["data"][i].columns and col not in used[i]:
188
+ used[i].append(col)
189
+
190
+ for col in s["mapping"].values():
191
+ for i in range(len(s["data"])):
192
+ use(i, col)
193
+ for layer in s["layers"]:
194
+ for col in layer["mapping"].values():
195
+ use(layer.get("data", 0), col)
196
+ facet = s["facet"] or {}
197
+ for col in (facet.get("wrap"), facet.get("rows"), facet.get("cols")):
198
+ if col:
199
+ for i in range(len(s["data"])):
200
+ use(i, col)
201
+ data = [{"columns": {c: to_column(frame[c]) for c in used[i]}} for i, frame in enumerate(s["data"])]
202
+ return {
203
+ "version": 1,
204
+ "data": data,
205
+ "mapping": s["mapping"],
206
+ "layers": s["layers"],
207
+ "facet": s["facet"],
208
+ "scales": s["scales"],
209
+ "labels": s["labels"],
210
+ "theme": s["theme"],
211
+ }
212
+
213
+ def save(self, path, scale=2, width=None, height=None):
214
+ """Write the plot as .svg or .png. scale: PNG px per figure px. width, height: the figure size in px."""
215
+ spec = self.spec()
216
+ if width is not None or height is not None:
217
+ spec["theme"] = {**spec["theme"], **{k: v for k, v in {"width": width, "height": height}.items() if v is not None}}
218
+ output.save(spec, path, scale)
219
+
220
+ def __repr__(self):
221
+ s = self._s
222
+ layers = "+".join(layer["type"] for layer in s["layers"]) or "no layers"
223
+ facet = s["facet"] or {}
224
+ by = facet.get("wrap") or " x ".join(c for c in (facet.get("rows"), facet.get("cols")) if c)
225
+ extra = f", facet {by}" if by else ""
226
+ mode = ", interactive" if self._interactive else ""
227
+ return f"<plotto.Plot {layers}{extra}, {len(s['data'][0])} rows{mode}>"
228
+
229
+ def _repr_mimebundle_(self, include=None, exclude=None):
230
+ return {"text/html": output.html(self.spec(), self._interactive), "text/plain": repr(self)}
231
+
232
+
233
+ def _constant(key, value, where, columns):
234
+ kind = GRAMMAR["options"].get(key)
235
+ if key in ("tooltip", "group"):
236
+ raise ValueError(f"{where}: {key} must be a column. Columns: {', '.join(columns)}")
237
+ if isinstance(kind, list) and value not in kind:
238
+ _fail(where, key, value, kind)
239
+ if kind == "number" and (not isinstance(value, (int, float)) or isinstance(value, bool)):
240
+ raise ValueError(f"{where}: {key} must be a number")
241
+ if kind == "boolean" and not isinstance(value, bool):
242
+ raise ValueError(f"{where}: {key} must be True or False")
243
+ if key in ("color", "label"):
244
+ return value
245
+ if isinstance(value, str) and not kind:
246
+ raise ValueError(f'{where}: "{value}" is not a column. Columns: {", ".join(columns)}')
247
+ return to_value(value)
248
+
249
+
250
+ def _need_column(frame, col, where):
251
+ if col not in frame.columns:
252
+ raise ValueError(f'{where}: "{col}" is not a column. Columns: {", ".join(map(str, frame.columns))}')
253
+
254
+
255
+ def _check_keys(options, valid, where):
256
+ for key in options:
257
+ if key not in valid:
258
+ _fail(where, "option", key, valid)
259
+
260
+
261
+ def _check_free(free, where):
262
+ if free is not None and free not in GRAMMAR["free"]:
263
+ _fail(where, "free", free, GRAMMAR["free"])
264
+
265
+
266
+ def _fail(where, what, value, valid):
267
+ raise ValueError(f'{where}: unknown {what} "{value}". Valid: {", ".join(valid)}')
@@ -0,0 +1,37 @@
1
+ [build-system]
2
+ requires = ["setuptools>=77"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "python-plotto"
7
+ dynamic = ["version"]
8
+ description = "Grammar-of-graphics plots for notebooks, web pages and slides with one renderer."
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ license-files = ["LICENSE"]
12
+ requires-python = ">=3.12"
13
+ dependencies = ["pandas>=2", "numpy"]
14
+ keywords = ["plot", "chart", "grammar of graphics", "ggplot", "notebook", "visualization"]
15
+ classifiers = [
16
+ "Development Status :: 4 - Beta",
17
+ "Framework :: Jupyter",
18
+ "Intended Audience :: Science/Research",
19
+ "Programming Language :: Python :: 3",
20
+ "Topic :: Scientific/Engineering :: Visualization",
21
+ ]
22
+
23
+ [project.urls]
24
+ Homepage = "https://theoradusz.github.io/plotto/"
25
+ Source = "https://github.com/theoradusz/plotto"
26
+
27
+ [project.optional-dependencies]
28
+ test = ["pytest", "ruff", "black", "nbconvert", "ipykernel"]
29
+
30
+ [tool.setuptools]
31
+ packages = ["plotto"]
32
+
33
+ [tool.setuptools.dynamic]
34
+ version = { attr = "plotto.__version__" }
35
+
36
+ [tool.setuptools.package-data]
37
+ plotto = ["_assets/*"]
@@ -0,0 +1,46 @@
1
+ Metadata-Version: 2.4
2
+ Name: python-plotto
3
+ Version: 0.1.0
4
+ Summary: Grammar-of-graphics plots for notebooks, web pages and slides with one renderer.
5
+ License-Expression: MIT
6
+ Project-URL: Homepage, https://theoradusz.github.io/plotto/
7
+ Project-URL: Source, https://github.com/theoradusz/plotto
8
+ Keywords: plot,chart,grammar of graphics,ggplot,notebook,visualization
9
+ Classifier: Development Status :: 4 - Beta
10
+ Classifier: Framework :: Jupyter
11
+ Classifier: Intended Audience :: Science/Research
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Topic :: Scientific/Engineering :: Visualization
14
+ Requires-Python: >=3.12
15
+ Description-Content-Type: text/markdown
16
+ License-File: LICENSE
17
+ Requires-Dist: pandas>=2
18
+ Requires-Dist: numpy
19
+ Provides-Extra: test
20
+ Requires-Dist: pytest; extra == "test"
21
+ Requires-Dist: ruff; extra == "test"
22
+ Requires-Dist: black; extra == "test"
23
+ Requires-Dist: nbconvert; extra == "test"
24
+ Requires-Dist: ipykernel; extra == "test"
25
+ Dynamic: license-file
26
+
27
+ # python-plotto
28
+
29
+ Grammar-of-graphics plots for notebooks, web pages and slides with one renderer.
30
+
31
+ ```
32
+ pip install python-plotto
33
+ ```
34
+
35
+ ```python
36
+ from plotto import plot
37
+
38
+ p = plot(df, x="time", y="value", color="sensor").line().facet("indicator", free="y")
39
+ p # in a notebook: drawn at the cell width, with copy and download buttons
40
+ p.interactive() # zoom, pan, hover, legend toggle
41
+ p.save("fig.png") # needs Node.js 20 or later
42
+ ```
43
+
44
+ - Gallery, grammar tutorial and reference: https://theoradusz.github.io/plotto/
45
+ - Source and full documentation: https://github.com/theoradusz/plotto
46
+ - The JavaScript package for web pages and React: `npm install plotto`
@@ -0,0 +1,18 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ plotto/__init__.py
5
+ plotto/assets.py
6
+ plotto/data.py
7
+ plotto/output.py
8
+ plotto/plot.py
9
+ plotto/_assets/grammar.json
10
+ plotto/_assets/plotto.js
11
+ plotto/_assets/theme.json
12
+ python_plotto.egg-info/PKG-INFO
13
+ python_plotto.egg-info/SOURCES.txt
14
+ python_plotto.egg-info/dependency_links.txt
15
+ python_plotto.egg-info/requires.txt
16
+ python_plotto.egg-info/top_level.txt
17
+ tests/test_builder.py
18
+ tests/test_output.py
@@ -0,0 +1,9 @@
1
+ pandas>=2
2
+ numpy
3
+
4
+ [test]
5
+ pytest
6
+ ruff
7
+ black
8
+ nbconvert
9
+ ipykernel
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,93 @@
1
+ import json
2
+ import subprocess
3
+ from pathlib import Path
4
+
5
+ import pandas as pd
6
+ import pytest
7
+
8
+ from plotto import plot
9
+
10
+ TESTS = Path(__file__).resolve().parents[2] / "tests"
11
+ FIXTURE = json.loads((TESTS / "fixtures" / "parity.json").read_text())
12
+
13
+
14
+ def python_spec(calls):
15
+ (_, _, mapping), *rest = calls
16
+ p = plot(pd.DataFrame(FIXTURE["rows"]), **mapping)
17
+ for method, args, options in rest:
18
+ p = getattr(p, method)(*args, **options)
19
+ return p.spec()
20
+
21
+
22
+ def test_python_and_js_builders_make_the_same_spec():
23
+ js = json.loads(subprocess.run(["node", str(TESTS / "parity.mjs")], capture_output=True, text=True, check=True).stdout)
24
+ for case, js_spec in zip(FIXTURE["cases"], js, strict=True):
25
+ assert python_spec(case["calls"]) == js_spec, case["name"]
26
+
27
+
28
+ def test_a_method_returns_a_new_plot_and_keeps_the_old_one():
29
+ base = plot(pd.DataFrame(FIXTURE["rows"]), x="t", y="v")
30
+ lines = base.line()
31
+ assert base.spec()["layers"] == []
32
+ assert len(lines.spec()["layers"]) == 1
33
+
34
+
35
+ def test_a_column_name_is_a_mapping_and_other_values_are_constants():
36
+ p = plot(pd.DataFrame(FIXTURE["rows"]), x="t", y="v").point(color="g").hline(y=0, color="pink")
37
+ point, hline = p.spec()["layers"]
38
+ assert point["mapping"] == {"color": "g"}
39
+ assert hline["set"] == {"y": 0, "color": "pink"}
40
+
41
+
42
+ def test_only_used_columns_go_into_the_spec():
43
+ spec = plot(pd.DataFrame(FIXTURE["rows"]), x="t", y="v").line().spec()
44
+ assert set(spec["data"][0]["columns"]) == {"t", "v"}
45
+
46
+
47
+ @pytest.mark.parametrize(
48
+ "build, message",
49
+ [
50
+ (lambda p: p.line(y="vv"), '"vv" is not a column'),
51
+ (lambda p: p.line(colour="g"), 'unknown option "colour"'),
52
+ (lambda p: p.facet("nope"), '"nope" is not a column'),
53
+ (lambda p: p.line(dash="wavy"), 'unknown dash "wavy"'),
54
+ (lambda p: p.x(type="polar"), 'unknown type "polar"'),
55
+ (lambda p: p.theme("wota", legend="left"), 'unknown legend "left"'),
56
+ (lambda p: p.theme("fancy"), 'unknown style "fancy"'),
57
+ (lambda p: p.theme(format="poster"), 'unknown format "poster"'),
58
+ (lambda p: p.hist(density="yes"), "density must be True or False"),
59
+ (lambda p: p.bar(position="beside"), 'unknown position "beside"'),
60
+ (lambda p: p.color(palette="rainbow"), 'unknown palette "rainbow"'),
61
+ (lambda p: p.y(limits=5), "limits must be a list"),
62
+ ],
63
+ )
64
+ def test_errors_name_the_problem_and_the_valid_names(build, message):
65
+ with pytest.raises(ValueError, match=message):
66
+ build(plot(pd.DataFrame(FIXTURE["rows"]), x="t", y="v"))
67
+
68
+
69
+ def test_limits_accept_none_and_times():
70
+ p = plot(pd.DataFrame(FIXTURE["rows"]), x="t", y="v").line().y(limits=[0, None]).x(limits=[pd.Timestamp("2026-10-20"), None])
71
+ scales = p.spec()["scales"]
72
+ assert scales["y"]["limits"] == [0, None]
73
+ assert scales["x"]["limits"][0] == int(pd.Timestamp("2026-10-20").value // 1_000_000)
74
+
75
+
76
+ def test_labels_replaces_labs():
77
+ p = plot(pd.DataFrame(FIXTURE["rows"]), x="t", y="v").line().labels(title="T", x=None)
78
+ assert p.spec()["labels"] == {"title": "T", "x": None}
79
+ assert not hasattr(p, "labs")
80
+
81
+
82
+ def test_times_are_wall_clock_ms_of_their_time_zone():
83
+ df = pd.DataFrame({"time": pd.date_range("2026-10-20 09:00", periods=2, freq="h", tz="Asia/Tokyo"), "v": [1, 2]})
84
+ column = plot(df, x="time", y="v").line().spec()["data"][0]["columns"]["time"]
85
+ assert column["type"] == "time"
86
+ assert column["values"][0] == int(pd.Timestamp("2026-10-20 09:00", tz="UTC").value // 1_000_000)
87
+
88
+
89
+ def test_categories_keep_their_order_and_missing_values_are_null():
90
+ df = pd.DataFrame({"day": pd.Categorical(["Tue", "Mon"], ["Mon", "Tue"]), "v": [1.5, float("nan")]})
91
+ columns = plot(df, x="day", y="v").point().spec()["data"][0]["columns"]
92
+ assert columns["day"]["levels"] == ["Mon", "Tue"]
93
+ assert columns["v"]["values"] == [1.5, None]
@@ -0,0 +1,46 @@
1
+ import pandas as pd
2
+
3
+ from plotto import plot
4
+
5
+ DF = pd.DataFrame({"t": [1, 2, 3, 4], "v": [1.0, 3.0, 2.0, 4.0], "g": ["a", "a", "b", "b"]})
6
+ P = plot(DF, x="t", y="v", color="g").line().labels(title="Test")
7
+
8
+
9
+ def test_save_writes_svg_and_png(tmp_path):
10
+ P.save(tmp_path / "fig.svg")
11
+ P.save(tmp_path / "fig.png", scale=1)
12
+ svg = (tmp_path / "fig.svg").read_text()
13
+ assert svg.startswith('<svg xmlns="http://www.w3.org/2000/svg"') and "Test" in svg
14
+ png = (tmp_path / "fig.png").read_bytes()
15
+ assert png[:8] == b"\x89PNG\r\n\x1a\n"
16
+ assert int.from_bytes(png[16:20], "big") == 900 # theme width at scale 1
17
+
18
+
19
+ def test_notebook_output_is_html_with_the_static_svg_inside():
20
+ for p, interactive in ((P, "false"), (P.interactive(), "true")):
21
+ bundle = p._repr_mimebundle_()
22
+ assert set(bundle) == {"text/html", "text/plain"}
23
+ html = bundle["text/html"]
24
+ assert '<svg xmlns="http://www.w3.org/2000/svg" width="100%"' in html
25
+ assert f"{{interactive: {interactive}}}" in html and "cdn.jsdelivr.net" in html
26
+
27
+
28
+ def test_save_width_lays_out_at_that_width(tmp_path):
29
+ P.save(tmp_path / "wide.png", scale=1, width=1600)
30
+ png = (tmp_path / "wide.png").read_bytes()
31
+ assert (int.from_bytes(png[16:20], "big"), int.from_bytes(png[20:24], "big")) == (1600, 800)
32
+
33
+
34
+ def test_repr_names_the_layers():
35
+ assert repr(P) == "<plotto.Plot line, 4 rows>"
36
+ assert repr(P.interactive().facet("g")) == "<plotto.Plot line, facet g, 4 rows, interactive>"
37
+
38
+
39
+ def test_python_and_npm_versions_agree():
40
+ import json
41
+ from pathlib import Path
42
+
43
+ import plotto
44
+
45
+ package = json.loads((Path(__file__).resolve().parents[2] / "package.json").read_text())
46
+ assert plotto.__version__ == package["version"]