如何在具有多个y轴的多折线图上绘制数据点 [英] How to plot data points on multi-line chart with multiple y axes

查看:81
本文介绍了如何在具有多个y轴的多折线图上绘制数据点的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我正在尝试将数据点添加到具有多个y轴的折线图中.单击此处作为我的小提琴.

I am trying to add data points to my line chart with multiple y axes. Click here for my fiddle.

    //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归因于点,但是我无法弄清楚自己在做错什么.任何帮助,我们将不胜感激!

Right now the console log is showing these errors: Error: attribute cx: Expected length, "NaN". Error: attribute cy: Expected length, "NaN". It seems like I am not attributing the correct cx and cy to points, but I can't figure out what I am doing wrongly. Any help is greatly appreciated!

推荐答案

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

Your data structure is an array of objects, each one containing an inner array with the real coordinates for the circles. Therefore, that single enter selection will not work.

通过最少的重构,我的解决方案是根据对象添加组,然后针对每个对象,根据内部数组添加圆.为了使yScale繁琐,您不能再依赖圆的索引了,所以我在这里使用局部变量:

With minimal refactoring, my solution here is appending groups according to the objects, and then, for each one, appending circles according to the inner arrays. For that cumbersome yScale to work you cannot rely on the circle's indices anymore, so I'm using a local variable here:

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...

以下是具有这些更改的代码:

Here is the code with those changes:

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标度方法.

Pay attention to the fact that one of the circles has an incorrect cy value. So, I'd suggest you to change your y scale approach.

这篇关于如何在具有多个y轴的多折线图上绘制数据点的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆