Updated to table captions and hiding the legend on the dashboard if it's empty

This commit is contained in:
Jonathan Bobel
2024-10-08 16:05:00 -04:00
parent 3865c5b952
commit 98fb68ad14
3 changed files with 174 additions and 217 deletions

View File

@@ -33,36 +33,57 @@
.attr('id', 'tooltip') .attr('id', 'tooltip')
.style('display', 'none'); .style('display', 'none');
} }
// Create legend
// Check if there is any data in deliveredData or failedData before creating the legend
const totalMessages = d3.sum(deliveredData) + d3.sum(failedData);
const legendContainer = d3.select('.chart-legend'); const legendContainer = d3.select('.chart-legend');
legendContainer.selectAll('*').remove(); // Clear any existing legend
const legendData = [ if (totalMessages === 0) {
{ label: 'Delivered', color: COLORS.delivered }, legendContainer.style('display', 'none'); // Try manually setting this in the console to see if it hides the legend
{ label: 'Failed', color: COLORS.failed } console.log('Hiding legend'); // Ensure this branch is being hit
]; } else {
legendContainer.style('display', 'flex');
console.log('Showing legend');
}
const legendItem = legendContainer.selectAll('.legend-item') if (totalMessages > 0) {
.data(legendData) // Create legend only if there is data
.enter() legendContainer.selectAll('*').remove(); // Clear any existing legend
.append('div')
.attr('class', 'legend-item');
legendItem.append('div') const legendData = [
.attr('class', 'legend-rect') { label: 'Delivered', color: COLORS.delivered },
.style('background-color', d => d.color) { label: 'Failed', color: COLORS.failed }
.style('display', 'inline-block') ];
.style('margin-right', '5px');
legendItem.append('span') const legendItem = legendContainer.selectAll('.legend-item')
.attr('class', 'legend-label') .data(legendData)
.text(d => d.label); .enter()
.append('div')
.attr('class', 'legend-item');
legendItem.append('div')
.attr('class', 'legend-rect')
.style('background-color', d => d.color)
.style('display', 'inline-block')
.style('margin-right', '5px');
legendItem.append('span')
.attr('class', 'legend-label')
.text(d => d.label);
legendContainer.style('display', 'flex'); // Ensure the legend is shown
} else {
// Hide legend container if there is no data
legendContainer.style('display', 'none'); // Hide the legend
}
// Proceed with creating the chart as usual
const x = d3.scaleBand() const x = d3.scaleBand()
.domain(labels) .domain(labels)
.range([0, width]) .range([0, width])
.padding(0.1); .padding(0.1);
// Adjust the y-axis domain to add some space above the tallest bar
const maxY = d3.max(deliveredData.map((d, i) => d + (failedData[i] || 0))); const maxY = d3.max(deliveredData.map((d, i) => d + (failedData[i] || 0)));
const y = d3.scaleSqrt() const y = d3.scaleSqrt()
.domain([0, maxY + 2]) // Add 2 units of space at the top .domain([0, maxY + 2]) // Add 2 units of space at the top
@@ -74,23 +95,20 @@
.attr('transform', `translate(0,${height})`) .attr('transform', `translate(0,${height})`)
.call(d3.axisBottom(x)); .call(d3.axisBottom(x));
// Generate the y-axis with whole numbers
const yAxis = d3.axisLeft(y) const yAxis = d3.axisLeft(y)
.ticks(Math.min(maxY + 2, 10)) // Generate up to 10 ticks based on the data .ticks(Math.min(maxY + 2, 10))
.tickFormat(d3.format('d')); // Ensure whole numbers on the y-axis .tickFormat(d3.format('d'));
svg.append('g') svg.append('g')
.attr('class', 'y axis') .attr('class', 'y axis')
.call(yAxis); .call(yAxis);
// Data for stacking
const stackData = labels.map((label, i) => ({ const stackData = labels.map((label, i) => ({
label: label, label: label,
delivered: deliveredData[i], delivered: deliveredData[i],
failed: failedData[i] || 0 // Ensure there's a value for failed, even if it's 0 failed: failedData[i] || 0 // Ensure there's a value for failed, even if it's 0
})); }));
// Stack the data
const stack = d3.stack() const stack = d3.stack()
.keys(['delivered', 'failed']) .keys(['delivered', 'failed'])
.order(d3.stackOrderNone) .order(d3.stackOrderNone)
@@ -98,203 +116,137 @@
const series = stack(stackData); const series = stack(stackData);
// Color scale
const color = d3.scaleOrdinal() const color = d3.scaleOrdinal()
.domain(['delivered', 'failed']) .domain(['delivered', 'failed'])
.range([COLORS.delivered, COLORS.failed]); .range([COLORS.delivered, COLORS.failed]);
// Create bars with animation const barGroups = svg.selectAll('.bar-group')
const barGroups = svg.selectAll('.bar-group') .data(series)
.data(series) .enter()
.enter() .append('g')
.append('g') .attr('class', 'bar-group')
.attr('class', 'bar-group') .attr('fill', d => color(d.key));
.attr('fill', d => color(d.key));
barGroups.selectAll('rect') barGroups.selectAll('rect')
.data(d => d) .data(d => d)
.enter() .enter()
.append('rect') .append('rect')
.attr('x', d => x(d.data.label)) .attr('x', d => x(d.data.label))
.attr('y', height) .attr('y', height)
.attr('height', 0) .attr('height', 0)
.attr('width', x.bandwidth()) .attr('width', x.bandwidth())
.on('mouseover', function(event, d) { .on('mouseover', function(event, d) {
const key = d3.select(this.parentNode).datum().key; const key = d3.select(this.parentNode).datum().key;
const capitalizedKey = key.charAt(0).toUpperCase() + key.slice(1); const capitalizedKey = key.charAt(0).toUpperCase() + key.slice(1);
tooltip.style('display', 'block') tooltip.style('display', 'block')
.html(`${d.data.label}<br>${capitalizedKey}: ${d.data[key]}`); .html(`${d.data.label}<br>${capitalizedKey}: ${d.data[key]}`);
}) })
.on('mousemove', function(event) { .on('mousemove', function(event) {
tooltip.style('left', `${event.pageX + 10}px`) tooltip.style('left', `${event.pageX + 10}px`)
.style('top', `${event.pageY - 20}px`); .style('top', `${event.pageY - 20}px`);
}) })
.on('mouseout', function() { .on('mouseout', function() {
tooltip.style('display', 'none'); tooltip.style('display', 'none');
}) })
.transition() .transition()
.duration(1000) .duration(1000)
.attr('y', d => y(d[1])) .attr('y', d => y(d[1]))
.attr('height', d => y(d[0]) - y(d[1])); .attr('height', d => y(d[0]) - y(d[1]));
}; };
// Function to create an accessible table const fetchData = function(type) {
const createTable = function(tableId, chartType, labels, deliveredData, failedData) { const ctx = document.getElementById('weeklyChart');
const table = document.getElementById(tableId); if (!ctx) {
table.innerHTML = ""; // Clear previous data return;
const captionText = document.querySelector(`#${chartType} .chart-subtitle`).textContent;
const caption = document.createElement('caption');
caption.textContent = captionText;
const thead = document.createElement('thead');
const tbody = document.createElement('tbody');
// Create table header
const headerRow = document.createElement('tr');
const headers = ['Day', 'Delivered', 'Failed'];
headers.forEach(headerText => {
const th = document.createElement('th');
th.textContent = headerText;
headerRow.appendChild(th);
});
thead.appendChild(headerRow);
// Create table body
labels.forEach((label, index) => {
const row = document.createElement('tr');
const cellDay = document.createElement('td');
cellDay.textContent = label;
row.appendChild(cellDay);
const cellDelivered = document.createElement('td');
cellDelivered.textContent = deliveredData[index];
row.appendChild(cellDelivered);
const cellFailed = document.createElement('td');
cellFailed.textContent = failedData[index];
row.appendChild(cellFailed);
tbody.appendChild(row);
});
table.appendChild(caption);
table.appendChild(thead);
table.append(tbody);
};
const fetchData = function(type) {
var ctx = document.getElementById('weeklyChart');
if (!ctx) {
return;
}
var url = type === 'service' ? `/daily_stats.json` : `/daily_stats_by_user.json`;
return fetch(url)
.then(response => {
if (!response.ok) {
throw new Error('Network response was not ok');
}
return response.json();
})
.then(data => {
labels = [];
deliveredData = [];
failedData = [];
let totalMessages = 0;
for (var dateString in data) {
if (data.hasOwnProperty(dateString)) {
const dateParts = dateString.split('-');
const formattedDate = `${dateParts[1]}/${dateParts[2]}/${dateParts[0].slice(2)}`;
labels.push(formattedDate);
deliveredData.push(data[dateString].sms.delivered);
failedData.push(data[dateString].sms.failure);
// Calculate the total number of messages
totalMessages += data[dateString].sms.delivered + data[dateString].sms.failure;
}
}
// Check if there are no messages sent
const subTitle = document.querySelector(`#activityChartContainer .chart-subtitle`);
if (totalMessages === 0) {
// Remove existing chart and render the alert message
d3.select('#weeklyChart').selectAll('*').remove();
d3.select('#weeklyChart')
.append('div')
.html(`
<div class="usa-alert usa-alert--info usa-alert--slim">
<div class="usa-alert__body">
<p class="usa-alert__text">
No messages sent in the last 7 days
</p>
</div>
</div>
`);
// Hide the subtitle
if (subTitle) {
subTitle.style.display = 'none';
}
} else {
// If there are messages, create the chart and table
createChart('#weeklyChart', labels, deliveredData, failedData);
createTable('weeklyTable', 'activityChart', labels, deliveredData, failedData);
}
return data;
})
.catch(error => console.error('Error fetching daily stats:', error));
};
const handleDropdownChange = function(event) {
const selectedValue = event.target.value;
const subTitle = document.querySelector(`#activityChartContainer .chart-subtitle`);
const selectElement = document.getElementById('options');
const selectedText = selectElement.options[selectElement.selectedIndex].text;
subTitle.textContent = `${selectedText} - last 7 days`;
fetchData(selectedValue);
// Update ARIA live region
const liveRegion = document.getElementById('aria-live-account');
liveRegion.textContent = `Data updated for ${selectedText} - last 7 days`;
// Switch tables based on dropdown selection
const selectedTable = selectedValue === "individual" ? "table1" : "table2";
const tables = document.querySelectorAll('.table-overflow-x-auto');
tables.forEach(function(table) {
table.classList.add('hidden'); // Hide all tables by adding the hidden class
table.classList.remove('visible'); // Ensure they are not visible
});
const tableToShow = document.getElementById(selectedTable);
tableToShow.classList.remove('hidden'); // Remove hidden class
tableToShow.classList.add('visible'); // Add visible class
};
document.addEventListener('DOMContentLoaded', function() {
// Initialize activityChart chart and table with service data by default
fetchData('service');
// Add event listener to the dropdown
const dropdown = document.getElementById('options');
dropdown.addEventListener('change', handleDropdownChange);
});
// Resize chart on window resize
window.addEventListener('resize', function() {
if (labels.length > 0 && deliveredData.length > 0 && failedData.length > 0) {
createChart('#weeklyChart', labels, deliveredData, failedData);
createTable('weeklyTable', 'activityChart', labels, deliveredData, failedData);
} }
});
// Export functions for testing const url = type === 'service' ? `/daily_stats.json` : `/daily_stats_by_user.json`;
window.createChart = createChart; return fetch(url)
window.createTable = createTable; .then(response => {
window.handleDropdownChange = handleDropdownChange; if (!response.ok) {
window.fetchData = fetchData; throw new Error('Network response was not ok');
}
return response.json();
})
.then(data => {
let labels = [];
let deliveredData = [];
let failedData = [];
let totalMessages = 0;
for (var dateString in data) {
if (data.hasOwnProperty(dateString)) {
const dateParts = dateString.split('-');
const formattedDate = `${dateParts[1]}/${dateParts[2]}/${dateParts[0].slice(2)}`;
labels.push(formattedDate);
deliveredData.push(data[dateString].sms.delivered || 0); // Fallback to 0 if missing
failedData.push(data[dateString].sms.failure || 0); // Fallback to 0 if missing
totalMessages += data[dateString].sms.delivered + data[dateString].sms.failure;
}
}
const subTitle = document.querySelector(`#activityChartContainer .chart-subtitle`);
const liveRegion = document.getElementById('aria-live-account');
if (totalMessages === 0) {
d3.select('#weeklyChart').selectAll('*').remove();
d3.select('#weeklyChart')
.append('div')
.html(`
<div class="usa-alert usa-alert--info usa-alert--slim" aria-live="polite">
<div class="usa-alert__body">
<p class="usa-alert__text">
No messages sent in the last 7 days
</p>
</div>
</div>
`);
if (subTitle) {
subTitle.style.display = 'none';
}
liveRegion.textContent = `No data available for ${type} - last 7 days.`;
// Here: Check if the legend exists and remove it if necessary
const legendContainer = document.querySelector('.chart-legend');
if (legendContainer) {
console.log('Legend exists, hiding it...');
legendContainer.style.display = 'none'; // Hide the legend
} else {
console.log('Legend does not exist at this point.');
}
} else {
createChart('#weeklyChart', labels, deliveredData, failedData);
liveRegion.textContent = `Data updated for ${type} - last 7 days.`;
// Check if legend should be shown after chart is created
const legendContainer = document.querySelector('.chart-legend');
if (legendContainer) {
console.log('Legend exists, showing it...');
legendContainer.style.display = 'flex'; // Show the legend
} else {
console.log('Legend does not exist at this point.');
}
}
return data;
})
.catch(error => console.error('Error fetching daily stats:', error));
};
document.addEventListener('DOMContentLoaded', function() {
fetchData('service');
const dropdown = document.getElementById('options');
dropdown.addEventListener('change', function(event) {
fetchData(event.target.value);
});
});
} }
})(window); })(window);

View File

@@ -60,7 +60,7 @@
<h2 class="margin-top-4 margin-bottom-1">Sent jobs</h2> <h2 class="margin-top-4 margin-bottom-1">Sent jobs</h2>
<div class="usa-table-container--scrollable-mobile"> <div class="usa-table-container--scrollable-mobile">
<table class="usa-table usa-table--compact job-table"> <table class="usa-table usa-table--compact job-table">
<caption></caption> <caption class="usa-sr-only">Table showing all sent jobs for this service</caption>
<thead class="table-field-headings"> <thead class="table-field-headings">
<tr> <tr>
<th scope="col" role="columnheader" class="table-field-heading-first" id="jobId"> <th scope="col" role="columnheader" class="table-field-heading-first" id="jobId">
@@ -103,7 +103,10 @@
<td class="table-field sender">{{ job.created_by.name }}</td> <td class="table-field sender">{{ job.created_by.name }}</td>
<td class="table-field report"> <td class="table-field report">
{% if job.time_left != "Data no longer available" %} {% if job.time_left != "Data no longer available" %}
<a href="{{ job.download_link }}"><img src="{{ url_for('static', filename='img/material-icons/file_download.svg') }}" alt="File Download Icon"></a> <a href="{{ job.download_link }}">
<img src="{{ url_for('static', filename='img/material-icons/file_download.svg') }}" alt="">
<span class="usa-sr-only">Download report link</span>
</a>
{% else %} {% else %}
<span>N/A</span> <span>N/A</span>
{% endif %} {% endif %}

View File

@@ -38,7 +38,7 @@
<form class="usa-form"> <form class="usa-form">
<label class="usa-label" for="options">Account</label> <label class="usa-label" for="options">Account</label>
<select class="usa-select margin-bottom-2" name="options" id="options"> <select class="usa-select margin-bottom-2" name="options" id="options">
<option value>- Select -</option> <option value disabled>- Select -</option>
<option value="service" selected>{{ current_service.name }}</option> <option value="service" selected>{{ current_service.name }}</option>
<option value="individual">{{ current_user.name }}</option> <option value="individual">{{ current_user.name }}</option>
</select> </select>
@@ -60,6 +60,7 @@
<div id="table1" class="table-overflow-x-auto hidden"> <div id="table1" class="table-overflow-x-auto hidden">
<h2 class="margin-top-4 margin-bottom-1">My activity</h2> <h2 class="margin-top-4 margin-bottom-1">My activity</h2>
<table class="usa-table job-table"> <table class="usa-table job-table">
<caption class="usa-sr-only">Table showing the sent jobs for {{current_user.name}}</caption>
<thead class="table-field-headings"> <thead class="table-field-headings">
<tr> <tr>
<th scope="col" class="table-field-heading-first" id="jobId"><span>Job ID#</span></th> <th scope="col" class="table-field-heading-first" id="jobId"><span>Job ID#</span></th>
@@ -100,6 +101,7 @@
<div id="table2" class="table-overflow-x-auto visible"> <div id="table2" class="table-overflow-x-auto visible">
<h2 class="margin-top-4 margin-bottom-1">Service activity</h2> <h2 class="margin-top-4 margin-bottom-1">Service activity</h2>
<table class="usa-table job-table"> <table class="usa-table job-table">
<caption class="usa-sr-only">Table showing the sent jobs for this service</caption>
<thead class="table-field-headings"> <thead class="table-field-headings">
<tr> <tr>
<th scope="col" role="columnheader" class="table-field-heading-first" id="jobId"><span>Job ID#</span></th> <th scope="col" role="columnheader" class="table-field-heading-first" id="jobId"><span>Job ID#</span></th>