I make a scatterplot using d3.js where the points are scattered according to their name and an accompanying number. The x axis displays the group_1 once per group.
The data that is plotted would look like this:
name, group_1, group_2, number
"a", "A", "1", 0.5
"b", "A", "1", 10.0
"c", "A", "1", 5.0
"d", "A", "2", 1.0
"e", "A", "2", 3.0
"f", "A", "2", 10.0
"g", "B", "1", 3.0
Starting point.
The current code (based on this SO answer/example on bl.ocks.org) ...
var margin = {
top: 10,
right: 10,
bottom: 30,
left: 30
},
width = 500 - margin.left - margin.right,
height = 300 - margin.top - margin.bottom,
offset = 7;
var chartData = [{
name: "a",
group_1: "A",
group_2: "1",
number: 0.5
}, {
name: "b",
group_1: "A",
group_2: "1",
number: 10
}, {
name: "c",
group_1: "A",
group_2: "1",
number: 5.0
}, {
name: "d",
group_1: "A",
group_2: "2",
number: 1.0
}, {
name: "e",
group_1: "A",
group_2: "2",
number: 3.0
}, {
name: "f",
group_1: "A",
group_2: "2",
number: 10.0
}, {
name: "g",
group_1: "B",
group_2: "1",
number: 3.0
}];
var x = d3.scale.ordinal()
.domain(chartData.map(function(d) {
return d.name;
}))
.rangePoints([0, width]);
var chartGroups = [];
chartData.forEach(function(d, i, array) {
if (i === 0) d.first = true;
else if (d.group_1 !== array[i - 1].group_1) d.first = true;
if (i === array.length - 1) d.last = true;
else if (d.group_1 !== array[i + 1].group_1) d.last = true;
if (d.first) chartGroups.push({
group: d.group_1,
start: i === 0 ? x(d.name) : ((x(d.name) + x(array[i - 1].name)) / 2)
});
if (d.last) chartGroups[chartGroups.length - 1].end = i === array.length - 1 ? x(d.name) : ((x(d.name) + x(array[i + 1].name)) / 2);
});
var y = d3.scale.linear()
.domain([0, d3.max(chartData, function(d) {
return d.number;
})])
.range([height, 0]),
xAxis = d3.svg.axis()
.scale(x)
.tickValues([])
.outerTickSize(offset)
.orient("bottom"),
yAxis = d3.svg.axis()
.scale(y)
.orient("left");
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 + ")");
svg.selectAll("circle.point")
.data(chartData)
.enter().append("circle")
.attr({
"class": "point",
cx: function(d) {
return x(d.name);
},
cy: function(d) {
return y(d.number);
},
r: 5
});
var groups = svg.selectAll("g.chartGroup")
.data(chartGroups)
.enter().append("g")
.attr("class", "chartGroup")
.attr("transform", "translate(" + 0 + "," + (height + offset) + ")");
groups.append("text")
.attr({
x: function(d) {
return (d.start + d.end) / 2;
},
dy: "1em",
"text-anchor": "middle"
})
.text(function(d) {
return d.group;
});
groups.append("path")
.attr("d", function(d) {
var t = d3.select(this.parentNode).select("text").node().getBBox(),
ttop = [t.x + t.width / 2, t.y];
console.log(d, t, ttop);
return "M" + d.start + ",0" + "V" + -offset;
});
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(xAxis);
svg.append("g")
.attr("class", "y axis")
.call(yAxis);
text {
font: 12px sans-serif;
}
.axis path,
.axis line,
g.chartGroup path {
fill: none;
stroke: #000;
shape-rendering: crispEdges;
}
circle.point {
fill: steelblue;
stroke: black;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
... generates this plot:
What I want to achieve.
1. Zoom Level: I want zoom in to only display all elements of A (from group_1) when clicking on the label A on the x axis. Besides the zooming, also the labeling of the x axis should change to incorporate data from group_2.
2. Zoom Level: On the 2. zoom level, this should be repeated. By clicking on 1 (from group_2), I want a zoom to all elements of this group. The x axis should change to incorporate data from name
What I tried.
I tried adding basic zooming function (from this example on bl.ocks.org) through these code parts, but even this didn't work out:
var zoom = d3.behavior.zoom()
.x(x)
.y(y)
.scaleExtent([1, 10])
.on("zoom", zoomed);
var svg = d3.select("body")
// ...
.call(zoom);
function zoomed() {
svg.select(".x.axis").call(xAxis);
svg.select(".y.axis").call(yAxis);
}
The array chartGroups
, which is based on group_1 is an array of calculated start and end coordinates for displaying the groups on the x axis. But I can't figure out, how to use them for zooming.
var margin = {
top: 10,
right: 10,
bottom: 30,
left: 30
},
width = 500 - margin.left - margin.right,
height = 300 - margin.top - margin.bottom,
offset = 7;
var chartData = [{
name: "a",
group_1: "A",
group_2: "1",
number: 0.5
}, {
name: "b",
group_1: "A",
group_2: "1",
number: 10
}, {
name: "c",
group_1: "A",
group_2: "1",
number: 5.0
}, {
name: "d",
group_1: "A",
group_2: "2",
number: 1.0
}, {
name: "e",
group_1: "A",
group_2: "2",
number: 3.0
}, {
name: "f",
group_1: "A",
group_2: "2",
number: 10.0
}, {
name: "g",
group_1: "B",
group_2: "1",
number: 3.0
}];
var x = d3.scale.ordinal()
.domain(chartData.map(function(d) {
return d.name;
}))
.rangePoints([0, width]);
var chartGroups = [];
chartData.forEach(function(d, i, array) {
if (i === 0) d.first = true;
else if (d.group_1 !== array[i - 1].group_1) d.first = true;
if (i === array.length - 1) d.last = true;
else if (d.group_1 !== array[i + 1].group_1) d.last = true;
if (d.first) chartGroups.push({
group: d.group_1,
start: i === 0 ? x(d.name) : ((x(d.name) + x(array[i - 1].name)) / 2)
});
if (d.last) chartGroups[chartGroups.length - 1].end = i === array.length - 1 ? x(d.name) : ((x(d.name) + x(array[i + 1].name)) / 2);
});
var y = d3.scale.linear()
.domain([0, d3.max(chartData, function(d) {
return d.number;
})])
.range([height, 0]),
xAxis = d3.svg.axis()
.scale(x)
.tickValues([])
.outerTickSize(offset)
.orient("bottom"),
yAxis = d3.svg.axis()
.scale(y)
.orient("left");
var zoom = d3.behavior.zoom()
.x(x)
.y(y)
.scaleExtent([1, 10])
.on("zoom", zoomed);
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 + ")")
.call(zoom);
svg.selectAll("circle.point")
.data(chartData)
.enter().append("circle")
.attr({
"class": "point",
cx: function(d) {
return x(d.name);
},
cy: function(d) {
return y(d.number);
},
r: 5
});
var groups = svg.selectAll("g.chartGroup")
.data(chartGroups)
.enter().append("g")
.attr("class", "chartGroup")
.attr("transform", "translate(" + 0 + "," + (height + offset) + ")");
groups.append("text")
.attr({
x: function(d) {
return (d.start + d.end) / 2;
},
dy: "1em",
"text-anchor": "middle"
})
.text(function(d) {
return d.group;
});
groups.append("path")
.attr("d", function(d) {
var t = d3.select(this.parentNode).select("text").node().getBBox(),
ttop = [t.x + t.width / 2, t.y];
console.log(d, t, ttop);
return "M" + d.start + ",0" + "V" + -offset;
});
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(xAxis);
svg.append("g")
.attr("class", "y axis")
.call(yAxis);
function zoomed() {
svg.select(".x.axis").call(xAxis);
svg.select(".y.axis").call(yAxis);
}
text {
font: 12px sans-serif;
}
.axis path,
.axis line,
g.chartGroup path {
fill: none;
stroke: #000;
shape-rendering: crispEdges;
}
circle.point {
fill: steelblue;
stroke: black;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
Question.
How can I zoom to specficic areas just by clicking on a text element?
Edit.
As suggested in the comments, I tried to adjust the domain and redraw the circles. I did this the following way:
function zoom_update(group) {
//adjust domain
x.domain(chartData.map(function (d) {
if (d.group_1 == group) {
return d.name;
}
}));
console.log(x.domain());
svg.selectAll(".xaxis")
.transition(750)
.call(xAxis);
// update circles
svg.selectAll("circle")
.transition(750)
.attr({
cx: function (d) {return x(d.name);}
});
}
var margin = {
top: 10,
right: 10,
bottom: 30,
left: 30
},
width = 500 - margin.left - margin.right,
height = 300 - margin.top - margin.bottom,
offset = 7;
var chartData = [{
name: "a",
group_1: "A",
group_2: "1",
number: 0.5
}, {
name: "b",
group_1: "A",
group_2: "1",
number: 10
}, {
name: "c",
group_1: "A",
group_2: "1",
number: 5.0
}, {
name: "d",
group_1: "A",
group_2: "2",
number: 1.0
}, {
name: "e",
group_1: "A",
group_2: "2",
number: 3.0
}, {
name: "f",
group_1: "A",
group_2: "2",
number: 10.0
}, {
name: "g",
group_1: "B",
group_2: "1",
number: 3.0
}];
var x = d3.scale.ordinal()
.rangePoints([0, width]);
x.domain(chartData.map(function(d) {
return d.name;
}));
var chartGroups = [];
chartData.forEach(function(d, i, array) {
if (i === 0) d.first = true;
else if (d.group_1 !== array[i - 1].group_1) d.first = true;
if (i === array.length - 1) d.last = true;
else if (d.group_1 !== array[i + 1].group_1) d.last = true;
if (d.first) chartGroups.push({
group: d.group_1,
start: i === 0 ? x(d.name) : ((x(d.name) + x(array[i - 1].name)) / 2)
});
if (d.last) chartGroups[chartGroups.length - 1].end = i === array.length - 1 ? x(d.name) : ((x(d.name) + x(array[i + 1].name)) / 2);
});
var y = d3.scale.linear()
.domain([0, d3.max(chartData, function(d) {
return d.number;
})])
.range([height, 0]);
var xAxis = d3.svg.axis()
.scale(x)
.tickValues([])
.outerTickSize(offset)
.orient("bottom");
var yAxis = d3.svg.axis()
.scale(y)
.orient("left");
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 + ")");
svg.selectAll("circle.point")
.data(chartData)
.enter().append("circle")
.attr({
"class": "point",
cx: function(d) {
return x(d.name);
},
cy: function(d) {
return y(d.number);
},
r: 5
});
var groups = svg.selectAll("g.chartGroup")
.data(chartGroups)
.enter().append("g")
.attr("class", "chartGroup")
.attr("transform", "translate(" + 0 + "," + (height + offset) + ")");
groups.append("text")
.attr({
x: function(d) {
return (d.start + d.end) / 2;
},
dy: "1em",
"text-anchor": "middle"
})
.on("click", function(d) {
zoom_update(d.group);
})
.text(function(d) {
return d.group;
});
groups.append("path")
.attr("d", function(d) {
var t = d3.select(this.parentNode).select("text").node().getBBox(),
ttop = [t.x + t.width / 2, t.y];
console.log(d, t, ttop);
return "M" + d.start + ",0" + "V" + -offset;
});
svg.append("g")
.attr("class", "xaxis")
.attr("transform", "translate(0," + height + ")")
.call(xAxis);
svg.append("g")
.attr("class", "yaxis")
.call(yAxis);
function zoom_update(group) {
x.domain(chartData.map(function(d) {
if (d.group_1 == group) {
return d.name;
}
}));
alert(x.domain());
svg.selectAll(".xaxis")
.transition(750)
.call(xAxis);
// update circles
svg.selectAll("circle")
.transition(750)
.attr({
cx: function(d) {
return x(d.name);
}
});
}
text {
font: 12px sans-serif;
}
.xaxis path,
.xaxis line,
.yaxis path,
.yaxis line,
.axis path,
.axis line,
g.chartGroup path {
fill: none;
stroke: #000;
shape-rendering: crispEdges;
}
circle.point {
fill: steelblue;
stroke: black;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
The log shows that the domain is set depending on which label is clicked. In addition, the circles that are outside the new domain are moved to x=0
, but the remaining circles are not spread over the whole domain.