Recreate example chart in d3.js - javascript

I'm just starting out with d3, and I want to recreate this connected scatter plot chart:
I don't know how to make the tooltip appear on hover along the y-axis --> like in the example.
I made the basic chart but can't find a solution for the grouped y-axis values.
This is as far as I got:
// set the dimensions and margins of the graph
const margin = { top: 10, right: 100, bottom: 30, left: 30 },
width = 750 - margin.left - margin.right,
height = 500 - margin.top - margin.bottom;
// append the svg object to the body of the page
const svg = d3
.select(".container")
.append("svg")
.attr("viewBox", `0 0 750 500`)
//.attr("width", width + margin.left + margin.right)
//.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", `translate(${margin.left},${margin.top})`);
//Read the data
d3.csv(
"https://raw.githubusercontent.com/VladGirboan/Point2/main/Test.csv"
).then(function (data) {
// List of groups (here I have one group per column)
const allGroup = ["valueA", "valueB", "valueC", "valueD", "valueE"];
// Reformat the data: we need an array of arrays of {x, y} tuples
const dataReady = allGroup.map(function (grpName) {
// .map allows to do something for each element of the list
return {
name: grpName,
values: data.map(function (d) {
return { time: d.time, value: +d[grpName] };
})
};
});
// I strongly advise to have a look to dataReady with
// console.log(dataReady)
// A color scale: one color for each group
const myColor = d3
.scaleOrdinal()
.domain(["1", "2", "3", "4", "5"])
.range(["#F98600", "#00BA34", "#0085FF", "#8367C7", "#FDB137"]);
// Add X axis --> it is a date format
const x = d3.scaleLinear().domain([2019, 2022]).range([50, width]);
svg
.append("g")
.attr("transform", `translate(0, ${height})`)
.call(d3.axisBottom(x).ticks(4, "").tickPadding(5))
.attr("font-family", "Montserrat")
.attr("color", "#969696");
d3.select("x.axis .tick:first-child").remove();
// Add Y axis
const y = d3.scaleLinear().domain([0, 20]).range([height, 0]);
svg
.append("g")
.call(d3.axisLeft(y))
.attr("font-family", "Montserrat")
.attr("color", "#969696");
// Add the lines
const line = d3
.line()
.x((d) => x(+d.time))
.y((d) => y(+d.value));
svg
.selectAll("myLines")
.data(dataReady)
.join("path")
.attr("d", (d) => line(d.values))
.attr("stroke", (d) => myColor(d.name))
.style("stroke-width", 2)
.style("fill", "none");
// create a tooltip
const tooltip = d3
.select(".container")
.append("div")
.style("position", "absolute")
.style("opacity", 0)
.attr("class", "tooltip")
.style("background-color", "#fafafa");
// Three function that change the tooltip when user hover / move / leave a cell
const mouseover = function (event, d) {
tooltip.style("opacity", 1);
};
const mousemove = function (event, d) {
tooltip
.html("Value: " + d.value + "<br/>" + "Year: " + d.time)
.style("left", `${event.layerX + 10}px`)
.style("top", `${event.layerY}px`);
};
const mouseleave = function (event, d) {
tooltip.style("opacity", 0);
};
// Add the points
svg
// First we need to enter in a group
.selectAll("myDots")
.data(dataReady)
.join("g")
.style("fill", (d) => myColor(d.name))
// Second we need to enter in the 'values' part of this group
.selectAll("myPoints")
.data((d) => d.values)
.join("circle")
.attr("cx", (d) => x(d.time))
.attr("cy", (d) => y(d.value))
.attr("r", 6)
.attr("stroke", "white")
.style("stroke-width", 0)
.on("mouseover", mouseover)
.on("mousemove", mousemove)
.on("mouseleave", mouseleave);
// Add a legend at the end of each line
svg
.selectAll("myLabels")
.data(dataReady)
.join("g")
.append("text")
.datum((d) => {
return { name: d.name, value: d.values[d.values.length - 1] };
}) // keep only the last value of each time series
.attr(
"transform",
(d) => `translate(${x(d.value.time)},${y(d.value.value)})`
) // Put the text at the position of the last point
.attr("x", 12) // shift the text a bit more right
.text((d) => d.name)
.style("font-family", "Montserrat")
.style("fill", (d) => myColor(d.name))
.style("font-size", "16");
});
text {
font-family: "Montserrat";
font-size: 12px;
}
.container {
height: 100%;
display: flex;
align-items: center;
max-width: 750px;
}
.tooltip {
font-family: "Montserrat";
font-size: 12px;
position: absolute;
background-color: #fafafa;
padding: 10px 20px 10px 20px;
border-radius: 5px;
box-shadow: 2px 2px 5px #bbbbbb;
}
<script src="https://d3js.org/d3.v6.min.js"></script>
<div class="container"></div>
Can anyone please help out with this?
Thanks a bunch! :)

One solution is getting the data for all the variables based on the time, and sorted by value:
const thisData = dataReady.map(e => ({
name: e.name,
value: e.values.find(f => f.time === d.time).value
})).sort((a, b) => b.value - a.value);
Then, you generate a string which you pass to the html() method:
dataString = thisData.map(e => `${e.name}: ${e.value}`).join("<br>");
tooltip.html(dataString)
Also, it's a better idea doing this on mouseover instead of mousemove.
Here's your code with those changes:
// set the dimensions and margins of the graph
const margin = {
top: 10,
right: 100,
bottom: 30,
left: 30
},
width = 750 - margin.left - margin.right,
height = 500 - margin.top - margin.bottom;
// append the svg object to the body of the page
const svg = d3
.select(".container")
.append("svg")
.attr("viewBox", `0 0 750 500`)
//.attr("width", width + margin.left + margin.right)
//.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", `translate(${margin.left},${margin.top})`);
//Read the data
d3.csv(
"https://raw.githubusercontent.com/VladGirboan/Point2/main/Test.csv"
).then(function(data) {
// List of groups (here I have one group per column)
const allGroup = ["valueA", "valueB", "valueC", "valueD", "valueE"];
// Reformat the data: we need an array of arrays of {x, y} tuples
const dataReady = allGroup.map(function(grpName) {
// .map allows to do something for each element of the list
return {
name: grpName,
values: data.map(function(d) {
return {
time: d.time,
value: +d[grpName]
};
})
};
});
// I strongly advise to have a look to dataReady with
// console.log(dataReady)
// A color scale: one color for each group
const myColor = d3
.scaleOrdinal()
.domain(["1", "2", "3", "4", "5"])
.range(["#F98600", "#00BA34", "#0085FF", "#8367C7", "#FDB137"]);
// Add X axis --> it is a date format
const x = d3.scaleLinear().domain([2019, 2022]).range([50, width]);
svg
.append("g")
.attr("transform", `translate(0, ${height})`)
.call(d3.axisBottom(x).ticks(4, "").tickPadding(5))
.attr("font-family", "Montserrat")
.attr("color", "#969696");
d3.select("x.axis .tick:first-child").remove();
// Add Y axis
const y = d3.scaleLinear().domain([0, 20]).range([height, 0]);
svg
.append("g")
.call(d3.axisLeft(y))
.attr("font-family", "Montserrat")
.attr("color", "#969696");
// Add the lines
const line = d3
.line()
.x((d) => x(+d.time))
.y((d) => y(+d.value));
svg
.selectAll("myLines")
.data(dataReady)
.join("path")
.attr("d", (d) => line(d.values))
.attr("stroke", (d) => myColor(d.name))
.style("stroke-width", 2)
.style("fill", "none");
// create a tooltip
const tooltip = d3
.select(".container")
.append("div")
.style("position", "absolute")
.style("opacity", 0)
.attr("class", "tooltip")
.style("background-color", "#fafafa");
// Three function that change the tooltip when user hover / move / leave a cell
const mouseover = function(event, d) {
tooltip.style("opacity", 1);
const thisData = dataReady.map(e => ({
name: e.name,
value: e.values.find(f => f.time === d.time).value
})).sort((a, b) => b.value - a.value),
dataString = thisData.map(e => `${e.name}: ${e.value}`).join("<br>");
tooltip.html(dataString)
.style("left", `${event.layerX + 10}px`)
.style("top", `${event.layerY}px`);
};
const mouseleave = function(event, d) {
tooltip.style("opacity", 0);
};
// Add the points
svg
// First we need to enter in a group
.selectAll("myDots")
.data(dataReady)
.join("g")
.style("fill", (d) => myColor(d.name))
// Second we need to enter in the 'values' part of this group
.selectAll("myPoints")
.data((d) => d.values)
.join("circle")
.attr("cx", (d) => x(d.time))
.attr("cy", (d) => y(d.value))
.attr("r", 6)
.attr("stroke", "white")
.style("stroke-width", 0)
.on("mouseover", mouseover)
.on("mouseleave", mouseleave);
// Add a legend at the end of each line
svg
.selectAll("myLabels")
.data(dataReady)
.join("g")
.append("text")
.datum((d) => {
return {
name: d.name,
value: d.values[d.values.length - 1]
};
}) // keep only the last value of each time series
.attr(
"transform",
(d) => `translate(${x(d.value.time)},${y(d.value.value)})`
) // Put the text at the position of the last point
.attr("x", 12) // shift the text a bit more right
.text((d) => d.name)
.style("font-family", "Montserrat")
.style("fill", (d) => myColor(d.name))
.style("font-size", "16");
});
text {
font-family: "Montserrat";
font-size: 12px;
}
.container {
height: 100%;
display: flex;
align-items: center;
max-width: 750px;
}
.tooltip {
font-family: "Montserrat";
font-size: 12px;
position: absolute;
background-color: #fafafa;
padding: 10px 20px 10px 20px;
border-radius: 5px;
box-shadow: 2px 2px 5px #bbbbbb;
}
<script src="https://d3js.org/d3.v6.min.js"></script>
<div class="container"></div>
PS: I'm not sure if you're asking about grouping the values (what I did) or about making the tooltip showing up when you hover over any empty part of the chart.

Related

How to make information box and attach it with chart in d3.js

Is it possible to create a chart with d3.js as given in the attached picture? Especially the information box. The context is from a specific start and end date if the data is missing on date place a dot instead of a bar in the chart. The difficulty I am facing is to attach the information box with the dots using a line using d3.js.
The whole chart should be implemented using (SVG) d3.js.
Can anyone give a solution example with any dataset?
This is just an example, hopefully it will be enough to get you started.
const url = "https://raw.githubusercontent.com/freeCodeCamp/ProjectReferenceData/master/GDP-data.json"
const margin = {
top: 30,
left: 40,
right: 40,
bottom: 100
};
const width = 800;
const height = 300;
const svg = d3.select("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom);
const g = svg.append('g')
.attr('transform', `translate(${margin.left}, ${margin.top})`);
const notice = svg.append('g')
.attr('transform', `translate(${margin.left}, ${margin.top + height})`)
notice.append('rect')
.classed('notice-box', true)
.attr('x', 0)
.attr('y', margin.top)
.attr('width', width)
.attr('height', margin.bottom - margin.top);
const warning = notice.append('text')
.attr('x', 10)
.attr('y', margin.top + 30);
const format = d3.timeFormat("Q%q %Y")
const setWarning = (data) => {
warning.text(`Missing data for ${data.map(d => format(d.date)).join(', ')}`);
notice.selectAll('line')
.data(data)
.enter()
.append('line')
.attr('stroke', 'black')
.attr('x1', d => x(d.date))
.attr('y1', margin.top)
.attr('x2', d => x(d.date))
.attr('y2', y(0) - height);
notice.selectAll('circle')
.data(data)
.enter()
.append('circle')
.attr('fill', 'black')
.attr('r', 3)
.attr('cx', d => x(d.date))
.attr('cy', y(0) - height);
}
var x = d3.scaleTime().range([0, width]);
const y = d3.scaleLinear().range([height, 0]);
// Since v5 d3.json is promise-based, hence the change.
d3.json(url)
.then(response => response.data)
.then(data => data.map(([date, value]) => ({
date: new Date(date),
value: value
})))
.then(data => {
data.filter(({
date
}) => date.getFullYear() >= 2000 && date.getFullYear() <= 2005 && date.getMonth() === 0)
.forEach(d => d.value = null);
x.domain(d3.extent(data.map(({
date
}) => date)));
const barWidth = width / data.length;
y.domain([0, d3.max(data.map(({
value
}) => value))]);
g.append('g')
.call(d3.axisBottom().scale(x))
.attr('id', 'x-axis')
.attr('transform', `translate(0, ${height})`);
g.append('g')
.call(d3.axisLeft(y))
.attr('id', 'y-axis');
g.selectAll('rect')
.data(data)
.enter()
.append('rect')
.attr('class', 'bar')
.attr('x', d => x(d.date))
.attr('y', d => y(d.value))
.attr('width', barWidth)
.attr('height', d => height - y(d.value))
.style('fill', '#33adff');
const missing = data.filter(({
value
}) => value === null);
setWarning(missing);
});
#y-axis path {
stroke: black;
stroke-width: 1;
fill: none;
}
#x-axis path {
stroke: black;
stroke-width: 1;
fill: none;
}
.notice-box {
fill: none;
stroke: black;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/6.2.0/d3.min.js"></script>
<svg></svg>

problem with D3 lasso circle element with text

I basically copied the example https://bl.ocks.org/skokenes/a85800be6d89c76c1ca98493ae777572
Then I got the code to work with my data. So, I can now get the lasso to work.
But when I try to add back my old code for the circles to display a text-type tool tip, the lasso breaks. The code then puts the class variables such as not_possible or selected on the "text" elements rather than on the "circle" elements where they need to be.
I found that is the issue by using Chrome developer tools.
When the tool tips code is commented out, the lasso code works and the DOM looks like this:
<circle cx="854" cy="37" fill="red" r="7" class="selected"></circle>
When the tool tips code is live, the tool tips work but the lasso code doesn't work and the DOM looks like this:
<circle cx="854" cy="37" fill="red" r="4.9">
<title r="3.5" class> ==$0
"curr = 89.7, prev = 89.5, geo = Alaska, measure = Percent Citizen, Born in the US"
</title>
</circle>
I've tried changing the styles for the classes, for example, from ".possible" to "circle.possible" but that doesn't help. I've googled for suggestions but haven't found anything that I could make work. I've tried passing the circle selection thru lasso.items(circles) but that doesn't work.
This is the lasso code that does work: the troublesome ".append title" and "text" lines are commented out.
var margin = {top: 20, right: 15, bottom: 60, left: 60}
, width = 960 - margin.left - margin.right
, height = 960 - margin.top - margin.bottom;
var xScale = d3.scaleLinear()
.domain([0, d3.max(data, function(d) { return d[1]; })])
.range([0, width]);
var yScale = d3.scaleLinear()
.domain([0, d3.max(data, function(d) { return d[0]; })])
.range([height, 0]);
var svgArea = d3.select('.content')
.append('svg')
.attr('width', width + margin.right + margin.left)
.attr('height', height + margin.top + margin.bottom)
.attr('class', 'chart');
var main = svgArea.append('g')
.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')')
.attr('width', width)
.attr('height', height)
.attr('class', 'main');
main.append('g')
.attr('transform', 'translate(0,' + height + ')')
.attr('class', 'main axis date')
.call(d3.axisBottom(xScale));
main.append('g')
.append("text")
.attr("x", width / 2)
.attr("y", height + margin.bottom - 10)
.style("text-anchor", "middle")
.style("font", "14px times")
.text("Current X");
main.append('g')
.attr('transform', 'translate(0,0)')
.attr('class', 'main axis date')
.call(d3.axisLeft(yScale));
main.append('g')
.append("text")
.attr("transform", "rotate(-90)")
.attr("x", 0 - (height / 2))
.attr("y", 0 - margin.left / 2)
.style("text-anchor", "middle")
.style("font", "14px times")
.text("Previous Y");
var rScale = d3.scaleLinear()
.domain([0, d3.max(data, function(d) { return d[1]; })])
.range([ 4, 5 ]);
var lasso_start = function() {
lasso.items()
.attr("r",7)
.classed("not_possible",true)
.classed("selected",false)
;
};
var lasso_draw = function() {
lasso.possibleItems()
.classed("not_possible",false)
.classed("possible",true)
;
lasso.notPossibleItems()
.classed("not_possible",true)
.classed("possible",false)
;
};
var lasso_end = function() {
lasso.items()
.classed("not_possible",false)
.classed("possible",false)
;
lasso.selectedItems()
.classed("selected",true)
.attr("r", 7)
;
lasso.notSelectedItems()
.attr("r", 3.5)
;
};
var circles = main.selectAll("circle")
.data(data)
.enter().append("circle")
.attr("cx", function (d,i) { return xScale(d[1]); } )
.attr("cy", function (d) { return yScale(d[0]); } )
.attr("fill", function (d) { if (d[1] > 75) { return "red"; } else { return "black"; } })
.attr("r", function (d) { return rScale(d[1]); })
//.append("title")
//.text(function(d) {
// return "curr = " + d[1] +
// ", prev = " + d[0] +
// ", geo = " + d[2] +
// ", measure = " + d[3];
// })
;
var lasso = d3.lasso()
.items(circles)
.closePathDistance(75) // max distance for the lasso loop to be closed
.closePathSelect(true) // can items be selected by closing the path?
.targetArea(svgArea) // area where the lasso can be started
.on("start",lasso_start) // lasso start function
.on("draw",lasso_draw) // lasso draw function
.on("end",lasso_end); // lasso end function
svgArea.call(lasso);
Why does including ".title" and ".text" cause a problem?
And how do I solve it?
I don't think the problem is with the CSS, but here it is:
<style>
// styling for D3 chart
.chart {
background: #fdfefe;
}
.main text {
font: 10px sans-serif;
}
// styling for D3-lasso
.axis path,
.axis line {
fill: none;
stroke: #000;
shape-rendering: crispEdges;
}
circle {
fill-opacity: 0.4;
}
.dot {
stroke: #000;
}
.lasso path {
stroke: rgb(80,80,80);
stroke-width: 2px;
}
.lasso .drawn {
fill-opacity: 0.05 ;
}
.lasso .loop_close {
fill: none;
stroke-dasharray: 4,4;
}
.lasso .origin {
fill: #3399FF;
fill-opacity: 0.5;
}
.not_possible {
fill: rgb(200,200,200);
}
.possible {
fill: #EC888C;
}
.selected {
fill: steelblue;
}
</style>
The problem appears to be that lasso is adding a radius attribute to the title elements here:
lasso.notSelectedItems()
.attr("r", 3.5)
;
resulting in all your not-selected elements, i.e., circles, and titles, having the attribute assigned, as your example suggests:
<title r="3.5" class>
Rather than calling lasso's selected and notSelected to change the radius and css class of the desired items, use a filter on the items array itself:
// Style the selected dots
lasso.items().filter(function(d) {return d.selected===true})
.classed(...)
.attr("r",7);
// Reset the style of the not selected dots
lasso.items().filter(function(d) {return d.selected===false})
.classed(...)
.attr("r",3.5);
You can get as specific as you want with the return value, i.e., omit any nodes (like title nodes) you don't want affected by the rules you apply to the selection.
The problem was that I couldn't get D3 lasso and my approach to tool tips to work together. I was appending a title element to each circle (point) on a scatter plot. This does NOT work:
var circles = main.selectAll("circle")
.data(data)
.enter().append("circle")
.attr("cx", function (d,i) { return xScale(d[1]); } )
.attr("cy", function (d) { return yScale(d[0]); } )
.attr("fill", function (d) { if (d[1] > 75) { return "red"; } else { return "black"; } })
.attr("r", function (d) { return rScale(d[1]); })
.append("title")
.text(function(d) {
return "curr = " + d[1] +
", prev = " + d[0] +
", geo = " + d[2] +
", measure = " + d[3];
})
;
I found a coding example by Mikhail Shabrikov that solved the issue by avoiding .append("title") altogether. This works:
A new CSS element:
.tooltip {
position: absolute;
z-index: 10;
visibility: hidden;
background-color: lightblue;
text-align: center;
padding: 4px;
border-radius: 4px;
font-weight: bold;
color: black;
}
A new DIV element:
var tooltip = d3.select("body")
.append("div")
.attr('class', 'tooltip');
And mainly a modified circles element:
var circles = main.selectAll("circle")
.data(data)
.enter().append("circle")
.attr("cx", function (d,i) { return xScale(d[1]); } )
.attr("cy", function (d) { return yScale(d[0]); } )
.attr("fill", function (d) { if (d[1] > 75) { return "red"; } else { return "black"; } })
.attr("r", 5)
.on("mouseover", function(d) {return tooltip.style("visibility", "visible")
.text(
"curr = " + d[1] +
", prev = " + d[0] +
", geo = " + d[2] +
", measure = " + d[3]
)
})
.on("mousemove", function() {
return tooltip.style("top", (event.pageY - 30) + "px")
.style("left", event.pageX + "px");
})
.on("mouseout", function() {
return tooltip.style("visibility", "hidden");
})
;
Shabrikov's code is near the very bottom of this item: circles, tool tips, mouse events

Cant get line in line graph to populate in D3.js

I'm trying to build a line graph in D3 but I'm not seeing any of my lines populate. Can someone look over my code and tell me what I'm doing wrong? When I inspect the line path in the console I see some NaN in the d path, I'm assuming this is the source of my problems however perhaps not
Here is my HTML
<label class="guideline">
Show Guideline & Curtain
<input type="checkbox" id="show_guideline" />
</label>
<svg></svg>
Here is my CSS
<style>
body {
font: 10px sans-serif;
margin: 0;
}
path.line {
fill: none;
stroke: #666;
stroke-width: 1.5px;
}
path.area {
fill: #e7e7e7;
}
.axis {
shape-rendering: crispEdges;
}
.x.axis line {
stroke: #fff;
}
.x.axis .minor {
stroke-opacity: .5;
}
.x.axis path {
display: none;
}
.y.axis line, .y.axis path {
fill: none;
stroke: #000;
}
.guideline {
margin-right: 100px;
float: right;
}
</style>
Here is my javascript
<script>
var margin = {top: 80, right: 80, bottom: 80, left: 80},
width = 960 - margin.left - margin.right,
height = 500 - margin.top - margin.bottom;
//var parse = d3.time.format("%b %Y").parse;
var parse = d3.time.format("%Y").parse;
// Scales and axes.
var x = d3.time.scale().range([0, width]),
y = d3.scale.linear().range([height, 0]),
xAxis = d3.svg.axis().scale(x).tickSize(-height).tickSubdivide(true),
yAxis = d3.svg.axis().scale(y).ticks(4).orient("right");
// An area generator, for the light fill.
var area = d3.svg.area()
.interpolate("monotone")
.x(function(d) { return x(d.date); })
.y0(height)
.y1(function(d) { return y(d.incidents); });
// A line generator, for the dark stroke.
var line = d3.svg.line()
.interpolate("monotone")
.x(function(d) { return x(d.date); })
.y(function(d) { return y(d.incidents); });
here is my csv doc
d3.csv("static/data/israel/incidents_linechart.csv", type,
function(error, data) {
// Filter to one symbol; the S&P 500.
var values = data.filter(function(d) {
return d.country == "Israel";;
});
var unitedStates = data.filter(function(d) {
return d.country == "unitedStates";
});
// Compute the minimum and maximum date, and the maximum price.
x.domain([values[0].date, values[values.length - 1].date]);
y.domain([0, d3.max(values, function(d) { return d.incidents;
})]).nice();
// Add an SVG element with the desired dimensions and margin.
var svg = d3.select("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top +
")")
// Add the clip path.
svg.append("clipPath")
.attr("id", "clip")
.append("rect")
.attr("width", width)
.attr("height", height);
// Add the x-axis.
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(xAxis);
// Add the y-axis.
svg.append("g")
.attr("class", "y axis")
.attr("transform", "translate(" + width + ",0)")
.call(yAxis);
var colors = d3.scale.category10();
entering my data values here
svg.selectAll('.line')
.data([values, unitedStates]) /// can add however many i want here
.enter()
.append('path')
.attr('class', 'line')
.style('stroke', function(d) {
return colors(Math.random() * 50);
})
.attr('clip-path', 'url(#clip)')
.attr('d', function(d) {
return line(d);
})
/* Add 'curtain' rectangle to hide entire graph */
var curtain = svg.append('rect')
.attr('x', -1 * width)
.attr('y', -1 * height)
.attr('height', height)
.attr('width', width)
.attr('class', 'curtain')
.attr('transform', 'rotate(180)')
.style('fill', '#ffffff')
/* Optionally add a guideline */
var guideline = svg.append('line')
.attr('stroke', '#333')
.attr('stroke-width', 0)
.attr('class', 'guide')
.attr('x1', 1)
.attr('y1', 1)
.attr('x2', 1)
.attr('y2', height)
/* Create a shared transition for anything we're animating */
var t = svg.transition()
.delay(750)
.duration(6000)
.ease('linear')
.each('end', function() {
d3.select('line.guide')
.transition()
.style('opacity', 0)
.remove()
});
t.select('rect.curtain')
.attr('width', 0);
t.select('line.guide')
.attr('transform', 'translate(' + width + ', 0)')
d3.select("#show_guideline").on("change", function(e) {
guideline.attr('stroke-width', this.checked ? 1 : 0);
curtain.attr("opacity", this.checked ? 0.75 : 1);
})
});
// Parse dates and numbers. We assume values are sorted by date.
function type(d) {
d.date = parse(d.date);
d.incidents = +d.incidents;
return d;
}
</script>
Here is some of my data:
any help would be immensely appreciated.
so when I console.log the data in the browser I get the following:
my incidents column is being repeated twice for every row, once correctly and once incorrectly yielding a NaN

Paths not drawn after dragging in parallel coordinate in d3 V4

Here is a simple parallel coordinate in d3 V4
http://plnkr.co/edit/Ejg7CI7STPqXKB2tot51?p=preview
It is similar to https://bl.ocks.org/jasondavies/1341281 , which is in V3.
Following are the steps to reproduce the issue:
Step1. Brush some area (say 0.8 to 0.4) in column1....
Step2. Brush some area (say 0.7 to 0.4) in column3....
Step3. Now drag the axis column3 to the position of column2. (So basically axis ordering will get changed, from Column1, 2 , 3, 4 .. it will change to column1, 3 ,2, 4.
Step4. Brush column3 (which is now next to column1) again. You will see no paths are being drawn.
Any help would be appreciated.
Thanks
<!DOCTYPE html>
<meta charset="utf-8">
<style>
svg {
font: 10px sans-serif;
}
.background path {
fill: none;
stroke: #ddd;
stroke-opacity: .4;
shape-rendering: crispEdges;
}
.foreground path {
fill: none;
stroke: steelblue;
stroke-opacity: .7;
}
.brush .extent {
fill-opacity: .3;
stroke: #fff;
shape-rendering: crispEdges;
}
.axis line,
.axis path {
fill: none;
stroke: #000;
shape-rendering: crispEdges;
}
.axis text {
text-shadow: 0 1px 0 #fff, 1px 0 0 #fff, 0 -1px 0 #fff, -1px 0 0 #fff;
cursor: move;
}
</style>
<body>
<script src="http://d3js.org/d3.v4.min.js"></script>
<script>
var margin = {top: 30, right: 10, bottom: 10, left: 10},
width = 600 - margin.left - margin.right,
height = 200 - margin.top - margin.bottom;
var x = d3.scalePoint().rangeRound([0, width]).padding(1),
y = {},
dragging = {};
var line = d3.line(),
//axis = d3.axisLeft(x),
background,
foreground,
extents;
var container = d3.select("body").append("div")
.attr("class", "parcoords")
.style("width", width + margin.left + margin.right + "px")
.style("height", height + margin.top + margin.bottom + "px");
var svg = container.append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
var quant_p = function(v){return (parseFloat(v) == v) || (v == "")};
d3.json("convertcsvSO.json", function(error, cars) {
dimensions = d3.keys(cars[0]);
x.domain(dimensions);
dimensions.forEach(function(d) {
var vals = cars.map(function(p) {return p[d];});
if (vals.every(quant_p)){
y[d] = d3.scaleLinear()
.domain(d3.extent(cars, function(p) {
return +p[d]; }))
.range([height, 0])
}
else{
vals.sort();
y[d] = d3.scalePoint()
.domain(vals.filter(function(v, i) {return vals.indexOf(v) == i;}))
.range([height, 0],1);
}
})
extents = dimensions.map(function(p) { return [0,0]; });
// Add grey background lines for context.
background = svg.append("g")
.attr("class", "background")
.selectAll("path")
.data(cars)
.enter().append("path")
.attr("d", path);
// Add blue foreground lines for focus.
foreground = svg.append("g")
.attr("class", "foreground")
.selectAll("path")
.data(cars)
.enter().append("path")
.attr("d", path);
// Add a group element for each dimension.
var g = svg.selectAll(".dimension")
.data(dimensions)
.enter().append("g")
.attr("class", "dimension")
.attr("transform", function(d) { return "translate(" + x(d) + ")"; })
.call(d3.drag()
.subject(function(d) { return {x: x(d)}; })
.on("start", function(d) {
dragging[d] = x(d);
background.attr("visibility", "hidden");
})
.on("drag", function(d) {
dragging[d] = Math.min(width, Math.max(0, d3.event.x));
foreground.attr("d", path);
dimensions.sort(function(a, b) { return position(a) - position(b); });
x.domain(dimensions);
g.attr("transform", function(d) { return "translate(" + position(d) + ")"; })
})
.on("end", function(d) {
delete dragging[d];
transition(d3.select(this)).attr("transform", "translate(" + x(d) + ")");
transition(foreground).attr("d", path);
background
.attr("d", path)
.transition()
.delay(500)
.duration(0)
.attr("visibility", null);
}));
// Add an axis and title.
var g = svg.selectAll(".dimension");
g.append("g")
.attr("class", "axis")
.each(function(d) { d3.select(this).call(d3.axisLeft(y[d]));})
//text does not show up because previous line breaks somehow
.append("text")
.attr("fill", "black")
.style("text-anchor", "middle")
.attr("y", -9)
.text(function(d) { return d; });
// Add and store a brush for each axis.
g.append("g")
.attr("class", "brush")
.each(function(d) {
if(y[d].name == 'r'){
// console.log(this);
d3.select(this).call(y[d].brush = d3.brushY().extent([[-8, 0], [8,height]]).on("start", brushstart).on("brush", brush_parallel_chart));
}
})
.selectAll("rect")
.attr("x", -8)
.attr("width", 16);
}); // closing
function position(d) {
var v = dragging[d];
return v == null ? x(d) : v;
}
function transition(g) {
return g.transition().duration(500);
}
// Returns the path for a given data point.
function path(d) {
return line(dimensions.map(function(p) { return [position(p), y[p](d[p])]; }));
}
// brush start function
function brushstart() {
d3.event.sourceEvent.stopPropagation();
}
// Handles a brush event, toggling the display of foreground lines.
function brush_parallel_chart() {
for(var i=0;i<dimensions.length;++i){
if(d3.event.target==y[dimensions[i]].brush) {
extents[i]=d3.event.selection.map(y[dimensions[i]].invert,y[dimensions[i]]);
}
}
foreground.style("display", function(d) {
return dimensions.every(function(p, i) {
if(extents[i][0]==0 && extents[i][0]==0) {
return true;
}
return extents[i][1] <= d[p] && d[p] <= extents[i][0];
}) ? null : "none";
});
}
</script>
In the drag callbacks, the dimensions are being sorted BUT the extents aren't. I've added a few lines that sorts extents array based on the new dimensions (by using origDimensions which is the original array)
Here's a fork of your plunkr: http://plnkr.co/edit/DquAXNv2mbbok7ssNuoX?p=preview
Relevant code:
var origDimensions = dimensions.slice(0);
And within the dragend callback:
// one way of sorting the extents array based on dimensions
var new_extents = [];
for(var i=0;i<dimensions.length;++i){
new_extents.push(extents[origDimensions.indexOf(dimensions[i])]);
}
extents = new_extents;
origDimensions = dimensions.slice(0); // setting origDimensions to the new array
Hope this helps. (and btw seems like the brushstart is empty which leads to showing NO curves on brush reset - try resetting brush on any axis).

D3 v4 Parallel Coordinate Plot Brush Selection

The parallel coordinate plot we are using and the data for the plot can be found here. This parallel coordinate plot does not work with version 4 of d3. We have made changes based on the API changes from v3 to v4. I think the main issue is in the brush function shown below.
function brush() {
let actives = dimensions.filter(function (p) {
return d3.brushSelection(y[p]) !== null;
});
console.log(actives);
let extents = actives.map(function (p) {
return d3.brushSelection(y[p]);
});
foreground.style("display", function (d) {
return actives.every(function (p, i) {
return extents[i][0] <= d[p] && d[p] <= extents[i][1];
}) ? null : "none";
});
}
The log shows "Array []" for actives. Currently we set each dimensions brush extent to be [[-8,0],[8,height]], which may be an issue as well. The full code is provided below.
<!DOCTYPE html>
<meta charset="utf-8">
<style>
svg {
font: 10px sans-serif;
}
.background path {
fill: none;
stroke: #ddd;
shape-rendering: crispEdges;
}
.foreground path {
fill: none;
stroke: steelblue;
}
.brush .extent {
fill-opacity: .3;
stroke: #fff;
shape-rendering: crispEdges;
}
.axis line,
.axis path {
fill: none;
stroke: #000;
shape-rendering: crispEdges;
}
.axis text {
text-shadow: 0 1px 0 #fff, 1px 0 0 #fff, 0 -1px 0 #fff, -1px 0 0 #fff;
cursor: move;
}
</style>
<body>
<script src="https://d3js.org/d3.v4.min.js"></script>
<script>
let margin = {top: 30, right: 10, bottom: 10, left: 10},
width = 960 - margin.left - margin.right,
height = 500 - margin.top - margin.bottom;
let x = d3.scalePoint().range([0, width]).padding(1),
y = {},
dragging = {};
let line = d3.line(),
axis = d3.axisLeft(), //Argument for axisLeft? Compare to code on original plot
background,
foreground;
let svg = d3.select("body").append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
d3.csv("cars.csv", function (error, cars) {
// Extract the list of dimensions and create a scale for each.
x.domain(dimensions = d3.keys(cars[0]).filter(function (d) {
return d !== "name" && (y[d] = d3.scaleLinear()
.domain(d3.extent(cars, function (p) {
return +p[d];
}))
.range([height, 0]));
}));
// Add grey background lines for context.
background = svg.append("g")
.attr("class", "background")
.selectAll("path")
.data(cars)
.enter().append("path")
.attr("d", path);
// Add blue foreground lines for focus.
foreground = svg.append("g")
.attr("class", "foreground")
.selectAll("path")
.data(cars)
.enter().append("path")
.attr("d", path);
// Add a group element for each dimension.
let g = svg.selectAll(".dimension")
.data(dimensions)
.enter().append("g")
.attr("class", "dimension")
.attr("transform", function (d) {
return "translate(" + x(d) + ")";
})
.call(d3.drag()
.subject(function (d) {
return {x: x(d)};
})
.on("start", function (d) {
dragging[d] = x(d);
background.attr("visibility", "hidden");
})
.on("drag", function (d) {
dragging[d] = Math.min(width, Math.max(0, d3.event.x));
foreground.attr("d", path);
dimensions.sort(function (a, b) {
return position(a) - position(b);
});
x.domain(dimensions);
g.attr("transform", function (d) {
return "translate(" + position(d) + ")";
})
})
.on("end", function (d) {
delete dragging[d];
transition(d3.select(this)).attr("transform", "translate(" + x(d) + ")");
transition(foreground).attr("d", path);
background
.attr("d", path)
.transition()
.delay(500)
.duration(0)
.attr("visibility", null);
}));
// Add an axis and title.
g.append("g")
.attr("class", "axis")
.each(function (d) {
d3.select(this).call(axis.scale(y[d]));
})
.append("text")
.style("text-anchor", "middle")
.attr("y", -9)
.text(function (d) {
return d;
});
// Add and store a brush for each axis.
g.append("g")
.attr("class", "brush")
.each(function (d) {
d3.select(this).call(y[d].brush = d3.brushY().extent([[-8,0],[8,height]]).on("start", brushstart).on("brush", brush));
})
.selectAll("rect")
.attr("x", -8)
.attr("width", 16);
});
function position(d) {
let v = dragging[d];
return v == null ? x(d) : v;
}
function transition(g) {
return g.transition().duration(500);
}
// Returns the path for a given data point.
function path(d) {
return line(dimensions.map(function (p) {
return [position(p), y[p](d[p])];
}));
}
function brushstart() {
d3.event.sourceEvent.stopPropagation();
}
// Handles a brush event, toggling the display of foreground lines.
function brush() {
//return !y[p].brush.empty was the original return value.
let actives = dimensions.filter(function (p) {
return d3.brushSelection(y[p]) !== null;
});
console.log(actives);
let extents = actives.map(function (p) {
return d3.brushSelection(y[p]);
});
foreground.style("display", function (d) {
return actives.every(function (p, i) {
return extents[i][0] <= d[p] && d[p] <= extents[i][1];
}) ? null : "none";
});
}
</script>
If anyone is familiar with d3 and could offer any guidance it would be greatly appreciated. We also tried using d3.event.selection and y[p].brush.selection in the brush function.
I stumbled upon the exact same issue but managed to resolve it after below changes.
Add brush for each axis this way:
y[d] = d3.scaleLinear().domain(d3.extent(data, function(p) {
return +p[d];
})).range([height, 0]);
y[d].brush = d3.brushY()
.extent([[-8, y[d].range()[1]], [8, y[d].range()[0]]])
.on('brush', brush);
Subsequently, give above as the brush callback when adding the brush group:
g.append('g')
.attr('class', 'brush')
.each(function(d) {
d3.select(this).call(y[d].brush);
})
.selectAll('rect')
.attr('x', -8)
.attr('width', 16);
Finally, change the brush handler to be:
function brush() {
const actives = [];
// filter brushed extents
svg.selectAll('.brush')
.filter(function(d): any {
return d3.brushSelection(this as any);
})
.each(function(d) {
actives.push({
dimension: d,
extent: d3.brushSelection(this as any)
});
});
// set un-brushed foreground line disappear
foreground.style('display', function(d) {
return actives.every(function(active) {
const dim = active.dimension;
return active.extent[0] <= y[dim](d[dim]) && y[dim](d[dim]) <= active.extent[1];
}) ? null : 'none';
});
}
If above is confusing, see this standalone example that helped me with correctly brushing on parallel coordinates with d3 v4 : https://gist.github.com/kotomiDu/d1fd0fe9397db41f5f8ce1bfb92ad20d

Categories

Resources