Related
I changed the domain of the scale when zooming and repositioned the circles displayed on the graph, but they are not properly reflected.
I used the following document reference for d3js zoom.
https://observablehq.com/#d3/pan-zoom-axes
I wrote the following code according to this document.
$(document).ready(function(){
var dataset = [
{x: 5, y: 20, name: "one", color: "blue"},
{x: 100, y: 50, name: "two", color: "red"},
{x: 250, y: 80, name: "three", color: "green"},
{x: 480, y: 90, name: "four", color: "black"},
];
var a = 1;
const width = 400;
const height = 300;
const margin = { "top": 30, "bottom": 60, "right": 30, "left": 60 };
const svgWidth = width + margin.left + margin.right;
const svgHeight = height + margin.top + margin.bottom;
var radius = 5;
var tooltipFlag = false;
var originalDomainX = [0, d3.max(dataset, function(d) { return d.x; }) + 10];
var originalDomainY = [0, d3.max(dataset, function(d) { return d.y; }) + 10];
const zoomAmount = 1.0
var svg = d3.select("#graph-area").append("svg")
.attr("width", svgWidth)
.attr("height", svgHeight)
.style("border-radius", "20px 20px 20px 20px")
.append("g")
.attr("transform", `translate(${margin.left}, ${margin.top})`);
var tooltip = d3.select("#graph-area")
.append("div")
.attr("id", "dc-tooltip")
.attr("class", "chart-tooltip");
var xScale = d3.scaleLinear()
.domain(originalDomainX)
.range([0, width]);
var yScale = d3.scaleLinear()
.domain(originalDomainY)
.range([height, 0]);
var axisx = d3.axisBottom().scale(xScale).ticks(5);
var axisy = d3.axisLeft().scale(yScale).ticks(5);
var gx = svg.append("g")
.attr("transform", "translate(" + 0 + "," + height + ")")
.call(axisx)
.attr("class", "axisx")
gx.append("text")
.attr("x", (width - margin.left - margin.right) / 2 + margin.left)
.attr("y", 35)
.attr("text-anchor", "middle")
.attr("font-size", "10pt")
.attr("font-weight", "bold")
.text("X Label")
.attr("class", "x_axis")
var gy = svg.append("g")
.call(axisy)
.attr("class", "axisy")
gy.append("text")
.attr("x", -(height - margin.top - margin.bottom) / 2 - margin.top)
.attr("y", -35)
.attr("transform", "rotate(-90)")
.attr("text-anchor", "middle")
.attr("font-weight", "bold")
.attr("font-size", "10pt")
.text("Y Label")
.attr("class", "y_axis")
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);
var zoom = d3.zoom().on("zoom", zoomed)
.scaleExtent([1, 10])
function zoomed({transform}){
gx.call(axisx.scale(transform.rescaleX(xScale)));
gy.call(axisy.scale(transform.rescaleY(yScale)));
circles.attr("cx", function(d){return d.x = xScale(d.rx)}).attr("cy", function(d){return d.y = yScale(d.ry)});
}
var zoomArea = svg.append("g")
.attr("class", "zoomArea")
.style("cursor","move")
.attr("clip-path", "url(#clip)");
var zoomRect = zoomArea.append("rect")
.attr("x", margin.x)
.attr("y", 0)
.attr("width", width)
.attr("height", height)
.attr("fill", "rgba(0, 0, 0, 0)")
.style("pointer-events", "all")
.style("cursor","move")
.call(zoom);
var circleg = svg.append("g")
.attr("id", "scatterplot")
.attr("clip-path", "url(#clip)");
var circles;
createCircles();
function createCircles(){
circles = circleg.selectAll("circle")
.data(dataset)
.enter()
.append("circle")
.attr("cx", function(d) {
d.ox = d.x;
d.rx = d.x;
return d.x = xScale(d.x);
})
.attr("cy", function(d) {
d.oy = d.y;
d.ry = d.y;
return d.y = yScale(d.y);
})
.attr("fill", function(d){return d.color})
.attr("r", radius)
.attr("opacity", 0.3)
}
})
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>D3 Scatter Plot</title>
<script src="https://code.jquery.com/jquery-3.6.0.slim.min.js" integrity="sha256-u7e5khyithlIdTpu22PHhENmPcRdFiHRjhAuHcs05RI=" crossorigin="anonymous"></script>
<script src="https://d3js.org/d3.v7.min.js"></script>
</head>
<body>
<div id="graph-area"></div>
</body>
</html>
However, the redrawn circle remains in the same place in the svg drawing area as before the change. I thought this was because the xScale used was not updated, so I fixed function zoomed as follows.
view.js
function zoomed({transform}){
xScale.domain(transform.rescaleX(xScale).domain());
yScale.domain(transform.rescaleY(yScale).domain());
axisx.scale(xScale);
axisy.scale(yScale);
gx.call(axisx);
gy.call(axisy);
circles.attr("cx", function(d){return d.x = xScale(d.rx)})
.attr("cy", function(d){return d.y = yScale(d.ry)});
}
However, when I run this code, the scaling by zooming does not work correctly. The following two appear to perform the same function, but what is the actual difference?
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!
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.
This question is actually a continuation of the question from here. I have made some changes to my fiddle and code since then but I am still facing the same problem. Link to my fiddle and code can be found here.
I am using a for loop to plot the lines as I want the chart to be dynamic which means the number of lines is drawn according to the number of arrays in the data array. In this case, there are 2 arrays in my data array as shown below.
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}]];
From my fiddle, the blue line will be toggled on and off when I click on both 'Y-Axis 1' and 'Y-Axis 2'. However, I want the red line to be toggled on and off when I click on Y-Axis 2. This is happening because I am assigning the same id to both lines in this piece of code.
//************* Plotting of graph ***************
var colors = ["blue", "red"];
//plot of chart
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)")
.on("mouseover", mouseover )
.on("mouseleave", mouseleave )
}
Is there a better way to plot the lines so that I can assign a specific id to each line being plotted and toggle the lines according to the legend? I have tried using forEach() but can't seem to get it to work. Any help is greatly appreciated!
First of all: you should not use a loop (for, while, forEach etc...) to append elements in a D3 code. That's not idiomatic, and you'll end up bending over backwards to fix things, like this very question will demonstrate.
The simplest fix without refactoring the code for a more idiomatic one, which will take a lot of work, is using the indices for setting the lines' IDs...
var paths = g.append("path")
.attr("class", "path1")
.attr("id", "blueLine" + i)
... and then, in the click listener, using this cumbersome and awkward window property, which is the elements' IDs:
.on("click", function(d, i) {
var active = window["blueLine" + i].active ? false : true,
newOpacity = active ? 0 : 1;
d3.select("#blueLine" + i).style("opacity", newOpacity);
window["blueLine" + i].active = active;
});
Here is your code with those changes:
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);
//************* 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")
// A function that change this tooltip when the user hover a point.
// Its opacity is set to 1: we can now see it. Plus it set the text and position of tooltip depending on the datapoint (d)
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)
}
//************* Plotting of graph ***************
var colors = ["blue", "red"];
//plot of chart
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" + 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)")
.on("mouseover", mouseover)
.on("mouseleave", mouseleave)
}
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 -= 20;
textTranslation += 20;
}
//************* Legend ***************
var legend = svg.selectAll('.legend')
.data(data)
.enter()
.append('g')
.attr('class', 'legend');
legend.append('rect')
.attr('x', width - 5)
.attr('y', function(d, i) {
return (i * 20) + 120;
})
.attr('width', 18)
.attr('height', 4)
.attr("fill", function(d, i) {
return colors[i]
});
legend.append('text')
.attr('x', width - 10)
.attr('y', function(d, i) {
return (i * 20) + 120;
})
.attr("dy", ".35em")
.style("text-anchor", "end")
.text(function(d, i) {
return yLabelArray[i]
})
.on("click", function(d, i) {
//Determine if current line is visible
var active = window["blueLine" + i].active ? false : true,
newOpacity = active ? 0 : 1;
//Hide or show the elements
d3.select("#blueLine" + i).style("opacity", newOpacity);
//Update whether or not the elements are active
window["blueLine" + i].active = active;
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.13.0/d3.min.js"></script>
<svg class="xy_chart"></svg>
I want to create a circle inside a g element. I can create the groups and translate the element just fine. However, I haven't figure out how to create the circle into the g elements.
With this code I'm getting the element g and circles but the circles are outside the g:
var data = [
{"x": 10, "y": 10},
{"x": 20, "y": 20},
{"x": 30, "y": 30},
];
var svg = d3.select('svg');
var t = d3.transition().duration(750);
var group = svg
.selectAll(".groups")
.data(data) // JOIN
group.exit() // EXIT
.remove();
var groups = group.enter() // ENTER
.append("g")
.attr('class', 'groups')
.merge(group)
.attr('transform', function(d) { return 'translate(' + d.x + ',' + d.y + ')'; });
var circles = svg.selectAll('.circles')
.data(data)
.style("fill", "blue");
circles.exit()
.style("fill", "brown")
.transition(t)
.style("fill-opacity", 1e-6)
.remove();
circles.enter()
.append('circle')
.attr('r', 2.5)
.attr('class', 'circles')
.style("fill", "green")
.merge(circles)
.attr("r", (d) => d.y/5)
.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; })
How can I fix it?
Here is the snippet: https://plnkr.co/edit/pnoSGSEv7pDo28HxRifd
You need to append to the groups by selecting the groups and not the svg:
var circles = groups.selectAll('.circles')
.data(data)
.style("fill", "blue");
Here is the updated code:https://plnkr.co/edit/4WtXqUrDAdCkOYSdqori
I'm trying to use d3js to create a tree graph with two nodes connected to each other. My JS is as follows:
var width = window.innerWidth;
var height = window.innerHeight;
var nodes = [{"id":"1","name":"a"},{"id":"2","name":"b"}];
var links = [{"source":0,"target":1}];
var svg = d3.select("body").append("svg");
svg.attr("width", width);
svg.attr("height", height);
svg.append("svg:g");
var tree = d3.layout.tree();
tree.size([width, height]);
tree.nodes(nodes);
tree.links(links);
var diagonal = d3.svg.diagonal.radial()
.projection(function(d) { return [d.y, d.x / 180 * Math.PI]; });
var link = svg.selectAll(".link")
.data(links)
.enter().append("path")
.attr("class", "link")
.attr("d", diagonal);
var node = svg.selectAll(".node")
.data(nodes)
.enter().append("g")
.attr("class", "node")
.attr("transform", function(d) { return "rotate(" + (d.x - 90) + ")translate(" + d.y + ")"; })
node.append("circle")
.attr("r", 4.5);
node.append("text")
.text(function(d) { return d.name; });
So I'm setting the width and height of the SVG equal to the window width / height. And yet everything is bunched up at the top right.
My JS Fiddle: https://jsfiddle.net/eeLfog4m/1/
Any ideas?
What'd be helpful is a d3js demo without all the bells and whistles. http://bl.ocks.org/mbostock/4063550 rotates everything around the center. http://bl.ocks.org/d3noob/8375092 seems to have a lot of extra code for handling redrawing / collapsing of nodes and https://github.com/mbostock/d3/wiki/Tree-Layout doesn't have any examples at all.
The reason you are seeing everything squished at the top-left corner is because all attributes are either NaN or undefined. This is because you are using
.projection(function(d) { return [d.y, d.x / 180 * Math.PI]; });
and
.attr("transform", function(d) {
return "rotate(" + (d.x - 90) + ")translate(" + d.y + ")";
});
which is accessing d.x and d.y, which are undefined.
Ideally, d.x and d.y are generated by the layout algorithms like d3.layout.tree(). However, in the code you provided, the data that you are passing into the tree-layout algorithm is incorrect.
d3.tree.layout() expects a hierarchical data structure, whereas you are providing it links and nodes, which will not work without some major workaround. If you want to use tree-layout, I suggest you convert your data into a hierarchical structure and then visualize it. Here is an example of doing that
var width = window.innerWidth;
var height = window.innerHeight;
var root = {
"name": "1",
"children": [
{"name": "2"}
]
};
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
var tree = d3.layout.tree()
.size([width, height-40]);
var nodes = tree.nodes(root);
var links = tree.links(nodes);
var path = d3.svg.line()
.x(function(d) { return d.x; })
.y(function(d) { return d.y; });
var link = svg.selectAll(".link")
.data(links)
.enter()
.append("path")
.attr("class", "link")
.attr("stroke", "black")
.attr("stroke-width", 2);
.attr("d", function(d){
return path([d.source, d.target])
});
var node = svg.selectAll(".node")
.data(nodes)
.enter().append("g")
.attr("class", "node")
.attr("transform", function(d) {
return "translate(" + d.x + "," + (d.y + 20) + ")"; })
node.append("circle")
.attr("r", 4.5);
node.append("text")
.text(function(d) { return d.name; });
If however, you want to stick with the current data format:
var nodes = [{"id":"1","name":"a"},{"id":"2","name":"b"}];
var links = [{"source":0,"target":1}];
you should use force-directed layout.
Here is an example of using force-directed layout with the data structure you have (http://jsfiddle.net/ankit89/3kL11j6j/)
var graph = {
"nodes": [
{"name": "Leo"},
{"name": "Mike"},
{"name": "Raph"},
{"name": "Don"},
{"name": "Splinter"}
],
"links": [
{"source": 0, "target": 4, "relation": "son"},
{"source": 1, "target": 4, "relation": "son"},
{"source": 2, "target": 4, "relation": "son"},
{"source": 3, "target": 4, "relation": "son"}
]}
var force = d3.layout.force()
.nodes(graph.nodes)
.links(graph.links)
.size([400, 400])
.linkDistance(120)
.charge(-30)
.start();
var svg = d3.select("svg");
var link = svg.selectAll("line")
.data(graph.links)
.enter().append("line")
.style("stroke", "black");
var node = svg.selectAll("circle")
.data(graph.nodes)
.enter().append("circle")
.attr("r", 20)
.style("fill", "grey")
.call(force.drag);
node.append("title")
.text(function(d) { return d.name; });
force.on("tick", function() {
link.attr("x1", function(d) { return d.source.x; })
.attr("y1", function(d) { return d.source.y; })
.attr("x2", function(d) { return d.target.x; })
.attr("y2", function(d) { return d.target.y; });
node.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; });
})
And finally, you do not necessarily need d3 layouts, you can even use your custom layout like this
http://jsfiddle.net/ankit89/uts5orrd/5/
var graph = {
"nodes": [
{"name": "Leo", "level": 1},
{"name": "Mike", "level": 1},
{"name": "Raph", "level": 1},
{"name": "Don", "level": 1},
{"name": "Splinter", "level": 2}
],
"links": [
{"source": 0, "target": 4, "relation": "son"},
{"source": 1, "target": 4, "relation": "son"},
{"source": 2, "target": 4, "relation": "son"},
{"source": 3, "target": 4, "relation": "son"}
]}
var svg = d3.select("svg");
svg.selectAll("circle")
.data(graph.nodes)
.enter()
.append("circle")
.attr({
"cx": function(d, i){
var x;
if(d.level == 1){
x = i*100 + 100;
}else{
x = 250;
}
d.x = x;
return x;
},
"cy": function(d, i){
var y;
if(d.level == 1){
y = 260;
}else{
y = 60;
}
d.y = y;
return y;
},
"r" : 30,
"fill": "gray",
"opacity": .5
})
svg.selectAll("text")
.data(graph.nodes)
.enter()
.append("text")
.attr({
"x": function(d){return d.x},
"y": function(d){return d.y},
fill: "steelblue"
})
.text(function(d){
return d.name;
})
svg.selectAll("line")
.data(graph.links)
.enter()
.append("line")
.attr({
"x1": sourceX,
"y1": sourceY,
"x2": targetX,
"y2": targetY,
"stroke-width": 2,
"stroke": "grey"
})
function sourceX(d, i){
var t = graph.nodes[d.source].x;
return t;
}
function sourceY(d, i){
var t = graph.nodes[d.source].y;
return t;
}
function targetX(d, i){
var t = graph.nodes[d.target].x;
return t;
}
function targetY(d, i){
var t = graph.nodes[d.target].y;
return t;
}
//console.log(graph.nodes)
Here is the gist of the story:
If using d3 layout, make your data structure match the data-structure expected by the layout and d3 will compute the x and y coordinates for you.
If using want a custom layout, write function to determine the x and y coordinates for nodes and the links.