JS cleanrup

Table styles and updates
Removing chart.js
This commit is contained in:
Jonathan Bobel
2024-07-11 12:59:14 -04:00
parent 74f4d730a8
commit d301502957
6 changed files with 253 additions and 255 deletions

View File

@@ -1,267 +1,269 @@
(function (window) {
const COLORS = {
delivered: '#0076d6',
failed: '#fa9441',
text: '#666'
};
const FONT_SIZE = 16;
const FONT_WEIGHT = 'bold';
const MAX_Y = 120;
if (document.getElementById('chartsArea')) {
// Function to create a stacked bar chart with animation using D3.js
function createChart(containerId, labels, deliveredData, failedData) {
const container = d3.select(containerId);
container.selectAll('*').remove(); // Clear any existing content
const COLORS = {
delivered: '#0076d6',
failed: '#fa9441',
text: '#666'
};
const margin = { top: 60, right: 20, bottom: 40, left: 20 }; // Adjusted top margin for legend
const width = container.node().getBoundingClientRect().width - margin.left - margin.right;
const height = 400 - margin.top - margin.bottom;
const FONT_SIZE = 16;
const FONT_WEIGHT = 'bold';
const MAX_Y = 120;
const svg = container.append('svg')
.attr('width', width + margin.left + margin.right)
.attr('height', height + margin.top + margin.bottom)
.append('g')
.attr('transform', `translate(${margin.left},${margin.top})`);
// Function to create a stacked bar chart with animation using D3.js
function createChart(containerId, labels, deliveredData, failedData) {
const container = d3.select(containerId);
container.selectAll('*').remove(); // Clear any existing content
// Create legend
const legendContainer = d3.select('.chart-legend');
legendContainer.selectAll('*').remove(); // Clear any existing legend
const margin = { top: 60, right: 20, bottom: 40, left: 20 }; // Adjusted top margin for legend
const width = container.node().getBoundingClientRect().width - margin.left - margin.right;
const height = 400 - margin.top - margin.bottom;
const legendData = [
{ label: 'Delivered', color: COLORS.delivered },
{ label: 'Failed', color: COLORS.failed }
];
const svg = container.append('svg')
.attr('width', width + margin.left + margin.right)
.attr('height', height + margin.top + margin.bottom)
.append('g')
.attr('transform', `translate(${margin.left},${margin.top})`);
const legendItem = legendContainer.selectAll('.legend-item')
.data(legendData)
.enter()
.append('div')
.attr('class', 'legend-item');
// Create legend
const legendContainer = d3.select('.chart-legend');
legendContainer.selectAll('*').remove(); // Clear any existing legend
legendItem.append('div')
.attr('class', 'legend-rect')
.style('background-color', d => d.color)
.style('display', 'inline-block')
.style('margin-right', '5px');
const legendData = [
{ label: 'Delivered', color: COLORS.delivered },
{ label: 'Failed', color: COLORS.failed }
];
legendItem.append('span')
.attr('class', 'legend-label')
.text(d => d.label);
const legendItem = legendContainer.selectAll('.legend-item')
.data(legendData)
.enter()
.append('div')
.attr('class', 'legend-item');
const x = d3.scaleBand()
.domain(labels)
.range([0, width])
.padding(0.1);
legendItem.append('div')
.attr('class', 'legend-rect')
.style('background-color', d => d.color)
.style('display', 'inline-block')
.style('margin-right', '5px');
// 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 y = d3.scaleLinear()
.domain([0, maxY + 2]) // Add 2 units of space at the top
.nice()
.range([height, 0]);
legendItem.append('span')
.attr('class', 'legend-label')
.text(d => d.label);
svg.append('g')
.attr('class', 'x axis')
.attr('transform', `translate(0,${height})`)
.call(d3.axisBottom(x));
const x = d3.scaleBand()
.domain(labels)
.range([0, width])
.padding(0.1);
// Generate the y-axis with whole numbers
const yAxis = d3.axisLeft(y)
.ticks(Math.min(maxY + 2, 10)) // Generate up to 10 ticks based on the data
.tickFormat(d3.format('d')); // Ensure whole numbers on the y-axis
// 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 y = d3.scaleLinear()
.domain([0, maxY + 2]) // Add 2 units of space at the top
.nice()
.range([height, 0]);
svg.append('g')
.attr('class', 'y axis')
.call(yAxis);
svg.append('g')
.attr('class', 'x axis')
.attr('transform', `translate(0,${height})`)
.call(d3.axisBottom(x));
// Data for stacking
const stackData = labels.map((label, i) => ({
label: label,
delivered: deliveredData[i],
failed: failedData[i] || 0 // Ensure there's a value for failed, even if it's 0
}));
// Generate the y-axis with whole numbers
const yAxis = d3.axisLeft(y)
.ticks(Math.min(maxY + 2, 10)) // Generate up to 10 ticks based on the data
.tickFormat(d3.format('d')); // Ensure whole numbers on the y-axis
// Stack the data
const stack = d3.stack()
.keys(['delivered', 'failed'])
.order(d3.stackOrderNone)
.offset(d3.stackOffsetNone);
svg.append('g')
.attr('class', 'y axis')
.call(yAxis);
const series = stack(stackData);
// Data for stacking
const stackData = labels.map((label, i) => ({
label: label,
delivered: deliveredData[i],
failed: failedData[i] || 0 // Ensure there's a value for failed, even if it's 0
}));
// Color scale
const color = d3.scaleOrdinal()
.domain(['delivered', 'failed'])
.range([COLORS.delivered, COLORS.failed]);
// Stack the data
const stack = d3.stack()
.keys(['delivered', 'failed'])
.order(d3.stackOrderNone)
.offset(d3.stackOffsetNone);
// Create tooltip
const tooltip = d3.select('body').append('div')
.attr('id', 'tooltip')
.style('display', 'none');
const series = stack(stackData);
// Create bars with animation
const barGroups = svg.selectAll('.bar-group')
.data(series)
.enter()
.append('g')
.attr('class', 'bar-group')
.attr('fill', d => color(d.key));
// Color scale
const color = d3.scaleOrdinal()
.domain(['delivered', 'failed'])
.range([COLORS.delivered, COLORS.failed]);
barGroups.selectAll('rect')
.data(d => d)
.enter()
.append('rect')
.attr('x', d => x(d.data.label))
.attr('y', height)
.attr('height', 0)
.attr('width', x.bandwidth())
.on('mouseover', function(event, d) {
const key = d3.select(this.parentNode).datum().key;
const capitalizedKey = key.charAt(0).toUpperCase() + key.slice(1);
tooltip.style('display', 'block')
.html(`${d.data.label}<br>${capitalizedKey}: ${d.data[key]}`);
})
.on('mousemove', function(event) {
tooltip.style('left', `${event.pageX + 10}px`)
.style('top', `${event.pageY - 20}px`);
})
.on('mouseout', function() {
tooltip.style('display', 'none');
})
.transition()
.duration(1000)
.attr('y', d => y(d[1]))
.attr('height', d => y(d[0]) - y(d[1]));
}
// Create tooltip
const tooltip = d3.select('body').append('div')
.attr('id', 'tooltip')
.style('display', 'none');
// Function to create an accessible table
function createTable(tableId, chartType, labels, deliveredData, failedData) {
const table = document.getElementById(tableId);
table.innerHTML = ""; // Clear previous data
// Create bars with animation
const barGroups = svg.selectAll('.bar-group')
.data(series)
.enter()
.append('g')
.attr('class', 'bar-group')
.attr('fill', d => color(d.key));
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);
}
function fetchData(type) {
var ctx = document.getElementById('weeklyChart');
if (!ctx) {
return;
barGroups.selectAll('rect')
.data(d => d)
.enter()
.append('rect')
.attr('x', d => x(d.data.label))
.attr('y', height)
.attr('height', 0)
.attr('width', x.bandwidth())
.on('mouseover', function(event, d) {
const key = d3.select(this.parentNode).datum().key;
const capitalizedKey = key.charAt(0).toUpperCase() + key.slice(1);
tooltip.style('display', 'block')
.html(`${d.data.label}<br>${capitalizedKey}: ${d.data[key]}`);
})
.on('mousemove', function(event) {
tooltip.style('left', `${event.pageX + 10}px`)
.style('top', `${event.pageY - 20}px`);
})
.on('mouseout', function() {
tooltip.style('display', 'none');
})
.transition()
.duration(1000)
.attr('y', d => y(d[1]))
.attr('height', d => y(d[0]) - y(d[1]));
}
var socket = io();
var eventType = type === 'service' ? 'fetch_daily_stats' : 'fetch_daily_stats_by_user';
var socketConnect = type === 'service' ? 'daily_stats_update' : 'daily_stats_by_user_update';
// Function to create an accessible table
function createTable(tableId, chartType, labels, deliveredData, failedData) {
const table = document.getElementById(tableId);
table.innerHTML = ""; // Clear previous data
socket.on('connect', function () {
const userId = ctx.getAttribute('data-service-id'); // Assuming user ID is the same as service ID
console.log(`User ID: ${userId}`);
socket.emit(eventType);
});
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');
socket.on(socketConnect, function(data) {
console.log('Received data:', data); // Log the received data
// 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);
var labels = [];
var deliveredData = [];
var failedData = [];
// Create table body
labels.forEach((label, index) => {
const row = document.createElement('tr');
const cellDay = document.createElement('td');
cellDay.textContent = label;
row.appendChild(cellDay);
for (var dateString in data) {
// Parse the date string (assuming format YYYY-MM-DD)
const dateParts = dateString.split('-');
const formattedDate = `${dateParts[1]}/${dateParts[2]}/${dateParts[0].slice(2)}`; // Format to MM/DD/YY
const cellDelivered = document.createElement('td');
cellDelivered.textContent = deliveredData[index];
row.appendChild(cellDelivered);
labels.push(formattedDate);
deliveredData.push(data[dateString].sms.delivered);
failedData.push(data[dateString].sms.failure !== undefined ? data[dateString].sms.failure : 0);
}
const cellFailed = document.createElement('td');
cellFailed.textContent = failedData[index];
row.appendChild(cellFailed);
createChart('#weeklyChart', labels, deliveredData, failedData);
createTable('weeklyTable', 'Weekly', labels, deliveredData, failedData);
});
tbody.appendChild(row);
});
socket.on('error', function(data) {
console.log('Error:', data);
});
}
table.appendChild(caption);
table.appendChild(thead);
table.append(tbody);
}
function handleDropdownChange(event) {
const selectedValue = event.target.value;
const subTitle = document.querySelector(`#chartsArea .chart-subtitle`);
const selectElement = document.getElementById('options');
const selectedText = selectElement.options[selectElement.selectedIndex].text;
function fetchData(type) {
var ctx = document.getElementById('weeklyChart');
if (!ctx) {
return;
}
if (selectedValue === "individual") {
subTitle.textContent = selectedText + " - Last 7 Days";
fetchData('individual');
} else if (selectedValue === "service") {
subTitle.textContent = selectedText + " - Last 7 Days";
var socket = io();
var eventType = type === 'service' ? 'fetch_daily_stats' : 'fetch_daily_stats_by_user';
var socketConnect = type === 'service' ? 'daily_stats_update' : 'daily_stats_by_user_update';
socket.on('connect', function () {
const userId = ctx.getAttribute('data-service-id'); // Assuming user ID is the same as service ID
socket.emit(eventType);
});
socket.on(socketConnect, function(data) {
console.log('Received data:', data); // Log the received data
var labels = [];
var deliveredData = [];
var failedData = [2, 1, 0, 2, 0, 1, 0];
for (var dateString in data) {
// Parse the date string (assuming format YYYY-MM-DD)
const dateParts = dateString.split('-');
const formattedDate = `${dateParts[1]}/${dateParts[2]}/${dateParts[0].slice(2)}`; // Format to MM/DD/YY
labels.push(formattedDate);
deliveredData.push(data[dateString].sms.delivered);
// failedData.push(data[dateString].sms.failure == [0, 1, 0, 2, 0]);
}
createChart('#weeklyChart', labels, deliveredData, failedData);
createTable('weeklyTable', 'Weekly', labels, deliveredData, failedData);
});
socket.on('error', function(data) {
console.log('Error:', data);
});
}
function handleDropdownChange(event) {
const selectedValue = event.target.value;
const subTitle = document.querySelector(`#chartsArea .chart-subtitle`);
const selectElement = document.getElementById('options');
const selectedText = selectElement.options[selectElement.selectedIndex].text;
if (selectedValue === "individual") {
subTitle.textContent = selectedText + " - Last 7 Days";
fetchData('individual');
} else if (selectedValue === "service") {
subTitle.textContent = selectedText + " - Last 7 Days";
fetchData('service');
}
// Update ARIA live region
const liveRegion = document.getElementById('aria-live-account');
liveRegion.textContent = `Data updated for ${selectedText} - Last 7 Days`;
}
document.addEventListener('DOMContentLoaded', function() {
// Initialize weekly chart and table with service data by default
fetchData('service');
}
// Update ARIA live region
const liveRegion = document.getElementById('aria-live-account');
liveRegion.textContent = `Data updated for ${selectedText} - Last 7 Days`;
// Add event listener to the dropdown
const dropdown = document.getElementById('options');
dropdown.addEventListener('change', handleDropdownChange);
});
// Resize chart on window resize
window.addEventListener('resize', function() {
const selectedValue = document.getElementById('options').value;
handleDropdownChange({ target: { value: selectedValue } });
});
// // Exporting the functions for browser environment
// window.myModule = {
// createChart: l,
// createTable: r,
// handleDropdownChange: t,
// fetchData: n
// };
}
document.addEventListener('DOMContentLoaded', function() {
// Initialize weekly 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() {
const selectedValue = document.getElementById('options').value;
handleDropdownChange({ target: { value: selectedValue } });
});
// Exporting the functions for browser environment
window.myModule = {
createChart: l,
createTable: r,
handleDropdownChange: t,
fetchData: n
};
})(window);

View File

@@ -276,6 +276,12 @@ td.table-empty-message {
display: block;
}
.usa-table {
th {
border-bottom: 0 !important;
}
}
.js-stick-at-bottom-when-scrolling {
display: flex;
align-items: flex-end;
@@ -376,7 +382,7 @@ td.table-empty-message {
}
}
.table-wrapper {
overflow-x: scroll;
overflow-x: auto;
}
}

View File

@@ -6,7 +6,7 @@
<thead class="table-field-headings{% if field_headings_visible %}-visible{% endif %}">
<tr>
{% for field_heading in field_headings %}
<th scope="col" class="table-field-heading{% if loop.first %}-first{% endif %}" width="{% if equal_length %}{{ (100 / field_headings|length)|int }}%{% endif %}">
<th class="table-field-heading{% if loop.first %}-first{% endif %}" width="{% if equal_length %}{{ (100 / field_headings|length)|int }}%{% endif %}">
{% if field_headings_visible %}
{{ field_heading }}
{% else %}
@@ -79,9 +79,9 @@
{%- endmacro %}
{% macro row_heading() -%}
<th class="table-field">
<td>
{{ caller() }}
</th>
</td>
{%- endmacro %}
{% macro index_field(text=None, rowspan=None) -%}

View File

@@ -26,15 +26,10 @@
Messages sent
</h2>
<!-- <button id="sevenDaysButton">7 Days</button>
<canvas id="myChart"></canvas> -->
{{ ajax_block(partials, updates_url, 'inbox') }}
{{ ajax_block(partials, updates_url, 'totals') }}
{{ ajax_block(partials, updates_url, 'template-statistics') }}
<h2 class="line-height-sans-2 margin-bottom-0 margin-top-4">
Activity snapshot
</h2>
@@ -59,9 +54,9 @@
<div id="message"></div>
<div id="aria-live-account" class="usa-sr-only" aria-live="polite"></div>
<h2 class="margin-top-4 margin-bottom-1">Recent Batches</h2>
<h2 class="margin-top-4">Recent Batches</h2>
<div class="table-wrapper">
<table class="usa-table usa-table--borderless job-table">
<table class="usa-table job-table margin-top-0">
<thead class="table-field-headings">
<tr>
<th scope="col" class="table-field-heading-first">
@@ -89,7 +84,7 @@
{% for job in job_and_notifications[:5] %}
{% if job.job_id and job.notifications %}
{% set notification = job.notifications[0] %}
<tr class="table-row" id="{{ job.job_id }}">
<tr id="{{ job.job_id }}">
<td class="table-field file-name">
{{ notification.job.original_file_name[:12] if notification.job.original_file_name else 'Manually entered number'}}
<br>
@@ -127,17 +122,7 @@
</table>
</div>
<h2 class="margin-top-4 margin-bottom-1">Message count</h2>
{% if current_user.has_permissions('manage_service') %}
<h3 class='margin-bottom-0' id="current-year"></h3>
{{ ajax_block(partials, updates_url, 'usage') }}
<p class="margin-top-0">During the pilot period, each service has an allowance of 250,000 message parts. Once this allowance is met, the
application will stop delivering messages. There's no monthly charge, no setup fee, and no procurement cost.</p>
<p class="align-with-heading-copy">
What counts as 1 text message part?<br />
See <a class="usa-link" href="{{ url_for('.pricing') }}">pricing</a>.
</p>
{% endif %}
</div>
<h2>Recent templates</h2>
{{ ajax_block(partials, updates_url, 'template-statistics') }}
{% endblock %}

View File

@@ -6,7 +6,8 @@
{% call(item, row_number) list_table(
template_statistics,
caption="Messages sent by template",
caption_visible=True,
caption_visible=False,
border_visible=True,
empty_message='',
field_headings=[
'Template',

View File

@@ -653,20 +653,24 @@ def test_should_not_show_recent_templates_on_dashboard_if_only_one_template_used
mock_template_stats.assert_called_once_with(SERVICE_ONE_ID, limit_days=7)
assert stats[0]["template_name"] == "one", f"Expected template_name to be 'one', but got {stats[0]['template_name']}"
assert (
stats[0]["template_name"] == "one"
), f"Expected template_name to be 'one', but got {stats[0]['template_name']}"
# Debugging: print the main content to understand where "one" is appearing
print(f"Main content: {main}")
# Check that "one" is not in the main content
assert stats[0]["template_name"] in main, f"Expected 'one' to not be in main, but it was found in: {main}"
assert (
stats[0]["template_name"] in main
), f"Expected 'one' to not be in main, but it was found in: {main}"
# count appears as total, but not per template
expected_count = stats[0]["count"]
assert expected_count == 50, f"Expected count to be 50, but got {expected_count}"
assert normalize_spaces(page.select_one("#total-sms .big-number-smaller").text) == (
"{} text messages sent in the last seven days".format(expected_count)
)
)
@freeze_time("2016-07-01 12:00") # 4 months into 2016 financial year