Легенда и оси не работают должным образом в многострочном графике d3 - PullRequest
0 голосов
/ 16 апреля 2020

Я адаптировал многострочный график с легендой и осью, который правильно отображается на сайте bl.ocks.org (http://bl.ocks.org/Matthew-Weber/5645518). Легенда реорганизуется, когда вы выбираете другой тип из выпадающего поля. На моей адаптации, когда легенда реорганизуется, элементы начинают перекрывать друг друга, когда выбираются некоторые типы. Также оси опираются друг на друга. В оригинальном коде используются подсказки, но я его не проверял.

// 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");

Мой адаптированный код (включая множество сообщений console.log) находится в jsfiddle https://jsfiddle.net/pwarwick43/13fpn567/2/

Я начинаю думать, что проблема может быть в версии d3 или jquery. Кто-нибудь получил предложения по этому поводу?

...