Reputation: 227
I am trying to add data points to my line chart with multiple y axes. Click here for my fiddle.
//after restructuring dataset array
var data = [{
data: [{
x: 0,
y: 0
}, {
x: 10,
y: 10
}, {
x: 20,
y: 20
}, {
x: 30,
y: 30
}, {
x: 40,
y: 40
}],
yAxis: 0,
}, {
data: [{
x: 0,
y: 0
}, {
x: 10,
y: 200
}, {
x: 20,
y: 300
}, {
x: 30,
y: 400
}, {
x: 40,
y: 500
}],
yAxis: 1,
}];
const margin = {
left: 20,
right: 20,
top: 20,
bottom: 80
};
const svg = d3.select('svg');
svg.selectAll("*").remove();
const width = 200 - margin.left - margin.right;
const height = 200 - margin.top - margin.bottom;
const g = svg.append('g').attr('transform', `translate(${80},${margin.top})`);
//************* Axes and Gridlines ***************
const xAxisG = g.append('g');
const yAxisG = g.append('g');
xAxisG.append('text')
.attr('class', 'axis-label')
.attr('x', width / 3)
.attr('y', -10)
.style('fill', 'black')
.text(function(d) {
return "X Axis";
});
yAxisG.append('text')
.attr('class', 'axis-label')
.attr('id', 'yAxisLabel0')
.attr('x', -height / 2)
.attr('y', -15)
.attr('transform', `rotate(-90)`)
.style('text-anchor', 'middle')
.style('fill', 'black')
.text(function(d) {
return "Y Axis 1";
});
// interpolator for X axis -- inner plot region
var x = d3.scaleLinear()
.domain([0, d3.max(xValueArray)])
.range([0, width])
.nice();
var yScale = new Array();
for (var i = 0; i < 2; i++) {
// interpolator for Y axis -- inner plot region
var y = d3.scaleLinear()
.domain([0, d3.max(arr[i])])
.range([0, height])
.nice();
yScale.push(y);
}
const xAxis = d3.axisTop()
.scale(x)
.ticks(5)
.tickPadding(2)
.tickSize(-height)
const yAxis = d3.axisLeft()
.scale(yScale[0])
.ticks(5)
.tickPadding(2)
.tickSize(-width);
yAxisArray = new Array();
yAxisArray.push(yAxis);
for (var i = 1; i < 2; i++) {
var yAxisSecondary = d3.axisLeft()
.scale(yScale[i])
.ticks(5)
yAxisArray.push(yAxisSecondary);
}
svg.append("g")
.attr("class", "x axis")
.attr("transform", `translate(80,${height-80})`)
.call(xAxis);
svg.append("g")
.attr("class", "y axis")
.attr("id", "ySecAxis0")
.attr("transform", "translate(80,20)")
.call(yAxis);
var translation = 50;
var textTranslation = 0;
var yLabelArray = ["Y Axis 1", "Y Axis 2"];
//loop starts from 1 as primary y axis is already plotted
for (var i = 1; i < 2; i++) {
svg.append("g")
.attr("transform", "translate(" + translation + "," + 20 + ")")
.attr("id", "ySecAxis" + i)
.call(yAxisArray[i]);
yAxisG.append('text')
.attr('x', -height / 2)
.attr('y', -60)
.attr('transform', `rotate(-90)`)
.attr("id", "yAxisLabel" + i)
.style('text-anchor', 'middle')
.style('fill', 'black')
.text(yLabelArray[i]);
translation -= 40;
textTranslation += 40;
}
//************* Lines and Data Points ***************
var colors = ["blue", "red"];
var thisScale;
var line = d3.line()
.x(d => x(d.x))
.y(d => thisScale(d.y))
.curve(d3.curveLinear);
var paths = g.selectAll("foo")
.data(data)
.enter()
.append("path");
paths.attr("stroke", function (d,i){return colors[i]})
.attr("d", d => {
thisScale = yScale[d.yAxis]
return line(d.data);
})
.attr("stroke-width", 2)
.attr("id", function (d,i){return "line" + i})
.attr("fill", "none");
var points = g.selectAll("dot")
.data(data)
.enter()
.append("circle");
points.attr("cx", function(d) { return x(d.x)} )
.attr("cy", function(d,i) { return yScale[i](d.y); } )
.attr("r", 3)
.attr("class", function (d,i){return "blackDot" + i})
.attr("clip-path", "url(#clip)")
Right now the console log is showing these errors: Error: attribute cx: Expected length, "NaN". Error: attribute cy: Expected length, "NaN". It seems like I am not attributing the correct cx and cy to points, but I can't figure out what I am doing wrongly. Any help is greatly appreciated!
Upvotes: 2
Views: 173
Reputation: 102174
Your data structure is an array of objects, each one containing an inner array with the real coordinates for the circles. Therefore, that single enter selection will not work.
With minimal refactoring, my solution here is appending groups according to the objects, and then, for each one, appending circles according to the inner arrays. For that cumbersome yScale
to work you cannot rely on the circle's indices anymore, so I'm using a local variable here:
var pointsGroup = g.selectAll(null)
.data(data)
.enter()
.append("g")
.attr("fill", function(d, i) {
local.set(this, yScale[i])
return colors[i];
});
var points = pointsGroup.selectAll(null)
.data(function(d) {
return d.data
})
.enter()
.append("circle")
.attr("cx", function(d) {
return x(d.x)
})
.attr("cy", function(d, i) {
return local.get(this)(d.y);
})
//etc...
Here is the code with those changes:
var local = d3.local();
var xValueArray = [0, 10, 20, 30, 40];
var arr = [
[0, 10, 20, 30, 40],
[0, 200, 300, 400, 500]
];
var dataset = [
[{
x: 0,
y: 0
}, {
x: 10,
y: 10
}, {
x: 20,
y: 20
}, {
x: 30,
y: 30
}, {
x: 40,
y: 40
}],
[{
x: 0,
y: 0
}, {
x: 10,
y: 200
}, {
x: 20,
y: 300
}, {
x: 30,
y: 400
}, {
x: 40,
y: 500
}]
];
var data = [];
for (var i = 0; i < 2; i++) {
data.push({
"data": dataset[i],
"yAxis": i
})
}
console.log(data);
//after restructuring dataset array
var data = [{
data: [{
x: 0,
y: 0
}, {
x: 10,
y: 10
}, {
x: 20,
y: 20
}, {
x: 30,
y: 30
}, {
x: 40,
y: 40
}],
yAxis: 0,
}, {
data: [{
x: 0,
y: 0
}, {
x: 10,
y: 200
}, {
x: 20,
y: 300
}, {
x: 30,
y: 400
}, {
x: 40,
y: 500
}],
yAxis: 1,
}];
const margin = {
left: 20,
right: 20,
top: 20,
bottom: 80
};
const svg = d3.select('svg');
svg.selectAll("*").remove();
const width = 200 - margin.left - margin.right;
const height = 200 - margin.top - margin.bottom;
const g = svg.append('g').attr('transform', `translate(${80},${margin.top})`);
//************* Axes and Gridlines ***************
const xAxisG = g.append('g');
const yAxisG = g.append('g');
xAxisG.append('text')
.attr('class', 'axis-label')
.attr('x', width / 3)
.attr('y', -10)
.style('fill', 'black')
.text(function(d) {
return "X Axis";
});
yAxisG.append('text')
.attr('class', 'axis-label')
.attr('id', 'yAxisLabel0')
.attr('x', -height / 2)
.attr('y', -15)
.attr('transform', `rotate(-90)`)
.style('text-anchor', 'middle')
.style('fill', 'black')
.text(function(d) {
return "Y Axis 1";
});
// interpolator for X axis -- inner plot region
var x = d3.scaleLinear()
.domain([0, d3.max(xValueArray)])
.range([0, width])
.nice();
var yScale = new Array();
for (var i = 0; i < 2; i++) {
// interpolator for Y axis -- inner plot region
var y = d3.scaleLinear()
.domain([0, d3.max(arr[i])])
.range([0, height])
.nice();
yScale.push(y);
}
const xAxis = d3.axisTop()
.scale(x)
.ticks(5)
.tickPadding(2)
.tickSize(-height)
const yAxis = d3.axisLeft()
.scale(yScale[0])
.ticks(5)
.tickPadding(2)
.tickSize(-width);
yAxisArray = new Array();
yAxisArray.push(yAxis);
for (var i = 1; i < 2; i++) {
var yAxisSecondary = d3.axisLeft()
.scale(yScale[i])
.ticks(5)
yAxisArray.push(yAxisSecondary);
}
svg.append("g")
.attr("class", "x axis")
.attr("transform", `translate(80,${height-80})`)
.call(xAxis);
svg.append("g")
.attr("class", "y axis")
.attr("id", "ySecAxis0")
.attr("transform", "translate(80,20)")
.call(yAxis);
var translation = 50;
var textTranslation = 0;
var yLabelArray = ["Y Axis 1", "Y Axis 2"];
//loop starts from 1 as primary y axis is already plotted
for (var i = 1; i < 2; i++) {
svg.append("g")
.attr("transform", "translate(" + translation + "," + 20 + ")")
.attr("id", "ySecAxis" + i)
.call(yAxisArray[i]);
yAxisG.append('text')
.attr('x', -height / 2)
.attr('y', -60)
.attr('transform', `rotate(-90)`)
.attr("id", "yAxisLabel" + i)
.style('text-anchor', 'middle')
.style('fill', 'black')
.text(yLabelArray[i]);
translation -= 40;
textTranslation += 40;
}
//************* Mouseover ***************
var tooltip = d3.select("body")
.append("div")
.style("opacity", 0)
.attr("class", "tooltip")
.style("background-color", "white")
.style("border", "solid")
.style("border-width", "1px")
.style("border-radius", "5px")
.style("padding", "10px")
.style("position", "absolute")
var mouseover = function(d) {
tooltip
.html("x: " + d.x + "<br/>" + "y: " + d.y)
.style("opacity", 1)
.style("left", (d3.mouse(this)[0] + 90) + "px")
.style("top", (d3.mouse(this)[1]) + "px")
}
// A function that change this tooltip when the leaves a point: just need to set opacity to 0 again
var mouseleave = function(d) {
tooltip
.transition()
.duration(200)
.style("opacity", 0)
}
//************* Lines and Data Points ***************
var colors = ["blue", "red"];
var thisScale;
var line = d3.line()
.x(d => x(d.x))
.y(d => thisScale(d.y))
.curve(d3.curveLinear);
var paths = g.selectAll("foo")
.data(data)
.enter()
.append("path");
paths.attr("stroke", function(d, i) {
return colors[i]
})
.attr("d", d => {
thisScale = yScale[d.yAxis]
return line(d.data);
})
.attr("stroke-width", 2)
.attr("id", function(d, i) {
return "line" + i
})
.attr("fill", "none");
var pointsGroup = g.selectAll(null)
.data(data)
.enter()
.append("g")
.attr("fill", function(d, i) {
local.set(this, yScale[i])
return colors[i];
});
var points = pointsGroup.selectAll(null)
.data(function(d) {
return d.data
})
.enter()
.append("circle")
.attr("cx", function(d) {
return x(d.x)
})
.attr("cy", function(d, i) {
return local.get(this)(d.y);
})
.attr("r", 3)
.attr("class", function(d, i) {
return "blackDot" + i
})
.attr("clip-path", "url(#clip)")
.on("mouseover", mouseover)
.on("mouseleave", mouseleave)
//plot lines (hard-coding)
/*var lineFunction1 = d3.line()
.x(function(d) {
return x(d.x);
})
.y(function(d) {
return yScale[0](d.y);
})
.curve(d3.curveLinear);
var lineFunction2 = d3.line()
.x(function(d) {
return x(d.x);
})
.y(function(d) {
return yScale[1](d.y);
})
.curve(d3.curveLinear);
var path1 = g.append("path")
.attr("class", "path" + 0)
.attr("id", "line" + 0)
.attr("d", lineFunction1(data[0]))
.attr("stroke", "blue")
.attr("stroke-width", 2)
.attr("fill", "none")
.attr("clip-path", "url(#clip)");
var path2 = g.append("path")
.attr("class", "path" + 1)
.attr("id", "line" + 1)
.attr("d", lineFunction2(data[1]))
.attr("stroke", "red")
.attr("stroke-width", 2)
.attr("fill", "none")
.attr("clip-path", "url(#clip)");*/
//plot lines and points using for loop
/*for (var i = 0; i < 2; i++) {
var lineFunction = d3.line()
.x(function(d) {
return x(d.x);
})
.y(function(d) {
return yScale[i](d.y);
})
.curve(d3.curveLinear);
var paths = g.append("path")
.attr("class", "path" + i)
.attr("id", "line" + i)
.attr("d", lineFunction(data[i]))
.attr("stroke", colors[i])
.attr("stroke-width", 2)
.attr("fill", "none")
.attr("clip-path", "url(#clip)")
//plot a circle at each data point
g.selectAll(".dot")
.data(data[i])
.enter().append("circle")
.attr("cx", function(d) { return x(d.x)} )
.attr("cy", function(d) { return yScale[i](d.y); } )
.attr("r", 3)
.attr("class", "blackDot" + i)
.attr("clip-path", "url(#clip)")
.on("mouseover", mouseover)
.on("mouseleave", mouseleave)
}*/
//************* Legend ***************
var legend = svg.selectAll(".legend")
.data(data)
.enter().append("g")
legend.append("rect")
.attr("x", width + 65)
.attr("y", function(d, i) {
return 30 + i * 20;
})
.attr("width", 18)
.attr("height", 4)
.style("fill", function(d, i) {
return colors[i];
})
legend.append("text")
.attr("x", width + 60)
.attr("y", function(d, i) {
return 30 + i * 20;
})
.attr("dy", ".35em")
.style("text-anchor", "end")
.text(function(d, i) {
return "Value" + (i + 1);
})
.on("click", function(d, i) {
// Determine if current line is visible
let opacity = d3.select("#line" + i).style("opacity");
let newOpacity;
if (opacity == 0) {
newOpacity = 1;
} else {
newOpacity = 0
}
d3.select("#line" + i).style("opacity", newOpacity);
d3.selectAll(".blackDot" + i).style("opacity", newOpacity);
d3.select("#ySecAxis" + i).style("opacity", newOpacity);
d3.select("#yAxisLabel" + i).style("opacity", newOpacity);
});
//************* Zoom & Brush***************
const margin2 = {
left: 80,
right: 0,
top: 80,
bottom: 0
};
const height2 = height - margin2.top - margin2.bottom;
var xZoom = d3.scaleLinear().range([0, width]);
var yZoom = d3.scaleLinear().range([height2, 0]);
var xAxis2 = d3.axisTop(xZoom);
var brush = d3.brushX()
.extent([
[0, 0],
[width, height2]
])
.on("brush end", brushed);
var zoom = d3.zoom()
.scaleExtent([1, Infinity])
.translateExtent([
[0, 0],
[width, height]
])
.extent([
[0, 0],
[width, height]
])
.on("zoom", zoomed);
var clip = svg.append("defs").append("svg:clipPath")
.attr("id", "clip")
.append("svg:rect")
.attr("width", width)
.attr("height", height)
.attr("x", 0)
.attr("y", 0);
xZoom.domain(x.domain());
yZoom.domain(y.domain());
var context = svg.append("g")
.attr("class", "context")
.attr("transform", "translate(" + margin2.left + "," + 125 + ")");
context.append("g")
.attr("class", "axis axis--x")
.attr("transform", "translate(0," + height2 + ")")
.call(xAxis2);
context.append("g")
.attr("class", "brush")
.call(brush)
.call(brush.move, x.range());
function brushed() {
if (d3.event.sourceEvent && d3.event.sourceEvent.type === "zoom") return;
var s = d3.event.selection || xZoom.range();
x.domain(s.map(xZoom.invert, xZoom));
svg.select(".x.axis").call(xAxis);
//svg.select(".path0").attr("d", lineFunction1(data[0]));
//svg.select(".path1").attr("d", lineFunction2(data[1]));
for (var i = 0; i < 2; i++) {
//svg.select(".path" + i).attr("d", lineFunction(data[i]));
g.selectAll(".blackDot" + i)
.attr("cx", function(d) {
return x(d.x);
})
.attr("cy", function(d) {
return yScale[i](d.y);
})
.attr("r", 3)
}
}
function zoomed() {
if (d3.event.sourceEvent && d3.event.sourceEvent.type === "brush") return;
var t = d3.event.transform;
x.domain(t.rescaleX(xZoom).domain());
svg.select(".x.axis").transiton(t).call(xAxis);
//svg.select(".path0").transiton(t).attr("d", lineFunction1(data[0]));
//svg.select(".path1").transiton(t).attr("d", lineFunction2(data[1]));
for (var i = 0; i < 2; i++) {
//svg.select(".path" + i).attr("d", lineFunction(data[i]));
g.selectAll(".blackDot" + i)
.attr("cx", function(d) {
return x(d.x);
})
.attr("cy", function(d) {
return yScale[i](d.y);
})
.attr("r", 3)
}
}
.xy_chart {
position: relative;
left: 70px;
top: 100px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.13.0/d3.min.js"></script>
<svg class="xy_chart"></svg>
Pay attention to the fact that one of the circles has an incorrect cy
value. So, I'd suggest you to change your y scale approach.
Upvotes: 1