Published
Edited
Jul 6, 2022
2 forks
1 star
Insert cell
# Interactive legend
Insert cell
height = 300
Insert cell
SpectrumData = FileAttachment("all-2").json()
Insert cell
function parseData(data){
let result = []
SpectrumData.forEach(item => item.DataJson.Spectrum.SmoothedPoints.forEach(point => {
result.push(Object.assign({Sample: item.SampleName}, point))
}))
return result
}
Insert cell
d3.schemeCategory10
Insert cell
Insert cell
data = parseData(SpectrumData)
Insert cell
// comments = [{Sample: "Sample 1", X: 260.5, Y: 5.876316038916, Comment: 'The value is over what is expected1'}, {Sample: "Sample 1", X: 315, Y: 0.828, Comment: 'The value is over what is expected2'}, {Sample: "Sample 5", X: 270.5, Y: 6.41477403271725, Comment: 'The value is over what is expected3'}, {Sample: "Sample 6", X: 280, Y: 9.27261250026078, Comment: 'The value is over what is expected4'}]
Insert cell
// commentsData = d3.group(comments, d => d.Sample)
Insert cell
// selected = ({domains: new Set(), locked: ''})
Insert cell
selected = {
console.log('ssssssssssssssssssssssssssssss')
return {
domains: new d3.InternSet(d3.map(SpectrumData, d => d.SampleName)),
locked: ''
}
}
Insert cell
Insert cell
Insert cell
Insert cell
chart = {
const Label = 'Wavelength (nm)'
const marginTop = 20 // top margin, in pixels
const marginRight = 30 // right margin, in pixels
const marginBottom = 30 // bottom margin, in pixels
const marginLeft = 20 // left margin, in pixels

const strokeWidth = 1.5 // stroke width of line
const X = d3.map(data, d => d.X);
const Y = d3.map(data, d => d.Y);
const Z = d3.map(data, d => d.Sample);
const O = d3.map(data, d => d);
// let minX = d3.min(X)
// let maxX = d3.max(X)
let selectedSample;
let D = d3.map(data, (d, i) => !isNaN(X[i]) && !isNaN(Y[i]));
// Compute default domains, and unique the z-domain.
const xDomain = d3.extent(X);
const yDomain = d3.extent(Y);
console.log(yDomain)
const zDomain = new d3.InternSet(Z);
console.log(zDomain)
const color = d3.scaleOrdinal(zDomain, d3.schemeTableau10);
// Omit any data not present in the z-domain.
const I = d3.range(X.length).filter(i => zDomain.has(Z[i]));
const xScale = d3.scaleLinear(xDomain, [marginLeft, width - marginRight]);
const yScale = d3.scaleLinear(yDomain, [height - marginBottom, marginTop]);
const xAxis = d3.axisBottom(xScale).ticks(width / 40).tickSizeOuter(0);
const yAxis = d3.axisLeft(yScale).ticks(height / 30);
// Compute titles.
const T = Z;
const selmodel = SelectionModel(zDomain);
const groups = d3.group(data, d => d.X)
// Construct a line generator.
const line = d3.line()
.defined(i => D[i])
.curve(d3.curveLinear)
.x(i => xScale(X[i]))
.y(i => yScale(Y[i]));
const svg = d3.create("svg")
.attr("width", width)
.attr("height", height)
.attr("viewBox", [0, 0, width, height])
.attr("style", "max-width: 100%; height: auto; height: intrinsic;")
.style("-webkit-tap-highlight-color", "transparent")
.on("pointerenter", pointerentered)
.on("pointermove", pointermoved)
.on("pointerleave", pointerleft)
.on("click", pointclicked)
.on("touchstart", event => event.preventDefault());
const compareLine = svg.append("g");

compareLine.append("line")
.attr("y1", marginTop)
.attr("y2", height - marginBottom + 5)
.attr("stroke", "currentColor");

const ruleLabel = compareLine.append("text")
.attr("y", height - marginBottom - 5)
.attr("x", 18)
.attr("font-family", "sans-serif")
.attr("font-size", 10)
.attr("text-anchor", "middle")
// .attr("dy", "1em");
const xg = svg.append("g")
.attr("transform", `translate(0,${height - marginBottom})`)
.call(xAxis)
.call(g => g.append("text")
.attr("x", width/2 - 50)
.attr("y", 28)
.attr("fill", "currentColor")
.attr("text-anchor", "start")
.text(Label));
const yg = svg.append("g")
.attr("transform", `translate(${marginLeft},0)`)
.call(yAxis)
//.call(g => g.select(".domain").remove())
.call(g => g.selectAll(".tick line").clone()
.attr("x2", width - marginLeft - marginRight)
.attr("stroke-opacity", 0.1))
.call(g => g.append("text")
.attr("x", -marginLeft)
.attr("y", 10)
.attr("fill", "currentColor")
.attr("text-anchor", "start"));
const path = svg.append("g")
.attr("fill", "none")
.attr("stroke", typeof color === "string" ? color : null)
.attr("stroke-width", strokeWidth)
.selectAll("path")
.data(d3.group(I, i => Z[i]))
.join("path")
.style("mix-blend-mode", "multiply")
.attr("stroke", typeof color === "function" ? ([z]) => color(z) : null)
.attr("d", ([, I]) => line(I))
.attr('stroke', d => selmodel.has(d[0]) ? color(d[0]) : 'transparent');
selmodel.on('change.chart', value => {
path.attr('stroke', d => selmodel.has(d[0]) ? color(d[0]) : 'transparent');
});
const dot = svg.append("g")
.attr("display", "none");

dot.append("circle")
.attr("r", 2.5);

dot.append("text")
.attr("font-family", "sans-serif")
.attr("font-size", 10)
.attr("text-anchor", "middle")
.attr("x", 75)
.attr("y", -8);

function pointclicked(event) {
if (!selected.locked) {
selected.locked = selectedSample
// svg.selectAll(".shapes").remove()
// selected.comments = commentsData.get(selectedSample)
// if (selected.comments) {
// svg.selectAll(".shapes")
// .data(selected.comments)
// .enter()
// .append('g').attr("class", "shapes")
// .attr("transform", (d, i) => `translate(${xScale(d.X)}, ${yScale(d.Y)})`)
// .call(g => g.append('path')
// .attr("d", function(d) {return d3.symbol().type(d3.symbolTriangle).size("30")()})
// .attr("fill", "red"))
// .call(g => g.append("text")
// .attr("y", -2)
// .attr("x", 3)
// .attr("font-family", "sans-serif")
// .attr("font-size", 11)
// .text(d => `(${d.X}:${d.Y.toFixed(2)})${d.Comment}`))
// }
} else {
selected.locked = ''
// svg.selectAll(".shapes").remove()
}
}
function pointermoved(event) {
const [xm, ym] = d3.pointer(event);
const I1 = selected.locked ? I.filter(i => Z[i] === selected.locked) : I.filter(i => selmodel.has(Z[i]));
const i = d3.least(I1, i => Math.hypot(xScale(X[i]) - xm, yScale(Y[i]) - ym)); // closest point
selectedSample = Z[i]
path.style("stroke", ([z]) => Z[i] === z ? null : (selmodel.has(z) ? "#ddd" : "transparent")).filter(([z]) => Z[i] === z).raise();
dot.attr("transform", `translate(${xScale(X[i])},${yScale(Y[i])})`);
compareLine.attr("transform", `translate(${xScale(X[i])},0)`).raise();
if (T) {
dot.select("text").text(`${T[i]}:(${Y[i]})`);
ruleLabel.text(`${X[i]}`);
}
const details = groups.get(X[i]).filter(d => selmodel.has(d.Sample))
d3.select('#chartwavelength').html(X[i])
d3.select('#chartdetail').html(
details.map(item => `<div class="de"><div style="flex: 1">${item.Sample}</div><div style="text-align: right;">${item.Y.toFixed(2)}</div></div>`).join('')
)
}

function pointerentered() {
path.style("mix-blend-mode", null).style("stroke", "#ddd");
dot.attr("display", null);
}

function pointerleft() {
selected.locked = ''
path.style("mix-blend-mode", "multiply").style("stroke", null);
dot.attr("display", "none");
compareLine.attr("transform", `translate(-40,0)`)
svg.selectAll(".shapes").remove()
}

return Object.assign(svg.node(), {scales: {color, selmodel},
update(focusX, focusY) {
const [minX, maxX] = focusX
const [minY, maxY] = focusY
// svg.selectAll(".tick line").remove()
xScale.domain(focusX)
yScale.domain([minY, maxY])
xg.call(xAxis)
yg.call(yAxis)
D = d3.map(D, (d, i) => X[i] >= minX && X[i] <= maxX);
path
.attr("d", ([, I]) => line(I))
.attr('stroke', d => selmodel.has(d[0]) ? color(d[0]) : 'transparent');
const maxIndex = d3.maxIndex(data, d => minX <= d.X && d.X <= maxX ? d.Y : NaN);
const minIndex = d3.minIndex(data, d => minX <= d.X && d.X <= maxX ? d.Y : NaN);
d3.select('#rangewavelength').html(`[${minX.toFixed(2)}, ${maxX.toFixed(2)}]`)
d3.select('#rangemax').html(`${Z[maxIndex]}: ${maxY.toFixed(2)}`)
d3.select('#rangemin').html(`${Z[minIndex]}: ${minY.toFixed(2)}`)
}
});
}
Insert cell
Insert cell
// Our selection model wraps two components:
// - A JavaScript Set for tracking the selected elements
// - A D3 dispatch helper for registering and invoking listener callbacks upon changes
function SelectionModel(domain) {
const dispatch = d3.dispatch('change');
//const state = new Set(domain);
console.log('aaaaaaaaaaaaaaaaaaaaaaa1333', selected, selected.domains, selected.domains.size)
if (!selected.domains.size) {
selected.domains = domain
}
const api = {
on: (type, fn) => (dispatch.on(type, fn), api),
//clear: () => (clear(), api),
has: value => {
return selected.domains.has(value)
},
// set: value => (update(value, true), api),
toggle: e => (update(e.target.dataset.legend), api)
};
// function clear() {
// if (state.size) {
// state.clear();
// dispatch.call('change', api, api);
// }
// }
function update(value) {
if (!selected.domains.has(value)) {
selected.domains.add(value);
} else {
selected.domains.delete(value);
}
dispatch.call('change', api, value);
}
return api;
}
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