Published
Edited
Jun 9, 2020
Insert cell
Insert cell
x = d3.scalePoint(attributes, [margin.left, width - margin.right])
Insert cell
Insert cell
y = {
let scales = new Map();
let originDomainContainer = ["US", "JP", "EU"]
// TODO: create a suitable scale for each attribute and add it to the map
// source: https://www.tutorialsteacher.com/d3js/scales-in-d3
attributes.forEach(function(attribute) {
let scaleValue = 0;
if(attribute == "origin"){
//works like a scale ordinal with equally distributed points
scaleValue = d3.scalePoint().domain(data.map(item => item[attribute]))
.range([height - margin.bottom, margin.top]);
/*
scaleValue = d3.scaleOrdinal().domain(data.map(item => item[attribute]))
// range for 3 equally distributed values
.range([height - margin.bottom, (height - margin.bottom) / 2, margin.top]);
*/
}else{
// Construct continuous linear scale where input data (domain) maps to specified output range.
scaleValue = d3.scaleLinear().domain(d3.extent(data, item => item[attribute]))
.range([height - margin.bottom, margin.top]).nice();
}
scales.set(attribute, scaleValue);
});
return scales;
}
Insert cell
Insert cell
Insert cell
Insert cell
// source: https://observablehq.com/@d3/parallel-coordinates
color = {
if(colorAttribute == "origin"){
/* Construct ordinal scale where input data includes alphabets and
are mapped to discrete numeric output range. */
return d3.scaleOrdinal(y.get(colorAttribute).domain(), d3.schemeSet1);
}else{
// Construct sequential scale where output range is fixed by interpolator function.
return d3.scaleSequential(y.get(colorAttribute).domain().reverse(), d3.interpolateRdYlGn); // RdGy BrBG
}
}
Insert cell
Insert cell
Insert cell
function path(d) {
//source: https://www.d3-graph-gallery.com/graph/parallel_basic.html
return d3.line()(attributes.map(function(key) { return [x(key), y.get(key)(d[key])]; }));
}
Insert cell

paracoords = {

const svg = d3.create("svg").attr("viewBox", [0, 0, width, height]);

// set the style of hidden data items
svg
.append("style")
.text("path.hidden { stroke: #000; stroke-opacity: 0.01;}");

// a map that holds any active brush per attribute
let activeBrushes = new Map();

const polylines = svg
.append("g")
.attr("fill", "none")
.attr("stroke-width", 1.5)
.attr("stroke-opacity", 0.6) //optional
.selectAll("path")
.data(data)
.join("path");
// TODO: create the polylines
polylines
.attr("d", path)
.style("fill", "none")
.style("stroke", /*"#69b3a2"*/ d => color(d[colorAttribute])) // TODO: apply the color scale from task 3
.style("opacity", 0.5);

// create the group nodes for the axes
const axes = svg
.append("g")
.selectAll("g")
.data(attributes)
.join("g")
//translate this element to its right position on the x axis
.attr("transform", d => `translate(${x(d)},0)`);

// TODO: add the visual representation of the axes
// source: https://www.d3-graph-gallery.com/graph/parallel_basic.html
axes
.each(function(d) { d3.select(this).call(d3.axisRight(y.get(d))); })
// add axis title
.call(g => g.append("text")
.attr("transform", "rotate(90)")
.style("text-anchor", "left")
.attr("y", 9)
.text(function(d) { return shortAttributeNames.get(d); })
.style("fill", "green"))
.call(g => g.selectAll("text")
.clone(true).lower() //clone and get the lower layer of text as background
.attr("fill", "none")
.attr("stroke-width", 5)
.attr("stroke-linejoin", "round")
.attr("stroke", "white"));
// TODO implement brushing & linking
function updateBrushing() {
// d3.event.selection == activeBrushes without key
if (activeBrushes === null){
polylines.classed("hidden",false);
}

polylines.classed("hidden", d => {

var key = 0;
var value_y = 0;
var active_domain_y = 0;
/*Checks for each attribute whether the polyline should be drawn by checking whether
it is in the active area or not */
for(var i=0; i < attributes.length; i++){
key = attributes[i];
value_y = y.get(key)(d[key]);
active_domain_y /*[y0, y1]*/ = activeBrushes.get(key);
if(active_domain_y != null){
if(value_y < active_domain_y[0] || value_y > active_domain_y[1]) {
return true;
}
}
}
return false;
});
}

function brushed(attribute) {
activeBrushes.set(attribute, d3.event.selection);
updateBrushing();
}

function brushEnd(attribute) {
if (d3.event.selection !== null) return;
activeBrushes.delete(attribute);
updateBrushing();
}

const brushes = axes.append("g").call(
d3
.brushY()
.extent([[-10, margin.top], [10, height - margin.bottom]])
.on("brush", brushed)
.on("end", brushEnd)
);
return svg.node();
}
Insert cell
md`## Appendix`
Insert cell
height = 500
Insert cell
margin = ({ top: 10, right: 30, bottom: 10, left: 10 })
Insert cell
data = d3.csvParse(await FileAttachment("cars.csv").text(), d3.autoType)
Insert cell
attributes = data.columns.filter(d => d !== "name")
Insert cell
shortAttributeNames = new Map(
Object.entries({
mpg: "MPG",
cylinders: "CYL",
displacement: "DPL",
horsepower: "HP",
weight: "WGT",
acceleration: "ACL",
year: "YEAR",
origin: "OGN"
})
)
Insert cell
import { select } from "@jashkenas/inputs"
Insert cell
d3 = require("d3@5")
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