0

I adapted a multi-line chart which has a legend and axis and displays correctly on the bl.ocks.org site (http://bl.ocks.org/Matthew-Weber/5645518). The legend reorganizes itself when you select a different type from the drop down field. On my adaptation when the legend reorganizes itself the items start to overlap each other when some types are selected. Also the axes draw on top of each other. The original code uses tipsy but I have not checked it.

// original author's code http://bl.ocks.org/Matthew-Weber/5645518;

//set the margins
var margin = {
    top: 50,
    right: 160,
    bottom: 80,
    left: 50
  },
  width = 900 - margin.left - margin.right,
  height = 500 - margin.top - margin.bottom;

//set dek and head to be as wide as SVG
d3.select('#dek')
  .style('width', width + 'px');
d3.select('#headline')
  .style('width', width + 'px');

//write out your source text here
var sourcetext = "xxx";

// set the type of number here, n is a number with a comma, .2% will get you a percent, .2f will get you 2 decimal points
var NumbType = d3.format(",");

// color array
var bluescale4 = ["red", "blue", "green", "orange", "purple"];

//color function pulls from array of colors stored in color.js
var color = d3.scale.ordinal().range(bluescale4);

//defines a function to be used to append the title to the tooltip. 
var maketip = function(d) {
  var tip = '<p class="tip3">' + d.name + '<p class="tip1">' + NumbType(d.value) + '</p> <p class="tip3">' + formatDate(d.date) + '</p>';
  return tip;
}

//define your year format here, first for the x scale, then if the date is displayed in tooltips
var parseDate = d3.time.format("%Y-%m-%d").parse;
var formatDate = d3.time.format("%b %d, '%y");

//create an SVG
var svg = d3.select("#graphic").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 + ")");

//make a rectangle so there is something to click on
svg.append("svg:rect")
  .attr("width", width)
  .attr("height", height)
  .attr("class", "plot"); //#fff


// force data to update when menu is changed    
var menu = d3.select("#menu select")
  .on("change", change);

//suck in the data, store it in a value called formatted, run the redraw function

d3.csv("/sites/default/d3_files/d3-provinces/statistics-april-15-2.csv", function(data) {
  formatted = data; 
  redraw(); 
});

d3.select(window)
  .on("keydown", function() {
    altKey = d3.event.altKey;
  })
  .on("keyup", function() {
    altKey = false;
  });

var altKey;

// set terms of transition that will take place
// when a new type (Death etc.)indicator is chosen   

function change() {
  d3.transition()
    .duration(altKey ? 7500 : 1500)
    .each(redraw);
} // end change


// REDRAW all the meat goes in the redraw function
function redraw() {

  // create data nests based on type indicator (series)
  var nested = d3.nest()
    .key(function(d) {
      return d.type;
    })
    .map(formatted)

  // get value from menu selection
  // the option values are set in HTML and correspond
  //to the [type] value we used to nest the data  

  var series = menu.property("value"); 

  // only retrieve data from the selected series, using the nest we just created

  var data = nested[series];

  // for object constancy we will need to set "keys", one for each type of data (column name) exclude all others.

  color.domain(d3.keys(data[0]).filter(function(key) {
    return (key !== "date" && key !== "type");
  }));

  var linedata = color.domain().map(function(name) {
    return {
      name: name,
      values: data.map(function(d) {
        return {
          name: name,
          date: parseDate(d.date),
          value: parseFloat(d[name], 10)
        };
      })
    };
  });

  //make an empty variable to stash the last values into so we can sort the legend // do we need to sort it?
  var lastvalues = [];

  //setup the x and y scales
  var x = d3.time.scale()
    .domain([
      d3.min(linedata, function(c) {
        return d3.min(c.values, function(v) {
          return v.date;
        });
      }),
      d3.max(linedata, function(c) {
        return d3.max(c.values, function(v) {
          return v.date;
        });
      })
    ])
    .range([0, width]);

  var y = d3.scale.linear()
    .domain([
      d3.min(linedata, function(c) {
        return d3.min(c.values, function(v) {
          return v.value;
        });
      }),
      d3.max(linedata, function(c) {
        return d3.max(c.values, function(v) {
          return v.value;
        });
      })
    ])
    .range([height, 0]);

  //will draw the line      
  var line = d3.svg.line()
    .x(function(d) {
      return x(d.date);
    })
    .y(function(d) {
      return y(d.value);
    });

  //create and draw the x axis - need to clear the existing axis

  var xAxis = d3.svg.axis()
    .scale(x)
    .orient("bottom")
    .tickPadding(8)
    .ticks(10);

  //create and draw the y axis  
  var yAxis = d3.svg.axis()
    .scale(y)
    .orient("left")
    .tickSize(0 - width)
    .tickPadding(8);

  svg.append("svg:g")
    .attr("class", "x axis");

  svg.append("svg:g")
    .attr("class", "y axis")
    .attr("transform", "translate(" + (0) + ",0)")
    .call(yAxis);

  //bind the data
  var thegraph = svg.selectAll(".thegraph")
    .data(linedata)

  //append a g tag for each line and set of tooltip circles and give it a unique ID based on the column name of the data     
  var thegraphEnter = thegraph.enter().append("g")
    .attr("class", "thegraph")
    .attr('id', function(d) {
      return d.name + "-line";
    })
    .style("stroke-width", 2.5)
    .on("mouseover", function(d) {
      d3.select(this) //on mouseover of each line, give it a nice thick stroke // works
        .style("stroke-width", '6px');

      var selectthegraphs = $('.thegraph').not(this); //select all the rest of the lines, except the one you are hovering on and drop their opacity
      d3.selectAll(selectthegraphs)
        .style("opacity", 0.2);

      var getname = document.getElementById(d.name); //use get element cause the ID names have spaces in them - not working

      var selectlegend = $('.legend').not(getname); //grab all the legend items that match the line you are on, except the one you are hovering on

      d3.selectAll(selectlegend) // drop opacity on other legend names
        .style("opacity", .2);

      d3.select(getname)
        .attr("class", "legend-select"); //change the class on the legend name that corresponds to hovered line to be bolder            
    }) // end of mouseover

    .on("mouseout", function(d) { //undo everything on the mouseout
      d3.select(this)
        .style("stroke-width", '2.5px');

      var selectthegraphs = $('.thegraph').not(this);
      d3.selectAll(selectthegraphs)
        .style("opacity", 1);

      var getname = document.getElementById(d.name);
      var getname2 = $('.legend[fakeclass="fakelegend"]')
      var selectlegend = $('.legend').not(getname2).not(getname);

      d3.selectAll(selectlegend)
        .style("opacity", 1);

      d3.select(getname)
        .attr("class", "legend");
    });

  //actually append the line to the graph
  thegraphEnter.append("path")
    .attr("class", "line")
    .style("stroke", function(d) {
      return color(d.name);
    })
    .attr("d", function(d) {
      return line(d.values[0]);
    })
    .transition()
    .duration(2000)
    .attrTween('d', function(d) {
      var interpolate = d3.scale.quantile()
        .domain([0, 1])
        .range(d3.range(1, d.values.length + 1));
      return function(t) {
        return line(d.values.slice(0, interpolate(t)));
      };
    });

  //then append some 'nearly' invisible circles at each data point
  thegraph.selectAll("circle")
    .data(function(d) { 
      return (d.values);
    })
    .enter()
    .append("circle")
    .attr("class", "tipcircle")
    .attr("cx", function(d, i) { 
      return x(d.date)
    })
    .attr("cy", function(d, i) { 
      return y(d.value)
    })
    .attr("r", 3) // was 12 
    .style('opacity', .2)
    .attr("title", maketip);

  //append the legend

  var legend = svg.selectAll('.legend')
    .data(linedata);

  var legendEnter = legend
    .enter()
    .append('g')
    .attr('class', 'legend')
    .attr('id', function(d) {
      return d.name;
    })
    .on('click', function(d) { //onclick function to toggle off the lines           
      if ($(this).css("opacity") == 1) {
        //uses the opacity of the item clicked on to determine whether to turn the line on or off           

        var elemented = document.getElementById(this.id + "-line"); //grab the line that has the same ID as this point along w/ "-line"  
        //use get element cause ID has spaces
        d3.select(elemented)
          .transition()
          .duration(1000)
          .style("opacity", 0)
          .style("display", 'none');

        d3.select(this)
          .attr('fakeclass', 'fakelegend')
          .transition()
          .duration(1000)
          .style("opacity", .2);
      } else {
        var elemented = document.getElementById(this.id + "-line");

        d3.select(elemented)
          .style("display", "block")
          .transition()
          .duration(1000)
          .style("opacity", 1);

        d3.select(this)
          .attr('fakeclass', 'legend')
          .transition()
          .duration(1000)
          .style("opacity", 1);
      }
    });

  //create a scale to pass the legend items through // this is broken for some types

  var legendscale = d3.scale.ordinal()
    .domain(lastvalues)
    .range([0, 30, 60, 90, 120, 150, 180, 210]);

  //actually add the circles to the created legend container
  legendEnter.append('circle')
    .attr('cx', width + 20) // cx=width+50 made circle overlap text 
    .attr('cy', function(d) {

      var newScale = (legendscale(d.values[d.values.length - 1].value) + 20);
      return newScale;
    })
    .attr('r', 7)
    .style('fill', function(d) {
      return color(d.name);
    });

  //add the legend text
  legendEnter.append('text')
    .attr('x', width + 35) // is this an issue?
    .attr('y', function(d) {
      return legendscale(d.values[d.values.length - 1].value);
    })
    .text(function(d) {
      return d.name;
    });

  // set variable for updating visualization
  var thegraphUpdate = d3.transition(thegraph);

  // change values of path and then the circles to those of the new series

  thegraphUpdate.select("path")
    .attr("d", function(d, i) {

      lastvalues[i] = d.values[d.values.length - 1].value;
      lastvalues.sort(function(a, b) {
        return b - a
      });
      legendscale.domain(lastvalues);
      return line(d.values);
      // }
    });


  thegraphUpdate.selectAll("circle")
    .attr("title", maketip) // displays HTML but not circle
    .attr("cy", function(d, i) {
      return y(d.value)
    })
    .attr("cx", function(d, i) {
      return x(d.date)
    });


  // and now for legend items
  var legendUpdate = d3.transition(legend);

  legendUpdate.select("circle")
    .attr('cy', function(d, i) {
      return legendscale(d.values[d.values.length - 1].value);
    });

  legendUpdate.select("text")
    .attr('y', function(d) {
      return legendscale(d.values[d.values.length - 1].value);
    });


  d3.transition(svg).select(".x.axis")
    .attr("transform", "translate(0," + height + ")")
    .call(xAxis);

  //make my tooltips work
  $('circle').tipsy({
    opacity: .9,
    gravity: 'n',
    html: true
  });

  //end of the redraw function
}

svg.append("svg:text")
  .attr("text-anchor", "start")
  .attr("x", 0 - margin.left)
  .attr("y", height + margin.bottom - 10)
  .text(sourcetext)
  .attr("class", "source");

My adapted code (including a lot of console.log messages) is in jsfiddle https://jsfiddle.net/pwarwick43/13fpn567/2/

I am beginning to think the problem might be with the version of d3 or jquery. Anyone got suggestions about this?

PatriciaW
  • 893
  • 1
  • 12
  • 30
  • My versions of software "https://d3js.org/d3.v3.min.js", "https://d3js.org/d3-axis.v1.min.js", "https://d3js.org/d3-selection-multi.v1.min.js" – PatriciaW Apr 16 '20 at 00:32
  • I have discovered that the problem is caused by the lastvalues ... when there are are types with equal lastvalues their items in the legend overlap. It may also cause the axis problem. – PatriciaW Apr 17 '20 at 21:25

0 Answers0