From d3015029577d76fa3f7c41aa0e8c1f96c05296e1 Mon Sep 17 00:00:00 2001 From: Jonathan Bobel Date: Thu, 11 Jul 2024 12:59:14 -0400 Subject: [PATCH] JS cleanrup Table styles and updates Removing chart.js --- .../javascripts/dashboardVisualization.js | 456 +++++++++--------- .../uswds/_uswds-theme-custom-styles.scss | 8 +- app/templates/components/table.html | 6 +- app/templates/views/dashboard/dashboard.html | 25 +- .../views/dashboard/template-statistics.html | 3 +- tests/app/main/views/test_dashboard.py | 10 +- 6 files changed, 253 insertions(+), 255 deletions(-) diff --git a/app/assets/javascripts/dashboardVisualization.js b/app/assets/javascripts/dashboardVisualization.js index 188a9d0cc..936a4f9ed 100644 --- a/app/assets/javascripts/dashboardVisualization.js +++ b/app/assets/javascripts/dashboardVisualization.js @@ -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}
${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}
${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); diff --git a/app/assets/sass/uswds/_uswds-theme-custom-styles.scss b/app/assets/sass/uswds/_uswds-theme-custom-styles.scss index 4976f70cc..638f90743 100644 --- a/app/assets/sass/uswds/_uswds-theme-custom-styles.scss +++ b/app/assets/sass/uswds/_uswds-theme-custom-styles.scss @@ -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; } } diff --git a/app/templates/components/table.html b/app/templates/components/table.html index dab53fc1f..e0dd347d8 100644 --- a/app/templates/components/table.html +++ b/app/templates/components/table.html @@ -6,7 +6,7 @@ {% for field_heading in field_headings %} - + {% if field_headings_visible %} {{ field_heading }} {% else %} @@ -79,9 +79,9 @@ {%- endmacro %} {% macro row_heading() -%} - + {{ caller() }} - + {%- endmacro %} {% macro index_field(text=None, rowspan=None) -%} diff --git a/app/templates/views/dashboard/dashboard.html b/app/templates/views/dashboard/dashboard.html index a9b31f935..abd127f57 100644 --- a/app/templates/views/dashboard/dashboard.html +++ b/app/templates/views/dashboard/dashboard.html @@ -26,15 +26,10 @@ Messages sent - - {{ ajax_block(partials, updates_url, 'inbox') }} {{ ajax_block(partials, updates_url, 'totals') }} - {{ ajax_block(partials, updates_url, 'template-statistics') }} -

Activity snapshot

@@ -59,9 +54,9 @@
-

Recent Batches

+

Recent Batches

- +
+
@@ -89,7 +84,7 @@ {% for job in job_and_notifications[:5] %} {% if job.job_id and job.notifications %} {% set notification = job.notifications[0] %} -
{{ notification.job.original_file_name[:12] if notification.job.original_file_name else 'Manually entered number'}}
@@ -127,17 +122,7 @@
-

Message count

- {% if current_user.has_permissions('manage_service') %} -

- {{ ajax_block(partials, updates_url, 'usage') }} -

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.

-

- What counts as 1 text message part?
- See pricing. -

- {% endif %} - +

Recent templates

+ {{ ajax_block(partials, updates_url, 'template-statistics') }} {% endblock %} diff --git a/app/templates/views/dashboard/template-statistics.html b/app/templates/views/dashboard/template-statistics.html index 86192d421..6d249145e 100644 --- a/app/templates/views/dashboard/template-statistics.html +++ b/app/templates/views/dashboard/template-statistics.html @@ -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', diff --git a/tests/app/main/views/test_dashboard.py b/tests/app/main/views/test_dashboard.py index 6e0f83a6a..e09791d91 100644 --- a/tests/app/main/views/test_dashboard.py +++ b/tests/app/main/views/test_dashboard.py @@ -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