Piyp791
Piyp791

Reputation: 669

Axis-range-slider alignment

I need to design a d3 component like the one shown in the figure below.

Desired Visualization

I referred to an existing code sample from this link, and modified it to create something like this.

My Intermediate working version

Left was changing the width of the axis, which I tried by changing the stroke-width property of the domain class. However, I ended with something like this.

Not working version

Problems:

  1. The slider handle isn't aligning with the axis.
  2. The axis color imprints on the slider.
  3. The ends of the axis are not perfectly round.

Questions:

  1. I can't figure out what do I translate/transform to align the sliders and the axis.
  2. I tried fiddling around with the opacity values, but didn't help.
  3. I set stroke-linecap to round, but it's still not completely round.

I am using d3 v4 for this. And the jsfiddle for my final code is here.

<!DOCTYPE html>
<html>

<head>
<meta charset="utf-8">
  <script src="https://d3js.org/d3.v5.min.js"></script>
  <style>
.tick{
  visibility:hidden;
}

 .domain {
    stroke: grey;
    stroke-width:10px;
    stroke-linecap: round;
  }
  
  .selection {
    fill:red
  }

</style>
</head>

<body>
  <div style="margin-left: 20px;margin-top: 20px;">
    <span></span> to <span></span>
  </div>


<script>

    var margin = 20,
        width = 400 - margin * 2,
        height = 15;

    // v3 = var x = d3.scale.linear()
    var x = d3.scaleLinear()
        .domain([0,100])
        .range([0, width]);

    /*
    var brush = d3.svg.brush()
      .x(x)
      .extent([20, 50]);
    */
    var brush = d3.brushX()
        .extent([[0,0], [width,height]])
        .on("brush", brushed);

    var svg = d3.select("body").append("svg")
        .attr("width", width + margin * 2)
        .attr("height", 100)
      .append("g")
        .attr("transform", "translate(" + margin + "," + margin + ")")
        .call(d3.axisBottom()
            .scale(x)
            .tickSize(0));
  
    var brushg = svg.append("g")
        .attr("class", "brush")
        .call(brush)
        
     // left circle
	
    
    var left_text = brushg.append("text")
    .attr("class", "label")
    .attr("fill", "black") 
    .attr("text-anchor", "middle")
    .text("hello world")
    .attr("transform", "translate(0," + (35) + ")")
    
    var right_text = brushg.append("text")
    .attr("class", "label")
    .attr("fill", "black") 
    .attr("text-anchor", "middle")
    .text("hello world")
    .attr("transform", "translate(0," + (35) + ")")
        
    
    /* 
      Height of the brush's rect is now 
        generated by brush.extent():
    brushg.selectAll("rect")
        .attr("height", height);
    */

    function brushed() {
      /*
        The brush attributes are no longer stored 
        in the brush itself, but rather in the 
        element it is brushing. That's where much of
        the confusion around v4's brushes seems to be.
        The new method is a little difficult to adapt
        to, but seems more efficient. I think much of
        this confusion comes from the fact that 
        brush.extent() still exists, but means
        something completely different.

        Instead of calling brush.extent() to get the 
        range of the brush, call 
        d3.brushSelection(node) on what is being 
        brushed.

      d3.select('#start-number')
        .text(Math.round(brush.extent()[0]));
      d3.select('#end-number')
        .text(Math.round(brush.extent()[1]));
      */


      var range = d3.brushSelection(this)
          .map(x.invert);
      
      console.log('range->'+range)
      d3.selectAll("span")
          .text(function(d, i) {
            console.log(Math.round(range[i]))
            return Math.round(range[i])
          })
          
      left_text.attr("x", x(range[0]));
      left_text.text(Math.round(range[0]));
      right_text.attr("x", x(range[1]));
      right_text.text(Math.round(range[1]));
      
      d3.selectAll("rect").attr("dy", "-5em")
          
    }
    

    // v3:  brushed();
    brush.move(brushg, [20, 40].map(x));

</script>
</body>
</html>

Upvotes: 2

Views: 860

Answers (2)

rioV8
rioV8

Reputation: 28763

The path in the axis is a closed shape and stroking that gives problems. Also you don't want ticks so why not draw the "axis" yourself. Then the round edge will be drawn correct.

    var svg = d3.select("body").append("svg")
        .attr("width", width + margin * 2)
        .attr("height", 100)
      .append("g")
        .attr("transform", "translate(" + margin + "," + margin + ")")
        // .call(d3.axisBottom()
        //     .scale(x)
        //     .tickSize(0))
        ;

    svg.append("path")
       .attr("class", "domain")
       .attr("d", `M${x(0)},0 ${x(100)},0`);

You have to match the brush extent to the stroked path surface

    var margin = 20,
        width = 400 - margin * 2,
        height = 10; // same as stroke width

    var brush = d3.brushX()
        .extent([[0,-height*0.5], [width,height*0.5]])
        .on("brush", brushed);

The dy attribute has no purpose

      //d3.selectAll("rect").attr("dy", "-5em")

Set the fill-opacity of the selection

.selection {
  fill:red;
  fill-opacity: 1;
}

<!DOCTYPE html>
<html>

<head>
<meta charset="utf-8">
  <script src="https://d3js.org/d3.v5.min.js"></script>
  <style>
.tick{
  visibility:hidden;
}

 .domain {
    stroke: grey;
    stroke-width:10;
    stroke-linecap: round;
}

.selection {
  fill:red;
  fill-opacity: 1;
}

</style>
</head>

<body>
  <div style="margin-left: 20px;margin-top: 20px;">
    <span></span> to <span></span>
</div>

<script>
    var margin = 20,
        width = 400 - margin * 2,
        height = 10; // same as stroke width

    // v3 = var x = d3.scale.linear()
    var x = d3.scaleLinear()
        .domain([0,100])
        .range([0, width]);

    /*
    var brush = d3.svg.brush()
      .x(x)
      .extent([20, 50]);
    */
    var brush = d3.brushX()
        .extent([[0,-height*0.5], [width,height*0.5]])
        .on("brush", brushed);

    var svg = d3.select("body").append("svg")
        .attr("width", width + margin * 2)
        .attr("height", 100)
      .append("g")
        .attr("transform", "translate(" + margin + "," + margin + ")")
        // .call(d3.axisBottom()
        //     .scale(x)
        //     .tickSize(0))
        ;
    svg.append("path")
       .attr("class", "domain")
       .attr("d", `M${x(0)},0 ${x(100)},0`);

    var brushg = svg.append("g")
        .attr("class", "brush")
        .call(brush)

     // left circle


    var left_text = brushg.append("text")
    .attr("class", "label")
    .attr("fill", "black")
    .attr("text-anchor", "middle")
    .text("hello world")
    .attr("transform", "translate(0," + (35) + ")")

    var right_text = brushg.append("text")
    .attr("class", "label")
    .attr("fill", "black")
    .attr("text-anchor", "middle")
    .text("hello world")
    .attr("transform", "translate(0," + (35) + ")")


    /*
      Height of the brush's rect is now
        generated by brush.extent():
    brushg.selectAll("rect")
        .attr("height", height);
    */

    function brushed() {
      /*
        The brush attributes are no longer stored
        in the brush itself, but rather in the
        element it is brushing. That's where much of
        the confusion around v4's brushes seems to be.
        The new method is a little difficult to adapt
        to, but seems more efficient. I think much of
        this confusion comes from the fact that
        brush.extent() still exists, but means
        something completely different.

        Instead of calling brush.extent() to get the
        range of the brush, call
        d3.brushSelection(node) on what is being
        brushed.

      d3.select('#start-number')
        .text(Math.round(brush.extent()[0]));
      d3.select('#end-number')
        .text(Math.round(brush.extent()[1]));
      */

      var range = d3.brushSelection(this)
          .map(x.invert);

      //console.log('range->'+range)
      d3.selectAll("span")
          .text(function(d, i) {
            //console.log(Math.round(range[i]))
            return Math.round(range[i])
          })

      left_text.attr("x", x(range[0]));
      left_text.text(Math.round(range[0]));
      right_text.attr("x", x(range[1]));
      right_text.text(Math.round(range[1]));

      //d3.selectAll("rect").attr("dy", "-5em")

    }

    // v3:  brushed();
    brush.move(brushg, [20, 40].map(x));

</script>
</body>
</html>

Upvotes: 0

Gerardo Furtado
Gerardo Furtado

Reputation: 102198

The axis and the brush are actually perfectly aligned!

You can see this if you set the stroke-width to 1px:

.as-console-wrapper { max-height: 30% !important;}
<!DOCTYPE html>
<html>

<head>
<meta charset="utf-8">
  <script src="https://d3js.org/d3.v5.min.js"></script>
  <style>
.tick{
  visibility:hidden;
}

 .domain {
    stroke: grey;
    stroke-width:1px;
    stroke-linecap: round;
  }
  
  .selection {
    fill:red
  }

</style>
</head>

<body>
  <div style="margin-left: 20px;margin-top: 20px;">
    <span></span> to <span></span>
  </div>


<script>

    var margin = 20,
        width = 400 - margin * 2,
        height = 15;

    // v3 = var x = d3.scale.linear()
    var x = d3.scaleLinear()
        .domain([0,100])
        .range([0, width]);

    /*
    var brush = d3.svg.brush()
      .x(x)
      .extent([20, 50]);
    */
    var brush = d3.brushX()
        .extent([[0,0], [width,height]])
        .on("brush", brushed);

    var svg = d3.select("body").append("svg")
        .attr("width", width + margin * 2)
        .attr("height", 100)
      .append("g")
        .attr("transform", "translate(" + margin + "," + margin + ")")
        .call(d3.axisBottom()
            .scale(x)
            .tickSize(0));
  
    var brushg = svg.append("g")
        .attr("class", "brush")
        .call(brush)
        
     // left circle
	
    
    var left_text = brushg.append("text")
    .attr("class", "label")
    .attr("fill", "black") 
    .attr("text-anchor", "middle")
    .text("hello world")
    .attr("transform", "translate(0," + (35) + ")")
    
    var right_text = brushg.append("text")
    .attr("class", "label")
    .attr("fill", "black") 
    .attr("text-anchor", "middle")
    .text("hello world")
    .attr("transform", "translate(0," + (35) + ")")
        
    
    /* 
      Height of the brush's rect is now 
        generated by brush.extent():
    brushg.selectAll("rect")
        .attr("height", height);
    */

    function brushed() {
      /*
        The brush attributes are no longer stored 
        in the brush itself, but rather in the 
        element it is brushing. That's where much of
        the confusion around v4's brushes seems to be.
        The new method is a little difficult to adapt
        to, but seems more efficient. I think much of
        this confusion comes from the fact that 
        brush.extent() still exists, but means
        something completely different.

        Instead of calling brush.extent() to get the 
        range of the brush, call 
        d3.brushSelection(node) on what is being 
        brushed.

      d3.select('#start-number')
        .text(Math.round(brush.extent()[0]));
      d3.select('#end-number')
        .text(Math.round(brush.extent()[1]));
      */


      var range = d3.brushSelection(this)
          .map(x.invert);
      
      console.log('range->'+range)
      d3.selectAll("span")
          .text(function(d, i) {
            console.log(Math.round(range[i]))
            return Math.round(range[i])
          })
          
      left_text.attr("x", x(range[0]));
      left_text.text(Math.round(range[0]));
      right_text.attr("x", x(range[1]));
      right_text.text(Math.round(range[1]));
      
      d3.selectAll("rect").attr("dy", "-5em")
          
    }
    

    // v3:  brushed();
    brush.move(brushg, [20, 40].map(x));

</script>
</body>
</html>

So, what's happening here? The issue is that when you tell the browser to take a line (in this case it's a path, but it doesn't matter) and increase its stroke to, let's say, 100 pixels, it will increase 50 pixels to one side and 50 pixels to the other side. So, the middle of that thick axis is right on the top of the brush's rectangle.

There are several solutions here, like drawing an rectangle. If, however, you want to keep your approach of increasing the .domain stroke-width, let's break the selections and move the axis half its stroke-width down (here I'm increasing the width to 20 pixels, so it's easier to see the alignment):

.as-console-wrapper { max-height: 30% !important;}
<!DOCTYPE html>
<meta charset="utf-8">
<script src="//d3js.org/d3.v4.min.js"></script>
<!-- 
  axes and brushes are styled out of the box, 
    so this is no longer needed
<style>
  
  .axis path, .axis line {
    fill: none;
    stroke: #000;
    shape-rendering: crispEdges;
  }
  .brush .extent {
    fill-opacity: .125;
    shape-rendering: crispEdges;
  }

</style>
-->
<style>
  .tick {
    visibility: hidden;
  }

  .domain {
    stroke: grey;
    stroke-width: 20px;
    stroke-linecap: round;
  }

  .selection {
    fill: red
  }

</style>

<body>
  <div style="margin-left: 20px;margin-top: 20px;">
    <span></span> to <span></span>
  </div>
</body>

<script>
  var margin = 20,
    width = 400 - margin * 2,
    height = 20;

  // v3 = var x = d3.scale.linear()
  var x = d3.scaleLinear()
    .domain([0, 100])
    .range([0, width]);

  /*
  var brush = d3.svg.brush()
    .x(x)
    .extent([20, 50]);
  */
  var brush = d3.brushX()
    .extent([
      [0, 0],
      [width, height]
    ])
    .on("brush", brushed);

  var svg = d3.select("body").append("svg")
    .attr("width", width + margin * 2)
    .attr("height", 100);

  svg.append("g")
    .attr("transform", "translate(" + margin + "," + (margin + 10) + ")")
    .call(d3.axisBottom()
      .scale(x)
      .tickSize(0));

  var brushg = svg.append("g")
    .attr("transform", "translate(" + margin + "," + margin + ")")
    .attr("class", "brush")
    .call(brush)

  // left circle


  var left_text = brushg.append("text")
    .attr("class", "label")
    .attr("fill", "black")
    .attr("text-anchor", "middle")
    .text("hello world")
    .attr("transform", "translate(0," + (35) + ")")

  var right_text = brushg.append("text")
    .attr("class", "label")
    .attr("fill", "black")
    .attr("text-anchor", "middle")
    .text("hello world")
    .attr("transform", "translate(0," + (35) + ")")


  /* 
    Height of the brush's rect is now 
      generated by brush.extent():
  brushg.selectAll("rect")
      .attr("height", height);
  */

  function brushed() {
    /*
      The brush attributes are no longer stored 
      in the brush itself, but rather in the 
      element it is brushing. That's where much of
      the confusion around v4's brushes seems to be.
      The new method is a little difficult to adapt
      to, but seems more efficient. I think much of
      this confusion comes from the fact that 
      brush.extent() still exists, but means
      something completely different.

      Instead of calling brush.extent() to get the 
      range of the brush, call 
      d3.brushSelection(node) on what is being 
      brushed.

    d3.select('#start-number')
      .text(Math.round(brush.extent()[0]));
    d3.select('#end-number')
      .text(Math.round(brush.extent()[1]));
    */


    var range = d3.brushSelection(this)
      .map(x.invert);

    console.log('range->' + range)
    d3.selectAll("span")
      .text(function(d, i) {
        console.log(Math.round(range[i]))
        return Math.round(range[i])
      })

    left_text.attr("x", x(range[0]));
    left_text.text(Math.round(range[0]));
    right_text.attr("x", x(range[1]));
    right_text.text(Math.round(range[1]));

    d3.selectAll("rect").attr("dy", "-5em")

  }


  // v3:  brushed();
  brush.move(brushg, [20, 40].map(x));

</script>

Upvotes: 2

Related Questions