Merge #22 chart click-to-filter into main
Lint / JS (eslint) (push) Successful in 19s
Lint / PHP requirements (version + extensions) (push) Successful in 56s
Lint / PHP (phpcs PSR-12) (push) Successful in 30s
Lint / Notify on failure (push) Skipped
Security / PHP Security (semgrep) (push) Successful in 2m3s
Lint / Deploy (push) Successful in 19s
Lint / JS (eslint) (push) Successful in 19s
Lint / PHP requirements (version + extensions) (push) Successful in 56s
Lint / PHP (phpcs PSR-12) (push) Successful in 30s
Lint / Notify on failure (push) Skipped
Security / PHP Security (semgrep) (push) Successful in 2m3s
Lint / Deploy (push) Successful in 19s
This commit is contained in:
+65
-2
@@ -277,9 +277,64 @@ include __DIR__ . '/layout_header.php';
|
||||
array_values($stats['by_category'] ?? [])
|
||||
))) ?>;
|
||||
|
||||
// ── Click-to-filter ────────────────────────────────────────────────────────
|
||||
// Charts navigate to the same URL filters the stat cards use.
|
||||
//
|
||||
// The status the click filters on has to be explicit rather than left to the
|
||||
// default: with no `status` param the controller falls back to the viewer's
|
||||
// default_status_filters preference, which can be anything, so the resulting
|
||||
// list would not necessarily match what the chart counted. StatsModel builds
|
||||
// by_priority and by_category with `status != 'Closed'`, while by_status spans
|
||||
// every status — so only the priority and category charts pin the open set.
|
||||
function openStatuses() {
|
||||
var all = window.TICKET_STATUSES || ['Open', 'Pending', 'In Progress', 'Closed'];
|
||||
return all.filter(function(s) { return s !== 'Closed'; }).join(',');
|
||||
}
|
||||
|
||||
function gotoFilter(params) {
|
||||
var qs = new URLSearchParams();
|
||||
Object.keys(params).forEach(function(k) {
|
||||
if (params[k] !== null && params[k] !== undefined && params[k] !== '') qs.set(k, params[k]);
|
||||
});
|
||||
window.location.href = '/?' + qs.toString();
|
||||
}
|
||||
|
||||
// Each chart maps a clicked label to a filter. Returns null when the label
|
||||
// can't be mapped, so the click is simply ignored.
|
||||
var CHART_FILTERS = {
|
||||
chartPriority: function(label) {
|
||||
var m = /^P(\d+)$/.exec(label);
|
||||
return m ? { priority: m[1], status: openStatuses() } : null;
|
||||
},
|
||||
chartStatus: function(label) {
|
||||
return label ? { status: label } : null;
|
||||
},
|
||||
chartCategory: function(label) {
|
||||
return label ? { category: label, status: openStatuses() } : null;
|
||||
}
|
||||
};
|
||||
|
||||
function filterOnClick(canvasId) {
|
||||
return function(evt, elements, chart) {
|
||||
if (!elements || !elements.length) return;
|
||||
var label = chart.data.labels[elements[0].index];
|
||||
var mapper = CHART_FILTERS[canvasId];
|
||||
var params = mapper && mapper(label);
|
||||
if (params) gotoFilter(params);
|
||||
};
|
||||
}
|
||||
|
||||
// Pointer cursor over clickable segments so the affordance is visible.
|
||||
function filterOnHover(evt, elements) {
|
||||
if (evt && evt.native && evt.native.target) {
|
||||
evt.native.target.style.cursor = (elements && elements.length) ? 'pointer' : 'default';
|
||||
}
|
||||
}
|
||||
|
||||
function makeDonut(canvasId, data, colorMap) {
|
||||
var ctx = document.getElementById(canvasId);
|
||||
if (!ctx || !data.length) return;
|
||||
ctx.title = 'Click a segment to filter the ticket list';
|
||||
return new Chart(ctx, {
|
||||
type: 'doughnut',
|
||||
data: {
|
||||
@@ -295,12 +350,14 @@ include __DIR__ . '/layout_header.php';
|
||||
},
|
||||
options: {
|
||||
responsive: true, maintainAspectRatio: false,
|
||||
onClick: filterOnClick(canvasId),
|
||||
onHover: filterOnHover,
|
||||
plugins: {
|
||||
legend: {
|
||||
position: 'bottom',
|
||||
labels: { color: '#8fa3b1', font: { family: 'monospace', size: 10 }, padding: 8, boxWidth: 10 }
|
||||
},
|
||||
tooltip: { callbacks: { label: function(ctx) { return ' ' + ctx.label + ': ' + ctx.parsed; } } }
|
||||
tooltip: { callbacks: { label: function(ctx) { return ' ' + ctx.label + ': ' + ctx.parsed + ' — click to filter'; } } }
|
||||
},
|
||||
cutout: '68%'
|
||||
}
|
||||
@@ -310,6 +367,7 @@ include __DIR__ . '/layout_header.php';
|
||||
function makeBar(canvasId, data) {
|
||||
var ctx = document.getElementById(canvasId);
|
||||
if (!ctx || !data.length) return;
|
||||
ctx.title = 'Click a bar to filter the ticket list';
|
||||
return new Chart(ctx, {
|
||||
type: 'bar',
|
||||
data: {
|
||||
@@ -323,7 +381,12 @@ include __DIR__ . '/layout_header.php';
|
||||
},
|
||||
options: {
|
||||
indexAxis: 'y', responsive: true, maintainAspectRatio: false,
|
||||
plugins: { legend: { display: false } },
|
||||
onClick: filterOnClick(canvasId),
|
||||
onHover: filterOnHover,
|
||||
plugins: {
|
||||
legend: { display: false },
|
||||
tooltip: { callbacks: { label: function(ctx) { return ' ' + ctx.parsed.x + ' — click to filter'; } } }
|
||||
},
|
||||
scales: {
|
||||
x: { ticks: { color: '#8fa3b1', font: { size: 10 } }, grid: { color: 'rgba(0,255,65,0.06)' } },
|
||||
y: { ticks: { color: '#8fa3b1', font: { family: 'monospace', size: 10 } }, grid: { display: false } }
|
||||
|
||||
Reference in New Issue
Block a user