javascript - 如何在具有多个 y 轴的多折线图上绘制数据点

标签 javascript d3.js

我正在尝试将数据点添加到具有多个 y 轴的折线图中。点击here为了我的 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 归因于点,但我无法弄清楚我做错了什么。非常感谢任何帮助!

最佳答案

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

通过最少的重构,我这里的解决方案是根据对象附加组,然后,对于每个对象,根据内部数组附加圆圈。要使繁琐的 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 比例方法。

关于javascript - 如何在具有多个 y 轴的多折线图上绘制数据点,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/58829869/

相关文章:

javascript - 将网页保存为 pdf 格式,与在浏览器中的外观完全相同

javascript - 如何获取d3.js Treemap布局中元素的对象属性onclick

javascript - 在 D3 中使用不同颜色更改每个堆叠条形图的颜色

javascript - 使用 webpack 在 html 中通过 src 加载图像

javascript - 如何在 JavaScript 中引用函数体内当前正在执行的函数

javascript - SVG 在浏览器上不显示任何内容

javascript - 当根节点传递给它时,d3 TreeMap 布局是否被缓存?

javascript - d3.按属性值选择

javascript - 计算并自动填充动态元素

javascript - 如何编写一个正则表达式 'and NO whitespaces' ?