i've been trying to create an interactive multi line d3 chart.it's very simple right now, and this is what it looks like
enter image description here
the vertical line will show the values at each x point. this mouseover works. but the mouseover/mouseout I want is to select the line I am on, and blur the rest.
I basically want something like this: http://bl.ocks.org/Matthew-Weber/5645518
But nothing I do really works..
Here's the code
// Set the dimensions of the canvas / graph
var margin = {top: 30, right: 20, bottom: 60, left: 50},
width = 1200 - margin.left - margin.right,
height = 670 - margin.top - margin.bottom;
// Parse the date / time
var parseDate = d3.time.format("%b %Y").parse;
// Set the ranges
var x = d3.time.scale().range([0, width]);
var y = d3.scale.linear().range([height, 0]);
// Define the axes
var xAxis = d3.svg.axis().scale(x)
.orient("bottom").ticks(5);
var yAxis = d3.svg.axis().scale(y)
.orient("left").ticks(5);
// Define the line
var priceline = d3.svg.line()
.x(function(d) { return x(d.date); })
.y(function(d) { return y(d.price); })
// Adds the svg canvas
var 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 + ")");
// Get the data
d3.csv("data.csv", function(error, data) {
// if (error) return console.error(error);
data.forEach(function(d) {
d.date = parseDate(d.date.toString());
d.price = +d.price;
});
// Scale the range of the data
x.domain(d3.extent(data, function(d) { return d.date; }));
y.domain([0, d3.max(data, function(d) { return d.price; })]);
// Nest the entries by symbol
var dataNest = d3.nest()
.key(function(d) {return d.symbol;}) //change to name
.entries(data)
var color = d3.scale.category10();
// Loop through each symbol / key
dataNest.forEach(function(d) {
console.log("d", d);
svg.append("path")
.attr("class", "line")
.style("stroke", function() { // Add the colours dynamically
return d.color = color(d.key); })
.attr("d", priceline(d.values))
// .on("mouseover", mouseover) this doesn't work! I am thinking it's because I am nesting + using a for loop for each line, but I have to do this since my dataset is quite big and idk how many lines I will be needing
// .on("mouseout", mouseout)
});
function mouseover(d) {
console.log("no");
var me = this;
//d3.select(d.line).classed("line--hover", true);
d3.selectAll(".line").classed("line--hover", function() {
return (this === me);
}).classed("line--fade", function() {
return (this !== me);
});
}
function mouseout(d) {
d3.selectAll(".line")
.classed("line--hover", false)
.classed("line--fade", false);
}
// 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")
.call(yAxis)
.append("text")
.attr("transform", "rotate(-90)")
.attr("y", 6)
.attr("dy", ".71em")
.style("text-anchor", "end")
.text("Price in USD");
var dataL = 0;
var offset = 100;
var legend = d3.select('svg')
.append("g")
.selectAll("g")
.data(color.domain())
.enter()
.append('g')
.attr('class', 'legend')
.attr("transform", function (d, i) {
if (i === 0) {
dataL = d.length + offset
return "translate(0,0)"
} else {
var newdataL = dataL
dataL += d.length + offset
return "translate(" + (newdataL) + ",0)"
}
});
legend.append('text')
.attr("x", 68)
.attr("y", 650)
.text(function (d, i) {
return d
})
.attr("class", "textselected")
.style("text-anchor", "start")
.style("font-size", 15)
var mouseG = svg.append("g")
.attr("class", "mouse-over-effects");
mouseG.append("path") // this is the black vertical line to follow mouse
.attr("class", "mouse-line")
.style("stroke", "black")
.style("stroke-width", "1px")
.style("opacity", "0");
var lines = document.getElementsByClassName('line');
var mousePerLine = mouseG.selectAll('.mouse-per-line')
.data(dataNest)
.enter()
.append("g")
.attr("class", "mouse-per-line");
mousePerLine.append("text")
.attr("transform", "translate(10,3)");
mouseG.append('svg:rect') // append a rect to catch mouse movements on canvas
.attr('width', width) // can't catch mouse events on a g element
.attr('height', height)
.attr('fill', 'none')
.attr('pointer-events', 'all')
.on('mouseout', function() { // on mouse out hide line, circles and text
// console.log("bye");
d3.select(".mouse-line")
.style("opacity", "0");
d3.selectAll(".mouse-per-line circle")
.style("opacity", "0");
d3.selectAll(".mouse-per-line text")
.style("opacity", "0");
})
.on('mouseover', function() { // on mouse in show line, circles and text
d3.select(".mouse-line")
.style("opacity", "1");
d3.selectAll(".mouse-per-line circle")
.style("opacity", "1");
d3.selectAll(".mouse-per-line text")
.style("opacity", "1");
})
.on('mousemove', function() { // mouse moving over canvas
var mouse = d3.mouse(this);
d3.select(".mouse-line")
.attr("d", function() {
var d = "M" + mouse[0] + "," + height;
d += " " + mouse[0] + "," + 0;
return d;
});
d3.selectAll(".mouse-per-line")
.attr("transform", function(d, i) {
var xDate = x.invert(mouse[0]),
bisect = d3.bisector(function(d) { return d.date; }).right;
idx = bisect(d.values, xDate);
var beginning = 0,
end = lines[i].getTotalLength(),
target = null;
while (true){
target = Math.floor((beginning + end) / 2);
pos = lines[i].getPointAtLength(target);
if ((target === end || target === beginning) && pos.x !== mouse[0]) {
break;
}
if (pos.x > mouse[0]) end = target;
else if (pos.x < mouse[0]) beginning = target;
else break; //position found
}
d3.select(this).select('text')
.text(y.invert(pos.y).toFixed(2));
return "translate(" + 1050 + "," + 620 +")";
});
});
});
The data looks something like this:
symbol,date,price
MSFT,Jan 2000,39.81
MSFT,Feb 2000,36.35
MSFT,Mar 2000,43.22
MSFT,Apr 2000,28.37
MSFT,May 2000,25.45
MSFT,Jun 2000,32.54
MSFT,Jul 2000,28.4
MSFT,Aug 2000,28.4
MSFT,Sep 2000,24.53
MSFT,Oct 2000,28.02
MSFT,Nov 2000,23.34
MSFT,Dec 2000,17.65
MSFT,Jan 2001,24.84
MSFT,Feb 2001,24
MSFT,Mar 2001,22.25
MSFT,Apr 2001,27.56
symbol represents companies, which I might have around 100 of. (this is just sample data)
the css is very simple
body {
font: 12px Arial;
}
path {
stroke: steelblue;
stroke-width: 2;
fill: none;
}
.axis path,
.axis line {
fill: none;
stroke: grey;
stroke-width: 1;
shape-rendering: crispEdges;
}
.line--fade {
opacity: 0.3;
}
.line--hover {
stroke-width: 4px;
opacity: 1.0;
}
I also tried using tipsy.js but I'm not sure how to integrate it properly into my code.
Thank you!!
Related
I've got a line chart that needs circles on every other point for column A and C (not for column B). I've struggled to figure out how to do it. This is my line chart without the circles:
date,A=count,A=rank,B=count,B=rank,C=count,C=rank
2016-11-01,60588,213,51915,46,41200,10
2016-12-01,73344,216,58536,47,41230,10
2017-01-01,64164,219,53203,50,51220,12
2017-02-01,85295,224,34047,52,61000,15
2017-03-01,86089,226,44636,54,71200,16
2017-04-01,96871,230,55281,55,71000,10
2017-05-01,97622,234,85879,55,67900,10
I've tried dozens of solutions and I'm very stuck! Here is one of the things I've tried:
linesAndDots.selectAll("line-circle")
.data(data)
.enter().append("circle")
.attr("class", "data-circle")
.attr("r", 5)
.attr("cx", function(d) { return xScale(d.date); })
.attr("cy", function(d) { return yScale(d.measurement); });
But that is giving back NaN for the cx and cy values.
My full code looks like this:
<!DOCTYPE html>
<meta charset="utf-8">
<head>
<style>
path.line-0 {
fill: none;
stroke: #1F77B4;
}
path.line-1 {
fill: none;
stroke: #FF7F0E;
}
</style>
</head>
<!-- Body tag is where we will append our SVG and SVG objects-->
<body>
</body>
<!-- Load in the d3 library -->
<script src="lib/d3.v5.min.js"></script>
<div id="svgdiv"></div>
<script>
//------------------------1. PREPARATION------------------------//
//-----------------------------SVG------------------------------//
var columns=['A=count','B=count'];
var columnsB=['A=rank','B=rank'];
var width = 960;
var height = 500;
var margin = 5;
var padding = 5;
var adj = 75;
// we are appending SVG first
var svg = d3.select("body").append("svg")
.attr("preserveAspectRatio", "xMinYMin meet")
.attr("viewBox", "-"
+ adj + " -"
+ adj + " "
+ (width + adj *3) + " "
+ (height + adj*3))
.style("padding", padding)
.style("margin", margin)
.classed("svg-content", true);
//-----------------------------DATA-----------------------------//
var timeConv = d3.timeParse("%Y-%m-%d");
var formatTime = d3.timeFormat("%b %y")
var dataset = d3.csv("toShare.csv");
dataset.then(function(data) {
console.log(data.columns.slice(1))
var slices = columns.map(function(id) {
return {
id: id,
values: data.map(function(d){
return {
date: timeConv(d.date),
measurement: +d[id]
};
})
};
})
//----------------------------SCALES----------------------------//
var xScale = d3.scaleTime().range([0,width]);
var yScale = d3.scaleLinear().rangeRound([height, 0]);
xScale.domain(d3.extent(data, function(d){
return timeConv(d.date)}));
yScale.domain([(0), d3.max(slices, function(c) {
return d3.max(c.values, function(d) {
return d.measurement + 4; });
})
]);
//-----------------------------AXES-----------------------------//
var yaxis = d3.axisLeft()
.ticks(9)
.scale(yScale);
var xaxis = d3.axisBottom()
.ticks(7)
.scale(xScale);
//----------------------------LINES-----------------------------//
var line = d3.line()
.x(function(d) { return xScale(d.date); })
.y(function(d) { return yScale(d.measurement); });
let id = 0;
var ids = function () {
return "line-"+id++;
}
//-------------------------2. DRAWING---------------------------//
//-----------------------------AXES-----------------------------//
svg.append("g")
.attr("class", "axis")
.attr("transform", "translate(0," + height + ")")
.call(xaxis)
.append("text")
.attr("transform",
"translate(" + (width/2) + " ," +
50 + ")")
.style("text-anchor", "middle")
.text("Month");
svg.append("g")
.attr("class", "axis")
.call(yaxis)
.append("text")
.attr("transform", "rotate(-90)")
.attr("y", 0 - adj)
.attr("x",0 - (height / 2))
.attr("dy", "1em")
.style("text-anchor", "middle")
.text("Running Total");
//----------------------------LINES-----------------------------//
var linesAndDots = svg.selectAll("lines")
.data(slices)
.enter()
.append("g");
linesAndDots.append("path")
.attr("class", ids)
.attr("d", function(d) { return line(d.values); });
linesAndDots.selectAll("line-circle")
.data(data)
.enter().append("circle")
.attr("class", "data-circle")
.attr("r", 5)
.attr("cx", function(d) {
console.log("id", id)
return 5; })
.attr("cy", function(d) { return 40; });
//Add the label on the right
linesAndDots.append("text")
.attr("class", ids)
.datum(function(d) {
return {
id: d.id,
value: d.values[d.values.length - 1]}; })
.attr("transform", function(d) {
return "translate(" + (xScale(d.value.date) + 10)
+ "," + (yScale(d.value.measurement) + 5 ) + ")"; })
.attr("x", 5)
.text(function(d) { return d.id.replace("=count", ""); });
});
</script>
</body>
Thanks for the help!
The problem can be inspected by seeing what d really is in the cx and cy function.
cx's problem: you didnt parse the date string into a Date object like you did for the slices data; cy's problem: the data item has no measurement key.
You have used both data and slices. To make the code more consistent, I fix the code of circle drawing using slices too.
linesAndDots
.selectAll(".data-circle")
.data(d=>d.values) // `d` now is the one of the two entries of `slices`,
//and we want to use the `values` array of each entry.
.enter()
.append("circle")
.attr("class", "data-circle")
.attr("r", 5)
.attr("cx", function(d) {
return xScale(d.date);
})
.attr("cy", function(d) {
return yScale(d.measurement)
});
A demo here
I'm able to generate the following graph using D3 areas:
I want to create the following animation. When the webpage loads, you see the first figure. Then, each of the areas morphs into a bar. Finally, I would like to allow users to toggle between the two figures by clicking "B" or "D".
I was able to add the buttons and the corresponding bars to my figure, but I'm having troubles figuring out how to do the animation. This is the code that I have right now:
HTMLWidgets.widget({
name: 'IMposterior',
type: 'output',
factory: function(el, width, height) {
// TODO: define shared variables for this instance
return {
renderValue: function(opts) {
console.log("threshold: ", opts.threshold);
console.log("bars: ", opts.bars);
var margin = {left:50,right:50,top:40,bottom:125};
xMax = d3.max(opts.data, function(d) { return d.x ; });
yMax = d3.max(opts.data, function(d) { return d.y ; });
xMin = d3.min(opts.data, function(d) { return d.x ; });
yMin = d3.min(opts.data, function(d) { return d.y ; });
var y = d3.scaleLinear()
.domain([0,yMax])
.range([height-margin.bottom,0]);
var x = d3.scaleLinear()
.domain([xMin,xMax])
.range([0,width]);
var yAxis = d3.axisLeft(y);
var xAxis = d3.axisBottom(x);
var area = d3.area()
.x(function(d){ return x(d.x) ;})
.y0(height-margin.bottom)
.y1(function(d){ return y(d.y); });
var svg = d3.select(el).append('svg').attr("height","100%").attr("width","100%");
var chartGroup = svg.append("g").attr("transform","translate("+margin.left+","+margin.top+")");
chartGroup.append("path")
.attr("d", area(opts.data.filter(function(d){ return d.x< -opts.MME ;})))
.style("fill", opts.colors[0]);
if(opts.MME !==0){
chartGroup.append("path")
.attr("d", area(opts.data.filter(function(d){ return (d.x < opts.MME & d.x > -opts.MME) ;})))
.style("fill", opts.colors[1]);
}
chartGroup.append("path")
.attr("d", area(opts.data.filter(function(d){ return d.x > opts.MME ;})))
.style("fill", opts.colors[2]);
chartGroup.append("g")
.attr("class","axis x")
.attr("transform","translate(0,"+(height-margin.bottom)+")")
.call(xAxis);
var tooltip = d3.tip()
.attr('class', 'd3-tip chart-data-tip')
.offset([30, 0])
.direction('s')
.html(function(d, i) {
return "<strong>" + d + "</strong> <span style='color:" + "white" + "'>"+ "</span>";
});
svg.call(tooltip);
chartGroup.selectAll("path").data(opts.text).on('mouseover', tooltip.show).on('mouseout', tooltip.hide);
// Bars
var yBar = d3.scaleLinear()
.domain([0,1])
.range([height-margin.bottom,0]);
var xBar = d3.scaleBand()
.domain(opts.bars.map(function(d) { return d.x; }))
.rangeRound([0, width]).padding(0.1);
var yAxisBar = d3.axisLeft(yBar);
var xAxisBar = d3.axisBottom(xBar);
var g = svg.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
g.append("g")
.attr("class", "axis axis--x")
.attr("transform","translate(0,"+(height-margin.bottom)+")")
.call(d3.axisBottom(xBar));
g.append("g")
.attr("class", "axis axis--y")
.call(d3.axisLeft(yBar).ticks(10, "%"))
.append("text")
.attr("transform", "rotate(-90)")
.attr("y", 6)
.attr("dy", "0.71em")
.attr("text-anchor", "end")
.text("Probability");
g.selectAll(".bar")
.data(opts.bars)
.enter().append("rect")
.attr("class", "bar")
.attr("x", function(d) { return xBar(d.x); })
.attr("y", function(d) { return yBar(d.y); })
.attr("width", xBar.bandwidth())
.style("fill", function(d) { return d.color; })
.attr("height", function(d) { return height - margin.bottom - yBar(d.y); });
// Add buttons
//container for all buttons
var allButtons= svg.append("g")
.attr("id","allButtons");
//fontawesome button labels
var labels= ["B", "D"];
//colors for different button states
var defaultColor= "#E0E0E0";
var hoverColor= "#808080";
var pressedColor= "#000000";
//groups for each button (which will hold a rect and text)
var buttonGroups= allButtons.selectAll("g.button")
.data(labels)
.enter()
.append("g")
.attr("class","button")
.style("cursor","pointer")
.on("click",function(d,i) {
updateButtonColors(d3.select(this), d3.select(this.parentNode));
d3.select("#numberToggle").text(i+1);
})
.on("mouseover", function() {
if (d3.select(this).select("rect").attr("fill") != pressedColor) {
d3.select(this)
.select("rect")
.attr("fill",hoverColor);
}
})
.on("mouseout", function() {
if (d3.select(this).select("rect").attr("fill") != pressedColor) {
d3.select(this)
.select("rect")
.attr("fill",defaultColor);
}
});
var bWidth= 40; //button width
var bHeight= 25; //button height
var bSpace= 10; //space between buttons
var x0= 20; //x offset
var y0= 10; //y offset
//adding a rect to each toggle button group
//rx and ry give the rect rounded corner
buttonGroups.append("rect")
.attr("class","buttonRect")
.attr("width",bWidth)
.attr("height",bHeight)
.attr("x",function(d,i) {return x0+(bWidth+bSpace)*i;})
.attr("y",y0)
.attr("rx",5) //rx and ry give the buttons rounded corners
.attr("ry",5)
.attr("fill",defaultColor);
//adding text to each toggle button group, centered
//within the toggle button rect
buttonGroups.append("text")
.attr("class","buttonText")
.attr("x",function(d,i) {
return x0 + (bWidth+bSpace)*i + bWidth/2;
})
.attr("y",y0+bHeight/2)
.attr("text-anchor","middle")
.attr("dominant-baseline","central")
.attr("fill","white")
.text(function(d) {return d;});
function updateButtonColors(button, parent) {
parent.selectAll("rect")
.attr("fill",defaultColor);
button.select("rect")
.attr("fill",pressedColor);
}
},
resize: function(width, height) {
// TODO: code to re-render the widget with a new size
}
};
}
});
And this is the figure that that code produces:
This does the trick:
HTMLWidgets.widget({
name: 'IMPosterior',
type: 'output',
factory: function(el, width, height) {
// TODO: define shared variables for this instance
return {
renderValue: function(opts) {
//transition
var transDuration = 1000;
var dataDiscrete = opts.bars.map((b, i) => {
b.y = Number(b.y);
b.desc = opts.text[i];
return b;
});
var distParams = {
min: d3.min(opts.data, d => d.x),
max: d3.max(opts.data, d => d.x)
};
distParams.cuts = [-opts.MME, opts.MME, distParams.max];
opts.data = opts.data.sort((a,b) => a.x - b.x);
var dataContinuousGroups = [];
distParams.cuts.forEach((c, i) => {
let data = opts.data.filter(d => {
if (i === 0) {
return d.x < c;
} else if (i === distParams.cuts.length - 1) {
return d.x > distParams.cuts[i - 1];
} else {
return d.x < c && d.x > distParams.cuts[i - 1];
}
});
data.unshift({x:data[0].x, y:0});
data.push({x:data[data.length - 1].x, y:0});
dataContinuousGroups.push({
color: opts.colors[i],
data: data
});
});
var margin = {
top: 50,
right: 20,
bottom: 80,
left: 70
},
dims = {
width: width - margin.left - margin.right,
height: height - margin.top - margin.bottom
};
var xContinuous = d3.scaleLinear()
.domain([distParams.min - 1, distParams.max + 1])
.range([0, dims.width]);
var xDiscrete = d3.scaleBand()
.domain(dataDiscrete.map(function(d) { return d.x; }))
.rangeRound([0, dims.width]).padding(0.1);
var y = d3.scaleLinear()
.domain([0, 1])
.range([dims.height, 0]);
var svg = d3.select(el).append("svg")
.attr("width", dims.width + margin.left + margin.right)
.attr("height", dims.height + margin.top + margin.bottom);
var g = svg
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
var xAxis = d3.axisBottom()
.scale(xDiscrete);
var yAxis = d3.axisLeft()
.scale(y)
.ticks(10)
.tickFormat(d3.format(".0%"));
var yLabel = g.append("text")
.attr("class", "y-axis-label")
.attr("transform", "rotate(-90)")
.attr("y", -52)
.attr("x", -160)
.attr("dy", ".71em")
.style("text-anchor", "end")
.style("font-size", 14 + "px")
.text("Probability");
g.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + dims.height + ")")
.call(xAxis);
g.append("g")
.attr("class", "y axis")
.call(yAxis);
var areas = g.selectAll(".area")
.data(dataDiscrete)
.enter().append("path")
.attr("class", "area")
.style("fill", function(d) { return d.color; })
.attr("d", function(d, i) {
let numPts = dataContinuousGroups[i].data.length - 2;
var path = d3.path()
path.moveTo(xDiscrete(d.x), y(0));
for (j=0; j<numPts; j++) {
path.lineTo(xDiscrete(d.x) + j*xDiscrete.bandwidth()/(numPts-1), y(d.y))
}
path.lineTo(xDiscrete(d.x) + xDiscrete.bandwidth(), y(0));
return path.toString();
});
var tooltip = d3.tip()
.attr('class', 'd3-tip chart-data-tip')
.offset([30, 0])
.direction('s')
.html(function(d, i) {
return "<span>" + dataDiscrete[i].desc + "</span>";
});
g.call(tooltip);
areas
.on('mouseover', tooltip.show)
.on('mouseout', tooltip.hide);
var thresholdLine = g.append("line")
.attr("stroke", "black")
.style("stroke-width", "1.5px")
.style("stroke-dasharray", "5,5")
.style("opacity", 1)
.attr("x1", 0)
.attr("y1", y(opts.threshold))
.attr("x2", dims.width)
.attr("y2", y(opts.threshold));
var updateXAxis = function(type, duration) {
if (type === "continuous") {
xAxis.scale(xContinuous);
} else {
xAxis.scale(xDiscrete);
}
d3.select(".x").transition().duration(duration).call(xAxis);
};
var updateYAxis = function(data, duration) {
var extent = d3.extent(data, function(d) {
return d.y;
});
extent[0] = 0;
extent[1] = extent[1] + 0.2*(extent[1] - extent[0]);
y.domain(extent);
d3.select(".y").transition().duration(duration).call(yAxis);
};
var toggle = function(to, duration) {
if (to === "distribution") {
updateYAxis(dataContinuousGroups[0].data.concat(dataContinuousGroups[1].data).concat(dataContinuousGroups[2].data), 0);
updateXAxis("continuous", duration);
areas
.data(dataContinuousGroups)
.transition()
.duration(duration)
.attr("d", function(d) {
var gen = d3.line()
.x(function(p) {
return xContinuous(p.x);
})
.y(function(p) {
return y(p.y);
});
return gen(d.data);
});
thresholdLine
.style("opacity", 0);
g.select(".y.axis")
.style("opacity", 0);
g.select(".y-axis-label")
.style("opacity", 0);
} else {
y.domain([0, 1]);
d3.select(".y").transition().duration(duration).call(yAxis);
updateXAxis("discrete", duration);
areas
.data(dataDiscrete)
.transition()
.duration(duration)
.attr("d", function(d, i) {
let numPts = dataContinuousGroups[i].data.length - 2;
var path = d3.path()
path.moveTo(xDiscrete(d.x), y(0));
for (j=0; j<numPts; j++) {
path.lineTo(xDiscrete(d.x) + j*xDiscrete.bandwidth()/(numPts-1), y(d.y))
}
path.lineTo(xDiscrete(d.x) + xDiscrete.bandwidth(), y(0));
return path.toString();
});
thresholdLine
.transition()
.duration(0)
.delay(duration)
.style("opacity", 1)
.attr("y1", y(opts.threshold))
.attr("y2", y(opts.threshold));
g.select(".y.axis")
.transition()
.duration(0)
.delay(duration)
.style("opacity", 1);
g.select(".y-axis-label")
.transition()
.duration(0)
.delay(duration)
.style("opacity", 1);
}
};
// Add buttons
//container for all buttons
var allButtons = svg.append("g")
.attr("id", "allButtons");
//fontawesome button labels
var labels = ["B", "D"];
//colors for different button states
var defaultColor = "#E0E0E0";
var hoverColor = "#808080";
var pressedColor = "#000000";
//groups for each button (which will hold a rect and text)
var buttonGroups = allButtons.selectAll("g.button")
.data(labels)
.enter()
.append("g")
.attr("class", "button")
.style("cursor", "pointer")
.on("click", function(d, i) {
updateButtonColors(d3.select(this), d3.select(this.parentNode));
d3.select("#numberToggle").text(i + 1);
if (d === "D") {
toggle("distribution", transDuration);
} else {
toggle("discrete", transDuration);
}
})
.on("mouseover", function() {
if (d3.select(this).select("rect").attr("fill") != pressedColor) {
d3.select(this)
.select("rect")
.attr("fill", hoverColor);
}
})
.on("mouseout", function() {
if (d3.select(this).select("rect").attr("fill") != pressedColor) {
d3.select(this)
.select("rect")
.attr("fill", defaultColor);
}
});
var bWidth = 40; //button width
var bHeight = 25; //button height
var bSpace = 10; //space between buttons
var x0 = 20; //x offset
var y0 = 10; //y offset
//adding a rect to each toggle button group
//rx and ry give the rect rounded corner
buttonGroups.append("rect")
.attr("class", "buttonRect")
.attr("width", bWidth)
.attr("height", bHeight)
.attr("x", function(d, i) {
return x0 + (bWidth + bSpace) * i;
})
.attr("y", y0)
.attr("rx", 5) //rx and ry give the buttons rounded corners
.attr("ry", 5)
.attr("fill", defaultColor);
//adding text to each toggle button group, centered
//within the toggle button rect
buttonGroups.append("text")
.attr("class", "buttonText")
.attr("x", function(d, i) {
return x0 + (bWidth + bSpace) * i + bWidth / 2;
})
.attr("y", y0 + bHeight / 2)
.attr("text-anchor", "middle")
.attr("dominant-baseline", "central")
.attr("fill", "white")
.text(function(d) {
return d;
});
function updateButtonColors(button, parent) {
parent.selectAll("rect")
.attr("fill", defaultColor);
button.select("rect")
.attr("fill", pressedColor);
}
toggle("distribution", 0);
setTimeout(() => {
toggle("discrete", transDuration);
}, 1000);
},
resize: function(width, height) {
// TODO: code to re-render the widget with a new size
}
};
}
});
I am trying to follow this example here for a D3 stacked chart. I've tested it locally and it works fine.
I have adapted the code to match my csv dataset, but unfortunately I get issues with the calculation of y and height attributes:
Error: attribute y: Expected length, "NaN".
Error: attribute height: Expected length, "NaN".
Here is my adapted source code:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Enterprise Elements Analysis - In/Out of Scope</title>
<script src="http://d3js.org/d3.v4.min.js" charset="utf-8"></script>
<style type="text/css">
svg {
font: 10px sans-serif;
shape-rendering: crispEdges;
}
.axis path,
.axis line {
fill: none;
stroke: #000;
}
path.domain {
stroke: none;
}
.y .tick line {
stroke: #ddd;
}
</style>
</head>
<body>
<script type="text/javascript">
// Our D3 code will go here
var ratData = [];
d3.csv("./etcounts.csv", function(d) {
return {
type: d.type,
in_scope: +d.in_scope,
out_scope: +d.out_scope
};
}, function(error, rows) {
data = rows;
console.log(data);
createVisualization();
});
function createVisualization() {
// Setup svg using with margins
var margin = {bottom: 75, left: 15, right: 85};
var w = 200 - margin.left - margin.right;
var h = 175 - margin.bottom;
// get length of Array
var arrayLength = data.length; // length of dataset
var x_axisLength = 100; // length of x-axis in our layout
var y_axisLength = 100; // length of y-axis in our layout
var svg = d3.select("body")
.append("svg")
.attr("width", w + margin.left + margin.right)
.attr("height", h + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + ",10)");
// set up the properties for stack
var stack = d3.stack()
.keys(["In Scope", "Out Scope"])
.order(d3.stackOrderDescending)
.offset(d3.stackOffsetNone);
// transpose your data using stack
var series = stack(data);
// view the stack
console.log(series);
// setup the Y scale
var yScale = d3.scaleLinear()
.domain([0, d3.max(series, function(d) {
return d3.max(d, function(d) {
return d[1];
});
})])
.range([h, 0]);
// Set some colors into an array
var colors = ["#dfd6d6", "#d85f41"]; // choose colors
// Create groups for each series, rect elements for each segment
var groups = svg.selectAll("g.type")
.data(series)
.enter().append("g")
.attr("class", "type")
.style("fill", function(d, i) {
return colors[i]; // color the rectangles
});
// Create the rectangles
var rect = groups.selectAll("rect")
.data(function(d) {
return d;
})
.enter()
.append("rect")
.attr("x", function(d,i) {
return i * (x_axisLength/arrayLength) + 30; // Set x coordinate of rectangle to index of data value (i) *25
})
.attr("y", function(d) {
return yScale(d[1]); // set base of rectangle
})
.attr("height", function(d) {
return yScale(d[0]) - yScale(d[1]); // set height of rectangle
})
.attr("width", (x_axisLength/arrayLength) - 1) // set width of rectangle
.on("mouseover", function() {
tooltip.style("display", null); // hide tooltip
})
.on("mousemove", function(d) {
var xPosition = d3.mouse(this)[0] - 15;
var yPosition = d3.mouse(this)[1] - 25;
tooltip.attr("transform", "translate(" + xPosition + "," + yPosition + ")");
tooltip.select("text").text(d.data.city + ": " + (d[1] - d[0])); // populate tooltip
})
.on("mouseout", function() {
tooltip.style("display", "none");
});
// Draw legend
var legend = svg.selectAll(".legend")
.data(colors)
.enter().append("g")
.attr("class", "legend")
.attr("transform", function(d, i) { return "translate(" + i * 50 + ", 110)"; });
legend.append("rect")
.attr("x", w - 70)
.attr("width", 18)
.attr("height", 18)
.style("fill", function(d, i) {return colors.slice().reverse()[i];});
legend.append("text")
.attr("x", w - 49)
.attr("y", 9)
.attr("dy", ".35em")
.style("text-anchor", "start")
.text(function(d, i) {
switch (i) {
case 0: return "In";
case 1: return "Out";
}
});
// Prep the tooltip bits, initial display is hidden
var tooltip = svg.append("g")
.attr("class", "tooltip")
.style("display", "none");
tooltip.append("text")
.attr("x", 15)
.attr("dy", "1.2em")
.style("text-anchor", "middle")
.attr("font-size", "12px");
// Create y-axis
svg.append("line")
.attr("x1", 30)
.attr("y1", 0)
.attr("x2", 30)
.attr("y2", 100)
.attr("stroke-width", 2)
.attr("stroke", "black");
// y-axis label
svg.append("text")
.attr("class", "y label")
.attr("text-anchor", "middle")
.text("Elements")
.attr("transform", "translate(20, 50) rotate(-90)")
.attr("font-size", "14px")
.attr("font-family", "'Open Sans', sans-serif");
// Create x-axis
svg.append("line")
.attr("x1", 30)
.attr("y1", 100)
.attr("x2", 130)
.attr("y2", 100)
.attr("stroke-width", 2)
.attr("stroke", "black");
}
</script>
</body>
</html>
My Dataset (etcounts.csv) is here:
type,in_scope,out_scope
ERKRS,1,1
KKBER,6,5
KOKRS,1,31
BUKRS,78,143
VKORG,23,13
BWKEY,51,6
EKORG,5,6
WERKS,51,65
LGORT,9,180
SPART,9,3
VTWEG,2,0
PERSA,47,73
Unfortunately my D3/JS skills are not quite up to par, but I would appreciate any help. Thanks - John
Instead of
var stack = d3.stack()
.keys(["In Scope", "Out Scope"]) <-- there is no key as such
.order(d3.stackOrderDescending)
.offset(d3.stackOffsetNone);
it should have been:
var stack = d3.stack()
.keys(["in_scope", "out_scope"])
.order(d3.stackOrderDescending)
.offset(d3.stackOffsetNone);
Reason: there is no keys in your CSV "In Scope", "Out Scope"
It should have been "in_scope", "out_scope"
EDIT
For tool tip :
tooltip.select("text").text(d.data.city + ": " + (d[1] - d[0]));
should have been
tooltip.select("text").text(d.data.type + ": " + (d[1] - d[0]));
Reason: There is no data.city in your CSV.
working code here
I have a scatter plot which shows a trendline/average line based on the data provided. The only problem is that the trendline bleeds into the y axis label (see picture).
Here is my d3 code, how can I "trim" the trendline to fit only the plotted region of the chart?
scatterPlot.js
jQuery.sap.require("sap/ui/thirdparty/d3");
jQuery.sap.declare("pricingTool.ScatterPlot");
sap.ui.core.Element.extend("pricingTool.ScatterPlotItem", { metadata : {
properties : {
"quarter" : {type : "string", group : "Misc", defaultValue : null},
"values" : {type : "object", group : "Misc", defaultValue : null}
}
}});
sap.ui.core.Control.extend("pricingTool.ScatterPlot", {
metadata : {
properties: {
"title": {type : "string", group : "Misc", defaultValue : "ScatterPlot Title"}
},
aggregations : {
"items" : { type: "pricingTool.ScatterPlotItem", multiple : true, singularName : "item"}
},
defaultAggregation : "items",
events: {
"onPress" : {},
"onChange":{}
}
},
init: function() {
//console.log("vizConcept.ScatterPlot.init()");
this.sParentId = "";
},
createScatterPlot : function() {
//console.log("vizConcept.ScatterPlot.createScatterPlot()");
var oScatterPlotLayout = new sap.m.VBox({alignItems:sap.m.FlexAlignItems.Center,justifyContent:sap.m.FlexJustifyContent.Center});
var oScatterPlotFlexBox = new sap.m.FlexBox({height:"auto",alignItems:sap.m.FlexAlignItems.Center});
/* ATTENTION: Important
* This is where the magic happens: we need a handle for our SVG to attach to. We can get this using .getIdForLabel()
* Check this in the 'Elements' section of the Chrome Devtools:
* By creating the layout and the Flexbox, we create elements specific for this control, and SAPUI5 takes care of
* ID naming. With this ID, we can append an SVG tag inside the FlexBox
*/
this.sParentId=oScatterPlotFlexBox.getIdForLabel();
oScatterPlotLayout.addItem(oScatterPlotFlexBox);
return oScatterPlotLayout;
},
/**
* The renderer render calls all the functions which are necessary to create the control,
* then it call the renderer of the vertical layout
* #param oRm {RenderManager}
* #param oControl {Control}
*/
renderer: function(oRm, oControl) {
var layout = oControl.createScatterPlot();
oRm.write("<div");
oRm.writeControlData(layout); // writes the Control ID and enables event handling - important!
oRm.writeClasses(); // there is no class to write, but this enables
// support for ColorBoxContainer.addStyleClass(...)
oRm.write(">");
oRm.renderControl(layout);
oRm.addClass('verticalAlignment');
oRm.write("</div>");
},
onAfterRendering: function(){
//console.log("vizConcept.ScatterPlot.onAfterRendering()");
//console.log(this.sParentId);
var cItems = this.getItems();
var data = [];
for (var i=0;i<cItems.length;i++){
var oEntry = {};
for (var j in cItems[i].mProperties) {
oEntry[j]=cItems[i].mProperties[j];
}
data.push(oEntry);
}
$("svg").last().remove();
/*
* ATTENTION: See .createScatterPlot()
* Here we're picking up a handle to the "parent" FlexBox with the ID we got in .createScatterPlot()
* Now simply .append SVG elements as desired
* EVERYTHING BELOW THIS IS PURE D3.js
*/
var margin = {
top: 25,
right: 30,
bottom: 80,
left: 90
},
width = 600 - margin.left - margin.right,
height = 300 - margin.top - margin.bottom;
var tableData = data[0].values;
var dates = [];
for(var i = 0; i<tableData.length; i++){
dates[i] = new Date(tableData[i].date);
dates.sort(function(a,b) {
return a -b;
})
}
var minDate = dates[0],
maxDate = dates[dates.length-1];
//test//
var year = new Date(dates[0]);
minDate.setMonth(year.getMonth(), -2);
var year = new Date(dates[dates.length-1]);
console.log(year);
//maxDate.setMonth(year.getMonth(), 6);
console.log(maxDate);
//end test//
// Our X scale
//var x = d3.scale.linear()
var x = d3.time.scale()
.domain([minDate, maxDate])
.range([0, width]);
// Our Y scale
var y = d3.scale.linear()
.range([height, 0]);
// Our color bands
var color = d3.scale.ordinal()
.range(["#000", "#004460", "#0070A0", "#008BC6", "#009DE0", "#45B5E5", "8CCDE9", "#DAEBF2"]); //"#00A6ED",
// Use our X scale to set a bottom axis
var xAxis = d3.svg.axis()
.scale(x)
.orient("bottom")
.ticks(8)
.tickFormat(d3.time.format("%b-%Y"));
// Same for our left axis
var yAxis = d3.svg.axis()
.scale(y)
.orient("left");
var tip = d3.select("body").append("div")
.attr("class", "sctooltip")
.style("position", "absolute")
.style("text-align", "center")
.style("width", "80px")
.style("height", "42px")
.style("padding", "2px")
.style("font", "11px sans-serif")
.style("background", "#F0F0FF")
.style("border", "0px")
.style("border-radius", "8px")
.style("pointer-events", "none")
.style("opacity", 0);
var vis = d3.select("#" + this.sParentId);
var svg = vis.append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.style("background-color","white")
.style("font", "12px sans-serif")
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
x.domain([minDate, maxDate]);
// Our Y domain is from zero to our highest total
y.domain([0, d3.max(data, function (d) {
var max = d3.max(d.values, function (dd){
return(+dd.price);
})
return max;
})]);
var totalval = 0;
var totalval2 = 0;
var values = 0;
data.forEach(function (d) {
d.values.forEach(function (dd){
values +=1;
totalval += +dd.date;
totalval2 += +dd.price;
});
});
var priceAverage = totalval2/values;
var average = totalval/totalval2;
var value = data[0].values[0].price;
var line_data = [
{"x": 0, "y": y.domain()[0]},
{"x": y.domain()[1]*average, "y": y.domain()[1]}
];
var avgline = d3.svg.line()
.x(function(d){ return x(d.x); })
.y(function(d){ return y(d.y); })
.interpolate("linear");
svg.append("g")
.attr("class", "x axis")
.style("fill", "none")
.style("stroke", "grey")
.style("shape-rendering", "crispEdges")
.attr("transform", "translate(0," + height + ")")
.call(xAxis)
.selectAll("text")
.style("text-anchor", "end")
.attr("dx", "-.8em")
.attr("dy", ".15em")
.attr("transform", "rotate(-25)" );
svg.append("g")
.attr("class", "y-axis")
.style("fill", "none")
.style("stroke", "grey")
.style("shape-rendering", "crispEdges")
.call(yAxis);
//average line
svg.append("path")
.attr("class", "avgline")
.style("stroke", "#000")
.style("stroke-width", "1px")
.style("stroke-dasharray", ("4, 4"))
.attr("d", avgline(line_data));
var plot = svg.selectAll(".values") //changed this from quarter
.data(data)
.enter().append("g");
plot.selectAll("dot")
.data(function (d) {
return d.values;
})
.enter().append("circle")
.attr("class", "dot")
.attr("r", 5)
.attr("cx", function (d){
return x(d.date);
})
.attr("cy", function (d) {
return y(d.price);
})
.style("stroke", "#004460")
.style("fill", function (d) {
return color(d.name);
})
.style("opacity", .9)
.style("visibility", function(d){
if(+d.date != 0){
return "visible";
}else{
return "hidden";
}
})
.style("pointer-events", "visible")
.on("mouseover", function(d){
tip.transition()
.duration(200)
.style("opacity", .8);
tip.html(d.name + "<br/>" + d.quarter + "<br />" + "Avg. " +(+d.date/+d.price).toFixed(2))
.style("left", (d3.event.pageX-40) + "px")
.style("top", (d3.event.pageY-50) + "px");
})
.on("mouseout", function(d){
tip.transition()
.duration(500)
.style("opacity", 0);
});;
// var legend = svg.selectAll(".legend")
// .data(color.domain())
// .enter().append("g")
// .attr("class", "legend")
// .attr("transform", function (d, i) {
// return "translate(0," + i * 16 + ")";
// });
// legend.append("rect")
// .attr("x", width - 12)
// .attr("width", 12)
// .attr("height", 12)
// .style("fill", color);
// legend.append("text")
// .attr("x", width - 24)
// .attr("y", 6)
// .attr("dy", ".35em")
// .style("text-anchor", "end")
// .style("font", "11px sans-serif")
// .text(function (d) {
// return d;
// });
//y-axis label
svg.append("text")
.attr("transform", "rotate(-90)")
.attr("x", - (height/2))
.attr("y", 10 - margin.left)
.attr("dy", "1em")
.style("text-anchor", "middle")
.style("font", "16px sans-serif")
.text("PPI Cost($)");
//x-axis label
svg.append("text")
.attr("transform", "translate("+(width/2) +","+ (height +margin.top +50)+")")
.style("text-anchor", "middle")
.style("font", "16px sans-serif")
.text("Purchase Date");
var avglabel = svg.append("g")
.attr("transform", "translate(" + (width-40) + ",140)");
avglabel.append("text")
.style("text-anchor", "middle")
.text("Average: $" + priceAverage.toFixed(2));
}
});
Try setting the x values to the start and end of the graph:
var line_data = [
{x: x.domain()[0], y: average},
{x: x.domain()[1], y: average}
];
Alternatively you could set the path string directly without using a path generator:
svg.append("path")
.attr("class", "avgline")
.style("stroke", "#000")
.style("stroke-width", "1px")
.style("stroke-dasharray", ("4, 4"))
.attr("d", [
"M", x.range()[0], y(average),
"H", x.range()[1]
].join(' '))
I'm pretty new to D3 and just incorporated the below tooltips into my application. I have both a single line chart as well as a multi line chart.
Single line:
https://bl.ocks.org/alandunning/cfb7dcd7951826b9eacd54f0647f48d3
Multi Line: Multiseries line chart with mouseover tooltip
As you can see, the functionality of the two tooltips are different. The Single Line tooltip jumps from each data point whereas the Multi Line continually follows the chart path. I want to change the Multi Line functionality to mimic how the Single Line tooltip works.
Any help would be greatly appreciated. Please let me know if I need to provide more information. Also note that the data I'm working with is an array of arrays
Below is my code:
Single Line Chart:
let g = svg.append('g');
g.append("path")
.datum(this.dataObj)
.attr("class",`line-${this.yAxisData} line`)
.attr('d', line)
.attr("stroke",`${this.color(this.dataObj.label)}`)
.attr("fill",'none')
.attr("transform", `translate(${this.margin.left},${this.margin.top})`);
var focus = g.append("g")
.attr("class", "focus")
.style("display", "none");
focus.append("line")
.datum(this.dataObj)
.attr("class", "x-hover-line hover-line")
.attr("transform",`translate(${this.margin.left},${this.margin.top})`)
.attr("stroke",`${this.color(this.dataObj.label)}`)
.attr("y1", 0)
.attr("y2", height);
focus.append("circle")
.datum(this.dataObj)
.attr("transform",`translate(${this.margin.left},${this.margin.top})`)
.attr("stroke",`${this.color(this.dataObj.label)}`)
.attr("r", 7.5);
focus.append("text")
.attr("class","linetip")
.attr("x", 40)
.attr("dy", "0.5em");
svg.append("rect")
.attr("transform", `translate(${this.margin.left},${this.margin.top})`)
.attr("class", "overlay")
.attr("width", width)
.attr("height", height)
.on("mouseover", function() { focus.style("display", null); })
.on("mouseout", function() { focus.style("display", "none"); })
.on("mousemove", this.mousemove);
mousemove() {
var bisectDate = d3.bisector(function(d) { return d.date; }).left;
let mouse = d3.mouse(d3.event.currentTarget);
let svg = d3.select(this.container);
var x0 = this.x.invert(mouse[0]);
var i = bisectDate(this.dataObj, x0);
var d0 = this.dataObj[i - 1];
var d1 = this.dataObj[i];
var d = x0 - d0.date > d1.date - x0 ? d1 : d0;
var focus = svg.select(".focus");
focus.attr("transform", "translate(" + this.x(d[this.xAxisData]) + "," + this.y(d[this.yAxisData]) + ")");
focus.select("text").text(`[${d[this.yAxisData]}]`);
focus.select(".x-hover-line").attr("y2", this.height - this.y(d[this.yAxisData]));
focus.select(".y-hover-line").attr("x2", this.width + this.width);
}
Multi Line Chart:
//append paths
let g = svg.append('g');
let chartLines = g.selectAll('.lines')
.data(this.dataObj)
.enter()
.append('g')
.attr('class', 'lines');
chartLines.append('path')
.attr('class','line')
.attr('d', d => {
return line(d);
})
.attr('stroke', (d) => color(d[0].label))
.attr('fill','none')
.attr("transform", `translate(${this.margin.left},0)`);
var mouseG = svg.append("g")
.attr("class", "mouse-over-effects")
mouseG.append("path") // this is the black vertical line to follow mouse
.attr("class", "mouse-line")
.style("stroke", "black")
.style("stroke-width", "2px")
.style("stroke-dasharray", "3,3")
.style("opacity", "0");
var mousePerLine = mouseG.selectAll('.mouse-per-line')
.data(this.dataObj)
.enter()
.append("g")
.attr("class", "mouse-per-line");
mousePerLine.append("circle")
.datum(d=>{return d})
.attr("r", 7)
.attr("stroke", (d,i) => {
console.log(d)
return `${this.color(d[i].label)}`
})
.style("fill", "none")
.style("opacity", "0");
mousePerLine.append("text")
.datum(d=>{return d})
.attr("transform", "translate(10,3)");
mouseG.append('svg:rect') // append a rect to catch mouse movements on canvas
.attr("transform", `translate(${this.margin.left},0)`)
.attr('width', width) // can't catch mouse events on a g element
.attr('height', height)
.attr('fill', 'none')
.attr('pointer-events', 'all')
.on('mouseout', () => { // on mouse out hide line, circles and text
d3.select(".mouse-line")
.style("opacity", "0");
d3.selectAll(".mouse-per-line circle")
.style("opacity", "0");
d3.selectAll(".mouse-per-line text")
.style("opacity", "0");
})
.on('mouseover', () => { // on mouse in show line, circles and text
d3.select(".mouse-line")
.style("opacity", "1");
d3.selectAll(".mouse-per-line circle")
.style("opacity", "1");
d3.selectAll(".mouse-per-line text")
.style("opacity", "1");
})
.on('mousemove', () => {
let mouse = d3.mouse(d3.event.currentTarget);
d3.select(".mouse-line")
.attr("d", () => {
var d = "M" + mouse[0] + "," + height;
d += " " + mouse[0] + "," + 0;
return d;
});
d3.selectAll(".mouse-per-line")
.attr("transform", (d, i) => {
var lines = document.getElementsByClassName('line')
var xDate = this.x.invert(mouse[0])
var bisect = d3.bisector(function(d) { return d.date; }).right;
var idx = bisect(this.dataObj, xDate);
var beginning = 0,
end = lines[i].getTotalLength()
var target = null;
while (true){
var target = Math.floor((beginning + end) / 2);
var pos = lines[i].getPointAtLength(target);
if ((target === end || target === beginning) && pos.x !== mouse[0]) {
break;
}
if (pos.x > mouse[0]) end = target;
else if (pos.x < mouse[0]) beginning = target;
else break; //position found
}
d3.select('text')
.text(this.y.invert(pos.y));
return "translate(" + mouse[0] + "," + pos.y +")";
});
});
I took Mark's answer as a reference from the Multiseries line chart with mouseover tooltip you provided.
Basically, what you need to do is set the tooltips to show on each tick of the x-axis data, so instead of grabbing the position of the mouse with mouse[0] and moving the tooltips, you should move it to the position where the x-axis data is.
Here's the detail of the changes I made:
mouseG.append('svg:rect')
.attr('width', width)
.attr('height', height)
.attr('fill', 'none')
.attr('pointer-events', 'all')
.on('mouseout', () => {
d3.select(".mouse-line")
.style("opacity", "0");
d3.selectAll(".mouse-per-line circle")
.style("opacity", "0");
d3.selectAll(".mouse-per-line text")
.style("opacity", "0");
})
.on('mouseover', () => {
d3.select(".mouse-line")
.style("opacity", "1");
d3.selectAll(".mouse-per-line circle")
.style("opacity", "1");
d3.selectAll(".mouse-per-line text")
.style("opacity", "1");
})
.on('mousemove', () => {
let mouse = d3.mouse(d3.event.currentTarget);
// MOVE THIS BEFORE THE RETURN
// d3.select(".mouse-line")
// .attr("d", () => {
// var d = "M" + mouse[0] + "," + height;
// d += " " + mouse[0] + "," + 0;
// return d;
// });
d3.selectAll(".mouse-per-line")
.attr("transform", (d, i) => {
var lines = document.getElementsByClassName('line')
var xDate = this.x.invert(mouse[0])
var bisect = d3.bisector(function(d) { return d.date; }).right;
var idx = bisect(this.dataObj, xDate);
// GET RID OF THIS
// var beginning = 0,
// end = lines[i].getTotalLength()
// var target = null;
// while (true){
// var target = Math.floor((beginning + end) / 2);
// var pos = lines[i].getPointAtLength(target);
// if ((target === end || target === beginning) && pos.x !== mouse[0]) {
// break;
// }
// if (pos.x > mouse[0]) end = target;
// else if (pos.x < mouse[0]) beginning = target;
// else break; //position found
// }
// REPLACE pos.y WITH y(d.values[idx].temperature)
// AND mouse[0] WITH x(d.values[idx].date)
d3.select('text')
.text(this.y.invert(pos.y));
return "translate(" + mouse[0] + "," + pos.y +")";
});
});
Below is the fully working code with the changes applied. For this snippet I used interpolate('linear') to show the values correctly; if you use interpolate('basis'), the tooltips and lines will not match correctly:
<!DOCTYPE html>
<html>
<head>
<script data-require="d3#3.5.3" data-semver="3.5.3" src="http://cdnjs.cloudflare.com/ajax/libs/d3/3.5.3/d3.js"></script>
<style>
body {
font: 10px sans-serif;
}
.axis path,
.axis line {
fill: none;
stroke: #000;
shape-rendering: crispEdges;
}
.x.axis path {
display: none;
}
.line {
fill: none;
stroke: steelblue;
stroke-width: 1.5px;
}
</style>
</head>
<body>
<script>
var myData = "date New York San Francisco Austin\n\
20111001 63.4 62.7 72.2\n\
20111002 58.0 59.9 67.7\n\
20111003 53.3 59.1 69.4\n\
20111004 55.7 58.8 68.0\n\
20111005 64.2 58.7 72.4\n\
20111006 58.8 57.0 77.0\n\
20111007 57.9 56.7 82.3\n\
20111008 61.8 56.8 78.9\n\
20111009 69.3 56.7 68.8\n\
20111010 71.2 60.1 68.7\n\
20111011 68.7 61.1 70.3\n\
20111012 61.8 61.5 75.3\n\
20111013 63.0 64.3 76.6\n\
20111014 66.9 67.1 66.6\n\
20111015 61.7 64.6 68.0\n\
20111016 61.8 61.6 70.6\n\
20111017 62.8 61.1 71.1\n\
20111018 60.8 59.2 70.0\n\
20111019 62.1 58.9 61.6\n\
20111020 65.1 57.2 57.4\n\
20111021 55.6 56.4 64.3\n\
20111022 54.4 60.7 72.4\n";
var margin = {
top: 20,
right: 80,
bottom: 30,
left: 50
},
width = 400 - margin.left - margin.right,
height = 250 - margin.top - margin.bottom;
var parseDate = d3.time.format("%Y%m%d").parse;
var x = d3.time.scale()
.range([0, width]);
var y = d3.scale.linear()
.range([height, 0]);
var color = d3.scale.category10();
var xAxis = d3.svg.axis()
.scale(x)
.orient("bottom");
var yAxis = d3.svg.axis()
.scale(y)
.orient("left");
var line = d3.svg.line()
.interpolate("linear")
.x(function (d) {
return x(d.date);
})
.y(function (d) {
return y(d.temperature);
});
var 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 + ")");
var data = d3.tsv.parse(myData);
color.domain(d3.keys(data[0]).filter(function (key) {
return key !== "date";
}));
data.forEach(function (d) {
d.date = parseDate(d.date);
});
var cities = color.domain().map(function (name) {
return {
name: name,
values: data.map(function (d) {
return {
date: d.date,
temperature: +d[name]
};
})
};
});
x.domain(d3.extent(data, function (d) {
return d.date;
}));
y.domain([
d3.min(cities, function (c) {
return d3.min(c.values, function (v) {
return v.temperature;
});
}),
d3.max(cities, function (c) {
return d3.max(c.values, function (v) {
return v.temperature;
});
})
]);
var legend = svg.selectAll('g')
.data(cities)
.enter()
.append('g')
.attr('class', 'legend');
legend.append('rect')
.attr('x', width - 20)
.attr('y', function (d, i) {
return i * 20;
})
.attr('width', 10)
.attr('height', 10)
.style('fill', function (d) {
return color(d.name);
});
legend.append('text')
.attr('x', width - 8)
.attr('y', function (d, i) {
return (i * 20) + 9;
})
.text(function (d) {
return d.name;
});
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(xAxis);
svg.append("g")
.attr("class", "y axis")
.call(yAxis)
.append("text")
.attr("transform", "rotate(-90)")
.attr("y", 6)
.attr("dy", ".71em")
.style("text-anchor", "end")
.text("Temperature (ºF)");
var city = svg.selectAll(".city")
.data(cities)
.enter().append("g")
.attr("class", "city");
city.append("path")
.attr("class", "line")
.attr("d", function (d) {
return line(d.values);
})
.style("stroke", function (d) {
return color(d.name);
});
city.append("text")
.datum(function (d) {
return {
name: d.name,
value: d.values[d.values.length - 1]
};
})
.attr("transform", function (d) {
return "translate(" + x(d.value.date) + "," + y(d.value.temperature) + ")";
})
.attr("x", 3)
.attr("dy", ".35em")
.text(function (d) {
return d.name;
});
var mouseG = svg.append("g")
.attr("class", "mouse-over-effects");
mouseG.append("path") // this is the black vertical line to follow mouse
.attr("class", "mouse-line")
.style("stroke", "black")
.style("stroke-width", "1px")
.style("opacity", "0");
var lines = document.getElementsByClassName('line');
var mousePerLine = mouseG.selectAll('.mouse-per-line')
.data(cities)
.enter()
.append("g")
.attr("class", "mouse-per-line");
mousePerLine.append("circle")
.attr("r", 7)
.style("stroke", function (d) {
return color(d.name);
})
.style("fill", "none")
.style("stroke-width", "1px")
.style("opacity", "0");
mousePerLine.append("text")
.attr("transform", "translate(10,3)");
mouseG.append('svg:rect') // append a rect to catch mouse movements on canvas
.attr('width', width) // can't catch mouse events on a g element
.attr('height', height)
.attr('fill', 'none')
.attr('pointer-events', 'all')
.on('mouseout', function () { // on mouse out hide line, circles and text
d3.select(".mouse-line")
.style("opacity", "0");
d3.selectAll(".mouse-per-line circle")
.style("opacity", "0");
d3.selectAll(".mouse-per-line text")
.style("opacity", "0");
})
.on('mouseover', function () { // on mouse in show line, circles and text
d3.select(".mouse-line")
.style("opacity", "1");
d3.selectAll(".mouse-per-line circle")
.style("opacity", "1");
d3.selectAll(".mouse-per-line text")
.style("opacity", "1");
})
.on('mousemove', function () { // mouse moving over canvas
var mouse = d3.mouse(this);
d3.selectAll(".mouse-per-line")
.attr("transform", function (d, i) {
var xDate = x.invert(mouse[0]),
bisect = d3.bisector(function (d) { return d.date; }).left;
idx = bisect(d.values, xDate);
d3.select(this).select('text')
.text(y.invert(y(d.values[idx].temperature)).toFixed(2));
d3.select(".mouse-line")
.attr("d", function () {
var data = "M" + x(d.values[idx].date) + "," + height;
data += " " + x(d.values[idx].date) + "," + 0;
return data;
});
return "translate(" + x(d.values[idx].date) + "," + y(d.values[idx].temperature) + ")";
});
});
</script>
</body>
</html>