The “Lion CIty Soundscapes” dataset consists of 62 360° audiovisual recordings to characterise the soundscape in Singapore. The locations represent perceptual dimensions of “Full of Life & Exciting”, “Calm & Tranquil”, “Lifeless & Boring”, and “Chaotic & Restless”.
The ISO Pleasantness and ISO Eventfulness map layers computed from binaural recordings at each site represent the perceived affective quality of each site according to ISO 12913-3.
For more information: follow the hyperlinks to the associated publications detailing the site selection method and the recording and excerpting methodology. This is an enhancement of the original visualisation.
lcs_data_df = transpose(lcs_data)
// Loading the D3 library or data manipulation and visualization
d3 = require("d3@6")
// Include FontAwesome for icons
html`<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.4/css/all.min.css"`
filtered_sites = {
return lcs_data_df.filter(d =>
//d.sitecat === sitecat_select // selected site
sitecat_select.includes(d.sitecat) // selected sites
);
}
function createColorMap(data) {
const colorMap = new Map(data.map(d => [d.value, d.color]));
return value => colorMap.get(value) || "transparent";
}
pal_isopl = createColorMap(isoplColors);
pal_isoev = createColorMap(isoevColors);
// Function to extract unique values from GeoJSON
function getUniqueValues(geojson, property) {
const values = new Set();
geojson.features.forEach(feature => {
values.add(feature.properties[property]);
});
return Array.from(values);
}
// Load GeoJson files
//isoevGeoJson = await d3.json(`r_isoev.geojson`);
//isoplGeoJson = await d3.json(`r_isopl.geojson`);
//isoplColors
// Load Leaflet library
L = require('leaflet@1.9.4')
// Include Leaflet CSS and custom legend CSS
html`<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY=" crossorigin="" />`
html`
<style>
.info.legend {
line-height: 18px;
color: #555;
background: white;
background: rgba(255,255,255,0.8);
box-shadow: 0 0 15px rgba(0,0,0,0.2);
border-radius: 5px;
padding: 10px;
}
.info.legend i {
width: 18px;
height: 18px;
float: left;
margin-right: 8px;
opacity: 0.7;
}
</style>
`
map = {
// Set dimensions
let width = 850;
let height = width / 1.6;
// Create the container for the map
let container = DOM.element('div', {
style: `width: ${width}px; height: ${height}px`
});
yield container;
// Initialize the map
let map = L.map(container).setView([1.34,103.8], 11);
L.tileLayer('https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}@2x.png',
{attribution: '© <a href="http://osm.org/copyright">OpenStreetMap</a>contributors'}).addTo(map);
// Define icons
const iconSet = {
'Calm': L.icon({
iconUrl: calm_icon,
iconSize: [32, 32],
iconAnchor: [16, 32],
popupAnchor: [1, -25]
}),
'Chaotic': L.icon({
iconUrl: chaotic_icon,
iconSize: [32, 32],
iconAnchor: [16, 32],
popupAnchor: [1, -25]
}),
'Vibrant': L.icon({
iconUrl: vibrant_icon,
iconSize: [32, 32],
iconAnchor: [16, 32],
popupAnchor: [1, -25]
}),
'Monotonous': L.icon({
iconUrl: monotonous_icon,
iconSize: [32, 32],
iconAnchor: [16, 32],
popupAnchor: [1, -25]
})
};
// Add markers for each filtered site
const markers = L.layerGroup(filtered_sites.map(d => {
return L.marker([d.lat, d.lng], {
icon: iconSet[d.sitecat]
}).bindPopup(`
<h4 style='color: black'><a href='${d.yt_link}'>${d.desc}</a></h4>
<b>Type:</b> ${d.sitecat}<br>
<b>SPL:</b> ${d.spl.toFixed(1)} dB(A)<br>
<b>ISOPL:</b> ${d.isopl.toFixed(3)}<br>
<b>ISOEV:</b> ${d.isoev.toFixed(3)}<br>
${d.yt_iframe}
`, { maxWidth: 560 });
}));
markers.addTo(map);
// Add legend to the map
var legend = L.control({position: 'bottomright'});
legend.onAdd = function (map) {
var div = L.DomUtil.create('div', 'info legend'),
categories = ['Vibrant','Calm','Monotonous','Chaotic'],
colors = ['#CE2E6C', '#FFB5B5', '#F0DECB', '#504658'];
div.innerHTML += '<b>Site Type</b><br>';
// Loop through the categories to generate labels
for (let i = 0; i < categories.length; i++) {
div.innerHTML += `<i style="background:
${colors[i]}"></i> ${categories[i]}<br>`;
}
return div;
};
legend.addTo(map);
// Add polygons
const isoplLayer = L.geoJSON(isoplGeoJson, {
style: feature => ({
fillColor: pal_isopl(feature.properties.var1pred),
fillOpacity: 0.5,
weight: 0
})
}).addTo(map);
const isoevLayer = L.geoJSON(isoevGeoJson, {
style: feature => ({
fillColor: pal_isoev(feature.properties.var1pred),
fillOpacity: 0.5,
weight: 0
})
}).addTo(map);
// Add legend for ISOPL
const legendIsopl = L.control({ position: 'bottomleft' });
legendIsopl.onAdd = function (map) {
const div = L.DomUtil.create('div', 'info legend');
const minValue = -0.2889;
const maxValue = 0.4136;
const steps = 10; // Number of steps in the gradient
div.innerHTML += '<b>ISOPL<b><br>';
// Create a flex container
let legendContent = '<div style="display: flex; align-items: center;">';
// Create gradient bar
let gradientBar = '<div style="width: 20px; height: 200px;';
// Define linear gradient
gradientBar += 'background: linear-gradient(to bottom,';
gradientBar += 'rgba(69,117,180,0.5), rgba(145,191,219,0.5),';
gradientBar += 'rgba(224,243,248,0.5), rgba(255,255,191,0.5),';
gradientBar += 'rgba(254,224,144,0.5), rgba(252,141,89,0.5),';
gradientBar += 'rgba(215,48,39,0.5))"></div>';
legendContent += gradientBar;
// Add labels
legendContent += `<div style="width: 30px; height: 200px; display: flex; flex-direction: column; justify-content: space-between; margin-left: 5px;">
<span>${maxValue.toFixed(2)}</span>
<span>${(maxValue - (maxValue - minValue) * 0.2).toFixed(1)}</span>
<span>${(maxValue - (maxValue - minValue) * 0.4).toFixed(1)}</span>
<span>${(maxValue - (maxValue - minValue) * 0.6).toFixed(1)}</span>
<span>${(maxValue - (maxValue - minValue) * 0.8).toFixed(1)}</span>
<span>${minValue.toFixed(2)}</span>
</div>`;
legendContent += '</div>'; // Close the flex container
div.innerHTML += legendContent;
return div;
};
legendIsopl.addTo(map);
// Add legend for ISOEV
const legendIsoev = L.control({ position: 'bottomleft' });
legendIsoev.onAdd = function (map) {
const div = L.DomUtil.create('div', 'info legend');
const minValue = -0.4313712;
const maxValue = 0.392208;
const steps = 10; // Number of steps in the gradient
div.innerHTML += '<b>ISOEV<b><br>';
// Create a flex container
let legendContent = '<div style="display: flex; align-items: center;">';
// Create gradient bar
let gradientBar = '<div style="width: 20px; height: 200px;';
// Define linear gradient
gradientBar += 'background: linear-gradient(to bottom,';
gradientBar += 'rgba(27,120,55,0.5), rgba(127,191,123,0.5),';
gradientBar += 'rgba(217,240,211,0.5), rgba(247,247,247,0.5),';
gradientBar += 'rgba(231,212,232,0.5), rgba(175,141,195,0.5),';
gradientBar += 'rgba(118,42,131,0.5))"></div>';
legendContent += gradientBar;
// Add labels
legendContent += `<div style="width: 30px; height: 200px; display: flex; flex-direction: column; justify-content: space-between; margin-left: 5px;">
<span>${maxValue.toFixed(2)}</span>
<span>${(maxValue - (maxValue - minValue) * 0.2).toFixed(1)}</span>
<span>${(maxValue - (maxValue - minValue) * 0.4).toFixed(1)}</span>
<span>${(maxValue - (maxValue - minValue) * 0.6).toFixed(1)}</span>
<span>${(maxValue - (maxValue - minValue) * 0.8).toFixed(1)}</span>
<span>${minValue.toFixed(2)}</span>
</div>`;
legendContent += '</div>'; // Close the flex container
div.innerHTML += legendContent;
return div;
};
legendIsoev.addTo(map);
// Add layer control
const overlayMaps = {
'Markers': markers,
'ISO Pleasantness': isoplLayer,
'ISO Eventfulness': isoevLayer
};
L.control.layers(null, overlayMaps).addTo(map);
return map;
}
// Create the scatter plot with the unit circle and axes
Plot.plot({
width: 480,
height: 480,
x: {
domain: [-0.5, 0.5],
label: "ISOPL",
grid: true
},
y: {
domain: [-0.5, 0.5],
label: "ISOEV",
grid: true
},
//r: { type: "linear", domain: [0, 12], range: [0, 480 - 60] },
marks: [
//Plot.dot([[0, 0]], { r: [3] }), // scaled (channel r)
Plot.dot(filtered_sites, { x: "isopl", y: "isoev",
stroke: "sitecat", tip: true,
channels: {description: "desc", spl: "spl"}}),
Plot.crosshair(filtered_sites, {x: "isopl", y: "isoev"}),
Plot.ruleX([0], { stroke: "black" }),
Plot.ruleY([0], { stroke: "black" })
],
color:{domain: ["Calm", "Chaotic", "Monotonous", "Vibrant"], range: ['#FFB5B5','#504658', '#F0DECB', '#CE2E6C']}
});
//create a table with the filtered sites
viewof table = Inputs.table(filtered_sites, {
columns: [
"id",
"sitecat",
"desc",
"spl",
"isopl",
"isoev"
],
header: {
id: "ID",
sitecat: "Site Type",
desc: "Description",
spl: "dB(A)",
isopl: "ISOPleasantness",
isoev: "ISOEventfulness"
},
width: {
id: 40,
sitecat: 60,
desc: 150,
spl: 60,
isopl: 80,
isoev: 80
},
format: {
depth: x => Number.isInteger(x) ? x.toString() : x.toFixed(2)
},
rows: Infinity,
pagination: true
})