(() => { let qs = new URLSearchParams(window.location.search); const termInput = document.getElementById("term"); const channelSel = document.getElementById("channel"); const intervalSel = document.getElementById("interval"); const startInput = document.getElementById("start"); const endInput = document.getElementById("end"); const runBtn = document.getElementById("runBtn"); const summaryDiv = document.getElementById("summary"); const chartDiv = document.getElementById("chart"); function parseParams() { return { term: qs.get("term") || "", channel: qs.get("channel_id") || "all", interval: qs.get("interval") || "month", start: qs.get("start") || "", end: qs.get("end") || "", }; } function setFormFromParams() { const params = parseParams(); termInput.value = params.term; intervalSel.value = params.interval; startInput.value = params.start; endInput.value = params.end; return params; } function updateUrl(params) { const url = new URL(window.location.href); url.searchParams.set("term", params.term); url.searchParams.set("channel_id", params.channel); url.searchParams.set("interval", params.interval); if (params.start) url.searchParams.set("start", params.start); else url.searchParams.delete("start"); if (params.end) url.searchParams.set("end", params.end); else url.searchParams.delete("end"); history.pushState({}, "", url.toString()); qs = new URLSearchParams(url.search); } async function loadChannels(initialValue) { try { const res = await fetch("/api/channels"); const data = await res.json(); data.forEach((item) => { const opt = document.createElement("option"); opt.value = item.Id; opt.textContent = `${item.Name} (${item.Count})`; channelSel.appendChild(opt); }); } catch (err) { console.error("Failed to load channels", err); } channelSel.value = initialValue || "all"; } function drawChart(data) { chartDiv.innerHTML = ""; if (!data.length) { const msg = document.createElement("div"); msg.className = "muted"; msg.textContent = "No matching documents for this term."; chartDiv.appendChild(msg); return; } const parsed = data .map((d) => ({ date: d3.isoParse(d.date) || new Date(d.date), value: d.count, })) .filter((d) => d.date instanceof Date && !Number.isNaN(d.date.valueOf())); if (!parsed.length) { const msg = document.createElement("div"); msg.className = "muted"; msg.textContent = "Unable to parse dates for this series."; chartDiv.appendChild(msg); return; } const margin = { top: 20, right: 30, bottom: 40, left: 56 }; const fullWidth = chartDiv.clientWidth || 900; const fullHeight = 360; const width = fullWidth - margin.left - margin.right; const height = fullHeight - margin.top - margin.bottom; const svg = d3 .select(chartDiv) .append("svg") .attr("width", fullWidth) .attr("height", fullHeight); const g = svg .append("g") .attr("transform", `translate(${margin.left},${margin.top})`); const x = d3 .scaleTime() .domain(d3.extent(parsed, (d) => d.date)) .range([0, width]); const y = d3 .scaleLinear() .domain([0, d3.max(parsed, (d) => d.value) || 0]) .nice() .range([height, 0]); const xAxis = d3.axisBottom(x).ticks(6).tickFormat(d3.timeFormat("%Y-%m-%d")); const yAxis = d3.axisLeft(y).ticks(6); g.append("g") .attr("class", "axis") .attr("transform", `translate(0,${height})`) .call(xAxis) .selectAll("text") .attr("text-anchor", "end") .attr("transform", "rotate(-35)") .attr("dx", "-0.8em") .attr("dy", "0.15em"); g.append("g").attr("class", "axis").call(yAxis); const line = d3 .line() .x((d) => x(d.date)) .y((d) => y(d.value)); g.append("path") .datum(parsed) .attr("class", "line") .attr("d", line); g.selectAll(".dot") .data(parsed) .enter() .append("circle") .attr("class", "dot") .attr("r", 3) .attr("cx", (d) => x(d.date)) .attr("cy", (d) => y(d.value)) .append("title") .text((d) => `${d3.timeFormat("%Y-%m-%d")(d.date)}: ${d.value}`); } async function runFrequency(pushState = true) { const term = termInput.value.trim(); if (!term) { summaryDiv.textContent = "Enter a term to begin."; chartDiv.innerHTML = ""; return; } const params = { term, channel: channelSel.value, interval: intervalSel.value, start: startInput.value, end: endInput.value, }; if (pushState) updateUrl(params); const search = new URLSearchParams(); search.set("term", term); if (params.channel && params.channel !== "all") { search.set("channel_id", params.channel); } search.set("interval", params.interval); if (params.start) search.set("start", params.start); if (params.end) search.set("end", params.end); summaryDiv.textContent = "Loading…"; chartDiv.innerHTML = ""; try { const res = await fetch(`/api/frequency?${search.toString()}`); if (!res.ok) { throw new Error(`Request failed: ${res.status}`); } const payload = await res.json(); const total = payload.totalResults || 0; summaryDiv.textContent = `Matches: ${total.toLocaleString()} • Buckets: ${ (payload.buckets || []).length } • Interval: ${payload.interval}`; drawChart(payload.buckets || []); } catch (err) { console.error(err); summaryDiv.textContent = "Failed to load data."; } } runBtn.addEventListener("click", () => runFrequency()); termInput.addEventListener("keypress", (e) => { if (e.key === "Enter") runFrequency(); }); intervalSel.addEventListener("change", () => runFrequency()); channelSel.addEventListener("change", () => runFrequency()); startInput.addEventListener("change", () => runFrequency()); endInput.addEventListener("change", () => runFrequency()); window.addEventListener("popstate", () => { qs = new URLSearchParams(window.location.search); const params = setFormFromParams(); channelSel.value = params.channel; runFrequency(false); }); const initialParams = setFormFromParams(); loadChannels(initialParams.channel).then(() => { if (initialParams.term) { runFrequency(false); } else { summaryDiv.textContent = "Enter a term to begin."; } }); })();