Public
Edited
Feb 24
Fork of Untitled
1 fork
Insert cell
Insert cell
chartImproved = {
const data = almeraMassEmailList1;
// Maintain a filtered version of the data; initially, all data is shown.
let filteredData = data;

// Create a color scale for the Geographic Region
const colorScale = d3.scaleOrdinal()
.domain(["EUROPE", "NORTH AND LATIN AMERICA", "ASIA PACIFIC", "AFRICA", "MIDDLE EAST"])
.range(["red", "blue", "green", "orange", "purple"]);

// Create the canvas context for the map
const context = DOM.context2d(width, height);
const projection = d3.geoEqualEarth().fitSize([width, height], sphere);
const path = d3.geoPath(projection, context);

// Tooltip container
const tooltip = d3.select("body")
.append("div")
.style("position", "absolute")
.style("padding", "8px")
.style("background", "rgba(0, 0, 0, 0.7)")
.style("color", "white")
.style("border-radius", "4px")
.style("pointer-events", "none")
.style("font-size", "12px")
.style("display", "none"); // Initially hidden

// Create a search bar (position it as desired)
const searchBar = d3.select("body")
.append("input")
.attr("type", "text")
.attr("placeholder", "Search by Laboratory, Member State, City, or Contact Person")
.style("position", "absolute")
.style("top", "10px")
.style("left", "10px")
.style("padding", "8px")
.style("font-size", "14px");

// Update the filtered data whenever the search input changes.
searchBar.on("input", function() {
const query = this.value.toLowerCase();
filteredData = data.filter(d =>
(d["Member State"] && d["Member State"].toLowerCase().includes(query)) ||
(d["Laboratory Name"] && d["Laboratory Name"].toLowerCase().includes(query)) ||
(d["Geographic Region"] && d["Geographic Region"].toLowerCase().includes(query)) ||
(d["Contact Person"] && d["Contact Person"].toLowerCase().includes(query)) ||
(d["City"] && d["City"].toLowerCase().includes(query))
);
// Re-render the map using the filtered data.
points = render(land50);
});

// Track the currently selected point (for click-to-lock tooltips)
let selectedPoint = null;

// Clustering function
function clusterPoints(points, radius) {
const clusters = [];

points.forEach(point => {
let addedToCluster = false;

clusters.forEach(cluster => {
const dx = point.x - cluster.x;
const dy = point.y - cluster.y;
const distance = Math.sqrt(dx * dx + dy * dy);

if (distance < radius) {
cluster.points.push(point);
cluster.x = (cluster.x * (cluster.points.length - 1) + point.x) / cluster.points.length;
cluster.y = (cluster.y * (cluster.points.length - 1) + point.y) / cluster.points.length;
addedToCluster = true;
}
});

if (!addedToCluster) {
clusters.push({
x: point.x,
y: point.y,
points: [point]
});
}
});

return clusters;
}
// Function to render the map and dots
function render(land) {
context.clearRect(0, 0, width, height);

// Draw the sphere (background)
context.beginPath();
path(sphere);
context.fillStyle = "#fff";
context.fill();

// Draw the land with the new color
context.beginPath();
path(land);
context.fillStyle = "#66a5d2"; // New land color
context.fill();

// Draw the dots using filteredData
const points = filteredData.map(d => {
const [x, y] = projection([+d.Long, +d.Lat]) || [];
return {
x,
y,
color: colorScale(d["Geographic Region"] || "black"),
info: d["Laboratory Name"] || "Laboratory Missing",
city: d["City"] || "Unknown City",
memberState: d["Member State"] || "No member state",
contact: d["Contact Person"] || "No contact available",
telephone: d["Telephone"] || "No telephone available",
email: d["Email"] || "No email available",
address: d["Physical Address"] || "No address available",
update: d["Latest Update"] || "Unknown update"
};
});

// Cluster points to avoid overlap
const clusters = clusterPoints(points, 15);

clusters.forEach(cluster => {
if (cluster.points.length > 1) {
// Draw a cluster circle
context.beginPath();
context.arc(cluster.x, cluster.y, 10, 0, 2 * Math.PI);
context.fillStyle = "gray";
context.fill();
context.fillStyle = "white";
context.textAlign = "center";
context.textBaseline = "middle";
context.fillText(cluster.points.length, cluster.x, cluster.y);
} else {
const point = cluster.points[0];
context.beginPath();
context.arc(point.x, point.y, 5, 0, 2 * Math.PI);
context.fillStyle = point.color;
context.fill();
}
});

return points;
}

// Initial render with all data
let points = render(land50);

// Add mousemove detection for tooltips
d3.select(context.canvas)
.on("mousemove", event => {
if (selectedPoint) return; // If a point is selected, do not update tooltip on hover

const [mouseX, mouseY] = d3.pointer(event);
const hoveredPoint = points.find(point => {
const dx = mouseX - point.x;
const dy = mouseY - point.y;
return Math.sqrt(dx * dx + dy * dy) <= 5; // Check if within the radius of a dot
});

if (hoveredPoint) {
tooltip
.style("left", `${event.pageX + 10}px`)
.style("top", `${event.pageY + 10}px`)
.style("display", "block")
.html(`
<strong>${hoveredPoint.info}</strong><br>
🏢 ${hoveredPoint.address}<br>
📍 ${hoveredPoint.city}<br>
🌍 ${hoveredPoint.memberState}<br>
🧑‍💼 ${hoveredPoint.contact}<br>
📧 ${hoveredPoint.email}<br>
📞 ${hoveredPoint.telephone}<br>
🔄 ${new Date(hoveredPoint.update).toLocaleDateString()}<br>
`);
} else {
tooltip.style("display", "none");
}
});

// Add click detection for dots
d3.select(context.canvas)
.on("click", event => {
const [mouseX, mouseY] = d3.pointer(event);
const clickedPoint = points.find(point => {
const dx = mouseX - point.x;
const dy = mouseY - point.y;
return Math.sqrt(dx * dx + dy * dy) <= 5;
});

if (clickedPoint) {
// If a dot is clicked, store it and keep the tooltip visible.
selectedPoint = clickedPoint;
tooltip
.style("left", `${event.pageX + 10}px`)
.style("top", `${event.pageY + 10}px`)
.style("display", "block")
.html(`
<strong>${clickedPoint.info}</strong><br>
🏢 ${clickedPoint.address}<br>
📍 ${clickedPoint.city}<br>
🌍 ${clickedPoint.memberState}<br>
🧑‍💼 ${clickedPoint.contact}<br>
📧 ${clickedPoint.email}<br>
📞 ${clickedPoint.telephone}<br>
🔄 ${new Date(clickedPoint.update).toLocaleDateString()}<br>
`);
} else {
// If clicked elsewhere on the canvas, reset the selected point and hide the tooltip.
selectedPoint = null;
tooltip.style("display", "none");
}
});

// Handle smooth zoom events
const zoom = d3.zoom()
.scaleExtent([1, 250]) // Adjust the zoom limits
.on("zoom", (event) => {
// Update projection with the new transformation
const transform = event.transform;
projection.translate([transform.x, transform.y]).scale(transform.k * 300); // Scale dynamically

// Dynamically redraw the map and recalculate points
points = render(land50);
});

// Attach zoom behavior to the canvas and disable double-click zoom
d3.select(context.canvas)
.call(zoom)
.on("dblclick.zoom", null);
const title = d3.select("body")
.append("h2")
.text("Interactive ALMERA Laboratories (Private)")
.style("position", "absolute")
.style("top", "10px")
.style("left", "50%")
.style("transform", "translateX(-50%)")
.style("font-family", "Arial")
.style("font-size", "24px")
.style("color", "#333")
.style("text-align", "center");

return d3.select(context.canvas).node();
}

Insert cell
<div id="filters">
<label><input type="checkbox" value="h3"> H-3</label>
<label><input type="checkbox" value="c14"> C-14</label>
<label><input type="checkbox" value="k40"> K-40</label>
<label><input type="checkbox" value="sr90"> Sr-90</label>
<label><input type="checkbox" value="i131"> I-131</label>
<label><input type="checkbox" value="cs137"> Cs-137</label>
<label><input type="checkbox" value="pb210"> Pb-210</label>
<label><input type="checkbox" value="ra226"> Ra-226</label>
<label><input type="checkbox" value="th232"> Th-232</label>
<label><input type="checkbox" value="pu239"> Pu-239</label>
<label><input type="checkbox" value="am241"> Am-241</label>
</div>

Insert cell
chart = {
const data = almeraMassEmailList1;

// Create a color scale for the Geographic Region
const colorScale = d3.scaleOrdinal()
.domain(["EUROPE", "NORTH AND LATIN AMERICA", "ASIA PACIFIC", "AFRICA", "MIDDLE EAST"])
.range(["red", "blue", "green", "yellow", "purple"]);

// Create the canvas context for the map
const context = DOM.context2d(width, height);
const projection = d3.geoEqualEarth().fitSize([width, height], sphere);
const path = d3.geoPath(projection, context);

// Tooltip container
const tooltip = d3.select("body")
.append("div")
.style("position", "absolute")
.style("padding", "8px")
.style("background", "rgba(0, 0, 0, 0.7)")
.style("color", "white")
.style("border-radius", "4px")
.style("pointer-events", "none")
.style("font-size", "12px")
.style("display", "none"); // Initially hidden

// Track the currently selected point
let selectedPoint = null;

// Function to render the map and dots
function render(land) {
context.clearRect(0, 0, width, height);

// Draw the sphere (background)
context.beginPath();
path(sphere);
context.fillStyle = "#fff";
context.fill();

// Draw the land with the new color
context.beginPath();
path(land);
context.fillStyle = "#66a5d2"; // New land color
context.fill();

// Draw the dots
const points = data.map(d => {
const [x, y] = projection([+d.Long, +d.Lat]) || [];
return {
x,
y,
color: colorScale(d["Geographic Region"] || "black"),
info: d["Laboratory Name"] || "Laboratory Missing",
contact: d["Contact Person"] || "No contact available",
memberState: d["Member State"] || "No member state",
telephone: d["Telephone"] || "No telephone available",
email: d["Email"] || "No email available",
address: d["Physical Address"] || "No address available",
h3: d["H-3"] ? d["H-3"] : "No",
be7: d["Be-7"] ? d["B-7"] : "No",
c14: d["C-14"] ? d["C-14"] : "No",
k40: d["K-40"] ? d["K-40"] : "No",
co60: d["Co-60"] ? d["Co-60"] : "No",
sr90: d["Sr-90"] ? d["Sr-90"] : "No",
tc99: d["Tc-99"] ? d["Tc-99"] : "No",
i125: d["I-125"] ? d["I-125"] : "No",
i129: d["I-129"] ? d["I-129"] : "No",
i131: d["I-131"] ? d["I-131"] : "No",
ba133: d["Ba-133"] ? d["Ba-133"] : "No",
cs134: d["Cs-134"] ? d["Cs-134"] : "No",
cs137: d["Cs-137"] ? d["Cs-137"] : "No",
eu152: d["Eu-152"] ? d["Eu-152"] : "No",
lu177: d["Lu-177"] ? d["Lu-177"] : "No",
pb210: d["Pb-210"] ? d["Pb-210"] : "No",
po210: d["Po-210"] ? d["Po-210"] : "No",
rn: d["Rn"] ? d["Rn"] : "No",
rn222: d["Rn-222"] ? d["Rn-222"] : "No",
rn226: d["Rn-226"] ? d["Rn-226"] : "No",
ra226: d["Ra-226"] ? d["Ra-226"] : "No",
ra228: d["Ra-228"] ? d["Ra-228"] : "No",
ac228: d["Ac-228"] ? d["Ac-228"] : "No",
th228: d["Th-228"] ? d["Th-228"] : "No",
th232: d["Th-232"] ? d["Th-232"] : "No",
u234: d["U-234"] ? d["U-234"] : "No",
u235: d["U-235"] ? d["U-235"] : "No",
u238: d["U-238"] ? d["U-238"] : "No",
u: d["U"] ? d["U"] : "No",
pu: d["Pu"] ? d["Pu"] : "No",
pu238: d["Pu-238"] ? d["Pu-238"] : "No",
pu239: d["Pu-239"] ? d["Pu-239"] : "No",
pu240: d["Pu-240"] ? d["Pu-240"] : "No",
pu241: d["Pu-241"] ? d["Pu-241"] : "No",
am241: d["Am-241"] ? d["Am-241"] : "No",
cm242: d["Cm-242"] ? d["Cm-242"] : "No",
grossalphabeta: d["Gross alpha / beta"] ? d["Gross alpha / beta"] : "No",
gammaemit: d["Gamma emitters"] ? d["Gamma emitters"] : "No",
grossbeta: d["Gross beta"] ? d["Gross beta"] : "No",
};
});

points.forEach(point => {
if (point.x !== undefined && point.y !== undefined) {
context.beginPath();
context.arc(point.x, point.y, 5, 0, 2 * Math.PI);
context.fillStyle = point.color;
context.fill();
}
});

return points; // Return the points for interaction handling
}

// Initial render
let points = render(land50);

// Add mousemove detection for tooltips
d3.select(context.canvas)
.on("mousemove", event => {
if (selectedPoint) return; // Do nothing if a point is selected

const [mouseX, mouseY] = d3.pointer(event);
const hoveredPoint = points.find(point => {
const dx = mouseX - point.x;
const dy = mouseY - point.y;
return Math.sqrt(dx * dx + dy * dy) <= 5; // Check if within the radius of a dot
});

if (hoveredPoint) {
tooltip
.style("left", `${event.pageX + 10}px`)
.style("top", `${event.pageY + 10}px`)
.style("display", "block")
.html(`
<strong>${hoveredPoint.info}</strong><br>
🌍 ${hoveredPoint.memberState}<br>
H-3: ${hoveredPoint.h3} <br>
C-14: ${hoveredPoint.c14} <br>
K-40: ${hoveredPoint.k40} <br>
Sr-90: ${hoveredPoint.sr90} <br>
I-131: ${hoveredPoint.i131} <br>
Cs-137: ${hoveredPoint.cs137} <br>
Pb-210: ${hoveredPoint.pb210} <br>
Ra-226: ${hoveredPoint.ra226} <br>
Th-232: ${hoveredPoint.th232} <br>
Pu-239: ${hoveredPoint.pu239} <br>
Am-241: ${hoveredPoint.am241} <br>
`)
} else {
tooltip.style("display", "none");
}
});

// Add click detection for dots
d3.select(context.canvas)
.on("click", event => {
const [mouseX, mouseY] = d3.pointer(event);
const clickedPoint = points.find(point => {
const dx = mouseX - point.x;
const dy = mouseY - point.y;
return Math.sqrt(dx * dx + dy * dy) <= 5;
});

if (clickedPoint) {
// Store the clicked point and keep the tooltip visible
selectedPoint = clickedPoint;
tooltip
.style("left", `${event.pageX + 10}px`)
.style("top", `${event.pageY + 10}px`)
.style("display", "block")
.html(`
<strong>${clickedPoint.info}</strong><br>
🌍 ${clickedPoint.memberState}<br>
🏢 ${clickedPoint.address}<br>
🧑‍💼 ${clickedPoint.contact}<br>
📧 ${clickedPoint.email}<br>
📞 ${clickedPoint.telephone}<br>
H-3: ${clickedPoint.h3} <br>
Be-7: ${clickedPoint.be7} <br>
C-14: ${clickedPoint.c14} <br>
K-40: ${clickedPoint.k40} <br>
Co-60: ${clickedPoint.co60} <br>
Sr-90: ${clickedPoint.sr90} <br>
Tc-99: ${clickedPoint.tc99} <br>
I-125: ${clickedPoint.i125} <br>
I-129: ${clickedPoint.i129} <br>
I-131: ${clickedPoint.i131} <br>
Ba-133: ${clickedPoint.ba133} <br>
Cs-134: ${clickedPoint.cs134} <br>
Cs-137: ${clickedPoint.cs137} <br>
`);
} else {
// If clicked outside, hide the tooltip and reset the selected point
selectedPoint = null;
tooltip.style("display", "none");
}
});

// Handle smooth zoom events
const zoom = d3.zoom()
.scaleExtent([1, 8]) // Adjust the zoom limits
.on("zoom", (event) => {
// Update projection with the new transformation
const transform = event.transform;
projection.translate([transform.x, transform.y]).scale(transform.k * 300); // Scale dynamically

// Dynamically redraw the map and recalculate points
points = render(land50);
});

// Attach zoom behavior to the canvas
d3.select(context.canvas)
.call(zoom)
.on("dblclick.zoom", null); // Disable double-click zoom

return d3.select(context.canvas).node();
}

Insert cell
ALMERA Members Private Copy Radionuclides@4.csv
Type Table, then Shift-Enter. Ctrl-space for more options.

Insert cell
land = topojson.feature(land50m, land50m.objects.land)
Insert cell
world110m
Insert cell
import {world110m, world50m} from "@visionscarto/geo"
Insert cell
countries110m = topojson.feature(world110m, world110m.objects.countries)
Insert cell
projection = d3.geoEqualEarth()
Insert cell
height = {
const [[x0, y0], [x1, y1]] = d3.geoPath(projection.fitWidth(width, sphere)).bounds(sphere);
const dy = Math.ceil(y1 - y0), l = Math.min(Math.ceil(x1 - x0), dy);
projection.scale(projection.scale() * (l - 1) / l).precision(0.2);
return dy;
}

Insert cell
sphere = ({type: "Sphere"})
Insert cell
land50 = FileAttachment("land-50m.json").json().then(world => topojson.feature(world, world.objects.land))
Insert cell
land110 = FileAttachment("land-110m.json").json().then(world => topojson.feature(world, world.objects.land))
Insert cell
topojson = require("topojson-client@3")
Insert cell
versor = require("versor@0.0.4")
Insert cell
land50m = FileAttachment("land-50m.json").json()
Insert cell
viewof radionuclideCheckboxes = Inputs.checkbox(radionuclides, {label: "Select Radionuclides"});

Insert cell
chartRadionuclides = {
const data = almeraMembersPrivateCopyRadionuclides;
// Maintain a filtered version of the data; initially, all data is shown.
let filteredData = data;

// Create a color scale for the Geographic Region
const colorScale = d3.scaleOrdinal()
.domain(["EUROPE", "NORTH AND LATIN AMERICA", "ASIA PACIFIC", "AFRICA", "MIDDLE EAST"])
.range(["red", "blue", "green", "orange", "purple"]);

// Create the canvas context for the map
const context = DOM.context2d(width, height);
const projection = d3.geoEqualEarth().fitSize([width, height], sphere);
const path = d3.geoPath(projection, context);

// Tooltip container
const tooltip = d3.select("body")
.append("div")
.style("position", "absolute")
.style("padding", "8px")
.style("background", "rgba(0, 0, 0, 0.7)")
.style("color", "white")
.style("border-radius", "4px")
.style("pointer-events", "none")
.style("font-size", "12px")
.style("font-family", "Arial")
.style("display", "none"); // Initially hidden

// Create a search bar (position it as desired)
const searchBar = d3.select("body")
.append("input")
.attr("type", "text")
.attr("placeholder", "Search by Radionuclide, Laboratory, Member State, City, or Contact Person")
.style("position", "absolute")
.style("top", "10px")
.style("left", "10px")
.style("padding", "8px")
.style("font-size", "14px")
.style("font=family", "Arial");

// Update the filtered data whenever the search input changes.
searchBar.on("input", function() {
const query = this.value.toLowerCase();
filteredData = data.filter(d =>
(d["Member State"] && d["Member State"].toLowerCase().includes(query)) ||
(d["Laboratory Name"] && d["Laboratory Name"].toLowerCase().includes(query)) ||
(d["Geographic Region"] && d["Geographic Region"].toLowerCase().includes(query)) ||
(d["Contact Person"] && d["Contact Person"].toLowerCase().includes(query)) ||
(d["City"] && d["City"].toLowerCase().includes(query)) ||
(d["Radionuclides"] && d["Radionuclides"].toLowerCase().includes(query))
);
// Re-render the map using the filtered data.
points = render(land50);
});

// Track the currently selected point (for click-to-lock tooltips)
let selectedPoint = null;

// Clustering function
function clusterPoints(points, radius) {
const clusters = [];

points.forEach(point => {
let addedToCluster = false;

clusters.forEach(cluster => {
const dx = point.x - cluster.x;
const dy = point.y - cluster.y;
const distance = Math.sqrt(dx * dx + dy * dy);

if (distance < radius) {
cluster.points.push(point);
cluster.x = (cluster.x * (cluster.points.length - 1) + point.x) / cluster.points.length;
cluster.y = (cluster.y * (cluster.points.length - 1) + point.y) / cluster.points.length;
addedToCluster = true;
}
});

if (!addedToCluster) {
clusters.push({
x: point.x,
y: point.y,
points: [point]
});
}
});

return clusters;
}
// Function to render the map and dots
function render(land) {
context.clearRect(0, 0, width, height);

// Draw the sphere (background)
context.beginPath();
path(sphere);
context.fillStyle = "#fff";
context.fill();

// Draw the land with the new color
context.beginPath();
path(land);
context.fillStyle = "#66a5d2"; // New land color
context.fill();

// Draw the dots using filteredData
const points = filteredData.map(d => {
const [x, y] = projection([+d.Long, +d.Lat]) || [];
return {
x,
y,
color: colorScale(d["Geographic Region"] || "black"),
info: d["Laboratory Name"] || "Laboratory Missing",
city: d["City"] || "Unknown City",
memberState: d["Member State"] || "No member state",
contact: d["Contact Person"] || "No contact available",
telephone: d["Telephone"] || "No telephone available",
email: d["Email"] || "No email available",
address: d["Physical Address"] || "No address available",
update: d["Latest Update"] || "Unknown update",
radionuclides: d["Radionuclides"] || "No info on file"
};
});

// Cluster points to avoid overlap
const clusters = clusterPoints(points, 15);

clusters.forEach(cluster => {
if (cluster.points.length > 1) {
// Draw a cluster circle
context.beginPath();
context.arc(cluster.x, cluster.y, 10, 0, 2 * Math.PI);
context.fillStyle = "gray";
context.fill();
context.fillStyle = "white";
context.textAlign = "center";
context.textBaseline = "middle";
context.font = "12 px Arial";
context.fillText(cluster.points.length, cluster.x, cluster.y);
} else {
const point = cluster.points[0];
context.beginPath();
context.arc(point.x, point.y, 5, 0, 2 * Math.PI);
context.fillStyle = point.color;
context.fill();
}
});

return points;
}

// Initial render with all data
let points = render(land50);

// Add mousemove detection for tooltips
d3.select(context.canvas)
.on("mousemove", event => {
if (selectedPoint) return; // If a point is selected, do not update tooltip on hover

const [mouseX, mouseY] = d3.pointer(event);
const hoveredPoint = points.find(point => {
const dx = mouseX - point.x;
const dy = mouseY - point.y;
return Math.sqrt(dx * dx + dy * dy) <= 5; // Check if within the radius of a dot
});

if (hoveredPoint) {
tooltip
.style("left", `${event.pageX + 10}px`)
.style("top", `${event.pageY + 10}px`)
.style("display", "block")
.html(`
<strong>${hoveredPoint.info}</strong><br>
🏢 ${hoveredPoint.address}<br>
📍 ${hoveredPoint.city}<br>
🌍 ${hoveredPoint.memberState}<br>
🧑‍💼 ${hoveredPoint.contact}<br>
📧 ${hoveredPoint.email}<br>
📞 ${hoveredPoint.telephone}<br>
🔄 ${new Date(hoveredPoint.update).toLocaleDateString()}<br>
`);
} else {
tooltip.style("display", "none");
}
});

// Add click detection for dots
d3.select(context.canvas)
.on("click", event => {
const [mouseX, mouseY] = d3.pointer(event);
const clickedPoint = points.find(point => {
const dx = mouseX - point.x;
const dy = mouseY - point.y;
return Math.sqrt(dx * dx + dy * dy) <= 5;
});

if (clickedPoint) {
// If a dot is clicked, store it and keep the tooltip visible.
selectedPoint = clickedPoint;
tooltip
.style("left", `${event.pageX + 10}px`)
.style("top", `${event.pageY + 10}px`)
.style("display", "block")
.html(`
<strong>${clickedPoint.info}</strong><br>
🏢 ${clickedPoint.address}<br>
📍 ${clickedPoint.city}<br>
🌍 ${clickedPoint.memberState}<br>
🧑‍💼 ${clickedPoint.contact}<br>
📧 ${clickedPoint.email}<br>
📞 ${clickedPoint.telephone}<br>
🔄 ${new Date(clickedPoint.update).toLocaleDateString()}<br>
Radionuclides: ${clickedPoint.radionuclides.split(';').join('<br>')}<br>
`);
} else {
// If clicked elsewhere on the canvas, reset the selected point and hide the tooltip.
selectedPoint = null;
tooltip.style("display", "none");
}
});

// Handle smooth zoom events
const zoom = d3.zoom()
.scaleExtent([1, 250]) // Adjust the zoom limits
.on("zoom", (event) => {
// Update projection with the new transformation
const transform = event.transform;
projection.translate([transform.x, transform.y]).scale(transform.k * 300); // Scale dynamically

// Dynamically redraw the map and recalculate points
points = render(land50);
});

// Attach zoom behavior to the canvas and disable double-click zoom
d3.select(context.canvas)
.call(zoom)
.on("dblclick.zoom", null);
const title = d3.select("body")
.append("h2")
.text("Interactive ALMERA Laboratories (Private)")
.style("position", "absolute")
.style("top", "10px")
.style("left", "50%")
.style("transform", "translateX(-50%)")
.style("font-family", "Arial")
.style("font-size", "24px")
.style("color", "#333")
.style("text-align", "center");

return d3.select(context.canvas).node();
}


Insert cell
ALMERA Members Private Copy Radionuclides@5.csv
Type Table, then Shift-Enter. Ctrl-space for more options.

Insert cell
const width = 960;
const height = 500;

// Define the sphere and projection
const sphere = { type: "Sphere" };
const land50 = topojson.feature(world, world.objects.land); // Assuming you have TopoJSON data loaded

// Define the dataset
const data = almeraMassEmailList1;

// Define color scale for regions
const colorScale = d3.scaleOrdinal()
.domain(["EUROPE", "NORTH AND LATIN AMERICA", "ASIA PACIFIC", "AFRICA", "MIDDLE EAST"])
.range(["red", "blue", "green", "yellow", "purple"]);

// Create canvas context
const context = DOM.context2d(width, height);
const projection = d3.geoEqualEarth().fitSize([width, height], sphere);
const path = d3.geoPath(projection, context);

// Tooltip container
const tooltip = d3.select("body")
.append("div")
.style("position", "absolute")
.style("padding", "8px")
.style("background", "rgba(0, 0, 0, 0.7)")
.style("color", "white")
.style("border-radius", "4px")
.style("pointer-events", "none")
.style("font-size", "12px")
.style("display", "none");

// Track selected point
let selectedPoint = null;

// Function to render map and points
function render(land) {
context.clearRect(0, 0, width, height);

// Draw sphere
context.beginPath();
path(sphere);
context.fillStyle = "#fff";
context.fill();

// Draw land
context.beginPath();
path(land);
context.fillStyle = "#66a5d2";
context.fill();

// Draw points on the map
const points = data.map(d => {
const [x, y] = projection([+d.Long, +d.Lat]) || [];
return {
x, y,
color: colorScale(d["Geographic Region"] || "black"),
info: d["Laboratory Name"] || "Laboratory Missing",
memberState: d["Member State"] || "No member state",
h3: d["H-3"] || "No",
c14: d["C-14"] || "No",
k40: d["K-40"] || "No",
sr90: d["Sr-90"] || "No",
i131: d["I-131"] || "No",
cs137: d["Cs-137"] || "No",
pb210: d["Pb-210"] || "No",
ra226: d["Ra-226"] || "No",
th232: d["Th-232"] || "No",
pu239: d["Pu-239"] || "No",
am241: d["Am-241"] || "No",
};
});

// Draw points
points.forEach(point => {
if (point.x !== undefined && point.y !== undefined) {
context.beginPath();
context.arc(point.x, point.y, 5, 0, 2 * Math.PI);
context.fillStyle = point.color;
context.fill();
}
});

return points;
}

// Initial render
let points = render(land50);

// Mousemove for tooltips
d3.select(context.canvas).on("mousemove", event => {
if (selectedPoint) return;
const [mouseX, mouseY] = d3.pointer(event);
const hoveredPoint = points.find(point => {
const dx = mouseX - point.x;
const dy = mouseY - point.y;
return Math.sqrt(dx * dx + dy * dy) <= 5;
});

if (hoveredPoint) {
tooltip
.style("left", `${event.pageX + 10}px`)
.style("top", `${event.pageY + 10}px`)
.style("display", "block")
.html(`
<strong>${hoveredPoint.info}</strong><br>
🌍 ${hoveredPoint.memberState}<br>
H-3: ${hoveredPoint.h3}<br>
C-14: ${hoveredPoint.c14}<br>
K-40: ${hoveredPoint.k40}<br>
Sr-90: ${hoveredPoint.sr90}<br>
I-131: ${hoveredPoint.i131}<br>
Cs-137: ${hoveredPoint.cs137}<br>
Pb-210: ${hoveredPoint.pb210}<br>
Ra-226: ${hoveredPoint.ra226}<br>
Th-232: ${hoveredPoint.th232}<br>
Pu-239: ${hoveredPoint.pu239}<br>
Am-241: ${hoveredPoint.am241}<br>
`);
} else {
tooltip.style("display", "none");
}
});

// Click to select/deselect points
d3.select(context.canvas).on("click", event => {
const [mouseX, mouseY] = d3.pointer(event);
const clickedPoint = points.find(point => {
const dx = mouseX - point.x;
const dy = mouseY - point.y;
return Math.sqrt(dx * dx + dy * dy) <= 5;
});

if (clickedPoint) {
selectedPoint = clickedPoint;
tooltip
.style("left", `${event.pageX + 10}px`)
.style("top", `${event.pageY + 10}px`)
.style("display", "block")
.html(`
<strong>${clickedPoint.info}</strong><br>
🌍 ${clickedPoint.memberState}<br>
Address: ${clickedPoint.address || "No address"}<br>
Contact: ${clickedPoint.contact || "No contact"}<br>
Email: ${clickedPoint.email || "No email"}<br>
Phone: ${clickedPoint.telephone || "No phone"}<br>
`);
} else {
selectedPoint = null;
tooltip.style("display", "none");
}
});

// Add zoom functionality
const zoom = d3.zoom()
.scaleExtent([1, 8])
.on("zoom", event => {
const transform = event.transform;
projection.translate([transform.x, transform.y]).scale(transform.k * 150);
points = render(land50);
});

// Apply zoom behavior to the canvas
d3.select(context.canvas).call(zoom).on("dblclick.zoom", null);

// Filter functionality (assuming you have filter checkboxes)
let activeFilters = new Set();
d3.selectAll('#filters input[type="checkbox"]').on('change', function () {
const isotope = this.value;
if (this.checked) {
activeFilters.add(isotope);
} else {
activeFilters.delete(isotope);
}

// Filter and redraw points
points = render(land50).filter(point => {
return activeFilters.size === 0 || [...activeFilters].some(isotope => point[isotope] === "Yes");
});
});

Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more