Add chart manager for abortable fetches #49
3 changed files with 97 additions and 38 deletions
Add chart loading management
commit
5d2546ad60
|
@ -58,14 +58,17 @@ def _save_json(path: Path, data: List[Dict]) -> None:
|
||||||
|
|
||||||
|
|
||||||
def _copy_icons() -> None:
|
def _copy_icons() -> None:
|
||||||
"""Copy vendored icons to the output directory."""
|
"""Copy vendored icons and scripts to the output directory."""
|
||||||
src_dir = Path("static/icons")
|
src_dir = Path("static/icons")
|
||||||
dst_dir = OUTPUT_DIR / "icons"
|
dst_dir = OUTPUT_DIR / "icons"
|
||||||
if not src_dir.is_dir():
|
if src_dir.is_dir():
|
||||||
return
|
dst_dir.mkdir(parents=True, exist_ok=True)
|
||||||
dst_dir.mkdir(parents=True, exist_ok=True)
|
for icon in src_dir.glob("*.svg"):
|
||||||
for icon in src_dir.glob("*.svg"):
|
shutil.copy(icon, dst_dir / icon.name)
|
||||||
shutil.copy(icon, dst_dir / icon.name)
|
|
||||||
|
js_src = Path("static/chartManager.js")
|
||||||
|
if js_src.is_file():
|
||||||
|
shutil.copy(js_src, OUTPUT_DIR / js_src.name)
|
||||||
|
|
||||||
|
|
||||||
def _render_snippet(report: Dict, out_dir: Path) -> None:
|
def _render_snippet(report: Dict, out_dir: Path) -> None:
|
||||||
|
|
49
static/chartManager.js
Normal file
49
static/chartManager.js
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
export let currentLoad = null;
|
||||||
|
const loadInfo = new Map();
|
||||||
|
|
||||||
|
export function newLoad(container) {
|
||||||
|
if (currentLoad) {
|
||||||
|
abortLoad(currentLoad);
|
||||||
|
}
|
||||||
|
reset(container);
|
||||||
|
const controller = new AbortController();
|
||||||
|
const token = { controller, charts: new Map() };
|
||||||
|
loadInfo.set(token, token);
|
||||||
|
currentLoad = token;
|
||||||
|
return token;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function abortLoad(token) {
|
||||||
|
const info = loadInfo.get(token);
|
||||||
|
if (!info) return;
|
||||||
|
info.controller.abort();
|
||||||
|
info.charts.forEach(chart => {
|
||||||
|
try {
|
||||||
|
chart.destroy();
|
||||||
|
} catch (e) {}
|
||||||
|
});
|
||||||
|
loadInfo.delete(token);
|
||||||
|
if (currentLoad === token) {
|
||||||
|
currentLoad = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function registerChart(token, id, chart) {
|
||||||
|
const info = loadInfo.get(token);
|
||||||
|
if (info) {
|
||||||
|
info.charts.set(id, chart);
|
||||||
|
} else {
|
||||||
|
chart.destroy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function reset(container) {
|
||||||
|
if (!container) return;
|
||||||
|
container.querySelectorAll('canvas').forEach(c => {
|
||||||
|
const chart = Chart.getChart(c);
|
||||||
|
if (chart) {
|
||||||
|
chart.destroy();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
container.innerHTML = '';
|
||||||
|
}
|
|
@ -72,7 +72,14 @@
|
||||||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||||
<script src="https://cdn.jsdelivr.net/npm/jquery@3.7.0/dist/jquery.min.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/jquery@3.7.0/dist/jquery.min.js"></script>
|
||||||
<script src="https://cdn.datatables.net/1.13.4/js/jquery.dataTables.min.js"></script>
|
<script src="https://cdn.datatables.net/1.13.4/js/jquery.dataTables.min.js"></script>
|
||||||
<script>
|
<script type="module">
|
||||||
|
import {
|
||||||
|
newLoad,
|
||||||
|
abortLoad,
|
||||||
|
registerChart,
|
||||||
|
reset,
|
||||||
|
currentLoad,
|
||||||
|
} from './chartManager.js';
|
||||||
const intervalSelect = document.getElementById('interval-select');
|
const intervalSelect = document.getElementById('interval-select');
|
||||||
const domainSelect = document.getElementById('domain-select');
|
const domainSelect = document.getElementById('domain-select');
|
||||||
const intervalControl = document.getElementById('interval-control');
|
const intervalControl = document.getElementById('interval-control');
|
||||||
|
@ -105,20 +112,22 @@
|
||||||
let currentDomain = domainSelect.value;
|
let currentDomain = domainSelect.value;
|
||||||
let currentTab = 'overview';
|
let currentTab = 'overview';
|
||||||
|
|
||||||
function initReport(rep, base) {
|
function initReport(token, rep, base) {
|
||||||
fetch(base + '/' + rep.json)
|
fetch(base + '/' + rep.json, { signal: token.controller.signal })
|
||||||
.then(r => r.json())
|
.then(r => r.json())
|
||||||
.then(data => {
|
.then(data => {
|
||||||
|
if (token !== currentLoad) return;
|
||||||
const bucketField = rep.bucket || 'bucket';
|
const bucketField = rep.bucket || 'bucket';
|
||||||
if (rep.chart === 'table') {
|
if (rep.chart === 'table') {
|
||||||
const rows = data.map(x => [x[bucketField], x.value]);
|
const rows = data.map(x => [x[bucketField], x.value]);
|
||||||
new DataTable('#table-' + rep.name, {
|
const table = new DataTable('#table-' + rep.name, {
|
||||||
data: rows,
|
data: rows,
|
||||||
columns: [
|
columns: [
|
||||||
{ title: rep.bucket_label || 'Bucket' },
|
{ title: rep.bucket_label || 'Bucket' },
|
||||||
{ title: 'Value' }
|
{ title: 'Value' }
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
registerChart(token, rep.name, table);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -146,7 +155,7 @@
|
||||||
dataset.backgroundColor = 'rgba(54, 162, 235, 0.5)';
|
dataset.backgroundColor = 'rgba(54, 162, 235, 0.5)';
|
||||||
dataset.borderColor = 'rgba(54, 162, 235, 1)';
|
dataset.borderColor = 'rgba(54, 162, 235, 1)';
|
||||||
}
|
}
|
||||||
new Chart(document.getElementById('chart-' + rep.name), {
|
const chart = new Chart(document.getElementById('chart-' + rep.name), {
|
||||||
type: chartType,
|
type: chartType,
|
||||||
data: {
|
data: {
|
||||||
labels: labels,
|
labels: labels,
|
||||||
|
@ -154,6 +163,7 @@
|
||||||
},
|
},
|
||||||
options: options
|
options: options
|
||||||
});
|
});
|
||||||
|
registerChart(token, rep.name, chart);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -171,18 +181,7 @@
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function destroyCharts(container) {
|
// Reset helpers managed by chartManager
|
||||||
container.querySelectorAll('canvas').forEach(c => {
|
|
||||||
const chart = Chart.getChart(c);
|
|
||||||
if (chart) {
|
|
||||||
chart.destroy();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function destroyAllCharts() {
|
|
||||||
Object.values(containers).forEach(destroyCharts);
|
|
||||||
}
|
|
||||||
|
|
||||||
function loadReports() {
|
function loadReports() {
|
||||||
let path;
|
let path;
|
||||||
|
@ -196,27 +195,29 @@
|
||||||
} else {
|
} else {
|
||||||
container = containers.domain;
|
container = containers.domain;
|
||||||
if (!currentDomain) {
|
if (!currentDomain) {
|
||||||
destroyCharts(container);
|
reset(container);
|
||||||
container.innerHTML = '<p>Select a domain</p>';
|
container.innerHTML = '<p>Select a domain</p>';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
path = 'domains/' + encodeURIComponent(currentDomain) + '/' + currentInterval;
|
path = 'domains/' + encodeURIComponent(currentDomain) + '/' + currentInterval;
|
||||||
}
|
}
|
||||||
|
|
||||||
fetch(path + '/reports.json')
|
const token = newLoad(container);
|
||||||
.then(r => r.json())
|
|
||||||
.then(reports => {
|
fetch(path + '/reports.json', { signal: token.controller.signal })
|
||||||
destroyCharts(container);
|
.then(r => r.json())
|
||||||
container.innerHTML = '';
|
.then(reports => {
|
||||||
reports.forEach(rep => {
|
if (token !== currentLoad) return;
|
||||||
fetch(path + '/' + rep.html)
|
reports.forEach(rep => {
|
||||||
.then(r => r.text())
|
fetch(path + '/' + rep.html, { signal: token.controller.signal })
|
||||||
.then(html => {
|
.then(r => r.text())
|
||||||
container.insertAdjacentHTML('beforeend', html);
|
.then(html => {
|
||||||
initReport(rep, path);
|
if (token !== currentLoad) return;
|
||||||
});
|
container.insertAdjacentHTML('beforeend', html);
|
||||||
});
|
initReport(token, rep, path);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadAnalysis() {
|
function loadAnalysis() {
|
||||||
|
@ -300,7 +301,8 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
function switchTab(name) {
|
function switchTab(name) {
|
||||||
destroyAllCharts();
|
abortLoad(currentLoad);
|
||||||
|
Object.values(containers).forEach(reset);
|
||||||
currentTab = name;
|
currentTab = name;
|
||||||
tabs.forEach(tab => {
|
tabs.forEach(tab => {
|
||||||
tab.classList.toggle('is-active', tab.dataset.tab === name);
|
tab.classList.toggle('is-active', tab.dataset.tab === name);
|
||||||
|
@ -322,11 +324,16 @@
|
||||||
|
|
||||||
intervalSelect.addEventListener('change', () => {
|
intervalSelect.addEventListener('change', () => {
|
||||||
currentInterval = intervalSelect.value;
|
currentInterval = intervalSelect.value;
|
||||||
|
abortLoad(currentLoad);
|
||||||
|
reset(containers.all);
|
||||||
|
reset(containers.domain);
|
||||||
loadReports();
|
loadReports();
|
||||||
});
|
});
|
||||||
|
|
||||||
domainSelect.addEventListener('change', () => {
|
domainSelect.addEventListener('change', () => {
|
||||||
currentDomain = domainSelect.value;
|
currentDomain = domainSelect.value;
|
||||||
|
abortLoad(currentLoad);
|
||||||
|
reset(containers.domain);
|
||||||
loadReports();
|
loadReports();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue