fmt = d3.format(".2f");
viewof slider = rangeSlider({
min: d3.extent(rawData, d => d.wavelength)[0],
max: d3.extent(rawData, d => d.wavelength)[1],
step: 0.7,
format: d => `${fmt(d)} nm`,
title: 'Select a range',
})
minWL = slider[0];
maxWL = slider[1];
categoryLookup = new Map([
["Hectorite", {category: "Minerals", color: "#4daf4a"}],
["Magnesite", {category: "Minerals", color: "#ff7f00"}],
["Saponite", {category: "Minerals", color: "#a65628"}],
["Sepiolite", {category: "Minerals", color: "#e377c2"}],
["Vermiculite", {category: "Minerals", color: "#7f7f7f"}],
["Oak leaf, new growth", {category: "Vegetation", color: "#3a8040"}],
["Oak leaf, one year old", {category: "Vegetation", color: "#ffdd57"}],
["Oak leaf, scenesced", {category: "Vegetation", color: "#1b9e77"}],
["Maple Leaf", {category: "Vegetation", color: "#7570b3"}],
["HDPE bottle", {category: "Plastics", color: "#1f78b4"}],
["HDPE film", {category: "Plastics", color: "#33a02c"}],
["PVC", {category: "Plastics", color: "#6a3d9a"}],
["Polysterene", {category: "Plastics", color: "#b15928"}]
]);
samples = d3.sort(d3.group(rawData, d => d.sample).keys());
categorizedData = rawData.map(d => {
const info = categoryLookup.get(d.sample)
return {
...d,
category : info.category,
color : info.color
};
});
// wrapping data in set removes duplicates
categories = Array.from(new Set(categorizedData.map(d => d.category)));
filteredData = categorizedData.filter(
d => d.wavelength >= minWL &&
d.wavelength <= maxWL &&
categorySel.includes(d.category) &&
sampleSel.includes(d.sample)
);
{
if (filteredData.length < 1) {
return html`<div style="color:#666;">No active selections to plot.</div>`;
}
const rowsBySample = d3.group(filteredData, d => d.sample); // Map<sample, rows[]>
const adjustedData = [];
const resolutionSize = Math.max(1, Math.round(resolutionSel / 0.7));
for (const [, rows] of rowsBySample) {
rows.sort((a, b) => d3.ascending(a.wavelength, b.wavelength));
var adjusted = rows.map(r => r.reflectance);
if (windowSel > 1){
adjusted = d3.blur(rows.map(r => r.reflectance), windowSel);
}
for (let i = 0; i < adjusted.length; i += resolutionSize) { // plot every nth point
adjustedData.push({
wavelength: rows[i].wavelength,
reflectance: adjusted[i],
sample: rows[i].sample
});
}
}
const activeSamples = Array.from(new Set(filteredData.map(d => d.sample)));
const activeColors = activeSamples.map(s => categoryLookup.get(s).color);
const tickStart = Math.ceil(minWL / 30) * 30;
const tickEnd = Math.floor(maxWL / 30) * 30;
const plotWidth = Math.min(width, 1500);
const maxY = d3.max(adjustedData, d=>d.reflectance) * 1.03;
const fontPx = Math.max(10, Math.round(plotWidth / 70)); //scale fontSize with plotWidth, minimum 16px
const rotate = plotWidth < 650 ? -45 : 0; // if container under 600px, rotate x tick text
return Plot.plot({
width: plotWidth,
height: Math.round(plotWidth * (13/20)), // achieve more square graph
marginRight: 150,
marginLeft: 55,
marginTop: 40,
marginBottom: 50,
x: {
label: "Wavelength (nm)",
grid: true,
tickPadding: 10,
tickRotate: rotate,
ticks: d3.range(tickStart, tickEnd + 1, 30),
tickFormat: d => d.toFixed(2),
},
y: {
label: "Reflectance",
grid: true,
tickPadding: 10,
domain: [0, maxY]
},
color: {
legend: true,
inset: {
anchor: "top-right",
inset: 10,
padding: 5
}
},
style: {
fontSize: `${fontPx}px`,
"--plot-font-size": `${fontPx}px` // so that the font size applies to the plot
},
marks: [
Plot.line(adjustedData, {
x: "wavelength",
y: "reflectance",
stroke: "sample",
title: "sample",
strokeWidth: 4
}),
Plot.dot(adjustedData, Plot.pointerX({
x: "wavelength",
y: "reflectance",
fill: "sample",
stroke: "white",
marker: "circle",
strokeWidth: 2.5,
r: 5
})),
Plot.text(adjustedData, Plot.pointerX({
px: "wavelength",
py: "reflectance",
frameAnchor: "top-left",
fontVariant: "tabular-nums",
fontSize: fontPx * 0.8,
text: d => `${d.sample} λ ${d.wavelength} nm ${d.reflectance.toFixed(4)}`
})),
Plot.text(adjustedData, Plot.selectLast({
x: "wavelength",
y: "reflectance",
z: "sample",
text: "sample",
textAnchor: "start",
fontSize: fontPx * 0.7,
dx: 3
}))
]
});
}
---
comments: false
page-layout: custom
format:
html:
margin-top: 0em
margin-bottom: 0em
minimal: true
smooth-scroll: true
fig-responsive: true
toc: false
echo: false
keep-hidden: true
code-tools: true
---
:::: {.hero-small .hero-gradient .hero-clouds}
::: {.container}
# **Spectra**
:::
::::
::::: {.container}
:::: {.about .px-md-7 .py-3 style="border-bottom: 1px solid #C8C7C7;"}
# About
::::
:::: {.plots .px-md-4 .py-3}
```{ojs}
import { rangeSlider as rangeSlider } from '@mootari/range-slider'
```
```{ojs}
rawData = FileAttachment("data/rfl_lowpass_shaped_v2.csv").csv({typed: true});
```
```{ojs}
fmt = d3.format(".2f");
viewof slider = rangeSlider({
min: d3.extent(rawData, d => d.wavelength)[0],
max: d3.extent(rawData, d => d.wavelength)[1],
step: 0.7,
format: d => `${fmt(d)} nm`,
title: 'Select a range',
})
minWL = slider[0];
maxWL = slider[1];
```
```{ojs}
categoryLookup = new Map([
["Hectorite", {category: "Minerals", color: "#4daf4a"}],
["Magnesite", {category: "Minerals", color: "#ff7f00"}],
["Saponite", {category: "Minerals", color: "#a65628"}],
["Sepiolite", {category: "Minerals", color: "#e377c2"}],
["Vermiculite", {category: "Minerals", color: "#7f7f7f"}],
["Oak leaf, new growth", {category: "Vegetation", color: "#3a8040"}],
["Oak leaf, one year old", {category: "Vegetation", color: "#ffdd57"}],
["Oak leaf, scenesced", {category: "Vegetation", color: "#1b9e77"}],
["Maple Leaf", {category: "Vegetation", color: "#7570b3"}],
["HDPE bottle", {category: "Plastics", color: "#1f78b4"}],
["HDPE film", {category: "Plastics", color: "#33a02c"}],
["PVC", {category: "Plastics", color: "#6a3d9a"}],
["Polysterene", {category: "Plastics", color: "#b15928"}]
]);
```
```{ojs}
samples = d3.sort(d3.group(rawData, d => d.sample).keys());
categorizedData = rawData.map(d => {
const info = categoryLookup.get(d.sample)
return {
...d,
category : info.category,
color : info.color
};
});
// wrapping data in set removes duplicates
categories = Array.from(new Set(categorizedData.map(d => d.category)));
```
::: {.grid .py-1 }
::: {.g-col-6}
```{ojs}
viewof categorySel = Inputs.checkbox(categories, {
label: "Categories",
value: ["Plastics"]}
);
sampleOptions = Array.from(categoryLookup.keys()).filter(
s => samples.includes(s) && categorySel.includes(categoryLookup.get(s).category)
);
```
:::
::: {.g-col-6}
```{ojs}
viewof sampleSel = Inputs.checkbox(sampleOptions, {
value: sampleOptions
});
viewof samplePanel = categorySel.length
? html`<details class="sample-drop" open>
<summary style="color:#000;">Samples</summary>
${viewof sampleSel}
</details>`
: html``;
```
:::
::: {.g-col-6}
```{ojs}
viewof resolutionSel = Inputs.range([0.7, 15.4], {step: 0.7, value: 0.7, label: "Resolution (nm)"})
```
:::
::: {.g-col-6}
```{ojs}
viewof windowSel = Inputs.range([1, 15], {step: 1, value: 1, label: "Spectral Averaging Window"})
```
:::
:::
```{ojs}
filteredData = categorizedData.filter(
d => d.wavelength >= minWL &&
d.wavelength <= maxWL &&
categorySel.includes(d.category) &&
sampleSel.includes(d.sample)
);
```
```{ojs}
{
if (filteredData.length < 1) {
return html`<div style="color:#666;">No active selections to plot.</div>`;
}
const rowsBySample = d3.group(filteredData, d => d.sample); // Map<sample, rows[]>
const adjustedData = [];
const resolutionSize = Math.max(1, Math.round(resolutionSel / 0.7));
for (const [, rows] of rowsBySample) {
rows.sort((a, b) => d3.ascending(a.wavelength, b.wavelength));
var adjusted = rows.map(r => r.reflectance);
if (windowSel > 1){
adjusted = d3.blur(rows.map(r => r.reflectance), windowSel);
}
for (let i = 0; i < adjusted.length; i += resolutionSize) { // plot every nth point
adjustedData.push({
wavelength: rows[i].wavelength,
reflectance: adjusted[i],
sample: rows[i].sample
});
}
}
const activeSamples = Array.from(new Set(filteredData.map(d => d.sample)));
const activeColors = activeSamples.map(s => categoryLookup.get(s).color);
const tickStart = Math.ceil(minWL / 30) * 30;
const tickEnd = Math.floor(maxWL / 30) * 30;
const plotWidth = Math.min(width, 1500);
const maxY = d3.max(adjustedData, d=>d.reflectance) * 1.03;
const fontPx = Math.max(10, Math.round(plotWidth / 70)); //scale fontSize with plotWidth, minimum 16px
const rotate = plotWidth < 650 ? -45 : 0; // if container under 600px, rotate x tick text
return Plot.plot({
width: plotWidth,
height: Math.round(plotWidth * (13/20)), // achieve more square graph
marginRight: 150,
marginLeft: 55,
marginTop: 40,
marginBottom: 50,
x: {
label: "Wavelength (nm)",
grid: true,
tickPadding: 10,
tickRotate: rotate,
ticks: d3.range(tickStart, tickEnd + 1, 30),
tickFormat: d => d.toFixed(2),
},
y: {
label: "Reflectance",
grid: true,
tickPadding: 10,
domain: [0, maxY]
},
color: {
legend: true,
inset: {
anchor: "top-right",
inset: 10,
padding: 5
}
},
style: {
fontSize: `${fontPx}px`,
"--plot-font-size": `${fontPx}px` // so that the font size applies to the plot
},
marks: [
Plot.line(adjustedData, {
x: "wavelength",
y: "reflectance",
stroke: "sample",
title: "sample",
strokeWidth: 4
}),
Plot.dot(adjustedData, Plot.pointerX({
x: "wavelength",
y: "reflectance",
fill: "sample",
stroke: "white",
marker: "circle",
strokeWidth: 2.5,
r: 5
})),
Plot.text(adjustedData, Plot.pointerX({
px: "wavelength",
py: "reflectance",
frameAnchor: "top-left",
fontVariant: "tabular-nums",
fontSize: fontPx * 0.8,
text: d => `${d.sample} λ ${d.wavelength} nm ${d.reflectance.toFixed(4)}`
})),
Plot.text(adjustedData, Plot.selectLast({
x: "wavelength",
y: "reflectance",
z: "sample",
text: "sample",
textAnchor: "start",
fontSize: fontPx * 0.7,
dx: 3
}))
]
});
}
```
::::
:::::