tangram-explore 0.3.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,172 @@
1
+ <script setup lang="ts">
2
+ import { watch, inject, onUnmounted, reactive, computed } from "vue";
3
+ import { ScatterplotLayer } from "@deck.gl/layers";
4
+ import type { PickingInfo } from "@deck.gl/core";
5
+ import type { TangramApi, Disposable } from "@open-aviation/tangram-core/api";
6
+ import { layers, parseColor, pluginConfig } from "./store";
7
+
8
+ const api = inject<TangramApi>("tangramApi")!;
9
+ const layerDisposables = new Map<string, Disposable>();
10
+
11
+ const hoverInfo = reactive({
12
+ x: 0,
13
+ y: 0,
14
+ object: null as Record<string, unknown> | null,
15
+ layerLabel: ""
16
+ });
17
+
18
+ const enable3d = computed(() => {
19
+ if (pluginConfig.enable_3d === "inherit") return api.config.map.enable_3d;
20
+ return !!pluginConfig.enable_3d;
21
+ });
22
+
23
+ watch(
24
+ [layers, enable3d],
25
+ ([currentLayers, is3d]) => {
26
+ const activeIds = new Set(currentLayers.map(l => l.id));
27
+
28
+ for (const [id, disposable] of layerDisposables) {
29
+ if (!activeIds.has(id)) {
30
+ disposable.dispose();
31
+ layerDisposables.delete(id);
32
+ }
33
+ }
34
+
35
+ for (const entry of currentLayers) {
36
+ const table = entry.table;
37
+ const numRows = table.numRows;
38
+ const schema = table.schema;
39
+
40
+ const latField = schema.fields.find(
41
+ f => f.name === "latitude" || f.name === "lat"
42
+ )?.name;
43
+ const lonField = schema.fields.find(
44
+ f => f.name === "longitude" || f.name === "lon" || f.name === "lng"
45
+ )?.name;
46
+
47
+ let altField: string | undefined;
48
+ if (is3d) {
49
+ altField = schema.fields.find(
50
+ f => f.name === "altitude" || f.name === "alt" || f.name === "height"
51
+ )?.name;
52
+ }
53
+
54
+ if (!latField || !lonField) continue;
55
+
56
+ const latData = table.getChild(latField)!;
57
+ const lonData = table.getChild(lonField)!;
58
+ const altData = altField ? table.getChild(altField) : null;
59
+
60
+ if (entry.style.kind === "scatter") {
61
+ const opts = entry.style;
62
+ const deckLayer = new ScatterplotLayer({
63
+ id: `explore-layer-${entry.id}`,
64
+ data: { length: numRows },
65
+ visible: entry.visible,
66
+ pickable: opts.pickable,
67
+ opacity: opts.opacity,
68
+ stroked: opts.stroked,
69
+ filled: opts.filled,
70
+ radiusScale: opts.radius_scale,
71
+ radiusMinPixels: opts.radius_min_pixels,
72
+ radiusMaxPixels: opts.radius_max_pixels,
73
+ lineWidthMinPixels: opts.line_width_min_pixels,
74
+ // radiusUnits: "pixels",
75
+ getPosition: (_: unknown, { index }: { index: number }) => {
76
+ const lat = latData.get(index);
77
+ const lon = lonData.get(index);
78
+ const alt = altData ? altData.get(index) : 0;
79
+ return [lon, lat, alt];
80
+ },
81
+ getFillColor: parseColor(opts.fill_color, [255, 140, 0, 200]),
82
+ getLineColor: parseColor(opts.line_color, [0, 0, 0, 255]),
83
+ onHover: (info: PickingInfo) => {
84
+ if (info.index !== -1) {
85
+ const row = table.get(info.index);
86
+ hoverInfo.object = row ? row.toJSON() : null;
87
+ hoverInfo.x = info.x;
88
+ hoverInfo.y = info.y;
89
+ hoverInfo.layerLabel = entry.label;
90
+ } else {
91
+ hoverInfo.object = null;
92
+ }
93
+ },
94
+ updateTriggers: {
95
+ getPosition: [entry.id, is3d],
96
+ getFillColor: [entry.id, JSON.stringify(opts.fill_color)],
97
+ getLineColor: [entry.id, JSON.stringify(opts.line_color)]
98
+ }
99
+ });
100
+
101
+ if (!layerDisposables.has(entry.id)) {
102
+ layerDisposables.set(entry.id, api.map.setLayer(deckLayer));
103
+ } else {
104
+ api.map.setLayer(deckLayer);
105
+ }
106
+ }
107
+ }
108
+ },
109
+ { deep: true }
110
+ );
111
+
112
+ onUnmounted(() => {
113
+ for (const disposable of layerDisposables.values()) {
114
+ disposable.dispose();
115
+ }
116
+ layerDisposables.clear();
117
+ });
118
+ </script>
119
+
120
+ <template>
121
+ <div
122
+ v-if="hoverInfo.object"
123
+ class="explore-tooltip"
124
+ :style="{ left: `${hoverInfo.x}px`, top: `${hoverInfo.y}px` }"
125
+ >
126
+ <div class="tooltip-header">{{ hoverInfo.layerLabel }}</div>
127
+ <div class="tooltip-grid">
128
+ <template v-for="(val, key) in hoverInfo.object" :key="key">
129
+ <div class="key">{{ key }}</div>
130
+ <div class="val">
131
+ {{ typeof val === "number" && !Number.isInteger(val) ? val.toFixed(4) : val }}
132
+ </div>
133
+ </template>
134
+ </div>
135
+ </div>
136
+ </template>
137
+
138
+ <style scoped>
139
+ .explore-tooltip {
140
+ position: absolute;
141
+ background: white;
142
+ color: black;
143
+ padding: 6px 10px;
144
+ border-radius: 8px;
145
+ font-size: 11px;
146
+ font-family: "B612", sans-serif;
147
+ pointer-events: none;
148
+ transform: translate(12px, 12px);
149
+ z-index: 2000;
150
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
151
+ border: 1px solid rgba(0, 0, 0, 0.05);
152
+ max-width: 250px;
153
+ }
154
+
155
+ .tooltip-header {
156
+ font-weight: bold;
157
+ padding-bottom: 4px;
158
+ color: #333;
159
+ }
160
+
161
+ .tooltip-grid {
162
+ display: grid;
163
+ grid-template-columns: auto auto;
164
+ column-gap: 12px;
165
+ row-gap: 1px;
166
+ }
167
+
168
+ .key {
169
+ text-align: right;
170
+ font-weight: 500;
171
+ }
172
+ </style>
@@ -0,0 +1,170 @@
1
+ <template>
2
+ <div class="layer-list">
3
+ <div v-if="layers.length === 0" class="empty-state">no active layers</div>
4
+ <div v-for="layer in layers" :key="layer.id" class="layer-item">
5
+ <div class="left-col">
6
+ <button
7
+ class="visibility-btn"
8
+ :title="layer.visible ? 'hide layer' : 'show layer'"
9
+ @click="toggleLayerVisibility(layer.id)"
10
+ >
11
+ <svg
12
+ v-if="layer.visible"
13
+ xmlns="http://www.w3.org/2000/svg"
14
+ width="14"
15
+ height="14"
16
+ viewBox="0 0 24 24"
17
+ fill="none"
18
+ stroke="currentColor"
19
+ stroke-width="2"
20
+ stroke-linecap="round"
21
+ stroke-linejoin="round"
22
+ >
23
+ <path
24
+ d="M2.062 12.348a1 1 0 0 1 0-.696 10.75 10.75 0 0 1 19.876 0 1 1 0 0 1 0 .696 10.75 10.75 0 0 1-19.876 0"
25
+ />
26
+ <circle cx="12" cy="12" r="3" />
27
+ </svg>
28
+ <svg
29
+ v-else
30
+ xmlns="http://www.w3.org/2000/svg"
31
+ width="14"
32
+ height="14"
33
+ viewBox="0 0 24 24"
34
+ fill="none"
35
+ stroke="currentColor"
36
+ stroke-width="2"
37
+ stroke-linecap="round"
38
+ stroke-linejoin="round"
39
+ >
40
+ <path d="m15 18-.722-3.25" />
41
+ <path d="M2 8a10.645 10.645 0 0 0 20 0" />
42
+ <path d="m20 15-1.726-2.05" />
43
+ <path d="m4 15 1.726-2.05" />
44
+ <path d="m9 18 .722-3.25" />
45
+ </svg>
46
+ </button>
47
+ <span class="layer-label">{{ layer.label }}</span>
48
+ </div>
49
+ <div class="right-col">
50
+ <span class="layer-stats">{{ layer.table.numRows.toLocaleString() }} rows</span>
51
+ <div class="layer-chip">
52
+ <div
53
+ class="status-dot"
54
+ :style="{ backgroundColor: getLayerColor(layer.style) }"
55
+ ></div>
56
+ <span class="kind-label">{{ layer.style.kind }}</span>
57
+ </div>
58
+ </div>
59
+ </div>
60
+ </div>
61
+ </template>
62
+
63
+ <script setup lang="ts">
64
+ import { layers, toggleLayerVisibility, parseColor, type StyleOptions } from "./store";
65
+
66
+ function getLayerColor(style: StyleOptions): string {
67
+ let c;
68
+ if (style.kind === "scatter") {
69
+ c = style.fill_color;
70
+ } else {
71
+ c = [128, 128, 128];
72
+ }
73
+ const [r, g, b, a] = parseColor(c, [128, 128, 128, 255]);
74
+ return `rgba(${r}, ${g}, ${b}, ${a / 255})`;
75
+ }
76
+ </script>
77
+
78
+ <style scoped>
79
+ .layer-list {
80
+ display: flex;
81
+ flex-direction: column;
82
+ max-height: 300px;
83
+ overflow-y: auto;
84
+ }
85
+
86
+ .empty-state {
87
+ padding: 1rem;
88
+ text-align: center;
89
+ color: #666;
90
+ font-size: 0.9em;
91
+ font-style: italic;
92
+ }
93
+
94
+ .layer-item {
95
+ padding: 6px 12px;
96
+ border-bottom: 1px solid #eee;
97
+ display: flex;
98
+ justify-content: space-between;
99
+ align-items: center;
100
+ font-size: 0.9em;
101
+ }
102
+
103
+ .left-col {
104
+ display: flex;
105
+ align-items: center;
106
+ gap: 8px;
107
+ flex: 1;
108
+ min-width: 0;
109
+ }
110
+
111
+ .right-col {
112
+ display: flex;
113
+ align-items: center;
114
+ gap: 12px;
115
+ flex-shrink: 0;
116
+ }
117
+
118
+ .visibility-btn {
119
+ background: none;
120
+ border: none;
121
+ cursor: pointer;
122
+ padding: 2px;
123
+ color: #888;
124
+ display: flex;
125
+ align-items: center;
126
+ }
127
+
128
+ .visibility-btn:hover {
129
+ color: #333;
130
+ }
131
+
132
+ .layer-label {
133
+ font-weight: 500;
134
+ color: #333;
135
+ white-space: nowrap;
136
+ overflow: hidden;
137
+ text-overflow: ellipsis;
138
+ font-family: "B612", sans-serif;
139
+ }
140
+
141
+ .layer-chip {
142
+ display: flex;
143
+ align-items: center;
144
+ gap: 4px;
145
+ background-color: #f5f5f5;
146
+ padding: 2px 6px 2px 4px;
147
+ border-radius: 12px;
148
+ border: 1px solid #e0e0e0;
149
+ }
150
+
151
+ .status-dot {
152
+ width: 8px;
153
+ height: 8px;
154
+ border-radius: 50%;
155
+ }
156
+
157
+ .kind-label {
158
+ text-transform: lowercase;
159
+ font-size: 0.75em;
160
+ color: #555;
161
+ font-weight: 600;
162
+ line-height: 1;
163
+ }
164
+
165
+ .layer-stats {
166
+ color: #999;
167
+ font-variant-numeric: tabular-nums;
168
+ font-size: 0.85em;
169
+ }
170
+ </style>
@@ -0,0 +1,212 @@
1
+ from __future__ import annotations
2
+
3
+ import io
4
+ import uuid
5
+ from contextlib import asynccontextmanager
6
+ from dataclasses import KW_ONLY, dataclass, field, fields, is_dataclass
7
+ from typing import (
8
+ TYPE_CHECKING,
9
+ Annotated,
10
+ Any,
11
+ AsyncGenerator,
12
+ Literal,
13
+ Protocol,
14
+ cast,
15
+ runtime_checkable,
16
+ )
17
+
18
+ import arro3.core as ac
19
+ import arro3.io
20
+ import orjson
21
+ import tangram_core
22
+ from fastapi import APIRouter
23
+ from fastapi.responses import Response
24
+ from tangram_core.config import ExposeField
25
+
26
+ if TYPE_CHECKING:
27
+ from typing import TypeAlias
28
+
29
+ from tangram_core import BackendState
30
+
31
+ EXPLORE_CHANNEL = "explore"
32
+ EXPLORE_EVENT = "layers"
33
+
34
+ router = APIRouter(prefix="/explore", tags=["explore"])
35
+
36
+
37
+ LayerId: TypeAlias = str
38
+ """Unique UUID for a layer."""
39
+ ParquetBytes: TypeAlias = bytes
40
+ LayerConfig: TypeAlias = dict[str, Any]
41
+ """Serialised layer configuration without data or label."""
42
+
43
+
44
+ @dataclass
45
+ class ExploreState:
46
+ data: dict[LayerId, ParquetBytes] = field(default_factory=dict)
47
+ layers: dict[LayerId, LayerConfig] = field(default_factory=dict)
48
+ layer_order: list[LayerId] = field(default_factory=list)
49
+
50
+
51
+ @asynccontextmanager
52
+ async def lifespan(state: BackendState) -> AsyncGenerator[None, None]:
53
+ setattr(state, "explore_state", ExploreState())
54
+ yield
55
+ delattr(state, "explore_state")
56
+
57
+
58
+ def get_explore_state(state: tangram_core.InjectBackendState) -> ExploreState:
59
+ return cast(ExploreState, getattr(state, "explore_state"))
60
+
61
+
62
+ @router.get("/data/{data_id}")
63
+ async def get_explore_data(
64
+ data_id: str, state: tangram_core.InjectBackendState
65
+ ) -> Response:
66
+ explore_state = get_explore_state(state)
67
+ data = explore_state.data.get(data_id)
68
+ if data is None:
69
+ return Response(status_code=404) # shouldn't occur
70
+ return Response(content=data, media_type="application/vnd.apache.parquet")
71
+
72
+
73
+ @router.get("/layers")
74
+ async def get_layers(state: tangram_core.InjectBackendState) -> list[dict[str, Any]]:
75
+ explore_state = get_explore_state(state)
76
+ return [explore_state.layers[uid] for uid in explore_state.layer_order]
77
+
78
+
79
+ @runtime_checkable
80
+ class ArrowStreamExportable(Protocol):
81
+ def __arrow_c_stream__(self, requested_schema: Any = None) -> Any: ...
82
+
83
+
84
+ @runtime_checkable
85
+ class ExploreLayer(Protocol):
86
+ data: ArrowStreamExportable
87
+ """Any data structure that implements the
88
+ [Arrow C data interface](https://arrow.apache.org/docs/format/CDataInterface.html),
89
+ such as polars DataFrames or pyarrow Tables."""
90
+ label: str | None
91
+ """Unique name for the layer (optional).
92
+ If not provided, a random 8-character ID will be used."""
93
+
94
+
95
+ @dataclass(frozen=True, slots=True)
96
+ class ScatterLayer(ExploreLayer):
97
+ data: ArrowStreamExportable
98
+ _: KW_ONLY
99
+ radius_scale: float = 50.0
100
+ radius_min_pixels: float = 2.0
101
+ radius_max_pixels: float = 5.0
102
+ line_width_min_pixels: float = 1.0
103
+ fill_color: str | list[int] = "#027ec7"
104
+ line_color: str | list[int] = "#000000"
105
+ opacity: float = 0.8
106
+ stroked: bool = False
107
+ filled: bool = True
108
+ pickable: bool = True
109
+ label: str | None = None
110
+ kind: Literal["scatter"] = field(default="scatter", init=False)
111
+
112
+
113
+ def _to_parquet_bytes(df: Any) -> bytes:
114
+ if not isinstance(df, ArrowStreamExportable):
115
+ raise TypeError(
116
+ f"cannot convert {type(df).__name__} to arrow. "
117
+ "object must implement '__arrow_c_stream__' protocol. "
118
+ )
119
+
120
+ reader = ac.RecordBatchReader.from_arrow(df)
121
+ sink = io.BytesIO()
122
+ arro3.io.write_parquet(reader, sink)
123
+ return sink.getvalue()
124
+
125
+
126
+ @dataclass(frozen=True, slots=True)
127
+ class Layer:
128
+ id: str
129
+ _session: Session
130
+
131
+ async def remove(self) -> None:
132
+ await self._session.remove(self.id)
133
+
134
+
135
+ @dataclass(frozen=True, slots=True)
136
+ class Session:
137
+ state: BackendState
138
+
139
+ def __post_init__(self) -> None:
140
+ if not hasattr(self.state, "explore_state"):
141
+ raise RuntimeError(
142
+ "The 'tangram_explore' plugin is not active. "
143
+ "Ensure it is added to the [core.plugins] list in your config."
144
+ )
145
+
146
+ async def _broadcast(self, op: str, **kwargs: Any) -> None:
147
+ payload = {"op": op, **kwargs}
148
+ topic = f"to:{EXPLORE_CHANNEL}:{EXPLORE_EVENT}"
149
+ await self.state.redis_client.publish(topic, orjson.dumps(payload))
150
+
151
+ async def push(self, layer: ExploreLayer) -> Layer:
152
+ if not is_dataclass(layer): # required for fields()
153
+ raise TypeError("layer must be a dataclass")
154
+ if not isinstance(layer, ExploreLayer):
155
+ raise TypeError("layer must implement ExploreLayer protocol")
156
+
157
+ data_id = str(uuid.uuid4())
158
+ parquet_bytes = _to_parquet_bytes(layer.data)
159
+
160
+ explore_state = get_explore_state(self.state)
161
+ explore_state.data[data_id] = parquet_bytes
162
+
163
+ style = {
164
+ f.name: getattr(layer, f.name)
165
+ for f in fields(layer)
166
+ if f.name not in ("data", "label")
167
+ }
168
+ label = getattr(layer, "label", None)
169
+
170
+ layer_def = {
171
+ "id": data_id,
172
+ "label": label or data_id[:8],
173
+ "url": f"/explore/data/{data_id}",
174
+ "style": style,
175
+ }
176
+ explore_state.layers[data_id] = layer_def
177
+ explore_state.layer_order.append(data_id)
178
+
179
+ await self._broadcast("add", layer=layer_def)
180
+
181
+ return Layer(id=data_id, _session=self)
182
+
183
+ async def remove(self, data_id: str) -> None:
184
+ explore_state = get_explore_state(self.state)
185
+ if data_id in explore_state.data:
186
+ del explore_state.data[data_id]
187
+ if data_id in explore_state.layers:
188
+ del explore_state.layers[data_id]
189
+ if data_id in explore_state.layer_order:
190
+ explore_state.layer_order.remove(data_id)
191
+ await self._broadcast("remove", id=data_id)
192
+
193
+ async def clear(self) -> None:
194
+ explore_state = get_explore_state(self.state)
195
+ explore_state.data.clear()
196
+ explore_state.layers.clear()
197
+ explore_state.layer_order.clear()
198
+ await self._broadcast("clear")
199
+
200
+
201
+ @dataclass
202
+ class ExploreConfig:
203
+ enable_3d: Annotated[Literal["inherit"] | bool, ExposeField()] = "inherit"
204
+ """Whether to render scatter points in 3D"""
205
+
206
+
207
+ plugin = tangram_core.Plugin(
208
+ frontend_path="dist-frontend",
209
+ routers=[router],
210
+ config_class=ExploreConfig,
211
+ lifespan=lifespan,
212
+ )
@@ -0,0 +1,96 @@
1
+ import type { TangramApi } from "@open-aviation/tangram-core/api";
2
+ import initWasm, { readParquet, wasmMemory } from "parquet-wasm";
3
+ import { parseTable } from "arrow-js-ffi";
4
+ import ExploreLayers from "./ExploreLayers.vue";
5
+ import LayerList from "./LayerList.vue";
6
+ import {
7
+ addLayer,
8
+ clearLayers,
9
+ removeLayer,
10
+ type StyleOptions,
11
+ pluginConfig
12
+ } from "./store";
13
+
14
+ const EXPLORE_CHANNEL = "explore";
15
+ const EXPLORE_EVENT = "layers";
16
+
17
+ let wasmInitialized = false;
18
+
19
+ async function initParquetWasm() {
20
+ if (wasmInitialized) return;
21
+ await initWasm("/parquet_wasm_bg.wasm");
22
+ wasmInitialized = true;
23
+ }
24
+
25
+ interface LayerDefinition {
26
+ id: string;
27
+ label: string;
28
+ url: string;
29
+ style: StyleOptions;
30
+ }
31
+
32
+ interface StackMessage {
33
+ op: "add" | "remove" | "clear";
34
+ layer?: LayerDefinition;
35
+ id?: string;
36
+ }
37
+
38
+ async function loadAndAdd(def: LayerDefinition) {
39
+ const resp = await fetch(def.url);
40
+ const arrayBuffer = await resp.arrayBuffer();
41
+ const data = new Uint8Array(arrayBuffer);
42
+
43
+ const wasmTable = readParquet(data);
44
+ const ffiTable = wasmTable.intoFFI();
45
+ const memory = wasmMemory().buffer;
46
+
47
+ const arrowTable = parseTable(
48
+ memory,
49
+ ffiTable.arrayAddrs(),
50
+ ffiTable.schemaAddr(),
51
+ true // copying is required because adding new layers can grow wasm memory and detach
52
+ );
53
+ addLayer(def.id, def.label, arrowTable, wasmTable, def.style);
54
+ }
55
+
56
+ async function handleMessage(payload: StackMessage) {
57
+ if (payload.op === "add" && payload.layer) {
58
+ await loadAndAdd(payload.layer);
59
+ } else if (payload.op === "clear") {
60
+ clearLayers();
61
+ } else if (payload.op === "remove" && payload.id) {
62
+ removeLayer(payload.id);
63
+ }
64
+ }
65
+
66
+ async function syncLayers() {
67
+ const res = await fetch("/explore/layers");
68
+ const layerDefs: LayerDefinition[] = await res.json();
69
+ clearLayers();
70
+ for (const def of layerDefs) {
71
+ await loadAndAdd(def);
72
+ }
73
+ }
74
+
75
+ export async function install(
76
+ api: TangramApi,
77
+ config?: { enable_3d?: boolean | "inherit" }
78
+ ) {
79
+ if (config) {
80
+ if (config.enable_3d !== undefined) pluginConfig.enable_3d = config.enable_3d;
81
+ }
82
+
83
+ api.ui.registerWidget("explore-layers-map", "MapOverlay", ExploreLayers);
84
+ api.ui.registerWidget("explore-layers-list", "SideBar", LayerList, {
85
+ title: "Explore Layers"
86
+ });
87
+
88
+ await initParquetWasm();
89
+ await api.realtime.ensureConnected();
90
+
91
+ api.realtime.subscribe<StackMessage>(
92
+ `${EXPLORE_CHANNEL}:${EXPLORE_EVENT}`,
93
+ handleMessage
94
+ );
95
+ await syncLayers();
96
+ }
@@ -0,0 +1,96 @@
1
+ import { reactive, shallowRef, triggerRef } from "vue";
2
+ import type { Table } from "apache-arrow";
3
+ import type { Table as WasmTable } from "parquet-wasm";
4
+
5
+ export type ColorSpec =
6
+ | string
7
+ | [number, number, number]
8
+ | [number, number, number, number];
9
+
10
+ export interface ScatterOptions {
11
+ kind: "scatter";
12
+ radius_scale: number;
13
+ radius_min_pixels: number;
14
+ radius_max_pixels: number;
15
+ line_width_min_pixels: number;
16
+ fill_color: ColorSpec;
17
+ line_color: ColorSpec;
18
+ opacity: number;
19
+ stroked: boolean;
20
+ filled: boolean;
21
+ pickable: boolean;
22
+ }
23
+
24
+ export type StyleOptions = ScatterOptions;
25
+
26
+ export interface LayerEntry {
27
+ id: string;
28
+ label: string;
29
+ table: Table;
30
+ wasmRef: WasmTable;
31
+ style: StyleOptions;
32
+ visible: boolean;
33
+ }
34
+
35
+ export const layers = shallowRef<LayerEntry[]>([]);
36
+
37
+ export const pluginConfig = reactive({
38
+ enable_3d: "inherit" as boolean | "inherit"
39
+ });
40
+
41
+ export function addLayer(
42
+ id: string,
43
+ label: string,
44
+ table: Table,
45
+ wasmRef: WasmTable,
46
+ style: StyleOptions
47
+ ) {
48
+ if (layers.value.some(d => d.id === id)) return;
49
+ layers.value.push({ id, label, table, wasmRef, style, visible: true });
50
+ triggerRef(layers);
51
+ }
52
+
53
+ export function removeLayer(id: string) {
54
+ const idx = layers.value.findIndex(d => d.id === id);
55
+ if (idx !== -1) {
56
+ const entry = layers.value[idx];
57
+ entry.wasmRef.free();
58
+ layers.value.splice(idx, 1);
59
+ triggerRef(layers);
60
+ }
61
+ }
62
+
63
+ export function clearLayers() {
64
+ layers.value.forEach(e => e.wasmRef.free());
65
+ layers.value = [];
66
+ triggerRef(layers);
67
+ }
68
+
69
+ export function toggleLayerVisibility(id: string) {
70
+ const layer = layers.value.find(d => d.id === id);
71
+ if (layer) {
72
+ layer.visible = !layer.visible;
73
+ triggerRef(layers);
74
+ }
75
+ }
76
+
77
+ export function parseColor(
78
+ c: ColorSpec,
79
+ defaultColor: [number, number, number, number]
80
+ ): [number, number, number, number] {
81
+ if (Array.isArray(c)) {
82
+ return c.length === 3
83
+ ? ([...c, 255] as [number, number, number, number])
84
+ : (c as [number, number, number, number]);
85
+ }
86
+ if (typeof c === "string" && c.startsWith("#")) {
87
+ const hex = c.slice(1);
88
+ if (hex.length === 6) {
89
+ const r = parseInt(hex.slice(0, 2), 16);
90
+ const g = parseInt(hex.slice(2, 4), 16);
91
+ const b = parseInt(hex.slice(4, 6), 16);
92
+ return [r, g, b, 255];
93
+ }
94
+ }
95
+ return defaultColor;
96
+ }
@@ -0,0 +1,21 @@
1
+ Metadata-Version: 2.4
2
+ Name: tangram-explore
3
+ Version: 0.3.0
4
+ Summary: Tangram plugin for exploring any Arrow-backed dataframe
5
+ Requires-Python: >=3.10
6
+ Requires-Dist: arro3-core>=0.4.0
7
+ Requires-Dist: arro3-io>=0.4.0
8
+ Requires-Dist: tangram-core>=0.2.1
9
+ Provides-Extra: polars
10
+ Requires-Dist: polars>=1; extra == 'polars'
11
+ Description-Content-Type: text/markdown
12
+
13
+ # tangram_explore
14
+
15
+ The `tangram_explore` plugin provides an interactive environment for data exploration and visualization within Tangram.
16
+
17
+ It provides:
18
+
19
+ - A Python API to generate interactive plots.
20
+ - Real-time communication with the Tangram frontend.
21
+ - Flexible layer-based visualization system.
@@ -0,0 +1,9 @@
1
+ tangram_explore/ExploreLayers.vue,sha256=VbqiWvqzZK8k6ZICTd5gxdhG_EzQjgpTyJcFZhBM308,4989
2
+ tangram_explore/LayerList.vue,sha256=ozxx97TE_YY-G85h30R1XBVlRw48TNib_Ib5DvVqnb8,3804
3
+ tangram_explore/__init__.py,sha256=gx4tqdO7b2vpv4r3SSmX-vUPVKwUDrcFybyrNoX8qRg,6383
4
+ tangram_explore/index.ts,sha256=3vcsAtVYLq3blfchqVVQlRML0cH8mOkLoZKV7nRZMSY,2460
5
+ tangram_explore/store.ts,sha256=UCTZEdiG-JaYuq51E-uIi0gd0cg314QuH3xSFPiYgO0,2326
6
+ tangram_explore-0.3.0.dist-info/METADATA,sha256=EU6Eh6JE2UDI1F9_vQ_CVRQHPG_KwWzG4UxwSrMDyAQ,653
7
+ tangram_explore-0.3.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
8
+ tangram_explore-0.3.0.dist-info/entry_points.txt,sha256=-uGNZAL738Y1AixJRxmo9O8QwU26g33F6uLdVbtBbmM,64
9
+ tangram_explore-0.3.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.28.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [tangram_core.plugins]
2
+ tangram_explore = tangram_explore:plugin