initial
This commit is contained in:
279
staff.html
Normal file
279
staff.html
Normal file
@@ -0,0 +1,279 @@
|
||||
<!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/nws.php?officestats11', 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>
|
||||
Reference in New Issue
Block a user