Initial commit
This commit is contained in:
222
static/frequency.js
Normal file
222
static/frequency.js
Normal file
@@ -0,0 +1,222 @@
|
||||
(() => {
|
||||
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.";
|
||||
}
|
||||
});
|
||||
})();
|
||||
|
||||
Reference in New Issue
Block a user