<div class="outer-wrapper">
<div class="wrapper">
<div class="flex-col">
<div class="section-head">Result</div>
<div class="d-flex space-between me-1">
<!-- Chart Control -->
<div class="d-flex" data-chart-control>
<!-- Radio Buttons can be added and as long as the value is matches a Chartjs chart it should work without needing to do much in the js -->
<label for="line">
<input type="radio" id="line" name="chartControl" class="active" value="line" data-line-btn checked />
<span class="btn btn-toggle reset"> <i class="bi bi-graph-up"></i></span>
</label>
<label for="bar">
<input type="radio" id="bar" name="chartControl" class="" value="bar" data-bar-btn />
<span class="btn btn-toggle reset"><i class="bi bi-bar-chart-line"></i></span>
</label>
<label for="donut">
<input type="radio" id="donut" name="chartControl" class="" value="pie" />
<span class="btn btn-toggle reset"><i class="bi bi-opencollective"></i></span>
</label>
<label for="polar">
<input type="radio" id="polar" name="chartControl" class="" value="polarArea" />
<span class="btn btn-toggle reset"><i class="bi bi-crosshair2"></i></span>
</label>
</div>
<div class="d-flex" data-select-scheme>
<i class="bi bi-paint-bucket"></i>
<select name="scheme" id="colorScheme">
<option value="d" selected>Default</option>
<option value="b">Classic</option>
<option value="a">Cool</option>
<option value="c">Warm</option>
</select>
</div>
</div>
<!-- Chart -->
<div class="canvas-wrapper"><canvas id="lineChart"></canvas></div>
<!--<div id="example" class="parent">
<div class="child code">
<pre><code><p class="mute">/* Result */</p><p id="showCode"></p>
</code></pre>
</div>
</div>-->
<!-- Stats -->
<div class="d-flex space-between">
<div class="stat" data-stat-total></div>
<div class="stat" data-stat-avg></div>
</div>
</div>
<hr style="border: 6px solid black;"></hr>
<div class="drawer">
<div class="flex-col drawer-wrapper">
<div class="section-head">Cycle Data</div>
<form id="myForm" class="flex-col" style="gap: 1.5em; margin-inline: 0 .8em;">
<div class="grid">
<div class="d-flex" data-input-list>
<span><label for="num1" data-label>Cycle 1</label><input type="number" min="0" id="num1" placeholder="sec" data-input required></span>
<!--<span><label for="num2">Cycle 2</label><input type="number" id="num2" placeholder="0" data-input required></span>
<span><label for="num3">Cycle 3</label><input type="number" id="num3" placeholder="0" data-input required></span>
<span><label for="num1">Cycle 4</label><input type="number" id="num4" placeholder="0" data-input required></span>-->
</div>
<div class="flex-col controls" style="align-self: center;">
<!-- "Add" button -->
<button type="button" id="addButton" class="btn btn-icon reset"><i class="bi bi-plus"></i></button>
<!-- "Remove" button -->
<button type="button" id="removeButton" class="btn btn-icon reset" style="margin-bottom: 20px;"><i class="bi bi-dash"></i></button>
</div>
</div>
<div class="d-flex flex-end">
<button id="reset" type="reset" class="btn reset">Clear</button>
<button type="button" class="btn" data-submit-btn>Average</button>
</div>
</form>
</div>
<div class="footer d-flex">©2023 Deanuxd</div>
</div>
</div>
</div>
<div id="modal-overlay" class="modal-overlay">
</div>
@import url("https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css");
html {
background-color: black;
font-family: arial;
color: #fff;
height: 100vh;
}
body {
position: relative;
}
.modal-overlay {
position: absolute;
z-index: 20;
top: 0;
bottom: 0;
left: 0;
right: 0;
background: #000000b3;
display: none;
place-items: center;
}
.modal-show {
display: grid;
}
.modal {
display: flex;
flex-direction: column;
align-items: center;
gap: 1em;
padding: 2em 3em;
border-radius: 8px;
background: black;
border: 1px solid #262626;
width: 260px;
& h5 {
margin: 0;
}
& input {
padding: .7em .4em;
border-radius: 6px;
border: none;
background: #3C3F4D;
width: 100%;
}
}
.wrapper {
margin-inline: auto;
max-width: 740px;
overflow-x: hidden;
}
.parent {
border-radius:3px;
background-color: #3C3F4D;
padding: 1rem;
display: none;
}
.d-flex {
display: flex;
flex-wrap: wrap;
gap: 8px;
align-items: center;
}
.space-between {
justify-content: space-between;
}
.flex-col {
display: flex;
flex-direction: column;
gap: .8rem;
}
.grid {
display: grid;
grid-template-columns: auto 26px;
gap: 1rem;
}
.my-1 {
margin-block: .5em;
}
.me-1 {
margin-bottom: 1.2em;
}
.flex-end {
justify-content: flex-end;
}
.controls {
position: absolute;
right: 8px;
}
#example {
margin-top: 1.5em;
}
span {
position: relative;
margin-bottom: 1em;
}
span label {
position: absolute;
top: -10px;
left: 12px;
z-index: 1;
font-size: 12px;
opacity: .6;
transition: all 200ms ease-in-out;
}
span:hover label {
top: -20px;
opacity: 1;
}
input[type=number]::inner-spin-button {
appearance: none;
}
input[type="number"] {
height: 2.2rem;
width: 58px;
border: .15rem solid black;
border-radius: 8px;
text-align: center;
background-color: black;
position: relative;
color: #fff;
transition: all 200ms ease-in-out;
}
input[type="number"]:hover {
background-color: #3C3F4D;
border: .15rem solid slateblue;
}
input[type="number"]:focus-visible {
outline: none;
}
/*span:before {
content: "s";
font-size: 12px;
position: absolute;
top: 0;
right: 18px;
z-index: 2;
line-height: 38px;
opacity: .6;
}*/
span:not(:last-of-type):after {
content: '';
display: inline-flex;
vertical-align: middle;
height: 26px;
width: 3px;
margin-left: 8px;
background-color: #2f2f2f;
border-radius: 6px;
}
.code {
font-family: Courier;
color: white;
padding: 0 1rem;
height: 6rem;
overflow-y: scroll;
}
#showCode {
white-space: break-spaces;
word-break: break-word;
overflow-wrap: anywhere;
}
.mute {
color: #656C84;
}
.mth {
color: #8AE0DD;
}
.var {
color: #8AE0A1;
}
.val {
color: #CCAEF5;
}
.num {
color: #E84C88;
}
#example {
height: 6rem;
overflow-y: hidden;
}
.btn {
background-color: slateblue;
height: 2.6rem;
padding: .8rem 1.2rem;
font-size: 1rem;
line-height: .5rem;
color: white;
border: none;
border-radius: 8px;
cursor: pointer;
}
.btn:hover {
background-color: mediumslateblue;
}
.btn-sm {
padding: .4rem 1rem;
line-height: 1;
height: 2.4rem;
font-size: .85rem;
}
.btn-group {
display: flex;
justify-content: flex-end;
width: 100%;
gap: 1em;
}
.reset {
background-color: #000;
}
.reset:hover {
background-color: #3C3F4D;
}
.btn-icon {
padding: .3rem .3rem;
height: unset;
background-color: #3C3F4D;
}
.btn-icon:active {
background-color: #5b5e6c;
}
[data-chart-control] {
& input[type="radio"] {
opacity: 0; /* hidden but still tabable */
position: absolute;
}
& label {
position: relative;
& i {
opacity: .7;
}
}
& input:checked + span {
background-color: #3C3F4D;
& i {
opacity: 1;
}
}
& label::before {
content: attr(for);
font-family: Roboto, system, sans-serif;
text-transform: capitalize;
text-align: center;
font-size: 11px;
position: absolute;
z-index: 6;
top: 90%;
left: 0;
right: 0;
opacity: 0.8;
color: #fff;
padding: 4px;
border-radius: 3px;
display: block;
}
}
.btn-toggle {
font-size: 1.4rem;
padding-inline: .8rem;
padding-bottom: 1.2rem;
}
.canvas-wrapper {
position: relative;
width: 100%;
height: auto;
min-height: 280px;
}
.section-head {
width: 100%;
background-color: black;
border: solid #2f2f2f;
border-width: 1px 0 1px 0;
font-size: 1.1rem;
letter-spacing: .0675rem;
font-weight: 500;
text-align: center;
padding-block: .6em;
margin-bottom: 1em;
position: sticky;
top: 0;
z-index: 5;
}
.drawer {
position: sticky;
bottom: 0;
z-index: 5;
}
.drawer-wrapper {
background-color: black;
overflow-x: hidden;
}
.footer {
width: 100%;
background-color: black;
border: solid #2f2f2f;
border-width: 1px 0 1px 0;
font-size: .8rem;
font-weight: 500;
letter-spacing: .0675rem;
padding: .8em 1em;
margin-top: 2em;
}
.stat {
position: relative;
flex: 1;
text-align: center;
border-radius:3px;
background-color: #3C3F4D;
line-height: 1.4rem;
padding-block: 8px;
}
.stat p {
font-size: 14px;
font-weight: 600;
margin-block: 0;
padding-block: .5em;
}
.stat p:not(:last-of-type) {
border-bottom: 1.5px solid #2f2f2f;
}
[data-stat-total]:before {
content: 'Total';
display: block;
opacity: .6;
font-weight: 600;
font-size: 14px;
letter-spacing: .0675rem;
}
[data-stat-avg]:before {
content: 'Average';
display: block;
opacity: .6;
font-weight: 600;
font-size: 14px;
letter-spacing: .0675rem;
}
select {
background-color: #000;
color: white;
font-size: 11px;
font-family: Roboto, system, sans-serif;
border: 0;
outline: 0;
text-align: center;
text-indent: 1px;
text-overflow: '';
padding-top: 2.4rem;
border-radius: 6px;
&::expand {
display: none;
}
&:hover {
opacity: 1;
}
&:focus {
background-color: #000;
outline: none;
}
}
[data-select-scheme] {
position: relative;
background-color: #000;
font-size: 1.4rem;
margin-right: .4rem;
border-radius: 5px;
flex-direction: column;
gap: 0;
opacity: .8;
&:hover {
opacity: 1;
}
& i {
position: absolute;
top: 12px;
left: 20%;
pointer-events: none;
}
}
import chartMinJs from "https://esm.sh/chart.min.js";
/**
* Add event on multiple elements
* @param {NodeList} $element NodeList
* @param {String} eventType Event type string
* @param {Function} callback Callback function
*/
window.addEventOnElements = ($elements, eventType, callback) => {
for (const $element of $elements) {
$element.addEventListener(eventType, callback);
}
}
// Add opacity to rgb strings
function addOpacity(rgbArray, opacity) {
// Ensure the opacity value is within the valid range (0 to 1)
opacity = Math.min(1, Math.max(0, opacity));
// Process each RGB string in the array and add the opacity
const rgbaArray = rgbArray.map(rgbString => {
// Extract RGB values from the string
const rgbValues = rgbString.match(/\d+/g);
// Create a new RGB string with the specified opacity
return `rgb(${rgbValues.join(' ')} / ${opacity * 100}%)`;
});
// Return the new RGBA array
return rgbaArray;
}
/**
* Color Schemes
*/
const a = ['rgb(106 90 205)', 'rgb(0 140 248)', 'rgb(0 179 251)', 'rgb(0 211 221)', 'rgb(0 237 177)', 'rgb(181 255 142)'];
const b = ['rgb(106 90 205)', 'rgb(138 224 221)', 'rgb(138 224 161)', ' rgb(204 174 245)', 'rgb(232 76 136)', 'rgb(220 232 76)'];
const c = ['rgb(106 90 205)', 'rgb(203 82 185)', 'rgb(255 92 152)', 'rgb(255 128 113)', 'rgb(255 177 87)', 'rgb(255 225 91)'];
const d = 'rgb(106 90 205)';
var opacityValue = 0.2; // 20% opacity
// Color Schemes at 20%
const a2 = addOpacity(a, opacityValue);
const b2 = addOpacity(b, opacityValue);
const c2 = addOpacity(c, opacityValue);
const d2 = `rgba(106 90 205 / ${opacityValue})`;
var opacityValue = 0.6; // 60% opacity
// Color Schemes at 60%
const a3 = addOpacity(a, opacityValue);
const b3 = addOpacity(b, opacityValue);
const c3 = addOpacity(c, opacityValue);
const d3 = `rgba(106 90 205 / ${opacityValue})`;
document.addEventListener('DOMContentLoaded', function() {
let chart; // Variable to store the Chart.js instance
let chartType = 'line';
let beginAtZero = false;
let borderW = 1.5;
let tickD = true;
let gridD = true;
let scheme = d;
let fScheme = d2;
let dfScheme = d3;
/*
* Chart Control Buttons
*/
let /** {NodeElement} */ $chartControl = document.querySelector("[data-chart-control]");
let /** {NodeList} */ $chartBtns = $chartControl.getElementsByTagName("input");
let /** {NodeElement} */ $lineBtn = document.querySelector("[data-line-btn]"); // Default active
let /** {NodeElement} */ $barBtn = document.querySelector("[data-bar-btn]");
function setChartType(e) {
// toggle active class
if(this.classList.contains("active") === false) {
this.classList.toggle("active");
}
// update the value of chartType
chartType = this.value;
if(this.value === "line") {
beginAtZero = false;
borderW = 1.5;
tickD = true;
gridD = true;
} else if(this.value === "pie" || this.value === "polarArea"){
borderW = 0;
tickD = false;
gridD = false;
} else {
beginAtZero = true;
borderW = 1.5;
tickD = true;
gridD = true;
}
// get the current data values
let storedNumbersArray = getStoredNumbersArray();
// push changes
return chartType, beginAtZero, borderW, tickD, createOrUpdateChart(storedNumbersArray);
}
addEventOnElements($chartBtns, "click", setChartType);
const /** {NodeElement} */ $colorSelect = document.getElementById("colorScheme");
function setScheme(e) {
// Store the array locally
localStorage.setItem('userTheme', JSON.stringify(this.value));
let val = eval(this.value);
let val2 = eval(`${this.value}2`);
let val3 = eval(`${this.value}3`);
scheme = val;
fScheme = val2;
dfScheme = val3;
// get the current data values
let storedNumbersArray = getStoredNumbersArray();
// push changes
return scheme, fScheme, createOrUpdateChart(storedNumbersArray);
}
$colorSelect.addEventListener('input', setScheme);
/**
* Handle Form
*/
let /** {NodeElement} */ $inputList = document.querySelector("[data-input-list]");
let /** {NodeList} */ $inputs = document.querySelectorAll("[data-input]");
const /** {NodeElement} */ form = document.querySelector('#myForm');
const /** {NodeElement} */ btn = document.querySelector("[data-submit-btn]");
let /** {NodeList} */ $labels = document.querySelectorAll("[data-label]");
const $overlay /** {NodeElement} */ = document.querySelector('#modal-overlay');
btn.addEventListener('click', function(event) {
// Prevent the form from submitting if validation fails
event.preventDefault();
// Check if all input fields are filled out
let allInputsFilled = true;
for (let i = 0; i < $inputs.length; i++) {
if ($inputs.length === null || $inputs[i].value === '') {
allInputsFilled = false;
// Optional: Display an alert or other indication for the user
alert('Please fill out all input fields before submitting.');
break; // Exit the loop early if any field is empty
}
}
// Proceed with form submission if all inputs are filled out
if (allInputsFilled) {
// Create an array to store the values
let numbersArray = [];
// Iterate through the input nodes and store their values
for (let i = 0; i < $inputs.length; i++) {
let input = parseInt($inputs[i].value);
numbersArray.push(input);
}
// Store the array locally
localStorage.setItem('numbersArray', JSON.stringify(numbersArray));
// Create or update the stats
createOrUpdateStats(numbersArray);
// Create or update the chart
createOrUpdateChart(numbersArray);
}
});
function createOrUpdateStats(data) {
// Get Node Elements
let /** {NodeElement} */ $totalStat = document.querySelector("[data-stat-total]");
let /** {NodeElement} */ $avgStat = document.querySelector("[data-stat-avg]");
// Clear any existing stats
$totalStat.innerHTML = "";
$avgStat.innerHTML = "";
// Run Calculations on Data
let totalTime = getTotal(data);
let avgTime = calcAvg(data);
// Display the Stats
// Display Total
let displayTotal = document.createElement("p");
displayTotal.innerHTML = `${totalTime} sec (~ ${getTime(totalTime).time} ${getTime(totalTime).timeUnit})`;
$totalStat.appendChild(displayTotal);
// Display Avg
let displayAvg = document.createElement("p");
displayAvg.innerHTML = `${avgTime.toFixed(2)} sec`;
$avgStat.appendChild(displayAvg);
}
function createOrUpdateChart(data) {
// Destroy the existing chart if it exists
if (chart) {
chart.destroy();
}
// Get the canvas element
const ctx = document.getElementById('lineChart').getContext('2d');
// Create Gradient (disabled)
let gradient = ctx.createLinearGradient(0, 0, 0, 250);
gradient.addColorStop(0, 'rgb(106 90 205 / 60%)');
gradient.addColorStop(1, 'rgb(106 90 205 / 10%)');
// Calculate the average
let average = data.reduce((sum, value) => sum + value, 0) / data.length;
// Find the index of the maximum value in the dataset
let maxIndex = data.indexOf(Math.max(data));
// Find the index of the minimum value in the dataset
let minIndex = data.indexOf(Math.min(data));
// Set a different color for the maximum value (disabled)
let colorMax = data.map((value, index) => (index === maxIndex) ? 'red' : scheme);
const annotation = {
type: 'line',
borderColor: 'seagreen',
borderDash: [6, 6],
borderDashOffset: 0,
borderWidth: borderW,
label: {
display: false,
content: `AVG: ${average.toFixed(2)} sec`,
position: 'end',
backgroundColor: '#2e8b57c7'
},
scaleID: 'y',
value: average
};
let chartLabels = Array.from($labels).map(label => label.innerText);
// Create a new chart with Chart.js
chart = new Chart(ctx, {
type: chartType,
data: {
labels: chartLabels,
datasets: [{
label: 'Time (Sec)',
data: data,
borderColor: scheme,
backgroundColor: fScheme,
pointBorderColor: dfScheme,
pointBackgroundColor: scheme,
pointBorderWidth: 5,
pointRadius: 3.5,
pointHoverRadius: 5,
borderWidth: 2,
fill: 'origin',
skipNull: true,
tension: 0.25
}, {
label: 'Average (Sec)',
borderColor: 'seagreen',
borderWidth: 1.5,
borderDash: [6, 6]
}]
},
options: {
maintainAspectRatio: false,
scales: {
y: {
beginAtZero: true,
grid: {
color: '#252525',
display: gridD
},
ticks: {
display: tickD,
}
},
x: {
grid: {
color: '#252525',
display: gridD
},
ticks: {
display: tickD,
}
}
},
plugins: {
annotation: {
annotations: {
annotation
}
}
},
elements: {
point: {
pointHitRadius: 8,
pointHoverBorderWidth: 4,
}
}
}
});
};
// Calculations
const getTotal = function(array) {
return array.reduce((accumulator, currentValue) => accumulator + currentValue, 0);
};
const getLength = function(array) {
return array.length;
};
const calcAvg = function(array) {
return getTotal(array) / getLength(array);
};
// Add/Delete Inputs
const /** {NodeElement} */ $input = document.createElement("span");
function addInput() {
// Create a new span element
let newSpan = document.createElement('span');
// Generate a unique ID for the new input
let id = $inputs.length + 1;
let inputID = 'num' + id;
// Create a new label element
let newLabel = document.createElement('label');
newLabel.setAttribute('for', inputID);
newLabel.setAttribute('data-label', '');
newLabel.textContent = 'Cycle ' + id;
// Create a new input element
let newInput = document.createElement('input');
newInput.type = 'number';
newInput.id = inputID;
newInput.setAttribute('data-input', '');
newInput.placeholder = 'sec';
newInput.min = '0';
newInput.required = true;
// Append the label and input to the span
newSpan.appendChild(newLabel);
newSpan.appendChild(newInput);
// Append the new span to the form
$inputList.appendChild(newSpan);
$inputs = document.querySelectorAll("[data-input]");
$labels = document.querySelectorAll("[data-label]");
}
function removeInput() {
let $spans = $inputList.querySelectorAll('span');
// Check if there's at least one input to remove
if ($spans.length > 0) {
// Remove the last added input (last span)
$inputList.removeChild($spans[$spans.length - 1]);
}
$inputs = document.querySelectorAll("[data-input]");
// Update the chart after removing an input
createOrUpdateChart(getStoredNumbersArray());
}
function getStoredNumbersArray() {
// Retrieve the stored array locally
let storedNumbersArray = localStorage.getItem('numbersArray');
return storedNumbersArray ? JSON.parse(storedNumbersArray) : [];
}
function updateFromLocalStorage() {
let storedNumbersArray = getStoredNumbersArray();
// if previous data exists in localStorage, add the inputs
// else clear inputs
if (storedNumbersArray.length > 0) {
for (let i = 1; i < storedNumbersArray.length; i++) {
addInput();
}
} else {
let $spans = $inputList.querySelectorAll('span');
for (let i = 1; i < $spans.length; i++) {
removeInput();
}
}
// Set the values from localStorage to the input fields
for (let i = 0; i < $inputs.length; i++) {
$inputs[i].value = storedNumbersArray[i] || '';
}
// Chart: Create or update the chart based on the data in localStorage
createOrUpdateChart(storedNumbersArray);
// Stats: Create or update the stats based on the data in localStorage
if (storedNumbersArray.length == 0) {
// Get Node Elements
let /** {NodeElement} */ $totalStat = document.querySelector("[data-stat-total]");
let /** {NodeElement} */ $avgStat = document.querySelector("[data-stat-avg]");
// Clear any existing stats
$totalStat.innerHTML = "";
$avgStat.innerHTML = "";
return;
} else {
createOrUpdateStats(storedNumbersArray);
}
}
const getTheme = function() {
if(localStorage.getItem('userTheme')) {
let theme = localStorage.getItem('userTheme');
$colorSelect.querySelector(`[value=${theme}]`).setAttribute('selected', "");
$colorSelect.dispatchEvent(new Event('input'));
}
}
function closeModal() {
$overlay.classList.toggle('modal-show');
const /** {NodeElement} */ $modal = document.querySelector('.modal');
$overlay.removeChild($modal);
}
// Close when clicking outside modal
function toggleModal(e) {
if(e.target.id === 'modal-overlay'){
closeModal()
}
}
const editLabel = (e) => {
let targetLabel = e.target;
let currentValue = e.target.innerText;
console.log(currentValue);
$overlay.classList.toggle('modal-show')
const /** {NodeElement} */ $modal = document.createElement('div');
$modal.classList.add('modal');
$modal.innerHTML = `
<h5>Edit Label</h5>
<input type="text" placeholder="${currentValue}" value="${currentValue}" data-label-input />
<div class="btn-group">
<button class="btn reset btn-sm" data-close-modal>Cancel</button>
<button class="btn btn-sm" data-update-label>Save</button>
</div>
`
$overlay.appendChild($modal);
let $closBtn = document.querySelector("[data-close-modal]").addEventListener("click", closeModal);
let $saveBtn = document.querySelector("[data-update-label]").addEventListener("click", () => {
targetLabel.innerText = document.querySelector("[data-label-input]").value;
closeModal();
})
}
// Reset form
const /** {NodeElement} */ $reset = document.querySelector('#reset');
const clearLocalStorage = function() {
// Clear the data stored in localStorage
localStorage.removeItem('numbersArray');
};
$reset.addEventListener("click", clearLocalStorage , true);
$reset.addEventListener("click", updateFromLocalStorage, true);
// Attach the addInput function to the "Add" button's click event
document.getElementById('addButton').addEventListener('click', addInput);
// Attach the removeInput function to the "Remove" button's click event
document.getElementById('removeButton').addEventListener('click', removeInput);
// Populate inputs, chart, and stats on page load
updateFromLocalStorage();
getTheme();
addEventOnElements($labels, "click", editLabel)
$overlay.addEventListener("click", toggleModal)
});
/**
* @param {Number} second
* @returns {String}
*/
export const getTime = second => {
const /** {Number} */ minute = Math.floor(second / 60);
const /** {Number} */ hour = Math.floor(minute / 60);
const /** {Number} */ time = hour || minute || second;
const /** {Number} */ unitIndex = [hour, minute, second].lastIndexOf(time);
const /** {String} */ timeUnit = ["hrs", "min", "sec"][unitIndex];
return { time, timeUnit };
}
This Pen doesn't use any external CSS resources.
This Pen doesn't use any external JavaScript resources.