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

One platform to build and deploy the best data apps

Experiment and prototype by building visualizations in live JavaScript notebooks. Collaborate with your team and decide which concepts to build out.
Use Observable Framework to build data apps locally. Use data loaders to build in any language or library, including Python, SQL, and R.
Seamlessly deploy to Observable. Test before you ship, use automatic deploy-on-commit, and ensure your projects are always up-to-date.
Learn more