Reputation: 227
I am trying to assign a specific id to each line in a multi-line chart with multiple y-axes so that I can create an interactive legend which toggles the lines on and off when the legend is clicked. Here is the link to my fiddle.
var xValueArray = [0, 10, 20, 30, 40];
var arr = [[0, 10, 20, 30, 40], [0, 200, 300, 400, 500]];
//data array is obtained after structuring arr array
var data = [[{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}]];
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(${margin.left},${margin.top})`);
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', 'primaryYLabel')
.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("transform", "translate(80,20)")
.call(yAxis);
var colors = ["blue", "red"];
//plot lines
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);
//plot lines
var paths = g.append("path")
.attr("class", "path1")
.attr("id", "blueLine")
.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")
.attr("clip-path", "url(#clip)")
}
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 + ")")
.call(yAxisArray[i]);
yAxisG.append('text')
.attr('x', -height / 2 )
.attr('y', -60)
.attr('transform', `rotate(-90)`)
.style('text-anchor', 'middle')
.style('fill', 'black')
.text(yLabelArray[i]);
translation -= 40;
textTranslation += 40;
}
//************* Legend ***************
var legend = svg.selectAll(".legend")
.data(data)
.enter().append("g")
legend.append("rect")
.attr("x", width + 65)
.attr("y", 30)
.attr("width", 18)
.attr("height", 4)
.style("fill", "blue")
legend.append("text")
.attr("x", width + 60)
.attr("y", 30)
.attr("dy", ".35em")
.style("text-anchor", "end")
.on("click", function(){
// Determine if current line is visible
var active = blueLine.active ? false : true,
newOpacity = active ? 0 : 1;
// Hide or show the elements
d3.select("#blueLine").style("opacity", newOpacity);
// Update whether or not the elements are active
blueLine.active = active;
})
.text(function(d) {
return "Value1";
});
var legend1 = svg.selectAll(".legend")
.data(data)
.enter().append("g")
legend1.append("rect")
.attr("x", width + 65)
.attr("y", 50)
.attr("width", 18)
.attr("height", 4)
.style("fill", "red")
legend1.append("text")
.attr("x", width + 60)
.attr("y", 50)
.attr("dy", ".35em")
.style("text-anchor", "end")
.on("click", function(){
// Determine if current line is visible
var active = blueLine.active ? false : true,
newOpacity = active ? 0 : 1;
// Hide or show the elements
d3.select("#blueLine").style("opacity", newOpacity);
// Update whether or not the elements are active
blueLine.active = active;
})
.text(function(d) {
return "Value2";
});
var pointLegend = svg.selectAll(".pointLegend")
.data(data)
.enter().append("g")
pointLegend.append("circle")
.attr("r", 3)
.attr("cx", width + 70)
.attr("cy", 70)
pointLegend.append("text")
.attr("x", width + 60)
.attr("y", 70)
.attr("dy", ".35em")
.style("text-anchor", "end")
.on("click", function(d){
// Determine if dots are visible
var active = d.active ? false : true,
newOpacity = active ? 0 : 1;
// Hide or show the elements
d3.selectAll(".blackDot").style("opacity", newOpacity);
// Update whether or not the elements are active
d.active = active;
})
.text(function(d) {
return "Data";
});
The fact that the chart can be plotted means the for loops are viable to draw the chart. I am using the for loops as I want my chart to be plotted given the user's input where I have a parameter named fieldCount in my actual code to track the number of series in my user's input thus I am using "2" in the for loop as I only have 2 arrays in my data array for simplification.
From the fiddle, I am only able to toggle the blue line using my clickable legend and not the red line as I am assigning same "blueLine" id to all my lines. How can I assign specific ids to specific lines so that I can toggle the lines using my legend and is there a way to code the legend so that I don't have to declare so many legend variables? Any help is greatly appreciated!
Upvotes: 0
Views: 410
Reputation: 3142
When you plot the lines you can pass the index i
of the for loop into the id
for the path
as
//plot lines
var paths = g.append("path")
.attr("class", "path1")
.attr("id", "line" + i)
Then when you perform the on click function you can check the opacity as:
.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);
});
Also in your code you are drawing two legends unnecessarily. I have fixed that issue as well.
Here is a working fiddle with the solution: https://jsfiddle.net/7dgek9wq/1/
Full working example below:
var xValueArray = [0, 10, 20, 30, 40];
var arr = [
[0, 10, 20, 30, 40],
[0, 200, 300, 400, 500]
];
//data array is obtained after structuring arr array
var data = [
[{
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
}]
];
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(${margin.left},${margin.top})`);
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', 'primaryYLabel')
.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("transform", "translate(80,20)")
.call(yAxis);
var colors = ["blue", "red"];
//plot lines
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);
//plot lines
var paths = g.append("path")
.attr("class", "path1")
.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")
.attr("clip-path", "url(#clip)")
}
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 + ")")
.call(yAxisArray[i]);
yAxisG.append('text')
.attr('x', -height / 2)
.attr('y', -60)
.attr('transform', `rotate(-90)`)
.style('text-anchor', 'middle')
.style('fill', 'black')
.text(yLabelArray[i]);
translation -= 40;
textTranslation += 40;
}
//************* 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);
});
var pointLegend = svg.selectAll(".pointLegend")
.data(data)
.enter().append("g")
pointLegend.append("circle")
.attr("r", 3)
.attr("cx", width + 70)
.attr("cy", 70)
pointLegend.append("text")
.attr("x", width + 60)
.attr("y", 70)
.attr("dy", ".35em")
.style("text-anchor", "end")
.on("click", function(d) {
// Determine if dots are visible
var active = d.active ? false : true,
newOpacity = active ? 0 : 1;
// Hide or show the elements
d3.selectAll(".blackDot").style("opacity", newOpacity);
// Update whether or not the elements are active
d.active = active;
})
.text(function(d) {
return "Data";
});
.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>
Upvotes: 1