/**
* This file handles the scripting for the Reflectance Calculator.
* There are 3 parts to it:
* 1 - The sidebar filters
* 2 - The layers table
* 3 - The ZingChart chart
*
* Values from the sidebar and layers are converted into url search-params format (key/value - `?foo=bar&baz=biz`)
* and sent via a Fetch GET request to the server API endpoint where:
* 1 - It returns the data as series values for rendering in the chart (if the 'Plot' button pressed)
* 2 - It returns the data as a .txt file for download (if the 'Download' button pressed)
*
* When a user successfully adds a filter/layer combination that returns data, we store that config information
* in session storage. If the user refreshes the page or comes back to it, the filter/layers will start
* with that data instead of the default values.
*
* Please see the `init()` function to get started.
*/
// CONFIG
// -----------------------------
// Elements
let root = document.querySelector('reflectance-calculator');
let buttons = {
parent: document.querySelector('[refcalc-buttons]'),
// Buttons stored after load - 'Mobile' buttons cloned from matching base buttons in DOM
download: null,
downloadMobile: null,
plot: null,
plotMobile: null,
};
let chart = {
render: document.querySelector('refcalc-chart-render'),
plotting: document.querySelector('refcalc-layer-plotting'),
error: document.querySelector('refcalc-layer-error'),
}
let filters = {
angle: document.querySelector('[name="plot-angle"]'),
end: document.querySelector('[name="plot-end"]'),
polarization: document.querySelectorAll('[name="plot-polarization"]'),
spacing: document.querySelector('[name="plot-spacing"]'),
start: document.querySelector('[name="plot-start"]'),
type: document.querySelectorAll('[name="plot-type"]'),
};
let layer = {
parent: document.querySelector('refcalc-layer'),
medium: document.querySelector('[data-calc="medium"]'),
newLayers: document.querySelector('[data-calc="new-layers"]'),
thickness: document.querySelector('[data-calc="thickness"]'),
substrate: document.querySelector('[data-calc="substrate"]'),
};
let reflectanceIndex = {
dialog: document.querySelector('[data-calc="reflectance-index-dialog"]'),
input: document.querySelector('[data-calc="reflectance-index-input"]'),
no: document.querySelector('[data-calc="reflectance-index-no"]'),
yes: document.querySelector('[data-calc="reflectance-index-yes"]'),
};
// API Endpoints
let plotApiUrl = '/wp-json/reflectance-calculator/query';
let downloadApiUrl = '/wp-json/reflectance-calculator/file';
// Config
let materialRefractiveIndexLabel = 'Enter Refractive Index...';
let materialTypes = [materialRefractiveIndexLabel,'Acrylic','Ag','Air','Al','Al10Ga90As','Al20Ga80As','Al2O3','Al32Ga68As','Al42Ga58As','Al49Ga51As','Al59Ga41As','Al70Ga30As','Al80Ga20As','Al90Ga10As','AlAs','AlCu','AlN','AlSb','Au','BK7','CaF2','CdTe','Cellulose','Co','CoSi2','Cr','Cu','GaAs','GaN','GaP','GaSb','Ge','HfO2','InAs','InP','InSb','Insulator','ITO','KCl','MgF2','MgO','Mo','Nb','Ni','PbS','PbSe','PET','Polyacrylate','Polyethylene','Pt','Quartz','Rh','Si','Si3N4','SiO','SiO2','Styrene','Styrene','Ta','Ta2O5','Ti','TiN','TiO2','TiSi2','W','ZrO2',];
let materialOptions = materialTypes.map(type => ``);
let thicknessUOMs = {
A: { multiplier: 10000, symbol: 'Å' },
nm: { multiplier: 1000, symbol: 'nm', default: true },
um: { multiplier: 1, symbol: 'µm' },
kA: { multiplier: 10, symbol: 'kÅ' },
uin: { multiplier: 39.3701, symbol: 'µin' },
mils: { multiplier: 0.0393701, symbol: 'mils' },
mm: { multiplier: 0.001, symbol: 'mm' },
'in': { multiplier: 0.0000393701, symbol: 'in' }
}
// Layer
let mediumStart = 'Air';
let substrateStart = 'Si';
let materialTypeDropdownIndex = 'data-index';
// Session Storage
let sessionStorageVariable = 'reflectance-calculator-filters';
// Chart
let chartId = 'reflectance-calculator-chart';
let chartColors = {
black: '#0c0c12',
darkGrey: '#b6b6b7',
lightGrey: '#e7e3e8',
lightPurple: '#fcf6fd',
purple: '#aa1dd5',
};
let chartRenderShared = {
width: '100%',
height: '100%',
output: 'canvas',
}
// INIT
// -----------------------------
async function init() {
// Add init attribute to assist JS-enabled styling
initForStyling();
// Init the starting values in the plot sidebar
initPlotFilters();
// Init the starting items for the layers table
initLayers();
// Clone 'Plot' and 'Download' buttons for mobile
initButtonClones();
// Handle 'Plot' Button Events
await initPlotButtons();
// Handle 'Download' Button Events
await initDownloadButtons();
// Init chart section
await initChart();
}
await init(); // Comment out if imported. See export below.
// ---
/**
* Add init attribute to assist JS-enabled styling (show the calc differently if JS enabled or not)
*/
function initForStyling() {
root?.setAttribute('init', '');
}
/**
* Init the starting values in the filters sidebar. If the user previously used a set of filters that returned data,
* those filter settings were saved in session storage and we'll use them if the page is refreshed or reloaded.
* If not, then we'll populate the filter items with a predetermined set of filter values.
* @returns none
*/
function initPlotFilters() {
let {angle, end, polarization, spacing, start, type} = filters;
// If url has `?key=value&key=value` filters use them (urlFilters)
// If not, but user previous had a successful chart render, use the filters from session storage (sessionFilters)
// Finally, if the user's first page load of the session, populate the filters/layers/chart with starting, default data
let urlFilters = getFiltersFromUrl();
let sessionFilters = getFiltersFromSessionStorage();
let userFilters = urlFilters && urlFilters.size ? urlFilters : sessionFilters;
// Use previous user choice if matching session data found
if (userFilters) {
// Input
angle.value = userFilters.get('angle');
end.value = userFilters.get('wmax');
spacing.value = userFilters.get('wstep');
start.value = userFilters.get('wmin');
// Radio
let matchingPolarizationRadio = getMatchingRadio(polarization, userFilters.get('pol'));
if (matchingPolarizationRadio) matchingPolarizationRadio.checked = true;
let matchingTypeRadio = getMatchingRadio(type, userFilters.get('sptype'));
if (matchingTypeRadio) matchingTypeRadio.checked = true;
}
// Defaults, no user session values found
else {
// Input
angle.value = 0;
end.value = 1000;
spacing.value = 1;
start.value = 200;
// Radio
polarization[0].checked = true;
type[0].checked = true;
}
}
/**
* Init the layer section's table items
*/
function initLayers() {
let {medium, thickness, substrate} = layer;
// Init 'Reflective Index' Overlay (dialog)
// ---
// This is shown when selecting 'Enter Refractive Index...' choice from a 'Material Type' layer dropdown
function handleLayerMaterialDropdownChange(e) {
let isRefractiveIndexOption = e.target.value === materialRefractiveIndexLabel;
if (!isRefractiveIndexOption || !reflectanceIndex.dialog) return;
// When showing the overlay, if there was a previously-added value, set it as the input's value before showing
// Otherwise, make sure the input value is cleared out.
let selectedOptionValue = Array.from( e.target.querySelectorAll('option') ).filter(option => option.selected)[0].value;
let setInputValue = selectedOptionValue > 0 ? selectedOptionValue : 0;
if (reflectanceIndex.input) reflectanceIndex.input.value = setInputValue;
// Add the 'data-index="X"' value to the dialog so it can find which dropdown launched it
reflectanceIndex.dialog.setAttribute(materialTypeDropdownIndex, e.target.getAttribute(materialTypeDropdownIndex));
// Show overlay to enter value
reflectanceIndex.dialog.showModal();
// Focus the input field
reflectanceIndex.input.focus();
}
// Add 'Cancel' overlay button click event
reflectanceIndex.no?.addEventListener('click', e => {
reflectanceIndex.dialog?.close()
});
// Add 'Ok' overlay button click event
reflectanceIndex.yes?.addEventListener('click', e => {
// Get the calling 'Material Type' select dropdown by reading the `data-index="X"` value from the dialog. The matching dropdown will have the same attribute/value
let callingDropdown = document.querySelector(`select[${materialTypeDropdownIndex}="${reflectanceIndex.dialog.getAttribute(materialTypeDropdownIndex)}"]`);
// Add the value from the overlay's input as a new option (selected) in the calling ${ materialDropdownOptionsWithStartingOption.join('') }`;
thicknessCell.innerHTML = ``;
insertTarget.insertAdjacentElement(insertLocation, rowClone);
// Add 'Material' dropdown change event (will need to remove this listener when row is removed to avoid memory leak)
let materialSelect = materialCell.querySelector('select');
if (materialSelect) materialSelect.addEventListener('change', handleLayerMaterialDropdownChange);
// Renumber the 'Layer Number' column
let renumberRefEl = insertTarget.getAttribute('data-calc') === 'new-layers' ? insertTarget : insertTarget.closest('[data-calc="new-layers"]');
let rows = renumberRefEl.querySelectorAll('tr');
rows?.forEach((row, index) => setSequentialLayerNumber(row, index));
}
// 'Add' buttons added in more than one location so we want to keep button markup consistent
function addButtonMarkup() {
return ``;
}
// Set sequential number to target element
function setSequentialLayerNumber(el, index) {
if (!el || index < 0) return;
let layerNumber = el.querySelector('span')
let materialTypeDropdown = el.querySelector('select')
layerNumber.textContent = index + 1; // Update 'Layer Number' column text
materialTypeDropdown.setAttribute(materialTypeDropdownIndex, index+2); // Update '