chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,15 @@
|
||||
# Data Architecture
|
||||
|
||||
Application developers can choose from
|
||||
[Client (WebAssembly)](./architecture/client_only.md),
|
||||
[Server (Python/Node)](./architecture/server_only.md) or
|
||||
[Client/Server Replicated](./architecture/client_server.md) designs to bind
|
||||
data, and a web application can use one or a mix of these designs as needed. By
|
||||
serializing to Apache Arrow, tables are duplicated and synchronized across
|
||||
runtimes efficiently.
|
||||
|
||||
Perspective is a multi-language platform. The examples in this section use
|
||||
Python and JavaScript as an example, but the same general principles apply to
|
||||
any `Client`/`Server` combination.
|
||||
|
||||
<img src="./architecture/architecture.svg" />
|
||||
@@ -0,0 +1,197 @@
|
||||
digraph G {
|
||||
bgcolor=transparent
|
||||
graph [rankdir="LR" fontname="helvetica" labeljust="l"]
|
||||
node [shape="box" fontname="monospace" fontsize=8 color=gray70 style=filled fillcolor=white];
|
||||
edge [color="#EDEBDF" arrowsize=0.8]
|
||||
subgraph cluster_11 {
|
||||
label="\lPython Server";
|
||||
fontcolor=gray30
|
||||
margin=10
|
||||
color=none
|
||||
subgraph cluster_thread_2 {
|
||||
graph [
|
||||
label="\lPerspectiveManager - Thread 2";
|
||||
style=filled
|
||||
fillcolor="#91A4A8"
|
||||
color=none
|
||||
fontcolor="#EDEBDF"
|
||||
fontsize=10
|
||||
margin=10
|
||||
]
|
||||
table_thread_2 [
|
||||
label="table(df)"
|
||||
width=1
|
||||
color=none
|
||||
fillcolor="#E6E2DA"
|
||||
]
|
||||
view_thread_2 [
|
||||
label="view({\l group_by: [\"State\"]]\l})\l"
|
||||
width=2.5
|
||||
color=none
|
||||
fillcolor="#EDEBDF"
|
||||
]
|
||||
view_thread_2_2 [
|
||||
label="view({\l group_by: [\"City\"]]\l})\l"
|
||||
width=2.5
|
||||
color=none
|
||||
fillcolor="#EDEBDF"
|
||||
]
|
||||
table_thread_2 -> view_thread_2_2;
|
||||
table_thread_2 -> view_thread_2;
|
||||
}
|
||||
subgraph cluster_thread_1 {
|
||||
graph [
|
||||
label="\lPerspectiveManager - Thread 1";
|
||||
style=filled
|
||||
fillcolor="#91A4A8"
|
||||
color=none
|
||||
fontcolor="#EDEBDF"
|
||||
fontsize=10
|
||||
margin=10
|
||||
]
|
||||
table_thread_1 [
|
||||
label="table(arrow)"
|
||||
width=1
|
||||
color=none
|
||||
fillcolor="#E6E2DA"
|
||||
]
|
||||
view_thread_1 [
|
||||
label="view({\l group_by:'[\"State\"]\l split_by:'[\"Segment\"]'\l})\l"
|
||||
width=2.5
|
||||
color=none
|
||||
fillcolor="#EDEBDF"
|
||||
]
|
||||
table_thread_1 -> view_thread_1;
|
||||
}
|
||||
}
|
||||
subgraph cluster_browser {
|
||||
graph [
|
||||
label="\lBrowser";
|
||||
color="#CADEE1";
|
||||
margin=10
|
||||
style=filled;
|
||||
fontcolor=gray30
|
||||
]
|
||||
subgraph cluster_2 {
|
||||
graph [
|
||||
label="\lWebWorker 1";
|
||||
style=filled
|
||||
margin=10
|
||||
fillcolor="#2D4C68"
|
||||
color=none
|
||||
fontcolor="#EDEBDF"
|
||||
fontsize=10
|
||||
]
|
||||
table1 [
|
||||
label="table(csv)"
|
||||
width=1
|
||||
color=none
|
||||
fillcolor="#E6E2DA"
|
||||
]
|
||||
table_remote_view [
|
||||
label="table(json)"
|
||||
width=1
|
||||
color=none
|
||||
fillcolor="#E6E2DA"
|
||||
]
|
||||
view1 [
|
||||
label="view({\l group_by: [\"Category\"]\l filter: [[\"State\",\"==\",\"Texas\"]]\l})\l"
|
||||
width=2.5
|
||||
color=none
|
||||
fillcolor="#EDEBDF"
|
||||
]
|
||||
view2 [
|
||||
label="view({\l group_by: [\"Sub-Category\"]\l})\l"
|
||||
width=2.5
|
||||
color=none
|
||||
fillcolor="#EDEBDF"
|
||||
]
|
||||
view3 [
|
||||
label="view()\l"
|
||||
width=2.5
|
||||
color=none
|
||||
fillcolor="#EDEBDF"
|
||||
]
|
||||
table1 -> {view1 view2};
|
||||
table_remote_view -> view3;
|
||||
}
|
||||
|
||||
subgraph cluster_webworker2 {
|
||||
graph [
|
||||
label="\lWebWorker 2";
|
||||
style=filled
|
||||
margin=10
|
||||
fillcolor="#2D4C68"
|
||||
color=none
|
||||
fontcolor="#EDEBDF"
|
||||
fontsize=10
|
||||
]
|
||||
table12 [
|
||||
label="table(...)"
|
||||
width=1
|
||||
color=none
|
||||
fillcolor="#E6E2DA"
|
||||
]
|
||||
|
||||
view12 [
|
||||
label="view({\l group_by: [\"Category\"]\l filter: [[\"State\",\"==\",\"Texas\"]]\l})\l"
|
||||
width=2.5
|
||||
color=none
|
||||
fillcolor="#EDEBDF"
|
||||
]
|
||||
|
||||
table12 -> {view12} [color="#E6E2DA"];
|
||||
}
|
||||
|
||||
view_thread_2 -> table12 [penwidth=2 style=dashed arrowhead=none color="#D1A043"];
|
||||
|
||||
view1 -> viewer1 [penwidth=2 style=dashed arrowhead=none color="#666"];
|
||||
view2 -> viewer2 [penwidth=2 style=dashed arrowhead=none color="#666"];
|
||||
view3 -> viewer3 [penwidth=2 style=dashed arrowhead=none color="#666"];
|
||||
|
||||
subgraph cluster_41 {
|
||||
graph [
|
||||
label="\l<html>";
|
||||
color=none
|
||||
fillcolor=white
|
||||
fontcolor=gray30
|
||||
fontsize=10
|
||||
fontname="monospace" fontsize=8 color=none
|
||||
|
||||
]
|
||||
viewer1 [
|
||||
label = "<perspective-viewer\l view=\"Y Bar\"\l row-pivots='[\"Category\"]'\l filters='[[\"State\",\"==\",\"Texas\"]]'>\l"
|
||||
width=2.8
|
||||
];
|
||||
viewer2 [
|
||||
label = "<perspective-viewer\l view=\"xy_scatter\"\l row-pivots='[\"Sub-Category\"]'>\l"
|
||||
width=2.8
|
||||
color=lightgrey
|
||||
];
|
||||
viewer3 [
|
||||
label = "<perspective-viewer\l view=\"grid\">\l"
|
||||
width=2.8
|
||||
];
|
||||
viewerN [
|
||||
style=invis
|
||||
]
|
||||
|
||||
viewer5 [
|
||||
label = "<perspective-viewer\l view=\"heatmap\"\l row-pivots='[\"State\"]\l column-pivots='[\"Segment\"]'>\l"
|
||||
width=2.8
|
||||
];
|
||||
viewerZ [
|
||||
style=invis
|
||||
height=0.3
|
||||
]
|
||||
viewer4 [
|
||||
label = "<perspective-viewer\l view=\"heatmap\"\l row-pivots='[\"State\"]\l column-pivots='[\"Segment\"]'>\l"
|
||||
width=2.8
|
||||
];
|
||||
view_thread_1 -> viewer4 [penwidth=2 style=dashed arrowhead=none color="#D1A043" constraint=false];
|
||||
view12 -> viewer5 [penwidth=2 style=dashed arrowhead=none color="#666"];
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
digraph G {
|
||||
bgcolor=transparent
|
||||
graph [rankdir="LR" fontname="helvetica" labeljust="l"]
|
||||
node [shape="box" fontname="monospace" fontsize=8 color=gray70 style=filled fillcolor=white];
|
||||
edge [color="#EDEBDF" arrowsize=0.8]
|
||||
|
||||
subgraph cluster_browser {
|
||||
graph [
|
||||
label="\lBrowser";
|
||||
color="#CADEE1";
|
||||
margin=10
|
||||
style=filled;
|
||||
fontcolor=gray30
|
||||
]
|
||||
subgraph cluster_2 {
|
||||
graph [
|
||||
label="\lWebWorker 1";
|
||||
style=filled
|
||||
margin=10
|
||||
fillcolor="#2D4C68"
|
||||
color=none
|
||||
fontcolor="#EDEBDF"
|
||||
fontsize=10
|
||||
]
|
||||
table1 [
|
||||
label="table(csv)"
|
||||
width=1
|
||||
color=none
|
||||
fillcolor="#E6E2DA"
|
||||
]
|
||||
|
||||
|
||||
view2 [
|
||||
label="view({\l group_by: [\"Sub-Category\"]\l})\l"
|
||||
width=2.5
|
||||
color=none
|
||||
fillcolor="#EDEBDF"
|
||||
]
|
||||
|
||||
table1 -> {view2};
|
||||
}
|
||||
|
||||
view2 -> viewer2 [penwidth=2 style=dashed arrowhead=none color="#666"];
|
||||
|
||||
subgraph cluster_41 {
|
||||
graph [
|
||||
label="\l<html>";
|
||||
color=none
|
||||
fillcolor=white
|
||||
fontcolor=gray30
|
||||
fontsize=10
|
||||
fontname="monospace" fontsize=8 color=none
|
||||
|
||||
]
|
||||
|
||||
viewer2 [
|
||||
label = "<perspective-viewer\l view=\"xy_scatter\"\l row-pivots='[\"Sub-Category\"]'>\l"
|
||||
width=2.8
|
||||
color=lightgrey
|
||||
];
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
|
||||
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<!-- Generated by graphviz version 5.0.0 (20220707.1540)
|
||||
-->
|
||||
<!-- Title: G Pages: 1 -->
|
||||
<svg width="590pt" height="170pt"
|
||||
viewBox="0.00 0.00 590.00 170.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 166)">
|
||||
<title>G</title>
|
||||
<g id="clust1" class="cluster">
|
||||
<title>cluster_browser</title>
|
||||
<polygon fill="#cadee1" stroke="#cadee1" points="8,-8 8,-154 574,-154 574,-8 8,-8"/>
|
||||
<text text-anchor="middle" x="41" y="-122.8" font-family="Helvetica,sans-Serif" font-size="14.00" fill="#4d4d4d">Browser</text>
|
||||
</g>
|
||||
<g id="clust2" class="cluster">
|
||||
<title>cluster_2</title>
|
||||
<polygon fill="#2d4c68" stroke="transparent" points="18,-18 18,-105 326,-105 326,-18 18,-18"/>
|
||||
<text text-anchor="middle" x="55.5" y="-81" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#edebdf">WebWorker 1</text>
|
||||
</g>
|
||||
<g id="clust4" class="cluster">
|
||||
<title>cluster_41</title>
|
||||
<polygon fill="white" stroke="transparent" points="342,-18 342,-100 564,-100 564,-18 342,-18"/>
|
||||
<text text-anchor="middle" x="364" y="-80.6" font-family="monospace" font-size="8.00" fill="#4d4d4d"><html></text>
|
||||
</g>
|
||||
<!-- table1 -->
|
||||
<g id="node1" class="node">
|
||||
<title>table1</title>
|
||||
<polygon fill="#e6e2da" stroke="transparent" points="100,-64 28,-64 28,-28 100,-28 100,-64"/>
|
||||
<text text-anchor="middle" x="64" y="-44.1" font-family="monospace" font-size="8.00">table(csv)</text>
|
||||
</g>
|
||||
<!-- view2 -->
|
||||
<g id="node2" class="node">
|
||||
<title>view2</title>
|
||||
<polygon fill="#edebdf" stroke="transparent" points="316,-64 136,-64 136,-28 316,-28 316,-64"/>
|
||||
<text text-anchor="start" x="144" y="-53.1" font-family="monospace" font-size="8.00">view({</text>
|
||||
<text text-anchor="start" x="144" y="-44.1" font-family="monospace" font-size="8.00">    group_by: ["Sub-Category"]</text>
|
||||
<text text-anchor="start" x="144" y="-35.1" font-family="monospace" font-size="8.00">})</text>
|
||||
</g>
|
||||
<!-- table1->view2 -->
|
||||
<g id="edge1" class="edge">
|
||||
<title>table1->view2</title>
|
||||
<path fill="none" stroke="#edebdf" d="M100.11,-46C108.52,-46 117.91,-46 127.66,-46"/>
|
||||
<polygon fill="#edebdf" stroke="#edebdf" points="127.91,-48.8 135.91,-46 127.91,-43.2 127.91,-48.8"/>
|
||||
</g>
|
||||
<!-- viewer2 -->
|
||||
<g id="node3" class="node">
|
||||
<title>viewer2</title>
|
||||
<polygon fill="white" stroke="lightgrey" points="554,-64 352,-64 352,-28 554,-28 554,-64"/>
|
||||
<text text-anchor="start" x="360" y="-53.1" font-family="monospace" font-size="8.00"><perspective-viewer</text>
|
||||
<text text-anchor="start" x="360" y="-44.1" font-family="monospace" font-size="8.00">    view="xy_scatter"</text>
|
||||
<text text-anchor="start" x="360" y="-35.1" font-family="monospace" font-size="8.00">    row-pivots='["Sub-Category"]'></text>
|
||||
</g>
|
||||
<!-- view2->viewer2 -->
|
||||
<g id="edge2" class="edge">
|
||||
<title>view2->viewer2</title>
|
||||
<path fill="none" stroke="#000000" stroke-width="2" stroke-dasharray="5,2" d="M316.25,-46C327.9,-46 339.91,-46 351.73,-46"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.2 KiB |
@@ -0,0 +1,66 @@
|
||||
digraph G {
|
||||
bgcolor=transparent
|
||||
graph [rankdir="LR" fontname="helvetica" labeljust="l"]
|
||||
node [shape="box" fontname="monospace" fontsize=8 color=gray70 style=filled fillcolor=white];
|
||||
edge [color="#EDEBDF" arrowsize=0.8]
|
||||
subgraph cluster_11 {
|
||||
label="\lPython Server";
|
||||
fontcolor=gray30
|
||||
margin=10
|
||||
color=none
|
||||
|
||||
subgraph cluster_thread_1 {
|
||||
graph [
|
||||
label="\lPerspectiveManager - Thread 1";
|
||||
style=filled
|
||||
fillcolor="#91A4A8"
|
||||
color=none
|
||||
fontcolor="#EDEBDF"
|
||||
fontsize=10
|
||||
margin=10
|
||||
]
|
||||
table_thread_1 [
|
||||
label="table(arrow)"
|
||||
width=1
|
||||
color=none
|
||||
fillcolor="#E6E2DA"
|
||||
]
|
||||
view_thread_1 [
|
||||
label="view({\l group_by:'[\"State\"]\l split_by:'[\"Segment\"]'\l})\l"
|
||||
width=2.5
|
||||
color=none
|
||||
fillcolor="#EDEBDF"
|
||||
]
|
||||
table_thread_1 -> view_thread_1;
|
||||
}
|
||||
}
|
||||
subgraph cluster_browser {
|
||||
graph [
|
||||
label="\lBrowser";
|
||||
color="#CADEE1";
|
||||
margin=10
|
||||
style=filled;
|
||||
fontcolor=gray30
|
||||
]
|
||||
|
||||
subgraph cluster_41 {
|
||||
graph [
|
||||
label="\l<html>";
|
||||
color=none
|
||||
fillcolor=white
|
||||
fontcolor=gray30
|
||||
fontsize=10
|
||||
fontname="monospace" fontsize=8 color=none
|
||||
|
||||
]
|
||||
|
||||
viewer4 [
|
||||
label = "<perspective-viewer\l view=\"heatmap\"\l row-pivots='[\"State\"]\l column-pivots='[\"Segment\"]'>\l"
|
||||
width=2.8
|
||||
];
|
||||
view_thread_1 -> viewer4 [penwidth=2 style=dashed arrowhead=none color="#D1A043"];
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
|
||||
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<!-- Generated by graphviz version 5.0.0 (20220707.1540)
|
||||
-->
|
||||
<!-- Title: G Pages: 1 -->
|
||||
<svg width="602pt" height="178pt"
|
||||
viewBox="0.00 0.00 602.00 178.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 174)">
|
||||
<title>G</title>
|
||||
<g id="clust1" class="cluster">
|
||||
<title>cluster_11</title>
|
||||
<polygon fill="transparent" stroke="transparent" points="8,-8 8,-162 336,-162 336,-8 8,-8"/>
|
||||
<text text-anchor="middle" x="59" y="-130.8" font-family="Helvetica,sans-Serif" font-size="14.00" fill="#4d4d4d">Python Server</text>
|
||||
</g>
|
||||
<g id="clust2" class="cluster">
|
||||
<title>cluster_thread_1</title>
|
||||
<polygon fill="#91a4a8" stroke="transparent" points="18,-18 18,-113 326,-113 326,-18 18,-18"/>
|
||||
<text text-anchor="middle" x="94.5" y="-89" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#edebdf">PerspectiveManager - Thread 1</text>
|
||||
</g>
|
||||
<g id="clust3" class="cluster">
|
||||
<title>cluster_browser</title>
|
||||
<polygon fill="#cadee1" stroke="#cadee1" points="344,-8 344,-157 586,-157 586,-8 344,-8"/>
|
||||
<text text-anchor="middle" x="377" y="-125.8" font-family="Helvetica,sans-Serif" font-size="14.00" fill="#4d4d4d">Browser</text>
|
||||
</g>
|
||||
<g id="clust4" class="cluster">
|
||||
<title>cluster_41</title>
|
||||
<polygon fill="white" stroke="transparent" points="354,-18 354,-108 576,-108 576,-18 354,-18"/>
|
||||
<text text-anchor="middle" x="376" y="-88.6" font-family="monospace" font-size="8.00" fill="#4d4d4d"><html></text>
|
||||
</g>
|
||||
<!-- table_thread_1 -->
|
||||
<g id="node1" class="node">
|
||||
<title>table_thread_1</title>
|
||||
<polygon fill="#e6e2da" stroke="transparent" points="100,-68 28,-68 28,-32 100,-32 100,-68"/>
|
||||
<text text-anchor="middle" x="64" y="-48.1" font-family="monospace" font-size="8.00">table(arrow)</text>
|
||||
</g>
|
||||
<!-- view_thread_1 -->
|
||||
<g id="node2" class="node">
|
||||
<title>view_thread_1</title>
|
||||
<polygon fill="#edebdf" stroke="transparent" points="316,-72 136,-72 136,-28 316,-28 316,-72"/>
|
||||
<text text-anchor="start" x="144" y="-61.6" font-family="monospace" font-size="8.00">view({</text>
|
||||
<text text-anchor="start" x="144" y="-52.6" font-family="monospace" font-size="8.00">    group_by:'["State"]</text>
|
||||
<text text-anchor="start" x="144" y="-43.6" font-family="monospace" font-size="8.00">    split_by:'["Segment"]'</text>
|
||||
<text text-anchor="start" x="144" y="-34.6" font-family="monospace" font-size="8.00">})</text>
|
||||
</g>
|
||||
<!-- table_thread_1->view_thread_1 -->
|
||||
<g id="edge1" class="edge">
|
||||
<title>table_thread_1->view_thread_1</title>
|
||||
<path fill="none" stroke="#edebdf" d="M100.11,-50C108.52,-50 117.91,-50 127.66,-50"/>
|
||||
<polygon fill="#edebdf" stroke="#edebdf" points="127.91,-52.8 135.91,-50 127.91,-47.2 127.91,-52.8"/>
|
||||
</g>
|
||||
<!-- viewer4 -->
|
||||
<g id="node3" class="node">
|
||||
<title>viewer4</title>
|
||||
<polygon fill="white" stroke="#b3b3b3" points="566,-72 364,-72 364,-28 566,-28 566,-72"/>
|
||||
<text text-anchor="start" x="372" y="-61.6" font-family="monospace" font-size="8.00"><perspective-viewer</text>
|
||||
<text text-anchor="start" x="372" y="-52.6" font-family="monospace" font-size="8.00">    view="heatmap"</text>
|
||||
<text text-anchor="start" x="372" y="-43.6" font-family="monospace" font-size="8.00">    row-pivots='["State"]</text>
|
||||
<text text-anchor="start" x="372" y="-34.6" font-family="monospace" font-size="8.00">    column-pivots='["Segment"]'></text>
|
||||
</g>
|
||||
<!-- view_thread_1->viewer4 -->
|
||||
<g id="edge2" class="edge">
|
||||
<title>view_thread_1->viewer4</title>
|
||||
<path fill="none" stroke="#d1a043" stroke-width="2" stroke-dasharray="5,2" d="M316.26,-50C331.75,-50 347.94,-50 363.69,-50"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.8 KiB |
@@ -0,0 +1,97 @@
|
||||
digraph G {
|
||||
bgcolor=transparent
|
||||
graph [rankdir="LR" fontname="helvetica" labeljust="l"]
|
||||
node [shape="box" fontname="monospace" fontsize=8 color=gray70 style=filled fillcolor=white];
|
||||
edge [color="#EDEBDF" arrowsize=0.8]
|
||||
subgraph cluster_11 {
|
||||
label="\lPython Server";
|
||||
fontcolor=gray30
|
||||
margin=10
|
||||
color=none
|
||||
subgraph cluster_thread_2 {
|
||||
graph [
|
||||
label="\lPerspectiveManager - Thread 2";
|
||||
style=filled
|
||||
fillcolor="#91A4A8"
|
||||
color=none
|
||||
fontcolor="#EDEBDF"
|
||||
fontsize=10
|
||||
margin=10
|
||||
]
|
||||
table_thread_2 [
|
||||
label="table(df)"
|
||||
width=1
|
||||
color=none
|
||||
fillcolor="#E6E2DA"
|
||||
]
|
||||
view_thread_2 [
|
||||
label="view({\l group_by: [\"State\"]]\l})\l"
|
||||
width=2.5
|
||||
color=none
|
||||
fillcolor="#EDEBDF"
|
||||
]
|
||||
|
||||
|
||||
table_thread_2 -> view_thread_2;
|
||||
}
|
||||
|
||||
}
|
||||
subgraph cluster_browser {
|
||||
graph [
|
||||
label="\lBrowser";
|
||||
color="#CADEE1";
|
||||
margin=10
|
||||
style=filled;
|
||||
fontcolor=gray30
|
||||
]
|
||||
|
||||
subgraph cluster_webworker2 {
|
||||
graph [
|
||||
label="\lWebWorker 2";
|
||||
style=filled
|
||||
margin=10
|
||||
fillcolor="#2D4C68"
|
||||
color=none
|
||||
fontcolor="#EDEBDF"
|
||||
fontsize=10
|
||||
]
|
||||
table12 [
|
||||
label="table(...)"
|
||||
width=1
|
||||
color=none
|
||||
fillcolor="#E6E2DA"
|
||||
]
|
||||
|
||||
view12 [
|
||||
label="view({\l group_by: [\"Category\"]\l filter: [[\"State\",\"==\",\"Texas\"]]\l})\l"
|
||||
width=2.5
|
||||
color=none
|
||||
fillcolor="#EDEBDF"
|
||||
]
|
||||
|
||||
table12 -> {view12} [color="#E6E2DA"];
|
||||
}
|
||||
|
||||
view_thread_2 -> table12 [penwidth=2 style=dashed arrowhead=none color="#D1A043"];
|
||||
|
||||
subgraph cluster_41 {
|
||||
graph [
|
||||
label="\l<html>";
|
||||
color=none
|
||||
fillcolor=white
|
||||
fontcolor=gray30
|
||||
fontsize=10
|
||||
fontname="monospace" fontsize=8 color=none
|
||||
|
||||
]
|
||||
|
||||
viewer5 [
|
||||
label = "<perspective-viewer\l view=\"heatmap\"\l row-pivots='[\"State\"]\l column-pivots='[\"Segment\"]'>\l"
|
||||
width=2.8
|
||||
];
|
||||
view12 -> viewer5 [penwidth=2 style=dashed arrowhead=none color="#666"];
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
|
||||
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<!-- Generated by graphviz version 5.0.0 (20220707.1540)
|
||||
-->
|
||||
<!-- Title: G Pages: 1 -->
|
||||
<svg width="926pt" height="178pt"
|
||||
viewBox="0.00 0.00 926.00 178.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 174)">
|
||||
<title>G</title>
|
||||
<g id="clust1" class="cluster">
|
||||
<title>cluster_11</title>
|
||||
<polygon fill="transparent" stroke="transparent" points="8,-12 8,-158 336,-158 336,-12 8,-12"/>
|
||||
<text text-anchor="middle" x="59" y="-126.8" font-family="Helvetica,sans-Serif" font-size="14.00" fill="#4d4d4d">Python Server</text>
|
||||
</g>
|
||||
<g id="clust2" class="cluster">
|
||||
<title>cluster_thread_2</title>
|
||||
<polygon fill="#91a4a8" stroke="transparent" points="18,-22 18,-109 326,-109 326,-22 18,-22"/>
|
||||
<text text-anchor="middle" x="94.5" y="-85" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#edebdf">PerspectiveManager - Thread 2</text>
|
||||
</g>
|
||||
<g id="clust3" class="cluster">
|
||||
<title>cluster_browser</title>
|
||||
<polygon fill="#cadee1" stroke="#cadee1" points="344,-8 344,-162 910,-162 910,-8 344,-8"/>
|
||||
<text text-anchor="middle" x="377" y="-130.8" font-family="Helvetica,sans-Serif" font-size="14.00" fill="#4d4d4d">Browser</text>
|
||||
</g>
|
||||
<g id="clust4" class="cluster">
|
||||
<title>cluster_webworker2</title>
|
||||
<polygon fill="#2d4c68" stroke="transparent" points="354,-18 354,-113 662,-113 662,-18 354,-18"/>
|
||||
<text text-anchor="middle" x="391.5" y="-89" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#edebdf">WebWorker 2</text>
|
||||
</g>
|
||||
<g id="clust6" class="cluster">
|
||||
<title>cluster_41</title>
|
||||
<polygon fill="white" stroke="transparent" points="678,-18 678,-108 900,-108 900,-18 678,-18"/>
|
||||
<text text-anchor="middle" x="700" y="-88.6" font-family="monospace" font-size="8.00" fill="#4d4d4d"><html></text>
|
||||
</g>
|
||||
<!-- table_thread_2 -->
|
||||
<g id="node1" class="node">
|
||||
<title>table_thread_2</title>
|
||||
<polygon fill="#e6e2da" stroke="transparent" points="100,-68 28,-68 28,-32 100,-32 100,-68"/>
|
||||
<text text-anchor="middle" x="64" y="-48.1" font-family="monospace" font-size="8.00">table(df)</text>
|
||||
</g>
|
||||
<!-- view_thread_2 -->
|
||||
<g id="node2" class="node">
|
||||
<title>view_thread_2</title>
|
||||
<polygon fill="#edebdf" stroke="transparent" points="316,-68 136,-68 136,-32 316,-32 316,-68"/>
|
||||
<text text-anchor="start" x="144" y="-57.1" font-family="monospace" font-size="8.00">view({</text>
|
||||
<text text-anchor="start" x="144" y="-48.1" font-family="monospace" font-size="8.00">    group_by: ["State"]]</text>
|
||||
<text text-anchor="start" x="144" y="-39.1" font-family="monospace" font-size="8.00">})</text>
|
||||
</g>
|
||||
<!-- table_thread_2->view_thread_2 -->
|
||||
<g id="edge1" class="edge">
|
||||
<title>table_thread_2->view_thread_2</title>
|
||||
<path fill="none" stroke="#edebdf" d="M100.11,-50C108.52,-50 117.91,-50 127.66,-50"/>
|
||||
<polygon fill="#edebdf" stroke="#edebdf" points="127.91,-52.8 135.91,-50 127.91,-47.2 127.91,-52.8"/>
|
||||
</g>
|
||||
<!-- table12 -->
|
||||
<g id="node3" class="node">
|
||||
<title>table12</title>
|
||||
<polygon fill="#e6e2da" stroke="transparent" points="436,-68 364,-68 364,-32 436,-32 436,-68"/>
|
||||
<text text-anchor="middle" x="400" y="-48.1" font-family="monospace" font-size="8.00">table(...)</text>
|
||||
</g>
|
||||
<!-- view_thread_2->table12 -->
|
||||
<g id="edge3" class="edge">
|
||||
<title>view_thread_2->table12</title>
|
||||
<path fill="none" stroke="#d1a043" stroke-width="2" stroke-dasharray="5,2" d="M316.02,-50C332.91,-50 349.64,-50 363.61,-50"/>
|
||||
</g>
|
||||
<!-- view12 -->
|
||||
<g id="node4" class="node">
|
||||
<title>view12</title>
|
||||
<polygon fill="#edebdf" stroke="transparent" points="652,-72 472,-72 472,-28 652,-28 652,-72"/>
|
||||
<text text-anchor="start" x="480" y="-61.6" font-family="monospace" font-size="8.00">view({</text>
|
||||
<text text-anchor="start" x="480" y="-52.6" font-family="monospace" font-size="8.00">    group_by: ["Category"]</text>
|
||||
<text text-anchor="start" x="480" y="-43.6" font-family="monospace" font-size="8.00">    filter: [["State","==","Texas"]]</text>
|
||||
<text text-anchor="start" x="480" y="-34.6" font-family="monospace" font-size="8.00">})</text>
|
||||
</g>
|
||||
<!-- table12->view12 -->
|
||||
<g id="edge2" class="edge">
|
||||
<title>table12->view12</title>
|
||||
<path fill="none" stroke="#e6e2da" d="M436.11,-50C444.52,-50 453.91,-50 463.66,-50"/>
|
||||
<polygon fill="#e6e2da" stroke="#e6e2da" points="463.91,-52.8 471.91,-50 463.91,-47.2 463.91,-52.8"/>
|
||||
</g>
|
||||
<!-- viewer5 -->
|
||||
<g id="node5" class="node">
|
||||
<title>viewer5</title>
|
||||
<polygon fill="white" stroke="#b3b3b3" points="890,-72 688,-72 688,-28 890,-28 890,-72"/>
|
||||
<text text-anchor="start" x="696" y="-61.6" font-family="monospace" font-size="8.00"><perspective-viewer</text>
|
||||
<text text-anchor="start" x="696" y="-52.6" font-family="monospace" font-size="8.00">    view="heatmap"</text>
|
||||
<text text-anchor="start" x="696" y="-43.6" font-family="monospace" font-size="8.00">    row-pivots='["State"]</text>
|
||||
<text text-anchor="start" x="696" y="-34.6" font-family="monospace" font-size="8.00">    column-pivots='["Segment"]'></text>
|
||||
</g>
|
||||
<!-- view12->viewer5 -->
|
||||
<g id="edge4" class="edge">
|
||||
<title>view12->viewer5</title>
|
||||
<path fill="none" stroke="#000000" stroke-width="2" stroke-dasharray="5,2" d="M652.25,-50C663.9,-50 675.91,-50 687.73,-50"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 5.4 KiB |
@@ -0,0 +1,250 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
|
||||
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<!-- Generated by graphviz version 5.0.0 (20220707.1540)
|
||||
-->
|
||||
<!-- Title: G Pages: 1 -->
|
||||
<svg width="926pt" height="499pt"
|
||||
viewBox="0.00 0.00 926.00 499.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 495)">
|
||||
<title>G</title>
|
||||
<g id="clust1" class="cluster">
|
||||
<title>cluster_11</title>
|
||||
<polygon fill="transparent" stroke="transparent" points="8,-9 8,-314 336,-314 336,-9 8,-9"/>
|
||||
<text text-anchor="middle" x="59" y="-282.8" font-family="Helvetica,sans-Serif" font-size="14.00" fill="#4d4d4d">Python Server</text>
|
||||
</g>
|
||||
<g id="clust2" class="cluster">
|
||||
<title>cluster_thread_2</title>
|
||||
<polygon fill="#91a4a8" stroke="transparent" points="18,-124 18,-265 326,-265 326,-124 18,-124"/>
|
||||
<text text-anchor="middle" x="94.5" y="-241" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#edebdf">PerspectiveManager - Thread 2</text>
|
||||
</g>
|
||||
<g id="clust3" class="cluster">
|
||||
<title>cluster_thread_1</title>
|
||||
<polygon fill="#91a4a8" stroke="transparent" points="18,-19 18,-114 326,-114 326,-19 18,-19"/>
|
||||
<text text-anchor="middle" x="94.5" y="-90" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#edebdf">PerspectiveManager - Thread 1</text>
|
||||
</g>
|
||||
<g id="clust4" class="cluster">
|
||||
<title>cluster_browser</title>
|
||||
<polygon fill="#cadee1" stroke="#cadee1" points="344,-8 344,-483 910,-483 910,-8 344,-8"/>
|
||||
<text text-anchor="middle" x="377" y="-451.8" font-family="Helvetica,sans-Serif" font-size="14.00" fill="#4d4d4d">Browser</text>
|
||||
</g>
|
||||
<g id="clust5" class="cluster">
|
||||
<title>cluster_2</title>
|
||||
<polygon fill="#2d4c68" stroke="transparent" points="354,-231 354,-434 662,-434 662,-231 354,-231"/>
|
||||
<text text-anchor="middle" x="391.5" y="-410" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#edebdf">WebWorker 1</text>
|
||||
</g>
|
||||
<g id="clust7" class="cluster">
|
||||
<title>cluster_webworker2</title>
|
||||
<polygon fill="#2d4c68" stroke="transparent" points="354,-120 354,-215 662,-215 662,-120 354,-120"/>
|
||||
<text text-anchor="middle" x="391.5" y="-191" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#edebdf">WebWorker 2</text>
|
||||
</g>
|
||||
<g id="clust9" class="cluster">
|
||||
<title>cluster_41</title>
|
||||
<polygon fill="white" stroke="transparent" points="678,-18 678,-434 900,-434 900,-18 678,-18"/>
|
||||
<text text-anchor="middle" x="700" y="-414.6" font-family="monospace" font-size="8.00" fill="#4d4d4d"><html></text>
|
||||
</g>
|
||||
<!-- table_thread_2 -->
|
||||
<g id="node1" class="node">
|
||||
<title>table_thread_2</title>
|
||||
<polygon fill="#e6e2da" stroke="transparent" points="100,-197 28,-197 28,-161 100,-161 100,-197"/>
|
||||
<text text-anchor="middle" x="64" y="-177.1" font-family="monospace" font-size="8.00">table(df)</text>
|
||||
</g>
|
||||
<!-- view_thread_2 -->
|
||||
<g id="node2" class="node">
|
||||
<title>view_thread_2</title>
|
||||
<polygon fill="#edebdf" stroke="transparent" points="316,-170 136,-170 136,-134 316,-134 316,-170"/>
|
||||
<text text-anchor="start" x="144" y="-159.1" font-family="monospace" font-size="8.00">view({</text>
|
||||
<text text-anchor="start" x="144" y="-150.1" font-family="monospace" font-size="8.00">    group_by: ["State"]]</text>
|
||||
<text text-anchor="start" x="144" y="-141.1" font-family="monospace" font-size="8.00">})</text>
|
||||
</g>
|
||||
<!-- table_thread_2->view_thread_2 -->
|
||||
<g id="edge2" class="edge">
|
||||
<title>table_thread_2->view_thread_2</title>
|
||||
<path fill="none" stroke="#edebdf" d="M100.11,-173.07C108.61,-171.64 118.1,-170.04 127.95,-168.38"/>
|
||||
<polygon fill="#edebdf" stroke="#edebdf" points="128.49,-171.13 135.91,-167.03 127.56,-165.6 128.49,-171.13"/>
|
||||
</g>
|
||||
<!-- view_thread_2_2 -->
|
||||
<g id="node3" class="node">
|
||||
<title>view_thread_2_2</title>
|
||||
<polygon fill="#edebdf" stroke="transparent" points="316,-224 136,-224 136,-188 316,-188 316,-224"/>
|
||||
<text text-anchor="start" x="144" y="-213.1" font-family="monospace" font-size="8.00">view({</text>
|
||||
<text text-anchor="start" x="144" y="-204.1" font-family="monospace" font-size="8.00">    group_by: ["City"]]</text>
|
||||
<text text-anchor="start" x="144" y="-195.1" font-family="monospace" font-size="8.00">})</text>
|
||||
</g>
|
||||
<!-- table_thread_2->view_thread_2_2 -->
|
||||
<g id="edge1" class="edge">
|
||||
<title>table_thread_2->view_thread_2_2</title>
|
||||
<path fill="none" stroke="#edebdf" d="M100.11,-184.93C108.61,-186.36 118.1,-187.96 127.95,-189.62"/>
|
||||
<polygon fill="#edebdf" stroke="#edebdf" points="127.56,-192.4 135.91,-190.97 128.49,-186.87 127.56,-192.4"/>
|
||||
</g>
|
||||
<!-- table12 -->
|
||||
<g id="node11" class="node">
|
||||
<title>table12</title>
|
||||
<polygon fill="#e6e2da" stroke="transparent" points="436,-170 364,-170 364,-134 436,-134 436,-170"/>
|
||||
<text text-anchor="middle" x="400" y="-150.1" font-family="monospace" font-size="8.00">table(...)</text>
|
||||
</g>
|
||||
<!-- view_thread_2->table12 -->
|
||||
<g id="edge8" class="edge">
|
||||
<title>view_thread_2->table12</title>
|
||||
<path fill="none" stroke="#d1a043" stroke-width="2" stroke-dasharray="5,2" d="M316.02,-152C332.91,-152 349.64,-152 363.61,-152"/>
|
||||
</g>
|
||||
<!-- table_thread_1 -->
|
||||
<g id="node4" class="node">
|
||||
<title>table_thread_1</title>
|
||||
<polygon fill="#e6e2da" stroke="transparent" points="100,-69 28,-69 28,-33 100,-33 100,-69"/>
|
||||
<text text-anchor="middle" x="64" y="-49.1" font-family="monospace" font-size="8.00">table(arrow)</text>
|
||||
</g>
|
||||
<!-- view_thread_1 -->
|
||||
<g id="node5" class="node">
|
||||
<title>view_thread_1</title>
|
||||
<polygon fill="#edebdf" stroke="transparent" points="316,-73 136,-73 136,-29 316,-29 316,-73"/>
|
||||
<text text-anchor="start" x="144" y="-62.6" font-family="monospace" font-size="8.00">view({</text>
|
||||
<text text-anchor="start" x="144" y="-53.6" font-family="monospace" font-size="8.00">    group_by:'["State"]</text>
|
||||
<text text-anchor="start" x="144" y="-44.6" font-family="monospace" font-size="8.00">    split_by:'["Segment"]'</text>
|
||||
<text text-anchor="start" x="144" y="-35.6" font-family="monospace" font-size="8.00">})</text>
|
||||
</g>
|
||||
<!-- table_thread_1->view_thread_1 -->
|
||||
<g id="edge3" class="edge">
|
||||
<title>table_thread_1->view_thread_1</title>
|
||||
<path fill="none" stroke="#edebdf" d="M100.11,-51C108.52,-51 117.91,-51 127.66,-51"/>
|
||||
<polygon fill="#edebdf" stroke="#edebdf" points="127.91,-53.8 135.91,-51 127.91,-48.2 127.91,-53.8"/>
|
||||
</g>
|
||||
<!-- viewer4 -->
|
||||
<g id="node19" class="node">
|
||||
<title>viewer4</title>
|
||||
<polygon fill="white" stroke="#b3b3b3" points="890,-72 688,-72 688,-28 890,-28 890,-72"/>
|
||||
<text text-anchor="start" x="696" y="-61.6" font-family="monospace" font-size="8.00"><perspective-viewer</text>
|
||||
<text text-anchor="start" x="696" y="-52.6" font-family="monospace" font-size="8.00">    view="heatmap"</text>
|
||||
<text text-anchor="start" x="696" y="-43.6" font-family="monospace" font-size="8.00">    row-pivots='["State"]</text>
|
||||
<text text-anchor="start" x="696" y="-34.6" font-family="monospace" font-size="8.00">    column-pivots='["Segment"]'></text>
|
||||
</g>
|
||||
<!-- view_thread_1->viewer4 -->
|
||||
<g id="edge12" class="edge">
|
||||
<title>view_thread_1->viewer4</title>
|
||||
<path fill="none" stroke="#d1a043" stroke-width="2" stroke-dasharray="5,2" d="M316.2,-50.84C417.45,-50.66 582.21,-50.37 687.8,-50.18"/>
|
||||
</g>
|
||||
<!-- table1 -->
|
||||
<g id="node6" class="node">
|
||||
<title>table1</title>
|
||||
<polygon fill="#e6e2da" stroke="transparent" points="436,-364 364,-364 364,-328 436,-328 436,-364"/>
|
||||
<text text-anchor="middle" x="400" y="-344.1" font-family="monospace" font-size="8.00">table(csv)</text>
|
||||
</g>
|
||||
<!-- view1 -->
|
||||
<g id="node8" class="node">
|
||||
<title>view1</title>
|
||||
<polygon fill="#edebdf" stroke="transparent" points="652,-339 472,-339 472,-295 652,-295 652,-339"/>
|
||||
<text text-anchor="start" x="480" y="-328.6" font-family="monospace" font-size="8.00">view({</text>
|
||||
<text text-anchor="start" x="480" y="-319.6" font-family="monospace" font-size="8.00">    group_by: ["Category"]</text>
|
||||
<text text-anchor="start" x="480" y="-310.6" font-family="monospace" font-size="8.00">    filter: [["State","==","Texas"]]</text>
|
||||
<text text-anchor="start" x="480" y="-301.6" font-family="monospace" font-size="8.00">})</text>
|
||||
</g>
|
||||
<!-- table1->view1 -->
|
||||
<g id="edge4" class="edge">
|
||||
<title>table1->view1</title>
|
||||
<path fill="none" stroke="#edebdf" d="M436.11,-339.64C444.61,-338.1 454.1,-336.38 463.95,-334.59"/>
|
||||
<polygon fill="#edebdf" stroke="#edebdf" points="464.54,-337.33 471.91,-333.15 463.54,-331.82 464.54,-337.33"/>
|
||||
</g>
|
||||
<!-- view2 -->
|
||||
<g id="node9" class="node">
|
||||
<title>view2</title>
|
||||
<polygon fill="#edebdf" stroke="transparent" points="652,-393 472,-393 472,-357 652,-357 652,-393"/>
|
||||
<text text-anchor="start" x="480" y="-382.1" font-family="monospace" font-size="8.00">view({</text>
|
||||
<text text-anchor="start" x="480" y="-373.1" font-family="monospace" font-size="8.00">    group_by: ["Sub-Category"]</text>
|
||||
<text text-anchor="start" x="480" y="-364.1" font-family="monospace" font-size="8.00">})</text>
|
||||
</g>
|
||||
<!-- table1->view2 -->
|
||||
<g id="edge5" class="edge">
|
||||
<title>table1->view2</title>
|
||||
<path fill="none" stroke="#edebdf" d="M436.11,-352.36C444.61,-353.9 454.1,-355.62 463.95,-357.41"/>
|
||||
<polygon fill="#edebdf" stroke="#edebdf" points="463.54,-360.18 471.91,-358.85 464.54,-354.67 463.54,-360.18"/>
|
||||
</g>
|
||||
<!-- table_remote_view -->
|
||||
<g id="node7" class="node">
|
||||
<title>table_remote_view</title>
|
||||
<polygon fill="#e6e2da" stroke="transparent" points="436,-277 364,-277 364,-241 436,-241 436,-277"/>
|
||||
<text text-anchor="middle" x="400" y="-257.1" font-family="monospace" font-size="8.00">table(json)</text>
|
||||
</g>
|
||||
<!-- view3 -->
|
||||
<g id="node10" class="node">
|
||||
<title>view3</title>
|
||||
<polygon fill="#edebdf" stroke="transparent" points="652,-277 472,-277 472,-241 652,-241 652,-277"/>
|
||||
<text text-anchor="start" x="480" y="-257.1" font-family="monospace" font-size="8.00">view()</text>
|
||||
</g>
|
||||
<!-- table_remote_view->view3 -->
|
||||
<g id="edge6" class="edge">
|
||||
<title>table_remote_view->view3</title>
|
||||
<path fill="none" stroke="#edebdf" d="M436.11,-259C444.52,-259 453.91,-259 463.66,-259"/>
|
||||
<polygon fill="#edebdf" stroke="#edebdf" points="463.91,-261.8 471.91,-259 463.91,-256.2 463.91,-261.8"/>
|
||||
</g>
|
||||
<!-- viewer1 -->
|
||||
<g id="node13" class="node">
|
||||
<title>viewer1</title>
|
||||
<polygon fill="white" stroke="#b3b3b3" points="890,-344 688,-344 688,-300 890,-300 890,-344"/>
|
||||
<text text-anchor="start" x="696" y="-333.6" font-family="monospace" font-size="8.00"><perspective-viewer</text>
|
||||
<text text-anchor="start" x="696" y="-324.6" font-family="monospace" font-size="8.00">    view="Y Bar"</text>
|
||||
<text text-anchor="start" x="696" y="-315.6" font-family="monospace" font-size="8.00">    row-pivots='["Category"]'</text>
|
||||
<text text-anchor="start" x="696" y="-306.6" font-family="monospace" font-size="8.00">    filters='[["State","==","Texas"]]'></text>
|
||||
</g>
|
||||
<!-- view1->viewer1 -->
|
||||
<g id="edge9" class="edge">
|
||||
<title>view1->viewer1</title>
|
||||
<path fill="none" stroke="#000000" stroke-width="2" stroke-dasharray="5,2" d="M652.25,-318.98C663.9,-319.24 675.91,-319.51 687.73,-319.77"/>
|
||||
</g>
|
||||
<!-- viewer2 -->
|
||||
<g id="node14" class="node">
|
||||
<title>viewer2</title>
|
||||
<polygon fill="white" stroke="lightgrey" points="890,-398 688,-398 688,-362 890,-362 890,-398"/>
|
||||
<text text-anchor="start" x="696" y="-387.1" font-family="monospace" font-size="8.00"><perspective-viewer</text>
|
||||
<text text-anchor="start" x="696" y="-378.1" font-family="monospace" font-size="8.00">    view="xy_scatter"</text>
|
||||
<text text-anchor="start" x="696" y="-369.1" font-family="monospace" font-size="8.00">    row-pivots='["Sub-Category"]'></text>
|
||||
</g>
|
||||
<!-- view2->viewer2 -->
|
||||
<g id="edge10" class="edge">
|
||||
<title>view2->viewer2</title>
|
||||
<path fill="none" stroke="#000000" stroke-width="2" stroke-dasharray="5,2" d="M652.25,-376.98C663.9,-377.24 675.91,-377.51 687.73,-377.77"/>
|
||||
</g>
|
||||
<!-- viewer3 -->
|
||||
<g id="node15" class="node">
|
||||
<title>viewer3</title>
|
||||
<polygon fill="white" stroke="#b3b3b3" points="890,-282 688,-282 688,-246 890,-246 890,-282"/>
|
||||
<text text-anchor="start" x="696" y="-266.6" font-family="monospace" font-size="8.00"><perspective-viewer</text>
|
||||
<text text-anchor="start" x="696" y="-257.6" font-family="monospace" font-size="8.00">    view="grid"></text>
|
||||
</g>
|
||||
<!-- view3->viewer3 -->
|
||||
<g id="edge11" class="edge">
|
||||
<title>view3->viewer3</title>
|
||||
<path fill="none" stroke="#000000" stroke-width="2" stroke-dasharray="5,2" d="M652.25,-260.98C663.9,-261.24 675.91,-261.51 687.73,-261.77"/>
|
||||
</g>
|
||||
<!-- view12 -->
|
||||
<g id="node12" class="node">
|
||||
<title>view12</title>
|
||||
<polygon fill="#edebdf" stroke="transparent" points="652,-174 472,-174 472,-130 652,-130 652,-174"/>
|
||||
<text text-anchor="start" x="480" y="-163.6" font-family="monospace" font-size="8.00">view({</text>
|
||||
<text text-anchor="start" x="480" y="-154.6" font-family="monospace" font-size="8.00">    group_by: ["Category"]</text>
|
||||
<text text-anchor="start" x="480" y="-145.6" font-family="monospace" font-size="8.00">    filter: [["State","==","Texas"]]</text>
|
||||
<text text-anchor="start" x="480" y="-136.6" font-family="monospace" font-size="8.00">})</text>
|
||||
</g>
|
||||
<!-- table12->view12 -->
|
||||
<g id="edge7" class="edge">
|
||||
<title>table12->view12</title>
|
||||
<path fill="none" stroke="#e6e2da" d="M436.11,-152C444.52,-152 453.91,-152 463.66,-152"/>
|
||||
<polygon fill="#e6e2da" stroke="#e6e2da" points="463.91,-154.8 471.91,-152 463.91,-149.2 463.91,-154.8"/>
|
||||
</g>
|
||||
<!-- viewer5 -->
|
||||
<g id="node17" class="node">
|
||||
<title>viewer5</title>
|
||||
<polygon fill="white" stroke="#b3b3b3" points="890,-174 688,-174 688,-130 890,-130 890,-174"/>
|
||||
<text text-anchor="start" x="696" y="-163.6" font-family="monospace" font-size="8.00"><perspective-viewer</text>
|
||||
<text text-anchor="start" x="696" y="-154.6" font-family="monospace" font-size="8.00">    view="heatmap"</text>
|
||||
<text text-anchor="start" x="696" y="-145.6" font-family="monospace" font-size="8.00">    row-pivots='["State"]</text>
|
||||
<text text-anchor="start" x="696" y="-136.6" font-family="monospace" font-size="8.00">    column-pivots='["Segment"]'></text>
|
||||
</g>
|
||||
<!-- view12->viewer5 -->
|
||||
<g id="edge13" class="edge">
|
||||
<title>view12->viewer5</title>
|
||||
<path fill="none" stroke="#000000" stroke-width="2" stroke-dasharray="5,2" d="M652.25,-152C663.9,-152 675.91,-152 687.73,-152"/>
|
||||
</g>
|
||||
<!-- viewerN -->
|
||||
<!-- viewerZ -->
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 15 KiB |
@@ -0,0 +1,39 @@
|
||||
# Client-only
|
||||
|
||||
<img src="./architecture.sub1.svg" />
|
||||
|
||||
_For static datasets, datasets provided by the user, and simple server-less and
|
||||
read-only web applications._
|
||||
|
||||
In this design, Perspective is run as a client Browser WebAssembly library, the
|
||||
dataset is downloaded entirely to the client and all calculations and UI
|
||||
interactions are performed locally. Interactive performance is very good, using
|
||||
WebAssembly engine for near-native runtime plus WebWorker isolation for parallel
|
||||
rendering within the browser. Operations like scrolling and creating new views
|
||||
are responsive. However, the entire dataset must be downloaded to the client.
|
||||
Perspective is not a typical browser component, and datset sizes of 1gb+ in
|
||||
Apache Arrow format will load fine with good interactive performance!
|
||||
|
||||
Horizontal scaling is a non-issue, since here is no concurrent state to scale,
|
||||
and only uses client-side computation via WebAssembly client. Client-only
|
||||
perspective can support as many concurrent users as can download the web
|
||||
application itself. Once the data is loaded, no server connection is needed and
|
||||
all operations occur in the client browser, imparting no additional runtime cost
|
||||
on the server beyond initial load. This also means updates and edits are local
|
||||
to the browser client and will be lost when the page is refreshed, unless
|
||||
otherwise persisted by your application.
|
||||
|
||||
As the client-only design starts with creating a client-side Perspective
|
||||
`Table`, data can be provided by any standard web service in any Perspective
|
||||
compatible format (JSON, CSV or Apache Arrow).
|
||||
|
||||
## Javascript client
|
||||
|
||||
```javascript
|
||||
const worker = await perspective.worker();
|
||||
const table = await worker.table(csv);
|
||||
|
||||
const viewer = document.createElement("perspective-viewer");
|
||||
document.body.appendChild(viewer);
|
||||
await viewer.load(table);
|
||||
```
|
||||
@@ -0,0 +1,58 @@
|
||||
# Client/Server replicated
|
||||
|
||||
<img src="./architecture.sub2.svg" />
|
||||
|
||||
_For medium-sized, real-time, synchronized and/or editable data sets with many
|
||||
concurrent users._
|
||||
|
||||
The dataset is instantiated in-memory with a Python or Node.js Perspective
|
||||
server, and web applications create duplicates of these tables in a local
|
||||
WebAssembly client in the browser, synchonized efficiently to the server via
|
||||
Apache Arrow. This design scales well with additional concurrent users, as
|
||||
browsers only need to download the initial data set and subsequent update
|
||||
deltas, while operations like scrolling, pivots, sorting, etc. are performed on
|
||||
the client.
|
||||
|
||||
Python servers can make especially good use of additional threads, as
|
||||
Perspective will release the GIL for almost all operations. Interactive
|
||||
performance on the client is very good and identical to client-only
|
||||
architecture. Updates and edits are seamlessly synchonized across clients via
|
||||
their virtual server counterparts using websockets and Apache Arrow.
|
||||
|
||||
## Python and Tornado server
|
||||
|
||||
```python
|
||||
from perspective import Server, PerspectiveTornadoHandler
|
||||
|
||||
server = Server()
|
||||
client = server.new_local_client()
|
||||
client.table(csv, name="my_table")
|
||||
routes = [(
|
||||
r"/websocket",
|
||||
perspective.handlers.tornado.PerspectiveTornadoHandler,
|
||||
{"perspective_server": server},
|
||||
)]
|
||||
|
||||
app = tornado.web.Application(routes)
|
||||
app.listen(8080)
|
||||
loop = tornado.ioloop.IOLoop.current()
|
||||
loop.start()
|
||||
```
|
||||
|
||||
## Javascript client
|
||||
|
||||
Perspective's websocket client interfaces with the Python server, then
|
||||
_replicates_ the server-side Table.
|
||||
|
||||
```javascript
|
||||
const websocket = await perspective.websocket("ws://localhost:8080");
|
||||
const server_table = await websocket.open_table("my_table");
|
||||
const server_view = await server_table.view();
|
||||
|
||||
const worker = await perspective.worker();
|
||||
const client_table = await worker.table(server_view);
|
||||
|
||||
const viewer = document.createElement("perspective-viewer");
|
||||
document.body.appendChild(viewer);
|
||||
await viewer.load(client_table);
|
||||
```
|
||||
@@ -0,0 +1,33 @@
|
||||
# Server-only
|
||||
|
||||
<img src="./architecture.sub3.svg" />
|
||||
|
||||
_For extremely large datasets with a small number of concurrent users._
|
||||
|
||||
The dataset is instantiated in-memory with a Python or Node.js server, and web
|
||||
applications connect virtually. Has very good initial load performance, since no
|
||||
data is downloaded. Group-by and other operations will run column-parallel if
|
||||
configured.
|
||||
|
||||
But interactive performance is poor, as every user interaction must page the
|
||||
server to render. Operations like scrolling are not as responsive and can be
|
||||
impacted by network latency. Web applications must be "always connected" to the
|
||||
server via WebSocket. Disconnecting will prevent any interaction, scrolling,
|
||||
etc. of the UI. Does not use WebAssembly.
|
||||
|
||||
Each connected browser will impact server performance as long as the connection
|
||||
is open, which in turn impacts interactive performance of every client. This
|
||||
ultimately limits the horizontal scalabity of this architecture. Since each
|
||||
client reads the perspective `Table` virtually, changes like edits and updates
|
||||
are automatically reflected to all clients and persist across browser refresh.
|
||||
Using the same Python server as the previous design, we can simply skip the
|
||||
intermediate WebAssembly `Table` and pass the virtual table directly to `load()`
|
||||
|
||||
```javascript
|
||||
const websocket = await perspective.websocket("ws://localhost:8080");
|
||||
const server_table = await websocket.open_table("my_table");
|
||||
|
||||
const viewer = document.createElement("perspective-viewer");
|
||||
document.body.appendChild(viewer);
|
||||
await viewer.load(server_table);
|
||||
```
|
||||
@@ -0,0 +1,12 @@
|
||||
# Join
|
||||
|
||||
`Client::join` creates a read-only `Table` by joining two source tables on a
|
||||
shared key column. The `left` and `right` arguments can be `Table` objects or
|
||||
string table names (as returned by `get_hosted_table_names()`). The resulting
|
||||
table is _reactive_: whenever either source table is updated, the join is
|
||||
automatically recomputed and any `View` derived from the joined table will
|
||||
update accordingly.
|
||||
|
||||
Joined tables support the full `View` API — you can apply `group_by`,
|
||||
`split_by`, `sort`, `filter`, `expressions`, and all other `View` operations on
|
||||
the result, just as you would with any other `Table`.
|
||||
@@ -0,0 +1,25 @@
|
||||
# Join Types
|
||||
|
||||
`Client::join` supports three join types, specified via the `join_type` option.
|
||||
The default is `"inner"`.
|
||||
|
||||
## Inner Join (default)
|
||||
|
||||
An inner join includes only rows where the key column exists in _both_ source
|
||||
tables. Rows from either table that have no match in the other are excluded.
|
||||
|
||||
## Left Join
|
||||
|
||||
A left join includes all rows from the left table. For left rows that have no
|
||||
match in the right table, right-side columns are filled with `null`.
|
||||
|
||||
## Outer Join
|
||||
|
||||
An outer join includes all rows from both tables. Unmatched rows on either side
|
||||
have their missing columns filled with `null`.
|
||||
|
||||
| `join_type` | Left-only rows | Right-only rows |
|
||||
| ----------- | -------------- | --------------- |
|
||||
| `"inner"` | excluded | excluded |
|
||||
| `"left"` | included | excluded |
|
||||
| `"outer"` | included | included |
|
||||
@@ -0,0 +1,35 @@
|
||||
# Join Options
|
||||
|
||||
## `on` — Join Key Column
|
||||
|
||||
The `on` parameter specifies the column name used to match rows between the left
|
||||
and right tables. This column must exist in the left table and, by default, must
|
||||
also exist in the right table with the same name and compatible type.
|
||||
|
||||
The join key column becomes the index of the resulting table.
|
||||
|
||||
## `right_on` — Different Right Key Column
|
||||
|
||||
When the join key has a different name in the right table, use `right_on` to
|
||||
specify the right table's column name. The left table's column name (`on`) is
|
||||
used in the output schema; the right key column is excluded from the result.
|
||||
|
||||
The `on` and `right_on` columns must have compatible types. An error is thrown
|
||||
if the types do not match.
|
||||
|
||||
## `join_type` — Join Type
|
||||
|
||||
Controls which rows are included in the result. See
|
||||
[Join Types](./join_types.md) for details.
|
||||
|
||||
| Value | Behavior |
|
||||
| ----------- | ----------------------------------------------------- |
|
||||
| `"inner"` | Only rows with matching keys in both tables (default) |
|
||||
| `"left"` | All left rows; unmatched right columns are `null` |
|
||||
| `"outer"` | All rows from both tables; unmatched columns are `null` |
|
||||
|
||||
## `name` — Table Name
|
||||
|
||||
An optional name for the resulting joined table. If omitted, a random name is
|
||||
generated. This name is used to identify the table in the server's hosted table
|
||||
registry.
|
||||
@@ -0,0 +1,46 @@
|
||||
# Reactivity and Constraints
|
||||
|
||||
## Reactive Updates
|
||||
|
||||
Joined tables are fully reactive. When either source table receives an
|
||||
`update()`, the join is automatically recomputed and any `View` created from the
|
||||
joined table will reflect the new data. This includes:
|
||||
|
||||
- Updates that modify existing rows in either source table.
|
||||
- New rows added to either source table that create new matches.
|
||||
- Chained joins — if a joined table is itself used as input to another join,
|
||||
updates propagate through the entire chain.
|
||||
|
||||
## Duplicate Keys
|
||||
|
||||
Like SQL, `join()` produces a cross-product for each matching key value. When
|
||||
multiple rows in the left table share the same key, each is paired with every
|
||||
matching row in the right table (and vice versa). The number of output rows for
|
||||
a given key is `left_count × right_count`.
|
||||
|
||||
This behavior depends on whether the source tables are _indexed_:
|
||||
|
||||
- **Unindexed tables** (no `index` option) — rows are appended, so duplicate
|
||||
keys accumulate naturally. Each `update()` appends new rows, which may
|
||||
introduce additional duplicates.
|
||||
- **Indexed tables** (`index` set to the join key) — each key appears at most
|
||||
once per table, so the join produces at most one row per key. Updates replace
|
||||
existing rows in-place rather than appending.
|
||||
|
||||
## Read-Only
|
||||
|
||||
Joined tables are read-only. Calling `update()`, `remove()`, `clear()`, or
|
||||
`replace()` on a joined table will throw an error. Data can only change
|
||||
indirectly, by updating the source tables.
|
||||
|
||||
## Column Name Conflicts
|
||||
|
||||
The left and right tables must not have overlapping column names (other than the
|
||||
join key). If a non-key column name appears in both tables, `join()` throws an
|
||||
error. Rename columns in your source data or use `View` expressions to avoid
|
||||
conflicts.
|
||||
|
||||
## Source Table Deletion
|
||||
|
||||
A source table cannot be deleted while a joined table depends on it. You must
|
||||
delete the joined table first, then delete the source tables.
|
||||
@@ -0,0 +1,77 @@
|
||||
# What is `perspective-python`
|
||||
|
||||
Perspective for Python uses the exact same C++ data engine used by the
|
||||
[WebAssembly version](https://docs.rs/perspective-js/latest/perspective_js/) and
|
||||
[Rust version](https://docs.rs/crate/perspective/latest). The library consists
|
||||
of many of the same abstractions and API as in JavaScript, as well as
|
||||
Python-specific data loading support for [NumPy](https://numpy.org/),
|
||||
[Pandas](https://pandas.pydata.org/) (and
|
||||
[Apache Arrow](https://arrow.apache.org/), as in JavaScript).
|
||||
|
||||
Additionally, `perspective-python` provides a session manager suitable for
|
||||
integration into server systems such as
|
||||
[Tornado websockets](https://www.tornadoweb.org/en/stable/websocket.html),
|
||||
[AIOHTTP](https://docs.aiohttp.org/en/stable/web_quickstart.html#websockets), or
|
||||
[Starlette](https://www.starlette.io/websockets/)/[FastAPI](https://fastapi.tiangolo.com/advanced/websockets/),
|
||||
which allows fully _virtual_ Perspective tables to be interacted with by
|
||||
multiple `<perspective-viewer>` in a web browser. You can also interact with a
|
||||
Perspective table from python clients, and to that end client libraries are
|
||||
implemented for both Tornado and AIOHTTP.
|
||||
|
||||
## Example
|
||||
|
||||
A simple example which loads an [Apache Arrow](https://arrow.apache.org/) and
|
||||
computes a "Group By" operation, returning a new Arrow.
|
||||
|
||||
```python
|
||||
from perspective import Server
|
||||
|
||||
client = Server().new_local_client()
|
||||
table = client.table(arrow_bytes_data)
|
||||
view = table.view(group_by = ["CounterParty", "Security"])
|
||||
arrow = view.to_arrow()
|
||||
```
|
||||
|
||||
[More Examples](https://github.com/perspective-dev/perspective/tree/master/examples)
|
||||
are available on GitHub.
|
||||
|
||||
## What's included
|
||||
|
||||
The `perspective` module exports several tools:
|
||||
|
||||
- `Server` the constructor for a new instance of the Perspective data engine.
|
||||
- The `perspective.widget` module exports `PerspectiveWidget`, the JupyterLab
|
||||
widget for interactive visualization in a notebook cell.
|
||||
- The `perspective.handlers` modules exports web frameworks handlers that
|
||||
interface with a `perspective-client` in JavaScript.
|
||||
- `perspective.handlers.tornado.PerspectiveTornadoHandler` for
|
||||
[Tornado](https://www.tornadoweb.org/)
|
||||
- `perspective.handlers.starlette.PerspectiveStarletteHandler` for
|
||||
[Starlette](https://www.starlette.io/) and
|
||||
[FastAPI](https://fastapi.tiangolo.com)
|
||||
- `perspective.handlers.aiohttp.PerspectiveAIOHTTPHandler` for
|
||||
[AIOHTTP](https://docs.aiohttp.org),
|
||||
|
||||
### Virtual UI server
|
||||
|
||||
As `<perspective-viewer>` or any other Perspective `Client` will only consume
|
||||
the data necessary to render the current screen (or whatever else was requested
|
||||
via the API), this runtime mode allows large datasets without the need to copy
|
||||
them entirely to the Browser, at the expense of network latency on UI
|
||||
interaction/API calls.
|
||||
|
||||
### Jupyterlab
|
||||
|
||||
`PerspectiveWidget` is a JupyterLab widget that implements the same API as
|
||||
`<perspective-viewer>`, allows running such a viewer in
|
||||
[JupyterLab](https://jupyterlab.readthedocs.io/en/stable/) in either server or
|
||||
client (via WebAssembly) mode. `PerspectiveWidget` is compatible with Jupyterlab
|
||||
3 and Jupyter Notebook 6 via a
|
||||
[prebuilt extension](https://jupyterlab.readthedocs.io/en/stable/extension/extension_dev.html#prebuilt-extensions).
|
||||
To use it, simply install `perspective-python` and the extensions should be
|
||||
available.
|
||||
|
||||
`perspective-python`'s JupyterLab extension also provides convenient builtin
|
||||
viewers for `csv`, `json`, or `arrow` files. Simply right-click on a file with
|
||||
this extension and choose the appropriate `Perpective` option from the context
|
||||
menu.
|
||||
@@ -0,0 +1,14 @@
|
||||
# Table
|
||||
|
||||
`Table` is Perspective's columnar data frame, analogous to a Pandas `DataFrame`
|
||||
or Apache Arrow, supporting append & in-place updates, removal by index, and
|
||||
update notifications.
|
||||
|
||||
A `Table` contains columns, each of which have a unique name, are strongly and
|
||||
consistently typed, and contains rows of data conforming to the column's type.
|
||||
Each column in a `Table` must have the same number of rows, though not every row
|
||||
must contain data; null-values are used to indicate missing values in the
|
||||
dataset. The schema of a `Table` is _immutable after creation_, which means the
|
||||
column names and data types cannot be changed after the `Table` has been
|
||||
created. Columns cannot be added or deleted after creation either, but a `View`
|
||||
can be used to select an arbitrary set of columns from the `Table`.
|
||||
@@ -0,0 +1,23 @@
|
||||
# `Table::clear` and `Table::replace`
|
||||
|
||||
Calling `Table::clear` will remove all data from the underlying `Table`. Calling
|
||||
`Table::replace` with new data will clear the `Table`, and update it with a new
|
||||
dataset that conforms to Perspective's data types and the existing schema on the
|
||||
`Table`.
|
||||
|
||||
<div class="javascript">
|
||||
|
||||
```javascript
|
||||
table.clear();
|
||||
table.replace(json);
|
||||
```
|
||||
|
||||
</div>
|
||||
<div class="python">
|
||||
|
||||
```python
|
||||
table.clear()
|
||||
table.replace(df)
|
||||
```
|
||||
|
||||
</div>
|
||||
@@ -0,0 +1,47 @@
|
||||
# Construct a Table
|
||||
|
||||
Examples of constructing an empty `Table` from a schema.
|
||||
|
||||
<div class="javascript">
|
||||
|
||||
JavaScript:
|
||||
|
||||
```javascript
|
||||
var schema = {
|
||||
x: "integer",
|
||||
y: "string",
|
||||
z: "boolean",
|
||||
};
|
||||
|
||||
const table2 = await worker.table(schema);
|
||||
```
|
||||
|
||||
</div>
|
||||
<div class="python">
|
||||
|
||||
Python:
|
||||
|
||||
```python
|
||||
from datetime import date, datetime
|
||||
|
||||
schema = {
|
||||
"x": "integer",
|
||||
"y": "string",
|
||||
"z": "boolean",
|
||||
}
|
||||
|
||||
table2 = perspective.table(schema)
|
||||
```
|
||||
|
||||
</div>
|
||||
<div class="rust">
|
||||
|
||||
Rust:
|
||||
|
||||
```rust
|
||||
let data = TableData::Schema(vec![(" a".to_string(), ColumnType::FLOAT)]);
|
||||
let options = TableInitOptions::default();
|
||||
let table = client.table(data.into(), options).await?;
|
||||
```
|
||||
|
||||
</div>
|
||||
@@ -0,0 +1,87 @@
|
||||
# Loading data
|
||||
|
||||
A `Table` may also be created-or-updated by data in CSV,
|
||||
[Apache Arrow](https://arrow.apache.org/), JSON row-oriented or JSON
|
||||
column-oriented formats. In addition to these, `perspective-python` additionally
|
||||
supports `pyarrow.Table`, `polars.DataFrame` and `pandas.DataFrame` objects
|
||||
directly. These formats are otherwise identical to the built-in formats and
|
||||
don't exhibit any additional support or type-awareness; e.g., `pandas.DataFrame`
|
||||
support is _just_ `pyarrow.Table.from_pandas` piped into Perspective's Arrow
|
||||
reader.
|
||||
|
||||
`Client::table` and `Table::update` perform _coercion_ on their input for all
|
||||
input formats _except_ Arrow (which comes with its own schema and has no need
|
||||
for coercion). `"date"` and `"datetime"` column types do not have native JSON
|
||||
representations, so these column types _cannot_ be inferred from JSON input.
|
||||
Instead, for columns of these types for JSON input, a `Table` must first be
|
||||
constructed with a _schema_. Next, call `Table::update` with the JSON input -
|
||||
Perspective's JSON reader may _coerce_ a `date` or `datetime` from these native
|
||||
JSON types:
|
||||
|
||||
- `integer` as milliseconds-since-epoch.
|
||||
- `string` as a any of Perspective's built-in date format formats.
|
||||
- JavaScript `Date` and Python `datetime.date` and `datetime.datetime` are _not_
|
||||
supported directly. However, in JavaScript `Date` types are automatically
|
||||
coerced to correct `integer` timestamps by default when converted to JSON.
|
||||
|
||||
## Apache Arrow
|
||||
|
||||
The most efficient way to load data into Perspective, encoded as
|
||||
[Apache Arrow IPC format](https://arrow.apache.org/docs/python/ipc.html). In
|
||||
JavaScript:
|
||||
|
||||
```javascript
|
||||
const resp = await fetch(
|
||||
"https://cdn.jsdelivr.net/npm/superstore-arrow/superstore.lz4.arrow",
|
||||
);
|
||||
|
||||
const arrow = await resp.arrayBuffer();
|
||||
```
|
||||
|
||||
Apache Arrow input do not support type coercion, preferring Arrow's internal
|
||||
self-describing schema.
|
||||
|
||||
## CSV
|
||||
|
||||
Perspective relies on Apache Arrow's CSV parser, and as such uses mostly the
|
||||
same column-type inference logic as Arrow itself would use for parsing CSV.
|
||||
|
||||
## Row Oriented JSON
|
||||
|
||||
Row-oriented JSON is in the form of a list of objects. Each object in the list
|
||||
corresponds to a row in the table. For example:
|
||||
|
||||
```json
|
||||
[
|
||||
{ "a": 86, "b": false, "c": "words" },
|
||||
{ "a": 0, "b": true, "c": "" },
|
||||
{ "a": 12345, "b": false, "c": "here" }
|
||||
]
|
||||
```
|
||||
|
||||
## Column Oriented JSON
|
||||
|
||||
Column-Oriented JSON comes in the form of an object of lists. Each key of the
|
||||
object is a column name, and each element of the list is the corresponding value
|
||||
in the row.
|
||||
|
||||
```json
|
||||
{
|
||||
"a": [86, 0, 12345],
|
||||
"b": [false, true, false],
|
||||
"c": ["words", "", "here"]
|
||||
}
|
||||
```
|
||||
|
||||
## NDJSON
|
||||
|
||||
[NDJSON](https://github.com/ndjson/ndjson-spec) (sometimes also referred to as
|
||||
JSONL) is a streaming-friendly format where each line is a valid JSON object,
|
||||
separated by newlines. It is commonly used in data streaming and messaging
|
||||
queues.
|
||||
|
||||
```json
|
||||
{ "a": 86, "b": false, "c": "words" }
|
||||
{ "a": 0, "b": true, "c": "" }
|
||||
{ "a": 12345, "b": false, "c": "here" }
|
||||
```
|
||||
@@ -0,0 +1,59 @@
|
||||
## Index and Limit
|
||||
|
||||
<div class="warning">`limit` cannot be used in conjunction with `index`.</div>
|
||||
|
||||
Initializing a `Table` with an `index` tells Perspective to treat a column as
|
||||
the primary key, allowing in-place updates of rows. Only a single column (of any
|
||||
type) can be used as an `index`. Indexed `Table` instances allow:
|
||||
|
||||
- In-place _updates_ whenever a new row shares an `index` values with an
|
||||
existing row
|
||||
- _Partial updates_ when a data batch omits some column.
|
||||
- _Removes_ to delete a row by `index`.
|
||||
|
||||
To create an indexed `Table`, provide the `index` property with a string column
|
||||
name to be used as an index:
|
||||
|
||||
<div class="javascript">
|
||||
|
||||
JavaScript:
|
||||
|
||||
```javascript
|
||||
const indexed_table = await perspective.table(data, { index: "a" });
|
||||
```
|
||||
|
||||
</div>
|
||||
<div class="python">
|
||||
|
||||
Python
|
||||
|
||||
```python
|
||||
indexed_table = perspective.Table(data, index="a");
|
||||
```
|
||||
|
||||
</div>
|
||||
|
||||
Initializing a `Table` with a `limit` sets the total number of rows the `Table`
|
||||
is allowed to have. When the `Table` is updated, and the resulting size of the
|
||||
`Table` would exceed its `limit`, rows that exceed `limit` overwrite the oldest
|
||||
rows in the `Table`. To create a `Table` with a `limit`, provide the `limit`
|
||||
property with an integer indicating the maximum rows:
|
||||
|
||||
<div class="javascript">
|
||||
|
||||
JavaScript:
|
||||
|
||||
```javascript
|
||||
const limit_table = await perspective.table(data, { limit: 1000 });
|
||||
```
|
||||
|
||||
</div>
|
||||
<div class="python">
|
||||
|
||||
Python:
|
||||
|
||||
```python
|
||||
limit_table = perspective.Table(data, limit=1000);
|
||||
```
|
||||
|
||||
</div>
|
||||
@@ -0,0 +1,41 @@
|
||||
# Schema and column types
|
||||
|
||||
The mapping of a `Table`'s column names to data types is referred to as a
|
||||
`schema`. Each column has a unique name and a single data type, one of
|
||||
|
||||
- `float`
|
||||
- `integer`
|
||||
- `boolean`
|
||||
- `date`
|
||||
- `datetime`
|
||||
- `string`
|
||||
|
||||
A `Table` schema is fixed at construction, either by explicitly passing a schema
|
||||
dictionary to the `Client::table` method, or by passing _data_ to this method
|
||||
from which the schema is _inferred_ (if CSV or JSON format) or inherited (if
|
||||
Arrow).
|
||||
|
||||
## Type inference
|
||||
|
||||
When passing CSV or JSON data to the `Client::table` constructor, the type of
|
||||
each column is inferred automatically. In some cases, the inference algorithm
|
||||
may not return exactly what you'd like. For example, a column may be interpreted
|
||||
as a `datetime` when you intended it to be a `string`, or a column may have no
|
||||
values at all (yet), as it will be updated with values from a real-time data
|
||||
source later on. In these cases, create a `table()` with a _schema_.
|
||||
|
||||
Once the `Table` has been created, further `Table::update` calls will perform
|
||||
limited type _coercion_ based on the schema. While _coercion_ works similarly to
|
||||
_inference_, in that input data may be parsed based on the expected column type,
|
||||
`Table::update` will not _change_ the column's type further. For example, a
|
||||
number literal `1234` would be _inferred_ as an `"integer"`, but _in the context
|
||||
of an `Table::update` call on a known `"string"` column_, this will be parsed as
|
||||
the _string_ `"1234"`.
|
||||
|
||||
## `date` and `datetime` inference
|
||||
|
||||
Various string representations of `date` and `datetime` format columns can be
|
||||
_inferred_ as well _coerced_ from strings if they match one of Perspective's
|
||||
internal known datetime parsing formats, for example
|
||||
[ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) (which is also the format
|
||||
Perspective will _output_ these types for CSV).
|
||||
@@ -0,0 +1,88 @@
|
||||
# `Table::update` and `Table::remove`
|
||||
|
||||
Once a `Table` has been created, it can be updated with new data conforming to
|
||||
the `Table`'s schema. `Table::update` supports the same data formats as
|
||||
`Client::table`, minus _schema_.
|
||||
|
||||
<div class="javascript">
|
||||
|
||||
```javascript
|
||||
const schema = {
|
||||
a: "integer",
|
||||
b: "float",
|
||||
};
|
||||
|
||||
const table = await perspective.table(schema);
|
||||
table.update(new_data);
|
||||
```
|
||||
|
||||
</div>
|
||||
<div class="python">
|
||||
|
||||
```python
|
||||
schema = {"a": "integer", "b": "float"}
|
||||
|
||||
table = perspective.Table(schema)
|
||||
table.update(new_data)
|
||||
```
|
||||
|
||||
</div>
|
||||
|
||||
Without an `index` set, calls to `update()` _append_ new data to the end of the
|
||||
`Table`. Otherwise, Perspective allows
|
||||
[_partial updates_ (in-place)](#index-and-limit) using the `index` to determine
|
||||
which rows to update:
|
||||
|
||||
<div class="javascript">
|
||||
|
||||
```javascript
|
||||
indexed_table.update({ id: [1, 4], name: ["x", "y"] });
|
||||
```
|
||||
|
||||
</div>
|
||||
<div class="python">
|
||||
|
||||
```python
|
||||
indexed_table.update({"id": [1, 4], "name": ["x", "y"]})
|
||||
```
|
||||
|
||||
</div>
|
||||
|
||||
Any value on a `Client::table` can be unset using the value `null` in JSON or
|
||||
Arrow input formats. Values may be unset on construction, as any `null` in the
|
||||
dataset will be treated as an unset value. `Table::update` calls do not need to
|
||||
provide _all columns_ in the `Table`'s schema; missing columns will be omitted
|
||||
from the `Table`'s updated rows.
|
||||
|
||||
<div class="javascript">
|
||||
|
||||
```javascript
|
||||
table.update([{ x: 3, y: null }]); // `z` missing
|
||||
```
|
||||
|
||||
</div>
|
||||
<div class="python">
|
||||
|
||||
```python
|
||||
table.update([{"x": 3, "y": None}]) # `z` missing
|
||||
```
|
||||
|
||||
</div>
|
||||
|
||||
Rows can also be removed from an indexed `Table`, by calling `Table::remove`
|
||||
with an array of index values:
|
||||
|
||||
<div class="javascript">
|
||||
|
||||
```javascript
|
||||
indexed_table.remove([1, 4]);
|
||||
```
|
||||
|
||||
</div>
|
||||
<div class="python">
|
||||
|
||||
```python
|
||||
indexed_table.remove([1, 4])
|
||||
```
|
||||
|
||||
</div>
|
||||
@@ -0,0 +1,70 @@
|
||||
# View
|
||||
|
||||
The [`View`] struct is Perspective's query and serialization interface. It
|
||||
represents a query on the `Table`'s dataset and is always created from an
|
||||
existing `Table` instance via the [`Table::view`] method.
|
||||
|
||||
[`View`]s are immutable with respect to the arguments provided to the
|
||||
[`Table::view`] method; to change these parameters, you must create a new
|
||||
[`View`] on the same [`Table`]. However, each [`View`] is _live_ with respect to
|
||||
the [`Table`]'s data, and will (within a conflation window) update with the
|
||||
latest state as its parent [`Table`] updates, including incrementally
|
||||
recalculating all aggregates, pivots, filters, etc. [`View`] query parameters
|
||||
are composable, in that each parameter works independently _and_ in conjunction
|
||||
with each other, and there is no limit to the number of pivots, filters, etc.
|
||||
which can be applied.
|
||||
|
||||
<div class="javascript">
|
||||
<div class="warning">
|
||||
The examples in this module are in JavaScript. See <a href="https://docs.rs/crate/perspective/latest"><code>perspective</code></a> docs for the Rust API.
|
||||
</div>
|
||||
</div>
|
||||
<div class="python">
|
||||
<div class="warning">
|
||||
The examples in this module are in Python. See <a href="https://docs.rs/crate/perspective/latest"><code>perspective</code></a> docs for the Rust API.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
# Examples
|
||||
|
||||
<div class="javascript">
|
||||
|
||||
```javascript
|
||||
const table = await perspective.table({
|
||||
id: [1, 2, 3, 4],
|
||||
name: ["a", "b", "c", "d"],
|
||||
});
|
||||
|
||||
const view = await table.view({ columns: ["name"] });
|
||||
const json = await view.to_json();
|
||||
await view.delete();
|
||||
```
|
||||
|
||||
</div>
|
||||
<div class="python">
|
||||
|
||||
```python
|
||||
table = perspective.Table({
|
||||
"id": [1, 2, 3, 4],
|
||||
"name": ["a", "b", "c", "d"]
|
||||
});
|
||||
|
||||
view = table.view(columns=["name"])
|
||||
arrow = view.to_arrow()
|
||||
view.delete()
|
||||
```
|
||||
|
||||
</div>
|
||||
<div class="rust">
|
||||
|
||||
```rust
|
||||
let opts = TableInitOptions::default();
|
||||
let data = TableData::Update(UpdateData::Csv("x,y\n1,2\n3,4".into()));
|
||||
let table = client.table(data, opts).await?;
|
||||
|
||||
let view = table.view(None).await?;
|
||||
let arrow = view.to_arrow().await?;
|
||||
view.delete().await?;
|
||||
```
|
||||
|
||||
</div>
|
||||
@@ -0,0 +1,231 @@
|
||||
# Advanced View Operations
|
||||
|
||||
Beyond the standard query configuration, `View` provides additional methods for
|
||||
interacting with hierarchical results and introspecting data.
|
||||
|
||||
## Tree Hierarchy Operations
|
||||
|
||||
When a `View` has `group_by` applied, the results form a tree hierarchy.
|
||||
Perspective provides methods to control which levels of the tree are expanded or
|
||||
collapsed:
|
||||
|
||||
<div class="javascript">
|
||||
|
||||
```javascript
|
||||
const view = await table.view({ group_by: ["Region", "Country", "City"] });
|
||||
|
||||
// Collapse the tree at row index 5
|
||||
await view.collapse(5);
|
||||
|
||||
// Expand the tree at row index 5
|
||||
await view.expand(5);
|
||||
|
||||
// Set the expansion depth (0 = fully collapsed, 1 = first level, etc.)
|
||||
await view.set_depth(1);
|
||||
```
|
||||
|
||||
</div>
|
||||
<div class="python">
|
||||
|
||||
Using the sync API
|
||||
|
||||
```python
|
||||
view = table.view(group_by=["Region", "Country", "City"])
|
||||
|
||||
view.collapse(5)
|
||||
view.expand(5)
|
||||
view.set_depth(1)
|
||||
```
|
||||
|
||||
</div>
|
||||
<div class="rust">
|
||||
|
||||
```rust
|
||||
let view = table.view(Some(ViewConfigUpdate {
|
||||
group_by: Some(vec!["Region".into(), "Country".into(), "City".into()]),
|
||||
..ViewConfigUpdate::default()
|
||||
})).await?;
|
||||
|
||||
view.collapse(5).await?;
|
||||
view.expand(5).await?;
|
||||
view.set_depth(1).await?;
|
||||
```
|
||||
|
||||
</div>
|
||||
|
||||
<span class="warning">Perspective's built-in engine is lazy — aggregates for
|
||||
collapsed rows are not recalculated when the underlying `Table` is updated.
|
||||
Updates are only computed for rows that are currently visible (expanded). When a
|
||||
collapsed row is later expanded, its aggregates are calculated at that
|
||||
point.</span>
|
||||
|
||||
## Column Range Queries
|
||||
|
||||
`View::get_min_max` returns the minimum and maximum values for a given column,
|
||||
which is useful for setting up scales in custom visualizations:
|
||||
|
||||
<div class="javascript">
|
||||
|
||||
```javascript
|
||||
const [min, max] = await view.get_min_max("Sales");
|
||||
```
|
||||
|
||||
</div>
|
||||
<div class="python">
|
||||
|
||||
```python
|
||||
min_val, max_val = view.get_min_max("Sales")
|
||||
```
|
||||
|
||||
</div>
|
||||
|
||||
## Expression Validation
|
||||
|
||||
Before creating a `View` with expressions, you can validate them against the
|
||||
table's schema using `Table::validate_expressions`. This returns information
|
||||
about which expressions are valid and their inferred types:
|
||||
|
||||
<div class="javascript">
|
||||
|
||||
```javascript
|
||||
const result = await table.validate_expressions({
|
||||
expr1: '"Sales" + "Profit"',
|
||||
expr2: "invalid_column + 1",
|
||||
});
|
||||
// result.expression_schema contains valid expressions and their types
|
||||
// result.errors contains invalid expressions and error messages
|
||||
```
|
||||
|
||||
</div>
|
||||
<div class="python">
|
||||
|
||||
```python
|
||||
result = table.validate_expressions(['"Sales" + "Profit"', 'invalid + 1'])
|
||||
```
|
||||
|
||||
</div>
|
||||
|
||||
## View Dimensions
|
||||
|
||||
`View::dimensions` returns the number of rows and columns in the current view,
|
||||
including information about group-by header rows:
|
||||
|
||||
<div class="javascript">
|
||||
|
||||
```javascript
|
||||
const dims = await view.dimensions();
|
||||
// { num_view_rows, num_view_columns, num_table_rows, num_table_columns, ... }
|
||||
```
|
||||
|
||||
</div>
|
||||
<div class="python">
|
||||
|
||||
```python
|
||||
dims = view.dimensions()
|
||||
```
|
||||
|
||||
</div>
|
||||
|
||||
## View Configuration Introspection
|
||||
|
||||
`View::get_config` returns the full configuration used to create the view:
|
||||
|
||||
<div class="javascript">
|
||||
|
||||
```javascript
|
||||
const config = await view.get_config();
|
||||
// { group_by: [...], split_by: [...], sort: [...], filter: [...], ... }
|
||||
```
|
||||
|
||||
</div>
|
||||
<div class="python">
|
||||
|
||||
```python
|
||||
config = view.get_config()
|
||||
```
|
||||
|
||||
</div>
|
||||
|
||||
## Update Callbacks
|
||||
|
||||
Register a callback to be notified whenever the underlying `Table` is updated
|
||||
and the `View` has been recalculated:
|
||||
|
||||
<div class="javascript">
|
||||
|
||||
```javascript
|
||||
view.on_update(
|
||||
(updated) => {
|
||||
console.log("View updated", updated.port_id);
|
||||
},
|
||||
{ mode: "row" },
|
||||
);
|
||||
|
||||
// Later, remove the callback
|
||||
view.remove_update(callback);
|
||||
```
|
||||
|
||||
</div>
|
||||
<div class="python">
|
||||
|
||||
```python
|
||||
def on_update(port_id, delta):
|
||||
print("View updated", port_id)
|
||||
|
||||
view.on_update(on_update, mode="row")
|
||||
view.remove_update(on_update)
|
||||
```
|
||||
|
||||
</div>
|
||||
|
||||
When `mode` is set to `"row"`, the callback receives a delta of only the rows
|
||||
that changed (as Apache Arrow), which is useful for efficiently synchronizing
|
||||
tables across clients.
|
||||
|
||||
## Flattening a View into a Table
|
||||
|
||||
In Javascript, a [`Table`] can be constructed on a [`Table::view`] instance,
|
||||
which will return a new [`Table`] based on the [`Table::view`]'s dataset, and
|
||||
all future updates that affect the [`Table::view`] will be forwarded to the new
|
||||
[`Table`]. This is particularly useful for implementing a
|
||||
[Client/Server Replicated](server.md#clientserver-replicated) design, by
|
||||
serializing the `View` to an arrow and setting up an `on_update` callback.
|
||||
|
||||
<div class="javascript">
|
||||
|
||||
```javascript
|
||||
const worker1 = perspective.worker();
|
||||
const table = await worker.table(data);
|
||||
const view = await table.view({ filter: [["State", "==", "Texas"]] });
|
||||
const table2 = await worker.table(view);
|
||||
table.update([{ State: "Texas", City: "Austin" }]);
|
||||
```
|
||||
|
||||
</div>
|
||||
<div class="python">
|
||||
|
||||
```python
|
||||
table = perspective.Table(data);
|
||||
view = table.view(filter=[["State", "==", "Texas"]])
|
||||
table2 = perspective.Table(view.to_arrow());
|
||||
|
||||
def updater(port, delta):
|
||||
table2.update(delta)
|
||||
|
||||
view.on_update(updater, mode="Row")
|
||||
table.update([{"State": "Texas", "City": "Austin"}])
|
||||
```
|
||||
|
||||
</div>
|
||||
<div class="rust">
|
||||
|
||||
```rust
|
||||
let opts = TableInitOptions::default();
|
||||
let data = TableData::Update(UpdateData::Csv("x,y\n1,2\n3,4".into()));
|
||||
let table = client.table(data, opts).await?;
|
||||
let view = table.view(None).await?;
|
||||
let table2 = client.table(TableData::View(view)).await?;
|
||||
table.update(data).await?;
|
||||
```
|
||||
|
||||
</div>
|
||||
@@ -0,0 +1,149 @@
|
||||
# Expressions
|
||||
|
||||
The `expressions` property specifies _new_ columns in Perspective that are
|
||||
created using existing column values or arbitrary scalar values defined within
|
||||
the expression. In `<perspective-viewer>`, expressions are added using the "New
|
||||
Column" button in the side panel.
|
||||
|
||||
Expressions are strings parsed by Perspective's expression engine (based on
|
||||
[ExprTK](https://github.com/ArashPartow/exprtk)). Column names are referenced by
|
||||
wrapping them in double quotes, e.g. `"Sales"`:
|
||||
|
||||
<div class="javascript">
|
||||
|
||||
```javascript
|
||||
const view = await table.view({
|
||||
expressions: {
|
||||
"Profit Ratio": '"Profit" / "Sales"',
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
</div>
|
||||
<div class="python">
|
||||
|
||||
```python
|
||||
view = table.view(expressions={'Profit Ratio': '"Profit" / "Sales"'})
|
||||
```
|
||||
|
||||
</div>
|
||||
<div class="rust">
|
||||
|
||||
```rust
|
||||
let view = table.view(Some(ViewConfigUpdate {
|
||||
expressions: Some(Expressions([
|
||||
("Profit Ratio", "\"Profit\" / \"Sales\"".into())
|
||||
].into_iter().collect())),
|
||||
..ViewConfigUpdate::default()
|
||||
})).await?;
|
||||
```
|
||||
|
||||
</div>
|
||||
|
||||
## Type Conversion and Coercion
|
||||
|
||||
Perspective expressions are strongly typed — each column and literal has a fixed
|
||||
type, and most operators require matching types on both sides. To work across
|
||||
types, use the conversion functions:
|
||||
|
||||
| Function | Description |
|
||||
| --------------- | ------------------------------------------------------------ |
|
||||
| `to_string(x)` | Convert any type to string |
|
||||
| `to_integer(x)` | Convert to integer (null if not parsable) |
|
||||
| `to_float(x)` | Convert to float (null if not parsable) |
|
||||
| `to_boolean(x)` | Convert to boolean (truthy/falsy) |
|
||||
| `integer(x)` | Alias for `to_integer(x)` |
|
||||
| `float(x)` | Alias for `to_float(x)` |
|
||||
| `datetime(x)` | Construct a datetime from a POSIX timestamp (ms since epoch) |
|
||||
| `date(y, m, d)` | Construct a date from year, month, day |
|
||||
|
||||
### How coercion works
|
||||
|
||||
Perspective does not implicitly coerce types. For example, you cannot directly
|
||||
add an `integer` to a `float` — you must cast one side explicitly. Similarly,
|
||||
`datetime` and `date` values are not numeric: to perform arithmetic on them, you
|
||||
must first convert to a numeric representation, do the math, then convert back.
|
||||
|
||||
Internally, `datetime` values are stored as milliseconds since the Unix epoch
|
||||
(1970-01-01T00:00:00Z). Converting a `datetime` to a `float` yields this
|
||||
millisecond timestamp, and `datetime()` accepts a millisecond timestamp to
|
||||
produce a `datetime`.
|
||||
|
||||
### Example: offsetting a datetime by 7 days
|
||||
|
||||
This expression takes a `"Shipped Date"` column, converts it to its
|
||||
millisecond-epoch representation, adds 7 days worth of milliseconds (7 ×
|
||||
24 × 60 × 60 × 1000 = 604800000), and converts the result back
|
||||
to a `datetime`:
|
||||
|
||||
```
|
||||
// Due Date
|
||||
datetime(float("Shipped Date") + 604800000)
|
||||
```
|
||||
|
||||
## Operators
|
||||
|
||||
Standard arithmetic and comparison operators are supported:
|
||||
|
||||
| Operator | Description |
|
||||
| -------------------------------- | ----------- |
|
||||
| `+`, `-`, `*`, `/` | Arithmetic |
|
||||
| `%` | Modulo |
|
||||
| `==`, `!=`, `<`, `>`, `<=`, `>=` | Comparison |
|
||||
| `and`, `or`, `not` | Logical |
|
||||
| `if ... else ...` | Conditional |
|
||||
|
||||
## Numeric Functions
|
||||
|
||||
ExprTK provides a rich set of built-in numeric functions including `abs`,
|
||||
`ceil`, `floor`, `round`, `exp`, `log`, `log10`, `sqrt`, `min`, `max`, `pow`,
|
||||
`clamp`, `iclamp`, `inrange`, and trigonometric functions (`sin`, `cos`, `tan`,
|
||||
`asin`, `acos`, `atan`).
|
||||
|
||||
## String Functions
|
||||
|
||||
| Function | Description |
|
||||
| ------------------------------- | ------------------------------------------------------- |
|
||||
| `concat(a, b, ...)` | Concatenate strings |
|
||||
| `upper(s)` | Convert to uppercase |
|
||||
| `lower(s)` | Convert to lowercase |
|
||||
| `length(s)` | String length |
|
||||
| `contains(s, substr)` | Whether `s` contains `substr` |
|
||||
| `order(col, 'B', 'C', 'A')` | Custom sort order for a string column |
|
||||
| `match(s, pattern)` | Regex partial match (returns boolean) |
|
||||
| `match_all(s, pattern)` | Regex full match (returns boolean) |
|
||||
| `search(s, pattern)` | First capturing group match |
|
||||
| `indexof(s, pattern)` | Start index of first regex match |
|
||||
| `substring(s, start, end)` | Substring from `start` (inclusive) to `end` (exclusive) |
|
||||
| `replace(s, repl, pattern)` | Replace first regex match |
|
||||
| `replace_all(s, repl, pattern)` | Replace all regex matches |
|
||||
|
||||
## Date/Datetime Functions
|
||||
|
||||
| Function | Description |
|
||||
| ------------------------ | ------------------------------------------------------------------------ |
|
||||
| `today()` | Current date |
|
||||
| `now()` | Current datetime |
|
||||
| `date(year, month, day)` | Construct a date |
|
||||
| `datetime(timestamp_ms)` | Construct a datetime from a POSIX timestamp (ms since epoch) |
|
||||
| `hour_of_day(dt)` | Hour component (0-23) |
|
||||
| `day_of_week(dt)` | Day of the week as a string |
|
||||
| `month_of_year(dt)` | Month of the year as a string |
|
||||
| `bucket(dt, unit)` | Bucket datetime by unit: `'s'`, `'m'`, `'h'`, `'D'`, `'W'`, `'M'`, `'Y'` |
|
||||
|
||||
`bucket` also works on numeric columns: `bucket("Price", 10)` rounds values down
|
||||
to the nearest multiple of 10.
|
||||
|
||||
## Other Functions
|
||||
|
||||
| Function | Description |
|
||||
| ------------------------- | ----------------------------------------------------- |
|
||||
| `is_null(x)` | Whether the value is null |
|
||||
| `is_not_null(x)` | Whether the value is not null |
|
||||
| `percent_of(a, b)` | `a` as a percentage of `b` |
|
||||
| `inrange(low, val, high)` | Whether `val` is between `low` and `high` (inclusive) |
|
||||
| `min(a, b, ...)` | Minimum of inputs |
|
||||
| `max(a, b, ...)` | Maximum of inputs |
|
||||
| `random()` | Random float between 0.0 and 1.0 |
|
||||
| `col(name)` | Look up a column by string name at runtime |
|
||||
| `vlookup(col, key)` | Look up a value in another column by row key |
|
||||
@@ -0,0 +1,147 @@
|
||||
# Grouping and Pivots
|
||||
|
||||
## Group By
|
||||
|
||||
A group by _groups_ the dataset by the unique values of each column used as a
|
||||
group by - a close analogue in SQL to the `GROUP BY` statement. The underlying
|
||||
dataset is aggregated to show the values belonging to each group, and a total
|
||||
row is calculated for each group, showing the currently selected aggregated
|
||||
value (e.g. `sum`) of the column. Group by are useful for hierarchies,
|
||||
categorizing data and attributing values, i.e. showing the number of units sold
|
||||
based on State and City. In Perspective, group by are represented as an array of
|
||||
string column names to pivot, are applied in the order provided; For example, a
|
||||
group by of `["State", "City", "Postal Code"]` shows the values for each Postal
|
||||
Code, which are grouped by City, which are in turn grouped by State.
|
||||
|
||||
<div class="javascript">
|
||||
|
||||
```javascript
|
||||
const view = await table.view({ group_by: ["a", "c"] });
|
||||
```
|
||||
|
||||
</div>
|
||||
<div class="python">
|
||||
|
||||
```python
|
||||
view = table.view(group_by=["a", "c"])
|
||||
```
|
||||
|
||||
</div>
|
||||
<div class="rust">
|
||||
|
||||
```rust
|
||||
let view = table.view(Some(ViewConfigUpdate {
|
||||
group_by: Some(vec!["a".into(), "c".into()]),
|
||||
..ViewConfigUpdate::default()
|
||||
})).await?;
|
||||
```
|
||||
|
||||
</div>
|
||||
|
||||
## Split By
|
||||
|
||||
A split by _splits_ the dataset by the unique values of each column used as a
|
||||
split by. The underlying dataset is not aggregated, and a new column is created
|
||||
for each unique value of the split by. Each newly created column contains the
|
||||
parts of the dataset that correspond to the column header, i.e. a `View` that
|
||||
has `["State"]` as its split by will have a new column for each state. In
|
||||
Perspective, Split By are represented as an array of string column names to
|
||||
pivot:
|
||||
|
||||
<div class="javascript">
|
||||
|
||||
```javascript
|
||||
const view = await table.view({ split_by: ["a", "c"] });
|
||||
```
|
||||
|
||||
</div>
|
||||
<div class="python">
|
||||
|
||||
```python
|
||||
view = table.view(split_by=["a", "c"])
|
||||
```
|
||||
|
||||
</div>
|
||||
<div class="rust">
|
||||
|
||||
```rust
|
||||
let view = table.view(Some(ViewConfigUpdate {
|
||||
split_by: Some(vec!["a".into(), "c".into()]),
|
||||
..ViewConfigUpdate::default()
|
||||
})).await?;
|
||||
```
|
||||
|
||||
</div>
|
||||
|
||||
## Aggregates
|
||||
|
||||
Aggregates perform a calculation over an entire column, and are displayed when
|
||||
one or more [Group By](#group-by) are applied to the `View`. Aggregates can be
|
||||
specified by the user, or Perspective will use the following sensible default
|
||||
aggregates based on column type:
|
||||
|
||||
- "sum" for `integer` and `float` columns
|
||||
- "count" for all other columns
|
||||
|
||||
Perspective provides a selection of aggregate functions that can be applied to
|
||||
columns in the `View` constructor using a dictionary of column name to aggregate
|
||||
function name.
|
||||
|
||||
<div class="javascript">
|
||||
|
||||
```javascript
|
||||
const view = await table.view({
|
||||
aggregates: {
|
||||
a: "avg",
|
||||
b: "distinct count",
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
</div>
|
||||
<div class="python">
|
||||
|
||||
```python
|
||||
view = table.view(
|
||||
aggregates={
|
||||
"a": "avg",
|
||||
"b": "distinct count"
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
</div>
|
||||
<div class="rust">
|
||||
|
||||
```rust
|
||||
use std::collections::HashMap;
|
||||
let view = table.view(Some(ViewConfigUpdate {
|
||||
aggregates: Some(HashMap::from([
|
||||
("a".into(), "avg".into()),
|
||||
("b".into(), "distinct count".into()),
|
||||
])),
|
||||
..ViewConfigUpdate::default()
|
||||
})).await?;
|
||||
```
|
||||
|
||||
</div>
|
||||
|
||||
The available aggregate functions depend on the column type:
|
||||
|
||||
**Numeric columns** (`integer`, `float`): `sum`, `abs sum`, `sum abs`,
|
||||
`sum not null`, `any`, `avg`, `mean`, `count`, `distinct count`, `dominant`,
|
||||
`first`, `last`, `last by index`, `high`, `low`, `max`, `min`,
|
||||
`high minus low`, `last minus first`, `median`, `q1`, `q3`,
|
||||
`pct sum parent`, `pct sum total`, `stddev`, `var`, `unique`,
|
||||
`weighted mean`, `min by`, `max by`.
|
||||
|
||||
**String columns**: `count`, `any`, `distinct count`, `dominant`, `first`,
|
||||
`last`, `last by index`, `join`, `median`, `q1`, `q3`, `unique`, `min by`,
|
||||
`max by`.
|
||||
|
||||
**Date/Datetime columns**: `count`, `any`, `avg`, `distinct count`, `dominant`,
|
||||
`first`, `last`, `last by index`, `high`, `low`, `max`, `min`, `median`,
|
||||
`q1`, `q3`, `unique`.
|
||||
|
||||
**Boolean columns**: `count`, `any`, `distinct count`, `dominant`, `first`,
|
||||
`last`, `last by index`, `unique`.
|
||||
@@ -0,0 +1,138 @@
|
||||
# Selection and Ordering
|
||||
|
||||
## Columns
|
||||
|
||||
The `columns` property specifies which columns should be included in the
|
||||
`View`'s output. This allows users to show or hide a specific subset of columns,
|
||||
as well as control the order in which columns appear to the user. This is
|
||||
represented in Perspective as an array of string column names:
|
||||
|
||||
<div class="javascript">
|
||||
|
||||
```javascript
|
||||
const view = await table.view({
|
||||
columns: ["a"],
|
||||
});
|
||||
```
|
||||
|
||||
</div>
|
||||
<div class="python">
|
||||
|
||||
```python
|
||||
view = table.view(columns=["a"])
|
||||
```
|
||||
|
||||
</div>
|
||||
<div class="rust">
|
||||
|
||||
```rust
|
||||
let view = table.view(Some(ViewConfigUpdate {
|
||||
columns: Some(vec![Some("a".into())]),
|
||||
..ViewConfigUpdate::default()
|
||||
})).await?;
|
||||
```
|
||||
|
||||
</div>
|
||||
|
||||
## Sort
|
||||
|
||||
The `sort` property specifies columns on which the query should be sorted,
|
||||
analogous to `ORDER BY` in SQL. A column can be sorted regardless of its data
|
||||
type, and sorts can be applied in ascending or descending order. Perspective
|
||||
represents `sort` as an array of arrays, with the values of each inner array
|
||||
being a string column name and a string sort direction. When `split_by` are
|
||||
applied, the additional sort directions `"col asc"` and `"col desc"` will
|
||||
determine the order of pivot column groups.
|
||||
|
||||
<div class="javascript">
|
||||
|
||||
```javascript
|
||||
const view = await table.view({
|
||||
sort: [["a", "asc"]],
|
||||
});
|
||||
```
|
||||
|
||||
</div>
|
||||
<div class="python">
|
||||
|
||||
```python
|
||||
view = table.view(sort=[["a", "asc"]])
|
||||
```
|
||||
|
||||
</div>
|
||||
<div class="rust">
|
||||
|
||||
```rust
|
||||
let view = table.view(Some(ViewConfigUpdate {
|
||||
sort: Some(vec![Sort("a".into(), SortDir::Asc)]),
|
||||
..ViewConfigUpdate::default()
|
||||
})).await?;
|
||||
```
|
||||
|
||||
</div>
|
||||
|
||||
The available sort directions are:
|
||||
|
||||
| Direction | Description |
|
||||
|---|---|
|
||||
| `"asc"` | Ascending order |
|
||||
| `"desc"` | Descending order |
|
||||
| `"asc abs"` | Ascending by absolute value |
|
||||
| `"desc abs"` | Descending by absolute value |
|
||||
| `"col asc"` | Ascending order for pivot column groups (requires `split_by`) |
|
||||
| `"col desc"` | Descending order for pivot column groups (requires `split_by`) |
|
||||
| `"col asc abs"` | Ascending by absolute value for pivot column groups |
|
||||
| `"col desc abs"` | Descending by absolute value for pivot column groups |
|
||||
|
||||
## Filter
|
||||
|
||||
The `filter` property specifies columns on which the query can be filtered,
|
||||
returning rows that pass the specified filter condition. This is analogous to
|
||||
the `WHERE` clause in SQL. There is no limit on the number of columns where
|
||||
`filter` is applied, but the resulting dataset is one that passes all the filter
|
||||
conditions, i.e. the filters are joined with an `AND` condition. The join
|
||||
condition can be changed to `OR` via the `filter_op` property.
|
||||
|
||||
Perspective represents `filter` as an array of arrays, with the values of each
|
||||
inner array being a string column name, a string filter operator, and a filter
|
||||
operand in the type of the column:
|
||||
|
||||
<div class="javascript">
|
||||
|
||||
```javascript
|
||||
const view = await table.view({
|
||||
filter: [["a", "<", 100]],
|
||||
});
|
||||
```
|
||||
|
||||
</div>
|
||||
<div class="python">
|
||||
|
||||
```python
|
||||
view = table.view(filter=[["a", "<", 100]])
|
||||
```
|
||||
|
||||
</div>
|
||||
<div class="rust">
|
||||
|
||||
```rust
|
||||
let view = table.view(Some(ViewConfigUpdate {
|
||||
filter: Some(vec![Filter::new("a", "<", FilterTerm::Scalar(Scalar::Float(100.0)))]),
|
||||
..ViewConfigUpdate::default()
|
||||
})).await?;
|
||||
```
|
||||
|
||||
</div>
|
||||
|
||||
The available filter operators depend on the column type:
|
||||
|
||||
**String columns**: `==`, `!=`, `>`, `>=`, `<`, `<=`, `begins with`,
|
||||
`contains`, `ends with`, `in`, `not in`, `is not null`, `is null`.
|
||||
|
||||
**Numeric columns** (`integer`, `float`): `==`, `!=`, `>`, `>=`, `<`, `<=`,
|
||||
`is not null`, `is null`.
|
||||
|
||||
**Boolean columns**: `==`, `is not null`, `is null`.
|
||||
|
||||
**Date/Datetime columns**: `==`, `!=`, `>`, `>=`, `<`, `<=`, `is not null`,
|
||||
`is null`.
|
||||
@@ -0,0 +1,50 @@
|
||||
# Querying data
|
||||
|
||||
To query the table, create a [`Table::view`] on the table instance with an
|
||||
optional configuration object. A [`Table`] can have as many [`View`]s associated
|
||||
with it as you need - Perspective conserves memory by relying on a single
|
||||
[`Table`] to power multiple [`View`]s concurrently:
|
||||
|
||||
<div class="javascript">
|
||||
|
||||
```javascript
|
||||
const view = await table.view({
|
||||
columns: ["Sales"],
|
||||
aggregates: { Sales: "sum" },
|
||||
group_by: ["Region", "Country"],
|
||||
filter: [["Category", "in", ["Furniture", "Technology"]]],
|
||||
});
|
||||
```
|
||||
|
||||
</div>
|
||||
<div class="python">
|
||||
|
||||
```python
|
||||
view = table.view(
|
||||
columns=["Sales"],
|
||||
aggregates={"Sales": "sum"},
|
||||
group_by=["Region", "Country"],
|
||||
filter=[["Category", "in", ["Furniture", "Technology"]]]
|
||||
)
|
||||
```
|
||||
|
||||
</div>
|
||||
<div class="rust">
|
||||
|
||||
```rust
|
||||
use crate::config::*;
|
||||
let view = table
|
||||
.view(Some(ViewConfigUpdate {
|
||||
columns: Some(vec![Some("Sales".into())]),
|
||||
aggregates: Some(HashMap::from_iter(vec![("Sales".into(), "sum".into())])),
|
||||
group_by: Some(vec!["Region".into(), "Country".into()]),
|
||||
filter: Some(vec![Filter::new("Category", "in", &[
|
||||
"Furniture",
|
||||
"Technology",
|
||||
])]),
|
||||
..ViewConfigUpdate::default()
|
||||
}))
|
||||
.await?;
|
||||
```
|
||||
|
||||
</div>
|
||||
@@ -0,0 +1,83 @@
|
||||
# Virtual Servers
|
||||
|
||||
A Virtual Server allows Perspective to query external data sources (such as
|
||||
DuckDB or ClickHouse) without loading the entire dataset into Perspective's
|
||||
built-in data engine. Instead, Perspective translates its query operations
|
||||
(group by, sort, filter, etc.) into queries the external data source can execute
|
||||
natively, and only transfers the data needed for the current view.
|
||||
|
||||
The Virtual Server API works on any platform that has a Perspective Client —
|
||||
including JavaScript (both Node.js and the browser via WebAssembly), Python, and
|
||||
Rust. In the browser, this means a virtual server can front a WASM-based engine
|
||||
like `@duckdb/duckdb-wasm`, giving `<perspective-viewer>` the ability to query a
|
||||
database running entirely client-side without loading data into Perspective's
|
||||
own engine.
|
||||
|
||||
This is useful when:
|
||||
|
||||
- The dataset is too large to fit in browser memory or a single process.
|
||||
- Data already lives in a database and you want to avoid duplicating it.
|
||||
- You want to leverage a database's native query optimizations.
|
||||
- A WASM build of the data source is available in the browser (e.g.
|
||||
`@duckdb/duckdb-wasm`) and you want to query it directly.
|
||||
|
||||
## How it works
|
||||
|
||||
A virtual server implements a handler interface that Perspective calls to
|
||||
satisfy `Table` and `View` operations. The handler translates Perspective's view
|
||||
configuration into the external system's query language (typically SQL),
|
||||
executes the query, and returns the results as columnar data. Because the
|
||||
handler speaks the standard Perspective Client protocol, it can run anywhere a
|
||||
Client can — in-process, in a WebWorker, or on a remote server.
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────────────┐
|
||||
│ <perspective-viewer> │
|
||||
└──┬───────────────────────────────────────────────┘
|
||||
│ ┌──────────────────────────────────────────────────┐
|
||||
└──►│ Perspective Virtual Server Handler │
|
||||
└──┬───────────────────────────────────────────────┘
|
||||
│ ┌──────────────────────────────────────────────────┐
|
||||
└──►│ External DB (DuckDB, ClickHouse, …). │
|
||||
└──────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
The viewer communicates with the virtual server handler the same way it would
|
||||
with a regular Perspective server. The handler advertises its capabilities
|
||||
(which operations it supports) via a _features_ object, and the viewer UI adapts
|
||||
accordingly — disabling controls for unsupported operations.
|
||||
|
||||
## Built-in implementations
|
||||
|
||||
Perspective ships with virtual server implementations for:
|
||||
|
||||
- **DuckDB** — query DuckDB databases in-browser via WASM
|
||||
([JavaScript](../how_to/javascript/virtual_server/duckdb.md)) or server-side
|
||||
([Python](../how_to/python/virtual_server/duckdb.md)).
|
||||
- **ClickHouse** — query a ClickHouse server from the browser
|
||||
([JavaScript](../how_to/javascript/virtual_server/clickhouse.md)) or from
|
||||
Python ([Python](../how_to/python/virtual_server/clickhouse.md)).
|
||||
|
||||
## Custom implementations
|
||||
|
||||
You can implement your own virtual server to connect Perspective to any data
|
||||
source. See the language-specific guides:
|
||||
|
||||
- [JavaScript: Implementing a custom Virtual Server](../how_to/javascript/virtual_server/custom.md)
|
||||
- [Python: Implementing a custom Virtual Server](../how_to/python/virtual_server/custom.md)
|
||||
|
||||
## Features declaration
|
||||
|
||||
The `get_features()` / `getFeatures()` method returns an object that tells
|
||||
Perspective which query operations the virtual server supports. The viewer will
|
||||
only show controls for supported operations:
|
||||
|
||||
| Field | Type | Description |
|
||||
| ------------- | ------ | ----------------------------------------------------------- |
|
||||
| `group_by` | `bool` | Whether group-by aggregation is supported |
|
||||
| `split_by` | `bool` | Whether split-by (pivot) is supported |
|
||||
| `sort` | `bool` | Whether sorting is supported |
|
||||
| `expressions` | `bool` | Whether computed expressions are supported |
|
||||
| `filter_ops` | `dict` | Map of column type to list of supported filter operators |
|
||||
| `aggregates` | `dict` | Map of column type to list of supported aggregate functions |
|
||||
| `on_update` | `bool` | Whether update callbacks are supported |
|
||||
Reference in New Issue
Block a user