【问题标题】:How to plot data points on multi-line chart with multiple y axes如何在具有多个 y 轴的多折线图上绘制数据点
【发布时间】:2019-11-13 03:24:25
【问题描述】:

我正在尝试将数据点添加到具有多个 y 轴的折线图中。单击here 获取我的小提琴。

    //after restructuring dataset array
    var data = [{
      data: [{
        x: 0,
        y: 0
      }, {
        x: 10,
        y: 10
      }, {
        x: 20,
        y: 20
      }, {
        x: 30,
        y: 30
      }, {
        x: 40,
        y: 40
      }],
      yAxis: 0,
    }, {
      data: [{
        x: 0,
        y: 0
      }, {
        x: 10,
        y: 200
      }, {
        x: 20,
        y: 300
      }, {
        x: 30,
        y: 400
      }, {
        x: 40,
        y: 500
      }],
      yAxis: 1,
    }];

    const margin = {
      left: 20,
      right: 20,
      top: 20,
      bottom: 80
    };

    const svg = d3.select('svg');
    svg.selectAll("*").remove();

    const width = 200 - margin.left - margin.right;
    const height = 200 - margin.top - margin.bottom;
    const g = svg.append('g').attr('transform', `translate(${80},${margin.top})`);

    //************* Axes and Gridlines ***************
    const xAxisG = g.append('g');
    const yAxisG = g.append('g');

    xAxisG.append('text')
      .attr('class', 'axis-label')
      .attr('x', width / 3)
      .attr('y', -10)
      .style('fill', 'black')
      .text(function(d) {
      return "X Axis";
    });

    yAxisG.append('text')
      .attr('class', 'axis-label')
      .attr('id', 'yAxisLabel0')
      .attr('x', -height / 2)
      .attr('y', -15)
      .attr('transform', `rotate(-90)`)
      .style('text-anchor', 'middle')
      .style('fill', 'black')
      .text(function(d) {
      return "Y Axis 1";
    });

    // interpolator for X axis -- inner plot region
    var x = d3.scaleLinear()
    .domain([0, d3.max(xValueArray)])
    .range([0, width])
    .nice();

    var yScale = new Array();
    for (var i = 0; i < 2; i++) {
      // interpolator for Y axis -- inner plot region
      var y = d3.scaleLinear()
      .domain([0, d3.max(arr[i])])
      .range([0, height])
      .nice();
      yScale.push(y);
    }

    const xAxis = d3.axisTop()
    .scale(x)
    .ticks(5)
    .tickPadding(2)
    .tickSize(-height)

    const yAxis = d3.axisLeft()
    .scale(yScale[0])
    .ticks(5)
    .tickPadding(2)
    .tickSize(-width);

    yAxisArray = new Array();
    yAxisArray.push(yAxis);
    for (var i = 1; i < 2; i++) {
      var yAxisSecondary = d3.axisLeft()
      .scale(yScale[i])
      .ticks(5)
      yAxisArray.push(yAxisSecondary);
    }

    svg.append("g")
      .attr("class", "x axis")
      .attr("transform", `translate(80,${height-80})`)
      .call(xAxis);

    svg.append("g")
      .attr("class", "y axis")
      .attr("id", "ySecAxis0")
      .attr("transform", "translate(80,20)")
      .call(yAxis);

    var translation = 50;
    var textTranslation = 0;
    var yLabelArray = ["Y Axis 1", "Y Axis 2"];

    //loop starts from 1 as primary y axis is already plotted
    for (var i = 1; i < 2; i++) {
      svg.append("g")
        .attr("transform", "translate(" + translation + "," + 20 + ")")
        .attr("id", "ySecAxis" + i)
        .call(yAxisArray[i]);

      yAxisG.append('text')
        .attr('x', -height / 2)
        .attr('y', -60)
        .attr('transform', `rotate(-90)`)
        .attr("id", "yAxisLabel" + i)
        .style('text-anchor', 'middle')
        .style('fill', 'black')
        .text(yLabelArray[i]);

      translation -= 40;
      textTranslation += 40;
    }

    //************* Lines and Data Points ***************
    var colors = ["blue", "red"];

    var thisScale;

    var line = d3.line()
      .x(d => x(d.x))
      .y(d => thisScale(d.y))
      .curve(d3.curveLinear);

    var paths = g.selectAll("foo")
      .data(data)
      .enter()
      .append("path");

    paths.attr("stroke", function (d,i){return colors[i]})
      .attr("d", d => {
        thisScale = yScale[d.yAxis]
        return line(d.data);
      })
      .attr("stroke-width", 2)
      .attr("id", function (d,i){return "line" + i})
      .attr("fill", "none");

    var points = g.selectAll("dot")
      .data(data)
      .enter()
      .append("circle");

    points.attr("cx", function(d) { return x(d.x)} )
      .attr("cy", function(d,i) { return yScale[i](d.y); } )
      .attr("r", 3)
      .attr("class", function (d,i){return "blackDot" + i})
      .attr("clip-path", "url(#clip)")

现在控制台日志显示以下错误:错误:属性 cx:预期长度,“NaN”。错误:属性 cy:预期长度,“NaN”。 似乎我没有将正确的 cx 和 cy 归因于点,但我无法弄清楚我做错了什么。非常感谢任何帮助!

【问题讨论】:

    标签: javascript d3.js


    【解决方案1】:

    您的数据结构是一个对象数组,每个对象都包含一个内部数组,其中包含圆的真实坐标。因此,单个输入选择将不起作用。

    通过最少的重构,我的解决方案是根据对象附加组,然后根据内部数组为每个组附加圆圈。为了让那个麻烦的yScale 工作,你不能再依赖圆圈的索引了,所以我在这里使用了一个局部变量:

    var pointsGroup = g.selectAll(null)
      .data(data)
      .enter()
      .append("g")
      .attr("fill", function(d, i) {
        local.set(this, yScale[i])
        return colors[i];
      });
    
    var points = pointsGroup.selectAll(null)
      .data(function(d) {
        return d.data
      })
      .enter()
      .append("circle")
      .attr("cx", function(d) {
        return x(d.x)
      })
      .attr("cy", function(d, i) {
        return local.get(this)(d.y);
      })
      //etc...
    

    以下是这些更改的代码:

    var local = d3.local();
    var xValueArray = [0, 10, 20, 30, 40];
    var arr = [
      [0, 10, 20, 30, 40],
      [0, 200, 300, 400, 500]
    ];
    var dataset = [
      [{
        x: 0,
        y: 0
      }, {
        x: 10,
        y: 10
      }, {
        x: 20,
        y: 20
      }, {
        x: 30,
        y: 30
      }, {
        x: 40,
        y: 40
      }],
      [{
        x: 0,
        y: 0
      }, {
        x: 10,
        y: 200
      }, {
        x: 20,
        y: 300
      }, {
        x: 30,
        y: 400
      }, {
        x: 40,
        y: 500
      }]
    ];
    
    var data = [];
    for (var i = 0; i < 2; i++) {
      data.push({
        "data": dataset[i],
        "yAxis": i
      })
    }
    console.log(data);
    
    //after restructuring dataset array
    var data = [{
      data: [{
        x: 0,
        y: 0
      }, {
        x: 10,
        y: 10
      }, {
        x: 20,
        y: 20
      }, {
        x: 30,
        y: 30
      }, {
        x: 40,
        y: 40
      }],
      yAxis: 0,
    }, {
      data: [{
        x: 0,
        y: 0
      }, {
        x: 10,
        y: 200
      }, {
        x: 20,
        y: 300
      }, {
        x: 30,
        y: 400
      }, {
        x: 40,
        y: 500
      }],
      yAxis: 1,
    }];
    
    const margin = {
      left: 20,
      right: 20,
      top: 20,
      bottom: 80
    };
    
    const svg = d3.select('svg');
    svg.selectAll("*").remove();
    
    const width = 200 - margin.left - margin.right;
    const height = 200 - margin.top - margin.bottom;
    const g = svg.append('g').attr('transform', `translate(${80},${margin.top})`);
    
    //************* Axes and Gridlines ***************
    const xAxisG = g.append('g');
    const yAxisG = g.append('g');
    
    xAxisG.append('text')
      .attr('class', 'axis-label')
      .attr('x', width / 3)
      .attr('y', -10)
      .style('fill', 'black')
      .text(function(d) {
        return "X Axis";
      });
    
    yAxisG.append('text')
      .attr('class', 'axis-label')
      .attr('id', 'yAxisLabel0')
      .attr('x', -height / 2)
      .attr('y', -15)
      .attr('transform', `rotate(-90)`)
      .style('text-anchor', 'middle')
      .style('fill', 'black')
      .text(function(d) {
        return "Y Axis 1";
      });
    
    // interpolator for X axis -- inner plot region
    var x = d3.scaleLinear()
      .domain([0, d3.max(xValueArray)])
      .range([0, width])
      .nice();
    
    var yScale = new Array();
    for (var i = 0; i < 2; i++) {
      // interpolator for Y axis -- inner plot region
      var y = d3.scaleLinear()
        .domain([0, d3.max(arr[i])])
        .range([0, height])
        .nice();
      yScale.push(y);
    }
    
    const xAxis = d3.axisTop()
      .scale(x)
      .ticks(5)
      .tickPadding(2)
      .tickSize(-height)
    
    const yAxis = d3.axisLeft()
      .scale(yScale[0])
      .ticks(5)
      .tickPadding(2)
      .tickSize(-width);
    
    yAxisArray = new Array();
    yAxisArray.push(yAxis);
    for (var i = 1; i < 2; i++) {
      var yAxisSecondary = d3.axisLeft()
        .scale(yScale[i])
        .ticks(5)
      yAxisArray.push(yAxisSecondary);
    }
    
    svg.append("g")
      .attr("class", "x axis")
      .attr("transform", `translate(80,${height-80})`)
      .call(xAxis);
    
    svg.append("g")
      .attr("class", "y axis")
      .attr("id", "ySecAxis0")
      .attr("transform", "translate(80,20)")
      .call(yAxis);
    
    var translation = 50;
    var textTranslation = 0;
    var yLabelArray = ["Y Axis 1", "Y Axis 2"];
    
    //loop starts from 1 as primary y axis is already plotted
    for (var i = 1; i < 2; i++) {
      svg.append("g")
        .attr("transform", "translate(" + translation + "," + 20 + ")")
        .attr("id", "ySecAxis" + i)
        .call(yAxisArray[i]);
    
      yAxisG.append('text')
        .attr('x', -height / 2)
        .attr('y', -60)
        .attr('transform', `rotate(-90)`)
        .attr("id", "yAxisLabel" + i)
        .style('text-anchor', 'middle')
        .style('fill', 'black')
        .text(yLabelArray[i]);
    
      translation -= 40;
      textTranslation += 40;
    }
    
    //************* Mouseover ***************
    var tooltip = d3.select("body")
      .append("div")
      .style("opacity", 0)
      .attr("class", "tooltip")
      .style("background-color", "white")
      .style("border", "solid")
      .style("border-width", "1px")
      .style("border-radius", "5px")
      .style("padding", "10px")
      .style("position", "absolute")
    
    var mouseover = function(d) {
      tooltip
        .html("x: " + d.x + "<br/>" + "y: " + d.y)
        .style("opacity", 1)
        .style("left", (d3.mouse(this)[0] + 90) + "px")
        .style("top", (d3.mouse(this)[1]) + "px")
    }
    
    // A function that change this tooltip when the leaves a point: just need to set opacity to 0 again
    var mouseleave = function(d) {
      tooltip
        .transition()
        .duration(200)
        .style("opacity", 0)
    }
    
    //************* Lines and Data Points ***************
    var colors = ["blue", "red"];
    
    var thisScale;
    
    var line = d3.line()
      .x(d => x(d.x))
      .y(d => thisScale(d.y))
      .curve(d3.curveLinear);
    
    var paths = g.selectAll("foo")
      .data(data)
      .enter()
      .append("path");
    
    paths.attr("stroke", function(d, i) {
        return colors[i]
      })
      .attr("d", d => {
        thisScale = yScale[d.yAxis]
        return line(d.data);
      })
      .attr("stroke-width", 2)
      .attr("id", function(d, i) {
        return "line" + i
      })
      .attr("fill", "none");
    
    var pointsGroup = g.selectAll(null)
      .data(data)
      .enter()
      .append("g")
      .attr("fill", function(d, i) {
        local.set(this, yScale[i])
        return colors[i];
      });
    
    var points = pointsGroup.selectAll(null)
      .data(function(d) {
        return d.data
      })
      .enter()
      .append("circle")
      .attr("cx", function(d) {
        return x(d.x)
      })
      .attr("cy", function(d, i) {
        return local.get(this)(d.y);
      })
      .attr("r", 3)
      .attr("class", function(d, i) {
        return "blackDot" + i
      })
      .attr("clip-path", "url(#clip)")
      .on("mouseover", mouseover)
      .on("mouseleave", mouseleave)
    
    //plot lines (hard-coding)
    /*var lineFunction1 = d3.line()
    .x(function(d) {
      return x(d.x);
    })
    .y(function(d) {
      return yScale[0](d.y);
    })
    .curve(d3.curveLinear);
    
    var lineFunction2 = d3.line()
    .x(function(d) {
      return x(d.x);
    })
    .y(function(d) {
      return yScale[1](d.y);
    })
    .curve(d3.curveLinear);
    
    var path1 = g.append("path")
    .attr("class", "path" + 0)
    .attr("id", "line" + 0)
    .attr("d", lineFunction1(data[0]))
    .attr("stroke", "blue")
    .attr("stroke-width", 2)
    .attr("fill", "none")
    .attr("clip-path", "url(#clip)");
    
    var path2 = g.append("path")
    .attr("class", "path" + 1)
    .attr("id", "line" + 1)
    .attr("d", lineFunction2(data[1]))
    .attr("stroke", "red")
    .attr("stroke-width", 2)
    .attr("fill", "none")
    .attr("clip-path", "url(#clip)");*/
    
    //plot lines and points using for loop
    /*for (var i = 0; i < 2; i++) {
      var lineFunction = d3.line()
      .x(function(d) {
        return x(d.x);
      })
      .y(function(d) {
        return yScale[i](d.y);
      })
      .curve(d3.curveLinear);
    
      var paths = g.append("path")
      .attr("class", "path" + i)
      .attr("id", "line" + i)
      .attr("d", lineFunction(data[i]))
      .attr("stroke", colors[i])
      .attr("stroke-width", 2)
      .attr("fill", "none")
      .attr("clip-path", "url(#clip)")
    
      //plot a circle at each data point
      g.selectAll(".dot")
        .data(data[i])
        .enter().append("circle")
        .attr("cx", function(d) { return x(d.x)} )
        .attr("cy", function(d) { return yScale[i](d.y); } )
        .attr("r", 3)
        .attr("class", "blackDot" + i)
        .attr("clip-path", "url(#clip)")
        .on("mouseover", mouseover)
        .on("mouseleave", mouseleave)
    }*/
    
    //************* Legend ***************
    var legend = svg.selectAll(".legend")
      .data(data)
      .enter().append("g")
    
    legend.append("rect")
      .attr("x", width + 65)
      .attr("y", function(d, i) {
        return 30 + i * 20;
      })
      .attr("width", 18)
      .attr("height", 4)
      .style("fill", function(d, i) {
        return colors[i];
      })
    
    legend.append("text")
      .attr("x", width + 60)
      .attr("y", function(d, i) {
        return 30 + i * 20;
      })
      .attr("dy", ".35em")
      .style("text-anchor", "end")
      .text(function(d, i) {
        return "Value" + (i + 1);
      })
      .on("click", function(d, i) {
        // Determine if current line is visible
        let opacity = d3.select("#line" + i).style("opacity");
        let newOpacity;
        if (opacity == 0) {
          newOpacity = 1;
        } else {
          newOpacity = 0
        }
        d3.select("#line" + i).style("opacity", newOpacity);
        d3.selectAll(".blackDot" + i).style("opacity", newOpacity);
        d3.select("#ySecAxis" + i).style("opacity", newOpacity);
        d3.select("#yAxisLabel" + i).style("opacity", newOpacity);
      });
    
    //************* Zoom & Brush***************
    const margin2 = {
      left: 80,
      right: 0,
      top: 80,
      bottom: 0
    };
    const height2 = height - margin2.top - margin2.bottom;
    var xZoom = d3.scaleLinear().range([0, width]);
    var yZoom = d3.scaleLinear().range([height2, 0]);
    
    var xAxis2 = d3.axisTop(xZoom);
    
    var brush = d3.brushX()
      .extent([
        [0, 0],
        [width, height2]
      ])
      .on("brush end", brushed);
    
    var zoom = d3.zoom()
      .scaleExtent([1, Infinity])
      .translateExtent([
        [0, 0],
        [width, height]
      ])
      .extent([
        [0, 0],
        [width, height]
      ])
      .on("zoom", zoomed);
    
    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);
    
    xZoom.domain(x.domain());
    yZoom.domain(y.domain());
    
    var context = svg.append("g")
      .attr("class", "context")
      .attr("transform", "translate(" + margin2.left + "," + 125 + ")");
    
    context.append("g")
      .attr("class", "axis axis--x")
      .attr("transform", "translate(0," + height2 + ")")
      .call(xAxis2);
    
    context.append("g")
      .attr("class", "brush")
      .call(brush)
      .call(brush.move, x.range());
    
    function brushed() {
      if (d3.event.sourceEvent && d3.event.sourceEvent.type === "zoom") return;
      var s = d3.event.selection || xZoom.range();
      x.domain(s.map(xZoom.invert, xZoom));
      svg.select(".x.axis").call(xAxis);
      //svg.select(".path0").attr("d", lineFunction1(data[0]));
      //svg.select(".path1").attr("d", lineFunction2(data[1]));
      for (var i = 0; i < 2; i++) {
        //svg.select(".path" + i).attr("d", lineFunction(data[i]));
        g.selectAll(".blackDot" + i)
          .attr("cx", function(d) {
            return x(d.x);
          })
          .attr("cy", function(d) {
            return yScale[i](d.y);
          })
          .attr("r", 3)
      }
    }
    
    function zoomed() {
      if (d3.event.sourceEvent && d3.event.sourceEvent.type === "brush") return;
      var t = d3.event.transform;
      x.domain(t.rescaleX(xZoom).domain());
      svg.select(".x.axis").transiton(t).call(xAxis);
      //svg.select(".path0").transiton(t).attr("d", lineFunction1(data[0]));
      //svg.select(".path1").transiton(t).attr("d", lineFunction2(data[1]));
      for (var i = 0; i < 2; i++) {
        //svg.select(".path" + i).attr("d", lineFunction(data[i]));
        g.selectAll(".blackDot" + i)
          .attr("cx", function(d) {
            return x(d.x);
          })
          .attr("cy", function(d) {
            return yScale[i](d.y);
          })
          .attr("r", 3)
      }
    }
    .xy_chart {
      position: relative;
      left: 70px;
      top: 100px;
    }
    <script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.13.0/d3.min.js"></script>
    <svg class="xy_chart"></svg>

    请注意其中一个圆圈的cy 值不正确。所以,我建议你改变你的 y 比例方法。

    【讨论】:

    • 感谢您的回答!但是,当我的数据集在 (10,10) 处正确时,为什么在 (10, 0) 处的 cy 值不正确?
    • 因为你的 zoomed 函数。你读过我在最后一段中写的吗?
    • 是的,我做到了,只是无法弄清楚是什么原因造成的。无论如何,您是否知道放大线和点的解决方案?还是我必须提出另一个问题?
    • @ChewKahMeng 是的,很遗憾,您必须发布另一个问题:cmets 部分只是为了澄清。另外,请注意,您不能仅仅为了节省时间而提出一个包含多个问题的问题:该问题会因为过于宽泛而被关闭。在 S.O.我们保留每个问题一个问题
    • 注明。 @GerardoFurtado,我已经提出了另一个关于切换点 here 的问题。希望您能提供一些见解,谢谢!
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-05-18
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多