2

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:

initial 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.

1. zoom level

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

2. zoom level

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.

Community
  • 1
  • 1
user1251007
  • 15,891
  • 14
  • 50
  • 76
  • You don't need the zoom behaviour here. Zoom in this context is really only adjusting the domain of the scales and redrawing. So all you need to do is recompute the domain of the scales using only the data points of the group you're interested in and redraw. – Lars Kotthoff Mar 06 '15 at 18:47
  • @LarsKotthoff: Thanks for your input. I tried it the way you suggested (see edited question), but the plot is not updated. Is calling `.call(xAxis)` sufficient to redraw the plot? Or do I need something else/more? – user1251007 Mar 09 '15 at 16:54
  • You also have to reset the positions of all the circles. – Lars Kotthoff Mar 09 '15 at 16:56
  • @LarsKotthoff: Okay, I'm getting closer ... I updated my question again, now I'm stuck at resetting the circles. – user1251007 Mar 10 '15 at 16:08
  • You need to filter the domain values: http://jsfiddle.net/n7k4wg0b/ – Lars Kotthoff Mar 10 '15 at 16:25

0 Answers0