import { API } from "/static/api.js";
import { getEntityWindow, getEntityMetric, getEntityScale,
getEntityResolution, getEntityStyle, getEntityShowMeta,
getEntityMetric2, getEntityTiling }
from "/static/selection.js";
import { fmtVal, esc, drawLinePanel, LINE_COLOR, SECOND_COLOR }
from "/static/viz/lineplot.js";
let _symbol = null; let _overview = null;
let _chart = null;
let _ro = null; let _raf = 0;
export function setupEntity(_metrics) {
document.addEventListener("entity-window-changed", renderChart);
document.addEventListener("entity-metric-changed", renderChart);
document.addEventListener("entity-resolution-changed", renderChart);
document.addEventListener("entity-style-changed", renderChart);
document.addEventListener("entity-metric2-changed", renderChart);
document.addEventListener("entity-scale-changed", redrawCached);
document.addEventListener("entity-tiling-changed", redrawCached);
document.addEventListener("entity-meta-toggled", () => {
const split = document.querySelector(".entity-split");
if (split) split.classList.toggle("meta-hidden", !getEntityShowMeta());
redrawCached();
});
}
function redrawCached() {
if (!_chart) return;
_chart.scale = getEntityScale();
_chart.tiling = getEntityTiling();
const canvas = document.querySelector("#ent-canvas");
if (!canvas || !canvas.clientWidth) return;
draw(canvas, _chart);
}
function ensureResizeObserver(canvas) {
if (_ro || typeof ResizeObserver === "undefined") return;
_ro = new ResizeObserver(() => {
if (_raf) cancelAnimationFrame(_raf);
_raf = requestAnimationFrame(() => {
_raf = 0;
if (!_chart) return;
const c = document.querySelector("#ent-canvas");
if (!c || !c.clientWidth) return;
draw(c, _chart);
});
});
_ro.observe(canvas);
}
export async function renderEntity(symbol) {
_symbol = symbol;
_chart = null;
const root = document.querySelector("#tab-entity");
if (!root) return;
root.innerHTML = `
<div class="entity-split${getEntityShowMeta() ? "" : " meta-hidden"}">
<div class="entity-meta" id="ent-meta"><p class="muted">Loading…</p></div>
<div class="entity-chart"><div id="ent-canvas"></div></div>
</div>`;
try {
_overview = await API.entity(symbol);
} catch (e) {
document.querySelector("#ent-meta").innerHTML =
`<p class="err">${e.message}</p>`;
return;
}
renderMeta();
const canvas = document.querySelector("#ent-canvas");
if (canvas) ensureResizeObserver(canvas);
renderChart();
}
function factHtml(v) {
const s = String(v);
if (/^https?:\/\/\S+$/i.test(s))
return `<a href="${esc(s)}" target="_blank" rel="noopener">${esc(s)}</a>`;
if (/^www\.\S+$/i.test(s))
return `<a href="https://${esc(s)}" target="_blank" rel="noopener">${esc(s)}</a>`;
return esc(s);
}
function renderMeta() {
const o = _overview;
const meta = document.querySelector("#ent-meta");
if (!o) { meta.innerHTML = `<p class="err">No data.</p>`; return; }
const facts = (o.facts || []).filter(f => f.value != null && f.value !== "");
const metricRows = (o.metrics || [])
.map(m => `<tr><td class="ent-mlabel">${esc(m.label)}</td>
<td class="ent-mval">${fmtVal(m.value, m.unit)}</td></tr>`)
.join("");
meta.innerHTML = `
<div class="ent-head">
<h2 class="ent-name">${esc(o.label || _symbol)}</h2>
<div class="ent-sym">${esc(_symbol)}</div>
</div>
${facts.length ? `<dl class="ent-facts">${facts.map(f =>
`<div><dt>${esc(f.label)}</dt><dd>${factHtml(f.value)}</dd></div>`).join("")}</dl>` : ""}
${metricRows ? `<table class="ent-metrics"><tbody>${metricRows}</tbody></table>` : ""}
`;
}
function buildDataset(s) {
const isOHLC = !!s._ohlc;
const pts = [];
if (isOHLC) {
for (let i = 0; i < s.dates.length; i++) {
const o = s.open[i], h = s.high[i], l = s.low[i], c = s.close[i];
if ([o, h, l, c].some(x => x == null || Number.isNaN(x))) continue;
pts.push({ date: new Date(s.dates[i]), o: +o, h: +h, l: +l, c: +c, value: +c });
}
} else {
for (let i = 0; i < s.dates.length; i++) {
const v = s.values[i];
if (v == null || Number.isNaN(v)) continue;
pts.push({ date: new Date(s.dates[i]), value: +v });
}
}
if (pts.length === 0) return null;
return { pts, unit: s.unit, label: s.label, isOHLC };
}
async function renderChart() {
if (!_symbol) return;
const canvas = document.querySelector("#ent-canvas");
if (!canvas) return;
const metric = getEntityMetric();
if (!metric) { canvas.innerHTML = "";
return;
}
const metric2 = getEntityMetric2();
const window_ = getEntityWindow();
const resolution = getEntityResolution();
const style = getEntityStyle();
const tiling = getEntityTiling();
const wantOHLC = (style === "candle" || style === "bars") && metric === "price";
canvas.innerHTML = `<p class="muted">Loading…</p>`;
let primaryRaw, secondaryRaw = null;
try {
primaryRaw = wantOHLC
? await API.entityOhlc(_symbol, window_, resolution)
: await API.entitySeries(_symbol, metric, window_, resolution);
primaryRaw._ohlc = wantOHLC;
if (metric2)
secondaryRaw = await API.entitySeries(_symbol, metric2, window_, resolution);
} catch (e) {
canvas.innerHTML = `<p class="err">${e.message}</p>`;
return;
}
const primary = buildDataset(primaryRaw);
const secondary = secondaryRaw ? buildDataset(secondaryRaw) : null;
_chart = { primary, secondary, style, scale: getEntityScale(), tiling };
draw(canvas, _chart);
}
function draw(canvas, chart) {
canvas.innerHTML = "";
const { primary, secondary, style, scale, tiling } = chart;
if (!primary) {
canvas.innerHTML = `<p class="muted">No data for this window.</p>`;
return;
}
const W = canvas.clientWidth || 900;
const H = Math.max(380, canvas.clientHeight) || 480;
const svg = d3.select(canvas).append("svg").attr("class", "lp-canvas")
.attr("viewBox", `0 0 ${W} ${H}`).attr("width", W).attr("height", H)
.style("width", W + "px").style("height", H + "px");
const tip = d3.select(canvas).append("div").attr("class", "lp-tooltip")
.style("display", "none");
const toSeries = (ds, { kind, color, axis }) => ({
label: ds.label, color, unit: ds.unit, axis, isOHLC: ds.isOHLC,
kind: (kind === "candle" || kind === "bars") && !ds.isOHLC ? "line" : kind,
pts: ds.pts,
});
const wantSubplots = secondary && tiling && tiling !== "overlay";
if (!wantSubplots) {
const series = [toSeries(primary, { kind: style, color: LINE_COLOR, axis: "left" })];
if (secondary)
series.push(toSeries(secondary, { kind: "line", color: SECOND_COLOR, axis: "right" }));
drawLinePanel(svg.append("g"), { x: 0, y: 0, w: W, h: H },
{ series, scale, gridlines: true }, tip);
return;
}
const panels = [
toSeries(primary, { kind: style, color: LINE_COLOR, axis: "left" }),
toSeries(secondary, { kind: "line", color: SECOND_COLOR, axis: "left" }),
];
const regions = layoutRegions(tiling, panels.length, W, H);
panels.forEach((s, i) => {
drawLinePanel(svg.append("g"), regions[i],
{ series: [s], scale, gridlines: true,
leftAxisColor: i === 1 ? SECOND_COLOR : null }, tip);
});
}
function layoutRegions(tiling, n, W, H) {
if (tiling === "vertical") {
const h = H / n;
return d3.range(n).map(i => ({ x: 0, y: i * h, w: W, h }));
}
if (tiling === "horizontal") {
const w = W / n;
return d3.range(n).map(i => ({ x: i * w, y: 0, w, h: H }));
}
const cols = Math.ceil(Math.sqrt(n));
const rows = Math.ceil(n / cols);
const w = W / cols, h = H / rows;
return d3.range(n).map(i => ({
x: (i % cols) * w, y: Math.floor(i / cols) * h, w, h,
}));
}