block by renecnielsen 2732672f6bf449823d8cd31e29dd3dfb

d3-annotation: Responsive Types and Hover

Full Screen

Responsive Types and Hover Example

Made with d3.annotation. An example showing how you can dynamically change the annotation type and add hover events.

forked from susielu‘s block: d3-annotation: Responsive Types and Hover

index.js

"use strict";

// line chart code: https://bl.ocks.org/d3noob/402dd382a51a4f6eea487f9a35566de0
// time series from: http://bl.ocks.org/mbostock/3883245
// set the dimensions and margins of the graph
var margin = { top: 20, right: 20, bottom: 30, left: 50 },
    height = 500 - margin.top - margin.bottom;
var maxWidth = 860 - margin.left - margin.right;
var width = 860 - margin.left - margin.right;

var parseTime = d3.timeParse("%d-%b-%y");
var _x = d3.scaleTime().range([0, width]);
var _y = d3.scaleLinear().range([height, 0]);

var valueline = d3.line().x(function (d) {
  return _x(d.date);
}).y(function (d) {
  return _y(d.close);
});

var svg = d3.select("svg").attr("width", 960).attr("height", height + margin.top + margin.bottom).append("g").attr("transform", "translate(" + margin.left + "," + margin.top + ")");

d3.tsv("data.hidden.tsv", function (error, data) {
  if (error) throw error;

  data.forEach(function (d) {
    d.date = parseTime(d.date);
    d.close = +d.close;
  });

  _x.domain(d3.extent(data, function (d) {
    return d.date;
  }));
  _y.domain([0, d3.max(data, function (d) {
    return d.close;
  })]);

  svg.append("path").data([data]).attr("class", "line").attr("d", valueline);

  svg.append("g").attr("class", "x-axis").attr("transform", "translate(0," + height + ")").call(d3.axisBottom(_x));

  svg.append("g").call(d3.axisLeft(_y));

  //Add annotations
  var labels = [{
    data: { date: "9-Apr-12", close: 636.23 },
    dy: 37,
    dx: -142,
    subject: { text: 'C', y: "bottom" },
    id: "minimize-badge"
  }, {
    data: { date: "26-Feb-08", close: 119.15 },
    dy: -137,
    dx: 0,
    note: { align: "middle" },
    subject: { text: 'A', y: "bottom" },
    id: "minimize-badge"
  }, {
    data: { date: "18-Sep-09", close: 185.02 },
    dy: 37,
    dx: 42,
    subject: { text: 'B' },
    id: "minimize-badge"

  }].map(function (l) {
    l.note = Object.assign({}, l.note, { title: "Close: " + l.data.close,
      label: "" + l.data.date });
    return l;
  });

  //using a separate type of annotation to control the resize functionality
  var resize = [{
    subject: {
      y1: 0,
      y2: height
    },
    x: width,
    dx: 10,
    dy: height / 2,
    disable: ["connector"],
    note: {
      title: "< >",
      label: "drag to resize",
      lineType: "none"
    }
  }];

  var timeFormat = d3.timeFormat("%d-%b-%y");

  window.makeAnnotations = d3.annotation().annotations(labels).type(d3.annotationCalloutElbow).accessors({ x: function x(d) {
      return _x(parseTime(d.date));
    },
    y: function y(d) {
      return _y(d.close);
    }
  }).accessorsInverse({
    date: function date(d) {
      return timeFormat(_x.invert(d.x));
    },
    close: function close(d) {
      return _y.invert(d.y);
    }
  }).on('subjectover', function (annotation) {

    //cannot reference this if you are using es6 function syntax
    this.append('text').attr('class', 'hover').text(annotation.note.title).attr('text-anchor', 'middle').attr('y', annotation.subject.y && annotation.subject.y == "bottom" ? 50 : -40).attr('x', -15);

    this.append('text').attr('class', 'hover').text(annotation.note.label).attr('text-anchor', 'middle').attr('y', annotation.subject.y && annotation.subject.y == "bottom" ? 70 : -60).attr('x', -15);
  }).on('subjectout', function (annotation) {
    this.selectAll('text.hover').remove();
  });

  //Isn't using data for placement so accessors and accessorsInverse
  //are not necessary
  window.makeResize = d3.annotation().annotations(resize).type(d3.annotationXYThreshold);

  svg.append("g").attr("class", "annotation-test").call(makeAnnotations);

  svg.append("g").attr("class", "annotation-resize").call(makeResize);

  svg.select(".annotation.xythreshold").call(d3.drag().on("drag", function (d) {
    var newWidth = Math.max(0, Math.min(maxWidth, d.x + d3.event.dx));
    d.x = newWidth;

    var threshold = 400;
    if (newWidth < threshold && width >= threshold) {
      makeAnnotations.type(d3.annotationBadge);
      svg.select("g.annotation-test").call(makeAnnotations);
    } else if (newWidth > threshold && width <= threshold) {
      makeAnnotations.type(d3.annotationCalloutElbow);
      svg.select("g.annotation-test").call(makeAnnotations);
    }

    width = newWidth;

    _x.range([0, width]);
    makeAnnotations.updatedAccessors();
    makeResize.updatedAccessors();

    svg.select("g.x-axis").call(d3.axisBottom(_x));

    svg.select("path.line").attr("d", valueline);
  }));
});

index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <link href='https://fonts.googleapis.com/css?family=Lato:300,900' rel='stylesheet' type='text/css'>

    <style>
     body{
        background-color: whitesmoke;
     }

    :root {
      --accent-color: #E8336D;
    }

     svg {
        background-color: white;
        font-family: 'Lato';
     }

     path.line {
        fill: none;
        stroke: black;
        stroke-width: 1px;
      }

    .annotation path {
      stroke: var(--accent-color);
      fill: none;
    }

    .annotation path.connector-arrow{
      fill: var(--accent-color);
    }

    .annotation text {
      fill: var(--accent-color);
    }

    .annotation-note-title {
      font-weight: bold;
    }

    .annotation.xythreshold {
      cursor: move;
    }

    .annotation.xythreshold .annotation-subject{
      stroke-width: 3px;
    }

    .annotation.badge path.subject-pointer, .annotation.badge path.subject {
      fill: var(--accent-color);
      stroke-width: 3px;
      stroke-linecap: round;
    } 

    .annotation.badge path.subject-ring {
      fill: white;
      stroke-width: 3px;
    }

    .annotation.badge .badge-text {
      fill: white;
      font-size: .7em;
    }

    text.hover {
      font-size: .7em;
    }
    </style>
</head>
<body>
    <svg width=960 height=500></svg>
    <script src="https://d3js.org/d3.v4.js"></script>
    <script src="https://rawgit.com/susielu/d3-annotation/master/d3-annotation.min.js"></script>
    <script src="index.js"></script>
</body>
</html>

index-es6.js

   // line chart code: https://bl.ocks.org/d3noob/402dd382a51a4f6eea487f9a35566de0
    // time series from: http://bl.ocks.org/mbostock/3883245
    // set the dimensions and margins of the graph
    const margin = {top: 20, right: 20, bottom: 30, left: 50},
    height = 500 - margin.top - margin.bottom;
    const maxWidth = 860 - margin.left - margin.right;
    let width = 860 - margin.left - margin.right;


    const parseTime = d3.timeParse("%d-%b-%y");
    const x = d3.scaleTime().range([0, width]);
    const y = d3.scaleLinear().range([height, 0]);

    const valueline = d3.line()
    .x(d => x(d.date))
    .y(d => y(d.close));

    const svg = d3.select("svg")
    .attr("width", 960)
    .attr("height", height + margin.top + margin.bottom)
    .append("g")
    .attr("transform",
          "translate(" + margin.left + "," + margin.top + ")");

    d3.tsv("data.hidden.tsv", function(error, data) {
      if (error) throw error;

      data.forEach(function(d) {
        d.date = parseTime(d.date);
        d.close = +d.close;
      });

      x.domain(d3.extent(data, d => d.date));
      y.domain([0, d3.max(data, d => d.close)]);

      svg.append("path")
        .data([data])
        .attr("class", "line")
        .attr("d", valueline);

      svg.append("g")
        .attr("class", "x-axis")
        .attr("transform", "translate(0," + height + ")")
        .call(d3.axisBottom(x));

      svg.append("g")
        .call(d3.axisLeft(y));

      //Add annotations
      const labels = [
        {
          data: {date: "9-Apr-12",	close: 636.23},
          dy: 37,
          dx: -142,
          subject: { text: 'C', y:"bottom" },
          id: "minimize-badge"
        },
        {
          data: {date: "26-Feb-08",	close: 119.15},
          dy: -137,
          dx: 0,
          note: { align: "middle"},
          subject: { text: 'A', y:"bottom" },
          id: "minimize-badge"
        },
        {
          data: {date: "18-Sep-09",	close: 185.02},
          dy: 37,
          dx: 42,
          subject: { text: 'B'},
          id: "minimize-badge"

        }
      ].map(l => {
        l.note = Object.assign({}, l.note, { title: `Close: ${l.data.close}`,
          label: `${l.data.date}`})
        return l
      })

      //using a separate type of annotation to control the resize functionality
      const resize = [{
        subject: {
          y1: 0,
          y2: height
        },
        x: width,
        dx: 10,
        dy: height/2,
        disable: ["connector"],
        note: { 
          title: "< >",
          label: "drag to resize", 
          lineType: "none"
        }
      }]

      const timeFormat = d3.timeFormat("%d-%b-%y")
      
      window.makeAnnotations = d3.annotation()
        .annotations(labels)
        .type(d3.annotationCalloutElbow)
        .accessors({ x: d => x(parseTime(d.date)), 
          y: d => y(d.close)
        })
        .accessorsInverse({
          date: d => timeFormat(x.invert(d.x)),
          close: d => y.invert(d.y) 
        })
        .on('subjectover', function(annotation) {

          //cannot reference this if you are using es6 function syntax
          this.append('text')
            .attr('class', 'hover')
            .text(annotation.note.title)
            .attr('text-anchor', 'middle')
            .attr('y', (annotation.subject.y && annotation.subject.y == "bottom") ? 50 : -40)
            .attr('x', -15)

          this.append('text')
            .attr('class', 'hover')
            .text(annotation.note.label)
            .attr('text-anchor', 'middle')
            .attr('y', (annotation.subject.y && annotation.subject.y == "bottom") ? 70 : -60)
            .attr('x', -15)
        })
        .on('subjectout', function(annotation) {
          this.selectAll('text.hover')
          .remove()
        })

      //Isn't using data for placement so accessors and accessorsInverse
      //are not necessary
      window.makeResize = d3.annotation()
        .annotations(resize)
        .type(d3.annotationXYThreshold)

      svg.append("g")
        .attr("class", "annotation-test")
        .call(makeAnnotations)

      svg.append("g")
        .attr("class", "annotation-resize")
        .call(makeResize)

      svg.select(".annotation.xythreshold")
        .call(d3.drag()
          .on("drag", function(d) {
            const newWidth = Math.max(0, Math.min(maxWidth, d.x + d3.event.dx))
            d.x = newWidth

            const threshold = 400
            if (newWidth < threshold && width >= threshold){
              makeAnnotations.type(d3.annotationBadge)
              svg.select("g.annotation-test").call(makeAnnotations)
            } else if (newWidth > threshold && width <= threshold) {
              makeAnnotations.type(d3.annotationCalloutElbow)
              svg.select("g.annotation-test").call(makeAnnotations)
            }

            width = newWidth

            x.range([0, width])
            makeAnnotations.updatedAccessors()
            makeResize.updatedAccessors()

            svg.select("g.x-axis")
              .call(d3.axisBottom(x));

            svg.select("path.line")
              .attr("d", valueline);

          })
        )
    })