Как добавить bru sh и увеличить несколько графиков с несколькими осями y в D3 JS? - PullRequest
0 голосов
/ 18 июня 2020

Я новичок в d3 js и после обучения у меня, когда я начал свой проект. Я хочу построить диаграмму, как показано на следующем рисунке:

enter image description here

Я бы хотел увеличить масштаб контекстного графа (маленький) применяется ко всем графам фокуса (большим) одновременно. Мой код следующий (dr aws вдохновлен https://observablehq.com/@connor-roche / multi-line-chart-focus-context ).

    function generateRandomData(years){
  var randomData = [];
  labels = ['local', 'state', 'federal'];
  for (var x in labels){
    for (var i = 0; i < years; i++){
      randomData.push({type: labels[x], year: i/100, taxRate: _getRandomArbitrary()});
    }
  }

  return randomData;

  // function _randomLocalData(){return Math.min(0.25, Math.random()/4);}
  // function _randomStateData(){return Math.min(0.25, Math.random()/4);}
  // function _randomFederalData(){return Math.min(0.25, Math.random()/4);}
  function _randomLocalData(){return Math.random();}
  function _randomStateData(){return Math.random();}
  function _randomFederalData(){return Math.random();}
  function _getRandomArbitrary() {
    var min = -1000;
    var max = 1000;
    return Math.random() * (max - min) + min;
  }
}
    // ////// COMPOSE DATA
    var taxData = generateRandomData(200, 100000);
    // console.log(taxData);
    var taxDataByYear = d3.nest()
      .key(function(d){return d.year;})
      .entries(taxData);
    // console.log(taxDataByYear);
    var taxDataByType = d3.nest()
      .key(function(d){return d.type;})
      .entries(taxData);
    var taxDataForStack = taxDataByYear.reduce(function(taxData, data){
      var newData = {year: data.key};
      data.values.forEach(function(d){newData[d.type] = d.taxRate;});
      taxData.push(newData);
      return taxData;
    }, []);
    // console.log(taxDataForStack);
    var years = taxDataByYear.map(function(d){return +d.key;});
    // console.log(years);
    var localTaxData = taxDataByType[0].values;
    // console.log(localTaxData);
    var stateTaxData = taxDataByType[1].values;
    // console.log(stateTaxData);
    var federalTaxData = taxDataByType[2].values;
    // console.log(federalTaxData);

    ////// SETUP CONTAINERS
    var    focusChartMargin = {top: 40, right: 40, bottom: 100, left: 60};
        width = 960 - focusChartMargin.left - focusChartMargin.right,
        height = 500 - focusChartMargin.top - focusChartMargin.bottom,
        contextChartMargin = { top: 430, right: focusChartMargin.right, bottom: 20,  left: focusChartMargin.left };
    // width of both charts
    var chartWidth = width - focusChartMargin.left - focusChartMargin.right;
    // height of either chart
    var focusChartHeight = height - focusChartMargin.top - focusChartMargin.bottom;
    // var contextChartHeight = height - contextChartMargin.top - contextChartMargin.bottom;
    var contextChartHeight = 500 - contextChartMargin.top - contextChartMargin.bottom;

    var svg = d3.select("body").append("svg")
      .attr("width", width + focusChartMargin.left + focusChartMargin.right)
      .attr("height", height + focusChartMargin.top + focusChartMargin.bottom);

    var chartPadding = 15;
    var chartHeight = height / taxDataByType.length - 30;
    // var chartHeight = 90;

    // console.log('focusChartMargin.top: ', focusChartMargin.top);
    // console.log('focusChartMargin.right: ', focusChartMargin.right);
    // console.log('focusChartMargin.bottom: ', focusChartMargin.bottom);
    // console.log('focusChartMargin.left: ', focusChartMargin.left);
    // console.log('height:', height);
    // console.log('width:', width);
    // console.log('data len:', taxDataByType.length);
    // console.log('h2;', contextChartHeight);
    // console.log('contextChartMargin.top: ', contextChartMargin.top);
    // console.log('contextChartMargin.right: ', contextChartMargin.right);
    // console.log('contextChartMargin.bottom: ', contextChartMargin.bottom);
    // console.log('contextChartMargin.left: ', contextChartMargin.left);
    // console.log('chartHeight: ', focusChartHeight);
    ////// COMPOSE GENERATORS
    var generators = {
      'shared': {
        color: d3.scaleOrdinal(d3.schemeAccent)
                .domain(['local', 'state', 'federal']),
        stack: d3.stack()
                .keys(['local', 'state', 'federal'])
                  .order(d3.stackOrderDescending)
                    .offset(d3.stackOffsetNone),
        xContext: d3.scaleLinear()
                    .domain([d3.min(years), d3.max(years)])
                      .range([0, width]),  
        xFocus: d3.scaleLinear()
                  .domain([d3.min(years), d3.max(years)])
                    .range([0, width]),  
        yFocus: d3.scaleLinear()
                  .domain([d3.max(federalTaxData, function(d){return d.taxRate;}), d3.min(federalTaxData, function(d){return d.taxRate;})])
                    .range([0, height]),
        yContext: d3.scaleLinear()
                    .domain([d3.max(federalTaxData, function(d){return d.taxRate;}), d3.min(federalTaxData, function(d){return d.taxRate;})])
                      .range([0, contextChartHeight]),
      },
      'local': {},
      'state': {},
      'federal': { yFocus: d3.scaleLinear()
                    .domain([d3.max(federalTaxData, function(d){return d.taxRate;}), d3.min(federalTaxData, function(d){return d.taxRate;})])
                      .range([0, chartHeight])}
    };
    var stackTaxData = generators.shared.stack(taxDataForStack);
    var maxStackData = d3.max(stackTaxData, function(d){return d3.max(d, function(d){return d[1];});});

    generators.shared.xAxisFocus = d3.axisBottom(generators.shared.xFocus)
                                      .tickFormat(d3.format('.2r')) 
                                        .tickSizeInner(-height);
    generators.shared.xAxisContex = d3.axisBottom(generators.shared.xContext) 
                                        .tickFormat(d3.format('.2r'))
                                          .tickSizeInner(0);
    generators.shared.yAxisContex = d3.axisLeft(generators.shared.yContext)
                                        .tickFormat(d3.format('.0f'))
                                          .tickSize(1);
    generators.federal.yAxis = d3.axisLeft(generators.federal.yFocus)
                                    .tickFormat(d3.format('.0f'))
                                      .tickSize(1)
                                        .tickSizeInner(-width);

    generators.shared.yAxis = d3.axisLeft(generators.shared.yFocus)
                                  .tickFormat(d3.format('.2f')) 
                                    .tickSize(1);

    generators.shared.area = d3.line()
                                .x(function(d){return generators.shared.xFocus(d.year);})
                                  .y(function(d){return generators.federal.yFocus(d.taxRate);})
                                    .curve(d3.curveMonotoneX);
    var lineContext = d3
      .line()
        .x((d) => generators.shared.xContext(d.year))
          .y(d => generators.shared.yContext(d.taxRate));
    generators.local.area = d3.line(localTaxData)
                              .x(function(d){return generators.shared.xFocus(d.year);})
                                .y(function(d){return generators.federal.yFocus(d.taxRate);})
                                  .curve(d3.curveMonotoneX);
    generators.state.area = d3.line(stateTaxData)
                              .x(function(d){return generators.shared.xFocus(d.year);})
                                .y(function(d){return generators.federal.yFocus(d.taxRate);})
                                  .curve(d3.curveMonotoneX);
    generators.federal.area = d3.line(federalTaxData)
                                .x(function(d){return generators.shared.xFocus(d.year);})
                                  .y(function(d){return generators.federal.yFocus(d.taxRate);})  
                                    .curve(d3.curveMonotoneX);

    // build brush
    var brush = d3.brushX()
        .extent([[0, -10],[width, contextChartHeight],])
          .on("brush end", brushed);
    // build zoom for the focus chart
    // as specified in "filter" - zooming in/out can be done by pinching on the trackpad while mouse is over focus chart
    // zooming in can also be done by double clicking while mouse is over focus chart
    var zoom = d3.zoom()
        .scaleExtent([1, Infinity])
          .translateExtent([[0, 0],[chartWidth, focusChartHeight],])
            .extent([[0, 0],[chartWidth, focusChartHeight],])
              .on("zoom", zoomed)
                .filter(() => d3.event.ctrlKey || d3.event.type === "dblclick" || d3.event.type === "mousedown");

    // clip is created so when the focus chart is zoomed in the data lines don't extend past the borders
    var clip = svg
        .append("defs")
          .append("svg:clipPath")
            .attr("id", "clip")
              .append("svg:rect")
                .attr("width", width )
                  .attr("height", height)
                    .attr("x", 0)
                      .attr("y", 0);

    // append the clip
    var focusChartLines = svg
        .append("g")
          .attr("class", "focus")
            .attr("transform", "translate(" + focusChartMargin.left + "," + focusChartMargin.top + ")")
              .attr("clip-path", "url(#clip)");
    // create focus chart
    var focus = svg
        .append("g")
          .attr("class", "focus")
            .attr("transform", "translate(" + focusChartMargin.left + "," + focusChartMargin.top + ")");

    var context = svg
        .append("g")
          .attr("class", "context")
            .attr("transform", "translate(" + contextChartMargin.left + "," + (contextChartMargin.top) + ")");

    // xFocus.domain(d3.extent(dates));
    // yFocus.domain([0, maxYAxisValue]);
    // xContext.domain(d3.extent(dates));
    // yContext.domain(yFocus.domain());

    focus.append('g')
      .attr('class','x-axis')
        .attr('transform', 'translate(0,' + (height - 55) + ')')
          .call(generators.shared.xAxisFocus);

    focus.selectAll('.eachFocus').data(taxDataByType).enter()
      .append('g')
        .attr('transform', function(d, i){ return 'translate(0, ' + ((chartHeight + chartPadding) * i) + ')'; })
          .attr('class', 'eachFocus')
            .call(generators.federal.yAxis);
    var allFocus = focusChartLines.selectAll('.eachFocus').data(taxDataByType).enter()
      .append('g')
        .attr('transform', function(d, i){ return 'translate(0, ' + ((chartHeight + chartPadding) * i) + ')'; })
          .attr('class', 'eachFocus')
            .call(generators.federal.yAxis);

    allFocus.append('path')
        .attr('d', function(d){return generators[d.key].area(d.values);})
          .attr('class', 'stack-container')
            .attr('stroke', '#1c1f1d')
            .attr("stroke-width", "1")
              .attr("fill", "none");

    context
      .append("g")
        .attr("class", "x-axis")
          .attr("transform", "translate(0," + contextChartHeight + ")")
            .call(generators.shared.xAxisContex);
  // go through data and create/append lines to both charts

  for (let key of [0, 1, 2]) {
    let bucket = taxDataByType[key].values;
    var label = taxDataByType[key].key;
    context
      .append("path")
      .datum(bucket)
      .attr("class", "line")
      .attr("fill", "none")
      .attr("stroke", "black")
      .attr('stroke', (d)=>{return generators.shared.color(label);})
      .attr("stroke-width", 1.5)
       .attr("d", lineContext);
  }

  var contextBrush = context
    .append("g")
      .attr("class", "brush")
        .call(brush);


  var brushHandlePath = d => {
    var e = +(d.type === "e"),
      x = e ? 1 : -1,
      y = contextChartHeight + 10 ;
    return (
      "M" + 0.5 * x + "," + y +"A6,6 0 0 " +e +" " +6.5 * x +"," +(y + 6) +"V" +(2 * y - 6) +"A6,6 0 0 " +e +" " +0.5 * x +"," +2 * y +
      "Z" +"M" +2.5 * x +"," +(y + 8) +"V" +(2 * y - 8) +"M" +4.5 * x +"," +(y + 8) +"V" +(2 * y - 8)
    );
  };

  var brushHandle = contextBrush
    .selectAll(".handle--custom")
      .data([{ type: "w" }, { type: "e" }])
        .enter()
          .append("path")
            .attr("class", "handle--custom")
              .attr("stroke", "#000")
                .attr("cursor", "ew-resize")
                  .attr("d", brushHandlePath);


    svg
      .append("rect")
        .attr("cursor", "move")
          .attr("fill", "none")
            .attr("pointer-events", "all")
              .attr("class", "zoom")
                .attr("width", width)
                  .attr("height", height)
                    .attr("transform", "translate(" + focusChartMargin.left + "," + focusChartMargin.top + ")")
                      .call(zoom);

  contextBrush.call(brush.move, [0, width / 2]);




// functions
function brushed() {
  console.log("brush");
  var xContext = generators.shared.xContext;
  var xAxisFocus = generators.shared.xAxisFocus;
  if (d3.event.sourceEvent && d3.event.sourceEvent.type === "zoom") return; // ignore brush-by-zoom
  var s = d3.event.selection || xContext.range();
  xContext.domain(s.map(xContext.invert, xContext));

  allFocus.selectAll(".eachFocus").attr("d", generators.local.area);
  allFocus.select(".x-axis").call(xAxisFocus);
  svg.select(".zoom").call(zoom.transform, d3.zoomIdentity.scale(width / (s[1] - s[0])).translate(-s[0], 0));
  brushHandle
    .attr("display", null)
      .attr("transform", (d, i) => "translate(" + [s[i], -contextChartHeight - 20] + ")");
}
function zoomed() {
  console.log("zoomed");
  if (d3.event.sourceEvent && d3.event.sourceEvent.type === "brush") return; // ignore zoom-by-brush
  var t = d3.event.transform;
  var xContext = generators.shared.xContext;
  var xAxisFocus = generators.shared.xAxisFocus;
  var xFocus= generators.shared.xFocus;
  xFocus.domain(t.rescaleX(xContext).domain());
  allFocus.selectAll(".line").attr("d", generators.local.area);
  allFocus.select(".x-axis").call(xAxisFocus);
  var brushSelection = xFocus.range().map(t.invertX, t);
  context.select(".brush").call(brush.move, brushSelection);
  brushHandle
    .attr("display", null)
      .attr("transform", (d, i) => "translate(" + [brushSelection[i], -contextChartHeight - 20] + ")");
}

Хотя отображаются все графики, никакого взаимодействия не происходит. Я думаю, это потому, что я еще не понимаю, как работают bru sh и масштабирование. Несмотря на то, что я много читаю об этом топи c, я все еще не могу с ним справиться. Может ли кто-нибудь обратиться ко мне по правильному пути? Заранее спасибо.

...