コンテンツにスキップ

Multi-Table

このコンテンツはまだ日本語訳がありません。

Multiple independent tables with separate schemas, each displayed in its own chart.

examples/multi-table/server.ts
import { WebSocketServer } from "ws";
const PORT = 9005;
const wss = new WebSocketServer({ port: PORT });
console.log(`Multi-table WebSocket server listening on ws://localhost:${PORT}`);
wss.on("connection", (ws) => {
console.log("Client connected");
ws.send(
JSON.stringify({
type: "info",
description: "Two series at same rate for multi-table DuckDB alignment test",
tables: ["alpha", "beta"],
}),
);
// Send both series at the same dt but as separate table entries.
// Both start at t=0, advance by dt=0.5.
// This tests that time-bucket downsampling produces aligned timestamps
// when both tables share a unified tMax.
let t = 0;
const DT = 0.5;
const interval = setInterval(() => {
// alpha: sin wave
ws.send(
JSON.stringify({
type: "state",
table: "alpha",
t,
value: Math.sin(t * 0.1),
}),
);
// beta: cos wave with offset
ws.send(
JSON.stringify({
type: "state",
table: "beta",
t,
value: Math.cos(t * 0.1) + 2,
}),
);
t += DT;
}, 10); // 100 points/sec per series
ws.on("close", () => {
console.log("Client disconnected");
clearInterval(interval);
});
ws.on("error", (err) => {
console.error("WebSocket error:", err);
clearInterval(interval);
});
});
examples/multi-table/App.tsx
import { useCallback, useEffect, useRef, useState } from "react";
import {
createTable,
IngestBuffer,
insertPoints,
queryDerived,
type TableSchema,
TimeSeriesChart,
useDuckDB,
} from "../../src/index.js";
import { alignTimeSeries, type NamedTimeSeries } from "../../src/utils/alignTimeSeries.js";
interface DataPoint {
t: number;
value: number;
}
const baseSchema: TableSchema<DataPoint> = {
tableName: "placeholder",
columns: [
{ name: "t", type: "DOUBLE" },
{ name: "value", type: "DOUBLE" },
],
derived: [{ name: "value", sql: "value", unit: "" }],
toRow: (p) => [p.t, p.value],
};
function makeSchema(tableName: string): TableSchema<DataPoint> {
return { ...baseSchema, tableName };
}
const DISPLAY_MAX_POINTS = 500;
declare global {
interface Window {
__multiTableDebug: {
conn: unknown;
alphaCount: number;
betaCount: number;
/** Query both tables with unified tMax and return alignment stats. */
queryAlignment: () => Promise<{
alphaT: number[];
betaT: number[];
alphaValues: number[];
betaValues: number[];
unifiedTMax: number;
alignmentRatio: number;
alphaNanCount: number;
betaNanCount: number;
}>;
};
}
}
export function App() {
const { conn } = useDuckDB(baseSchema);
const alphaBufferRef = useRef(new IngestBuffer<DataPoint>());
const betaBufferRef = useRef(new IngestBuffer<DataPoint>());
const [alphaCount, setAlphaCount] = useState(0);
const [betaCount, setBetaCount] = useState(0);
const [chartData, setChartData] = useState<{
t: Float64Array;
values: Float64Array[];
labels: string[];
} | null>(null);
const alphaSchema = makeSchema("tbl_alpha");
const betaSchema = makeSchema("tbl_beta");
// Create tables on mount
useEffect(() => {
if (!conn) return;
(async () => {
await createTable(conn, alphaSchema);
await createTable(conn, betaSchema);
})();
// eslint-disable-next-line react-hooks/exhaustive-deps
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [conn]);
// Data source: WebSocket or mock (when ?mock is in URL)
useEffect(() => {
const isMock = new URLSearchParams(window.location.search).has("mock");
let aCount = 0,
bCount = 0;
const pushPoint = (table: string, t: number, value: number) => {
const point: DataPoint = { t, value };
if (table === "alpha") {
alphaBufferRef.current.push(point);
aCount++;
if (aCount % 50 === 0) setAlphaCount(aCount);
} else if (table === "beta") {
betaBufferRef.current.push(point);
bCount++;
if (bCount % 50 === 0) setBetaCount(bCount);
}
};
if (isMock) {
let t = 0;
const DT = 0.5;
const interval = setInterval(() => {
pushPoint("alpha", t, Math.sin(t * 0.1));
pushPoint("beta", t, Math.cos(t * 0.1) + 2);
t += DT;
}, 10);
return () => clearInterval(interval);
}
const ws = new WebSocket("ws://localhost:9005");
ws.onmessage = (event) => {
const msg = JSON.parse(event.data);
if (msg.type === "state") {
pushPoint(msg.table, msg.t, msg.value);
}
};
return () => ws.close();
}, []);
// Tick loop: drain buffers → insert → query → align → render
useEffect(() => {
if (!conn) return;
let cancelled = false;
let tickCount = 0;
const tick = async () => {
if (cancelled) return;
// Drain buffers and insert
const alphaPts = alphaBufferRef.current.drain();
const betaPts = betaBufferRef.current.drain();
if (alphaPts.length > 0) await insertPoints(conn, alphaSchema, alphaPts);
if (betaPts.length > 0) await insertPoints(conn, betaSchema, betaPts);
tickCount++;
if (tickCount % 4 === 0) {
// Compute unified tMax
const [aMaxRes, bMaxRes] = await Promise.all([
conn.query("SELECT MAX(t) FROM tbl_alpha"),
conn.query("SELECT MAX(t) FROM tbl_beta"),
]);
const aMax = Number(aMaxRes.getChildAt(0)!.get(0));
const bMax = Number(bMaxRes.getChildAt(0)!.get(0));
const unifiedTMax = Math.max(
Number.isFinite(aMax) ? aMax : -Infinity,
Number.isFinite(bMax) ? bMax : -Infinity,
);
const tMax = Number.isFinite(unifiedTMax) ? unifiedTMax : undefined;
// Query both tables with same tMax
const [alphaData, betaData] = await Promise.all([
queryDerived(conn, alphaSchema, undefined, DISPLAY_MAX_POINTS, tMax),
queryDerived(conn, betaSchema, undefined, DISPLAY_MAX_POINTS, tMax),
]);
// Align time series
const inputs: NamedTimeSeries[] = [];
if (alphaData.t.length > 0) {
inputs.push({ label: "alpha", t: alphaData.t, values: alphaData.value as Float64Array });
}
if (betaData.t.length > 0) {
inputs.push({ label: "beta", t: betaData.t, values: betaData.value as Float64Array });
}
if (inputs.length > 0) {
const aligned = alignTimeSeries(inputs);
setChartData(aligned);
}
}
if (!cancelled) {
setTimeout(tick, 200);
}
};
setTimeout(tick, 200);
return () => {
cancelled = true;
};
// eslint-disable-next-line react-hooks/exhaustive-deps
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [conn]);
// Expose debug API
const queryAlignment = useCallback(async () => {
if (!conn) throw new Error("no conn");
// Get counts
const [aCountRes, bCountRes] = await Promise.all([
conn.query("SELECT COUNT(*) FROM tbl_alpha"),
conn.query("SELECT COUNT(*) FROM tbl_beta"),
]);
const aC = Number(aCountRes.getChildAt(0)!.get(0));
const bC = Number(bCountRes.getChildAt(0)!.get(0));
if (aC === 0 || bC === 0) {
throw new Error(`Empty tables: alpha=${aC}, beta=${bC}`);
}
// Unified tMax
const [aMaxRes, bMaxRes] = await Promise.all([
conn.query("SELECT MAX(t) FROM tbl_alpha"),
conn.query("SELECT MAX(t) FROM tbl_beta"),
]);
const unifiedTMax = Math.max(
Number(aMaxRes.getChildAt(0)!.get(0)),
Number(bMaxRes.getChildAt(0)!.get(0)),
);
// Downsampled queries with unified tMax
const [alphaData, betaData] = await Promise.all([
queryDerived(conn, alphaSchema, undefined, DISPLAY_MAX_POINTS, unifiedTMax),
queryDerived(conn, betaSchema, undefined, DISPLAY_MAX_POINTS, unifiedTMax),
]);
const alphaT = Array.from(alphaData.t);
const betaT = Array.from(betaData.t);
const alphaValues = Array.from(alphaData.value as Float64Array);
const betaValues = Array.from(betaData.value as Float64Array);
// NaN check
const alphaNanCount = alphaValues.filter((v) => Number.isNaN(v)).length;
const betaNanCount = betaValues.filter((v) => Number.isNaN(v)).length;
// Alignment check
const aSet = new Set(alphaT);
const bSet = new Set(betaT);
let matching = 0;
for (const t of aSet) {
if (bSet.has(t)) matching++;
}
const totalUnique = new Set([...alphaT, ...betaT]).size;
const alignmentRatio = totalUnique > 0 ? matching / totalUnique : 0;
return {
alphaT,
betaT,
alphaValues,
betaValues,
unifiedTMax,
alignmentRatio,
alphaNanCount,
betaNanCount,
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [conn]);
useEffect(() => {
window.__multiTableDebug = {
conn,
alphaCount,
betaCount,
queryAlignment,
};
}, [conn, alphaCount, betaCount, queryAlignment]);
// Chart rendering
const alphaChartData = chartData?.labels.includes("alpha")
? ([chartData.t, chartData.values[chartData.labels.indexOf("alpha")]] as [
Float64Array,
Float64Array,
])
: null;
const betaChartData = chartData?.labels.includes("beta")
? ([chartData.t, chartData.values[chartData.labels.indexOf("beta")]] as [
Float64Array,
Float64Array,
])
: null;
// Count NaN in aligned data
const nanStats = chartData
? {
alpha: chartData.labels.includes("alpha")
? Array.from(chartData.values[chartData.labels.indexOf("alpha")]).filter((v) =>
Number.isNaN(v),
).length
: 0,
beta: chartData.labels.includes("beta")
? Array.from(chartData.values[chartData.labels.indexOf("beta")]).filter((v) =>
Number.isNaN(v),
).length
: 0,
}
: null;
return (
<div
style={{
padding: "1rem",
background: "#1a1a2e",
color: "#eee",
minHeight: "100vh",
}}
>
<h1>uneri: Multi-Table Alignment Test</h1>
<p data-testid="stats">
Alpha: {alphaCount} pts | Beta: {betaCount} pts | Chart: {chartData?.t?.length ?? 0} pts |
NaN: alpha={nanStats?.alpha ?? "-"} beta={nanStats?.beta ?? "-"}
</p>
<TimeSeriesChart title="alpha (sin)" yLabel="" data={alphaChartData} color="#4af" />
<TimeSeriesChart title="beta (cos+2)" yLabel="" data={betaChartData} color="#f84" />
</div>
);
}