Public
Edited
Mar 22, 2024
Insert cell
Insert cell
chart = {
const height = marginTop + (metrics.length * rowHeight);
const svg = d3.create("svg")
.attr("viewBox", [0, 0, width, height])
.style("display", "block");


const strategyGroup = svg.selectAll('.strategyGroup')
.data(strategies)
.join((group) => {
const enter = group.append('g').attr('class', 'strategyGroup');
enter.append('rect').attr('class', 'strategyBox');
enter.append('text').attr('class', 'strategyLabel');
enter.append('path').attr("class", 'strategyLine');
return enter;
});

strategyGroup.select(".strategyBox")
.attr("id", (d,i) => "strategy" + i)
.attr("width", 56)
.attr("height", 14)
.attr("fill", (d) => colorScale(d))
.attr("stroke", "grey")
.attr("stroke-width", 0.25)
.attr("rx", 2)
.attr("ry", 2)
.attr("y", 2)
.attr("x", (d,i) => i * 60)
.attr("transform", "translate(" + (width - (strategies.length * 60))/2 + ",0)")
.on("mouseover",(event, d) => {
d3.selectAll(".strategyLine").attr("opacity",0);
d3.selectAll(".strategyDot").attr("opacity",0);
d3.selectAll("#"+ event.currentTarget.id).attr("opacity",1);

})
.on("mouseout",(event, d) => {
d3.selectAll(".strategyLine").attr("opacity",1);
d3.selectAll(".strategyDot").attr("opacity",0);

});
;

strategyGroup.select(".strategyLabel")
.attr("pointer-events", "none")
.attr("fill", (d,i) => colorScale(strategies[7-i]))
.attr("font-size", 10)
.attr("text-anchor", "middle")
.attr("x", (d,i) => (i * 60) + 28)
.attr("y", 12)
.text((d) => d)
.attr("transform", "translate(" + (width - (strategies.length * 60))/2 + ",0)");

strategyGroup.select(".strategyLine")
.attr("id", (d,i) => "strategy" + i)
.attr("stroke", "#A0A0A0")
.attr("opacity",0)
.attr("fill", "transparent")
.attr("stroke-width", 0.5)
.attr("d", (d) => d3.line().x((l) => l.x).y((l) => l.y)(strategyData[d]))
.attr("transform", "translate(0," + marginTop + ")")
.on("mouseover",(event, d) => {
d3.selectAll(".strategyLine").attr("opacity",0);
d3.selectAll(".strategyDot").attr("opacity",0);
d3.selectAll("#"+ event.currentTarget.id).attr("opacity",1);

})
.on("mouseout",(event, d) => {
d3.selectAll(".strategyLine").attr("opacity",1);
d3.selectAll(".strategyDot").attr("opacity",1);

});
const metricGroup = svg.selectAll('.metricGroup')
.data(metrics)
.join((group) => {
const enter = group.append('g').attr('class', 'metricGroup');
const gradient = enter.append('defs')
.append('linearGradient').attr("class", "linearGradient");

gradient.append('stop').attr("class", "stopLeft");
gradient.append('stop').attr("class", "stopLeftMid");
gradient.append('stop').attr("class", "stopRightMid");
gradient.append('stop').attr("class", "stopRight");
enter.append('text').attr('class', 'metricLabel');
enter.append('g').attr('class', 'metricXAxis');
enter.append('rect').attr('class', 'metricLine');
enter.append('g').attr('class', 'metricDotGroup');
return enter;
});

metricGroup.select(".linearGradient")
.attr('id', (d, i) => 'linear-gradient' + i)
.attr('x1', '0%')
.attr('y1', '0%')
.attr('x2', '100%')
.attr('y2', '0%');

const calculateZeroPoint = (i) => {
const zeroPoint = scales[i](0);
const extent = scales[i].domain()[1] - scales[i].domain()[0];
if(!zeroPoint || extent === 0) {return "100%"}
console.log(String((zeroPoint/extent) * 100) + "%")
return String((zeroPoint/extent)) + "%";
}

metricGroup.select(".stopLeft")
.attr('offset', '0%')
.attr('stop-color', '#fc9272');

metricGroup.select(".stopLeftMid")
.attr('offset', (d,i) => calculateZeroPoint(i))
.attr('stop-color', '#fc9272');

metricGroup.select(".stopRightMid")
.attr('offset', (d,i) => calculateZeroPoint(i))
.attr('stop-color', '#99d8c9');
metricGroup.select(".stopRight")
.attr('offset', '100%')
.attr('stop-color', '#99d8c9');
metricGroup.attr("transform", (d,i) => "translate(0," + (marginTop + (rowHeight * i)) + ")");

metricGroup.select(".metricLabel")
.attr("fill", "black")
.attr("font-size", 12)
.attr("text-anchor", "end")
.attr("x", marginLeft - 10)
.attr("y",4)
.text((d) => d);

metricGroup.select(".metricLine")
.attr("x", marginLeft)
.attr("width", width - marginLeft - marginRight)
.attr("height", 3)
.attr("fill", (d,i) => "url(#linear-gradient" + i + ")");

metricGroup.select(".metricXAxis")
.each((d,i, axisGroups) => {
const axis = d3.select(axisGroups[i])
.call(d3.axisBottom(scales[i]));
axis.selectAll("path").attr("display","none");
axis.selectAll("text")
.attr("font-size", 8)
.attr("fill","grey")
.attr("dy","10");

axis.selectAll("line")
.attr("y1",-4)
.attr("stroke", "#A0A0A0")
.attr("stroke-width",0.5)
.attr("y2",8)

const metricDotsGroup = metricGroup.select(".metricDotGroup")
.selectAll('.metricDotsGroup')
.data((d,i) => {
const matchingRow = paperData.find((f) => f.Metric === d);
const dotData = [];
strategies.forEach((s, sIndex) => {
if(matchingRow[s] !== null){
dotData.push({strategyIndex: sIndex, strategy: s, value: matchingRow[s], scale: scales[i]})
}
})
return dotData
})
.join((group) => {
const enter = group.append('g').attr('class', 'metricDotsGroup');
enter.append('circle').attr('class', 'strategyDot');
return enter;
});


metricDotsGroup.select(".strategyDot")
.attr("id", (d) => "strategy" + d.strategyIndex)
.attr("r", 4)
.attr("fill", (d) => colorScale(d.strategy))
.attr("stroke", "grey")
.attr("stroke-width", 0.25)
.attr("cx", (d) => d.scale(d.value))
.attr("cy", 1.5)
.on("mouseover",(event, d) => {
d3.selectAll(".strategyL").attr("opacity",0);
d3.selectAll(".strategyDot").attr("opacity",0);
d3.selectAll("#"+ event.currentTarget.id).attr("opacity",1);

})
.on("mouseout",(event, d) => {
d3.selectAll(".strategyLine").attr("opacity",1);
d3.selectAll(".strategyDot").attr("opacity",1);

});

})

d3.selectAll(".metricXAxis path");

return svg.node();

}
Insert cell
colorScale = d3.scaleOrdinal(d3.schemeGreys[8]);
Insert cell
strategyData = {
const strategyObject = {};
strategies.forEach((s) => {
const metricData = [];
metrics.forEach((m,i) => {
const metricValues = paperData.find((f) => f.Metric === m);
if(metricValues[s] !== null){
metricData.push({x: scales[i](metricValues[s]), y: i * rowHeight})
}
})
strategyObject[s] = metricData
})
return strategyObject;
}
Insert cell
scales = {
const scalesArray = [];
metrics.forEach((d) => {
const metricRange = Object.values(paperData.find((f) => f.Metric === d)).filter((f,i) => i > 1 && f !== null);
const extent = d3.extent(metricRange);

scalesArray.push(d3.scaleLinear().domain(extent).range([marginLeft,width - marginRight]))
})
return scalesArray;
}
Insert cell
marginTop = 40
Insert cell
marginRight = 10
Insert cell
marginLeft = 60
Insert cell
width = 800
Insert cell
rowHeight = 35
Insert cell
metrics = paperData.map((m) => m.Metric)
Insert cell
strategies = Object.keys(paperData[0]).filter((f,i) => i > 1)
Insert cell
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