279 lines
12 KiB
HTML
279 lines
12 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
|
|
<script src="https://code.jquery.com/ui/1.13.1/jquery-ui.js" integrity="sha256-6XMVI0zB8cRzfZjqKcD01PBsAy3FlDASrlC8SxCpInY=" crossorigin="anonymous"></script>
|
|
<link rel="stylesheet" href="https://code.jquery.com/ui/1.13.1/themes/smoothness/jquery-ui.css">
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Table Chart - Office Stats</title>
|
|
<style>
|
|
body {
|
|
margin: 0;
|
|
font-family: Arial, sans-serif;
|
|
background-color: #f9f9f9;
|
|
}
|
|
#main-container {
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
h1 {
|
|
text-align: center;
|
|
margin: 5px 0;
|
|
}
|
|
#chart-container {
|
|
flex: 1;
|
|
display: flex;
|
|
justify-content: center;
|
|
align-items: center;
|
|
overflow: auto; /* Allows scrolling for the whole chart if it's too big */
|
|
}
|
|
table {
|
|
border-collapse: collapse;
|
|
background-color: #fff;
|
|
border: 1px solid #ddd;
|
|
}
|
|
|
|
/* --- MODIFIED/ADDED CSS FOR CELL SIZING --- */
|
|
th, td {
|
|
border: 1px solid #ddd;
|
|
padding: 2px; /* Reduced padding for smaller cells */
|
|
text-align: center;
|
|
font-size: 10px; /* Reduced font size for compactness */
|
|
width: 35px; /* Set a fixed width */
|
|
height: 22px; /* Set a fixed height */
|
|
box-sizing: border-box; /* Ensures padding & border are included in width/height */
|
|
overflow: hidden; /* Hide content that overflows cell boundaries */
|
|
text-overflow: ellipsis; /* Show '...' for truncated text (horizontally) */
|
|
white-space: nowrap; /* Prevent text from wrapping to new lines */
|
|
}
|
|
|
|
th {
|
|
background-color: #f2f2f2;
|
|
/* Header cells (dates) will also adhere to the width/height above. */
|
|
/* Ellipsis will show if date strings are too long for 35px width. */
|
|
}
|
|
|
|
/* Special styling for the first column header cells (office names in tbody) */
|
|
tbody tr th:first-child {
|
|
width: auto; /* Allow this column to be wider based on content */
|
|
min-width: 70px; /* Set a minimum width for office names */
|
|
text-align: left; /* Align office names to the left for readability */
|
|
white-space: normal; /* Allow office names to wrap if they are too long */
|
|
text-overflow: clip; /* Default behavior for overflow when wrapping is normal */
|
|
padding-left: 5px; /* Add some left padding */
|
|
/* Height will still be 22px from the th,td rule. If more height needed, override here. */
|
|
}
|
|
|
|
td {
|
|
/* max-width: 50px; */ /* Superseded by width in th,td */
|
|
/* max-height: 10px; */ /* Typo corrected and superseded by height in th,td */
|
|
transition: opacity 0.2s;
|
|
}
|
|
/* --- END OF MODIFIED/ADDED CSS --- */
|
|
|
|
td:hover {
|
|
opacity: 0.7;
|
|
}
|
|
.tooltip {
|
|
position: absolute;
|
|
background-color: #333;
|
|
color: #fff;
|
|
padding: 5px 10px;
|
|
border-radius: 3px;
|
|
font-size: 12px; /* Tooltip font size can remain larger for readability */
|
|
pointer-events: none;
|
|
visibility: hidden;
|
|
max-width: 300px;
|
|
white-space: pre-wrap;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div id="main-container">
|
|
<h1>WFO Staffing Based on ICAM Account Status</h1>
|
|
<div id="chart-container"></div>
|
|
</div>
|
|
<div class="tooltip" id="tooltip"></div>
|
|
|
|
<script>
|
|
// Process data
|
|
function processData(data) {
|
|
const dates = data.map(item => {
|
|
const dt = new Date(item.provided_datetime);
|
|
return `${dt.getMonth() + 1}/${dt.getDate()}/${dt.getFullYear()}`;
|
|
});
|
|
const offices = [...new Set(data.flatMap(item => item.data.map(d => d.office)))];
|
|
|
|
const officeDateMap = {};
|
|
const titleBreakdownMap = {};
|
|
|
|
offices.forEach(office => {
|
|
officeDateMap[office] = {};
|
|
titleBreakdownMap[office] = {};
|
|
dates.forEach((date, i) => {
|
|
const item = data[i];
|
|
const officeData = item.data.find(d => d.office === office);
|
|
officeDateMap[office][date] = officeData ? parseInt(officeData.unique_person_count, 10) : null;
|
|
|
|
if (officeData && officeData.title_counts_array) {
|
|
try {
|
|
const titlesString = officeData.title_counts_array
|
|
.replace(/^{|}$/g, '')
|
|
.replace(/\\"/g, '"');
|
|
|
|
const titlesArray = titlesString
|
|
.split('","')
|
|
.map(str => str.replace(/^"|"$/g, ''))
|
|
.map(str => {
|
|
try {
|
|
return JSON.parse(str);
|
|
} catch (e) {
|
|
console.error(`Failed to parse individual title: ${str}`, e);
|
|
return null;
|
|
}
|
|
})
|
|
.filter(t => t !== null);
|
|
|
|
titleBreakdownMap[office][date] = titlesArray;
|
|
} catch (e) {
|
|
console.error(`Failed to parse title_counts_array for ${office} on ${date}:`, officeData.title_counts_array, e);
|
|
titleBreakdownMap[office][date] = null;
|
|
}
|
|
} else {
|
|
titleBreakdownMap[office][date] = null;
|
|
}
|
|
});
|
|
});
|
|
|
|
return { dates, offices, officeDateMap, titleBreakdownMap };
|
|
}
|
|
|
|
|
|
// Color calculation (Modified for better change visualization)
|
|
function getColor(count, firstCount) {
|
|
if (count === null) {
|
|
return '#e0e0e0';
|
|
}
|
|
if (firstCount === undefined || firstCount === null || (firstCount === 0 && count === 0)) {
|
|
return 'hsl(0, 0%, 92%)';
|
|
}
|
|
if (firstCount === 0) {
|
|
if (count > 0) {
|
|
return 'hsl(120, 70%, 50%)';
|
|
}
|
|
else {
|
|
return 'hsl(0, 0%, 92%)';
|
|
}
|
|
}
|
|
const diff = (count - firstCount) / firstCount;
|
|
const maxExpectedChange = 0.5;
|
|
const intensity = Math.min(Math.abs(diff) / maxExpectedChange, 1.0);
|
|
let hue, saturation, lightness;
|
|
|
|
if (count < firstCount) {
|
|
hue = 0;
|
|
saturation = 70;
|
|
lightness = 92 - (42 * intensity);
|
|
return `hsl(${hue}, ${saturation}%, ${lightness}%)`;
|
|
|
|
} else if (count > firstCount) {
|
|
hue = 120;
|
|
saturation = 70;
|
|
lightness = 92 - (42 * intensity);
|
|
return `hsl(${hue}, ${saturation}%, ${lightness}%)`;
|
|
|
|
} else {
|
|
return 'hsl(0, 0%, 92%)';
|
|
}
|
|
}
|
|
|
|
// Generate table chart
|
|
function generateChart(dates, offices, officeDateMap, titleBreakdownMap) {
|
|
const table = document.createElement('table');
|
|
const thead = document.createElement('thead');
|
|
const tbody = document.createElement('tbody');
|
|
|
|
const headerRow = document.createElement('tr');
|
|
headerRow.appendChild(document.createElement('th'));
|
|
dates.forEach(date => {
|
|
const th = document.createElement('th');
|
|
th.textContent = date;
|
|
headerRow.appendChild(th);
|
|
});
|
|
thead.appendChild(headerRow);
|
|
table.appendChild(thead);
|
|
|
|
offices.forEach(office => {
|
|
const row = document.createElement('tr');
|
|
const officeCell = document.createElement('th'); // This will be styled by 'tbody tr th:first-child'
|
|
officeCell.textContent = office.split('/').pop();
|
|
row.appendChild(officeCell);
|
|
|
|
const firstCount = officeDateMap[office][dates[0]];
|
|
dates.forEach(date => {
|
|
const count = officeDateMap[office][date];
|
|
const titles = titleBreakdownMap[office][date];
|
|
const td = document.createElement('td');
|
|
td.textContent = count !== null ? count : '';
|
|
td.style.backgroundColor = getColor(count, firstCount);
|
|
|
|
td.addEventListener('mouseover', (e) => {
|
|
const tooltip = document.getElementById('tooltip');
|
|
tooltip.style.visibility = 'visible';
|
|
if (titles && titles.length > 0) {
|
|
const breakdown = titles.map(t => `${t.otitle}: ${t.count}`).join('\n');
|
|
tooltip.textContent = `${office} on ${date}\n${breakdown}`;
|
|
} else {
|
|
tooltip.textContent = `${office} on ${date}: No data`;
|
|
}
|
|
tooltip.style.left = `${e.pageX + 10}px`;
|
|
tooltip.style.top = `${e.pageY - 10}px`;
|
|
});
|
|
td.addEventListener('mouseout', () => {
|
|
document.getElementById('tooltip').style.visibility = 'hidden';
|
|
});
|
|
|
|
row.appendChild(td);
|
|
});
|
|
tbody.appendChild(row);
|
|
});
|
|
table.appendChild(tbody);
|
|
|
|
const container = document.getElementById('chart-container');
|
|
container.innerHTML = '';
|
|
container.appendChild(table);
|
|
}
|
|
|
|
function fetchWeatherData(url, callback) {
|
|
$.getJSON(url, function(weatherdata) {
|
|
callback(weatherdata);
|
|
}).fail(function(jqXHR, textStatus, errorThrown) {
|
|
console.error('Error fetching weather data:', textStatus, errorThrown);
|
|
callback(null);
|
|
});
|
|
}
|
|
|
|
function main() {
|
|
fetchWeatherData('https://wx.stoat.org/main.php?service=nws&officestats', function(weatherdata) {
|
|
if (weatherdata) {
|
|
const jsonData = weatherdata;
|
|
const { dates, offices, officeDateMap, titleBreakdownMap } = processData(jsonData);
|
|
generateChart(dates, offices, officeDateMap, titleBreakdownMap);
|
|
|
|
window.addEventListener('resize', () => {
|
|
// Note: Re-processing data on resize is fine for this dataset size,
|
|
// but for very large datasets, you might only regenerate the chart.
|
|
const { dates, offices, officeDateMap, titleBreakdownMap } = processData(jsonData);
|
|
generateChart(dates, offices, officeDateMap, titleBreakdownMap);
|
|
});
|
|
} else {
|
|
console.log('Failed to retrieve weather data.');
|
|
}
|
|
});
|
|
}
|
|
|
|
main();
|
|
</script>
|
|
</body>
|
|
</html> |