如何在d3的每行中制作带有虚线段的多线图表

how to make multiline chart with dashed line segment in each line in d3

数据如下所示:

var data = [
  {
    name: 'USA',
    values: [
      { date: '2000', price: '100', isDashed:true },
      { date: '2001', price: '110', isDashed:true },
      { date: '2002', price: '145', isDashed:false },...
      ,
  {
    name: 'Canada',
    values: [
      { date: '2000', price: '200',isDashed:true  },
      { date: '2001', price: '120',isDashed:true  },
      { date: '2002', price: '33',isDashed:true  }...
    ]
  }
]

我正在关注这个 link https://codesandbox.io/s/multi-line-chart-example-forked-hjix5?file=/src/index.js

问题是,在我的数据中有一个标志 isDashed 当它为真时我需要在图表中像这样(例如)虚线段。

var data = [{
    name: "USA",
    values: [{
        date: "2000",
        price: "100",
        r: 1
      },
      {
        date: "2001",
        price: "110",
        r: 1
      },
      {
        date: "2002",
        price: "145"
      },
      {
        date: "2003",
        price: "241"
      },
      {
        date: "2004",
        price: "101"
      },
      {
        date: "2005",
        price: "90"
      },
      {
        date: "2006",
        price: "10"
      },
      {
        date: "2007",
        price: "35"
      },
      {
        date: "2008",
        price: "21"
      },
      {
        date: "2009",
        price: "201"
      }
    ]
  },
  {
    name: "Canada",
    values: [{
        date: "2000",
        price: "200"
      },
      {
        date: "2001",
        price: "120"
      },
      {
        date: "2002",
        price: "33"
      },
      {
        date: "2003",
        price: "21"
      },
      {
        date: "2004",
        price: "51"
      },
      {
        date: "2005",
        price: "190"
      },
      {
        date: "2006",
        price: "120"
      },
      {
        date: "2007",
        price: "85"
      },
      {
        date: "2008",
        price: "221"
      },
      {
        date: "2009",
        price: "101"
      }
    ]
  },
  {
    name: "Maxico",
    values: [{
        date: "2000",
        price: "50"
      },
      {
        date: "2001",
        price: "10"
      },
      {
        date: "2002",
        price: "5"
      },
      {
        date: "2003",
        price: "71"
      },
      {
        date: "2004",
        price: "20"
      },
      {
        date: "2005",
        price: "9"
      },
      {
        date: "2006",
        price: "220"
      },
      {
        date: "2007",
        price: "235"
      },
      {
        date: "2008",
        price: "61"
      },
      {
        date: "2009",
        price: "10"
      }
    ]
  }
];

var width = 500;
var height = 300;
var margin = 50;
var duration = 250;

var lineOpacity = "0.25";
var lineOpacityHover = "0.85";
var otherLinesOpacityHover = "0.1";
var lineStroke = "1.5px";
var lineStrokeHover = "2.5px";

var circleOpacity = "0.85";
var circleOpacityOnLineHover = "0.25";
var circleRadius = 3;
var circleRadiusHover = 6;

/* Format Data */
var parseDate = d3.timeParse("%Y");
data.forEach(function(d) {
  d.values.forEach(function(d) {
    d.date = parseDate(d.date);
    d.price = +d.price;
    d.r = +d.r;
  });
});

/* Scale */
var xScale = d3
  .scaleTime()
  .domain(d3.extent(data[0].values, (d) => d.date))
  .range([0, width - margin]);

var yScale = d3
  .scaleLinear()
  .domain([0, d3.max(data[0].values, (d) => d.price)])
  .range([height - margin, 0]);

var color = d3.scaleOrdinal(d3.schemeCategory10);

/* Add SVG */
var svg = d3
  .select("#chart")
  .append("svg")
  .attr("width", width + margin + "px")
  .attr("height", height + margin + "px")
  .append("g")
  .attr("transform", `translate(${margin}, ${margin})`);

/* Add line into SVG */
var line = d3
  .line()
  .x((d) => xScale(d.date))
  .y((d) => yScale(d.price));

let lines = svg.append("g").attr("class", "lines");

lines
  .selectAll(".line-group")
  .data(data)
  .enter()
  .append("g")
  .attr("class", "line-group")
  .on("mouseover", function(d, i) {
    svg
      .append("text")
      .attr("class", "title-text")
      .style("fill", color(i))
      .text(d.name)
      .attr("text-anchor", "middle")
      .attr("x", (width - margin) / 2)
      .attr("y", 5)
      .attr("r", 5);
  })
  .on("mouseout", function(d) {
    svg.select(".title-text").remove();
  })
  .append("path")
  .attr("class", "line")
  .attr("d", (d) => line(d.values))

  .style("stroke", (d, i) => color(i))
  .style("opacity", lineOpacity)
  .on("mouseover", function(d) {
    d3.selectAll(".line").style("opacity", otherLinesOpacityHover);
    d3.selectAll(".circle").style("opacity", circleOpacityOnLineHover);
    d3.select(this)
      .style("opacity", lineOpacityHover)
      .style("stroke-width", lineStrokeHover)
      .style("cursor", "pointer");
  })
  .on("mouseout", function(d) {
    d3.selectAll(".line").style("opacity", lineOpacity);
    d3.selectAll(".circle").style("opacity", circleOpacity);
    d3.select(this)
      .style("stroke-width", lineStroke)
      .style("cursor", "none");
  });

/* Add circles in the line */
lines
  .selectAll("circle-group")
  .data(data)
  .enter()
  .append("g")
  .style("fill", (d, i) => color(i))
  .selectAll("circle")
  .data((d) => d.values)
  .enter()
  .append("g")
  .attr("class", "circle")
  .on("mouseover", function(d) {
    d3.select(this)
      .style("cursor", "pointer")
      .append("text")
      .attr("class", "text")
      .text(`${d.price}`)
      .attr("x", (d) => xScale(d.date) + 5)
      .attr("y", (d) => yScale(d.price) - 10);
  })
  .on("mouseout", function(d) {
    d3.select(this)
      .style("cursor", "none")
      .transition()
      .duration(duration)
      .selectAll(".text")
      .remove();
  })
  .append("circle")
  .attr("cx", (d) => xScale(d.date))
  .attr("cy", (d) => yScale(d.price))
  .attr("r", circleRadius)
  .style("opacity", circleOpacity)
  .on("mouseover", function(d) {
    d3.select(this)
      .transition()
      .duration(duration)
      .attr("r", circleRadiusHover);
  })
  .on("mouseout", function(d) {
    d3.select(this).transition().duration(duration).attr("r", circleRadius);
  });

/* Add Axis into SVG */
var xAxis = d3.axisBottom(xScale).ticks(5);
var yAxis = d3.axisLeft(yScale).ticks(5);

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

svg
  .append("g")
  .attr("class", "y axis")
  .call(yAxis)
  .append("text")
  .attr("y", 15)
  .attr("transform", "rotate(-90)")
  .attr("fill", "#000")
  .text("Total values");
.App {
  font-family: sans-serif;
  text-align: center;
}

svg {
  font-family: Sans-Serif, Arial;
}

.line {
  stroke-width: 2;
  fill: none;
}

.axis path {
  stroke: black;
}

.text {
  font-size: 12px;
}

.title-text {
  font-size: 12px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<div class="App">
  <div id="chart" />
</div>

正如我在评论中所说,您似乎希望线段既是虚线又是稍微亮一点的颜色。最简单的解决方法是只绘制两条单独的线段。更难的解决方案是使用 stroke-dasharray 和线性渐变的组合作为描边,但这可能是 bug-prone.

由于您似乎不太关心,下面的代码显示了更简单的解决方法:

var data = [{
    name: "USA",
    values: [{
        date: "2000",
        price: "100",
        r: 1,
      isDashed: true
      },
      {
        date: "2001",
        price: "110",
        r: 1,
        isDashed: true
      },
      {
        date: "2002",
        price: "145",
        isDashed: true
      },
      {
        date: "2003",
        price: "241",
        isDashed: true
      },
      {
        date: "2004",
        price: "101",
        isDashed: true
      },
      {
        date: "2005",
        price: "90",
        isDashed: true
      },
      {
        date: "2006",
        price: "10",
        isDashed: true
      },
      {
        date: "2007",
        price: "35"
      },
      {
        date: "2008",
        price: "21"
      },
      {
        date: "2009",
        price: "201"
      }
    ]
  },
  {
    name: "Canada",
    values: [{
        date: "2000",
        price: "200"
      },
      {
        date: "2001",
        price: "120"
      },
      {
        date: "2002",
        price: "33"
      },
      {
        date: "2003",
        price: "21"
      },
      {
        date: "2004",
        price: "51",
        isDashed: true
      },
      {
        date: "2005",
        price: "190",
        isDashed: true
      },
      {
        date: "2006",
        price: "120",
        isDashed: true
      },
      {
        date: "2007",
        price: "85"
      },
      {
        date: "2008",
        price: "221"
      },
      {
        date: "2009",
        price: "101"
      }
    ]
  },
  {
    name: "Maxico",
    values: [{
        date: "2000",
        price: "50"
      },
      {
        date: "2001",
        price: "10"
      },
      {
        date: "2002",
        price: "5",
        isDashed: true
      },
      {
        date: "2003",
        price: "71"
      },
      {
        date: "2004",
        price: "20"
      },
      {
        date: "2005",
        price: "9"
      },
      {
        date: "2006",
        price: "220"
      },
      {
        date: "2007",
        price: "235"
      },
      {
        date: "2008",
        price: "61"
      },
      {
        date: "2009",
        price: "10"
      }
    ]
  }
];

var width = 500;
var height = 300;
var margin = 50;
var duration = 250;

var lineOpacity = "0.25";
var lineOpacityHover = "0.85";
var otherLinesOpacityHover = "0.1";
var lineStroke = "1.5px";
var lineStrokeHover = "2.5px";

var circleOpacity = "0.85";
var circleOpacityOnLineHover = "0.25";
var circleRadius = 3;
var circleRadiusHover = 6;

/* Format Data */
var parseDate = d3.timeParse("%Y");
data.forEach(function(d) {
  d.values.forEach(function(d) {
    d.date = parseDate(d.date);
    d.price = +d.price;
    d.r = +d.r;
  });
});

/* Scale */
var xScale = d3
  .scaleTime()
  .domain(d3.extent(data[0].values, (d) => d.date))
  .range([0, width - margin]);

var yScale = d3
  .scaleLinear()
  .domain([0, d3.max(data[0].values, (d) => d.price)])
  .range([height - margin, 0]);

var color = d3.scaleOrdinal(d3.schemeCategory10);

/* Add SVG */
var svg = d3
  .select("#chart")
  .append("svg")
  .attr("width", width + margin + "px")
  .attr("height", height + margin + "px")
  .append("g")
  .attr("transform", `translate(${margin}, ${margin})`);

/* Add line into SVG */
var line = d3
  .line()
  .defined(d => !isNaN(d.price)) // to make it deal with NaNs
  .x((d) => xScale(d.date))
  .y((d) => yScale(d.price));

let lines = svg.append("g").attr("class", "lines");

let lineGroups = lines
  .selectAll(".line-group")
  .data(data)
  .enter()
  .append("g")
  .attr("class", "line-group")
  .on("mouseover", function(d, i) {
    svg
      .append("text")
      .attr("class", "title-text")
      .style("fill", color(i))
      .text(d.name)
      .attr("text-anchor", "middle")
      .attr("x", (width - margin) / 2)
      .attr("y", 5)
      .attr("r", 5);
  })
  .on("mouseout", function(d) {
    svg.select(".title-text").remove();
  });

function getDashedParts(values, lookingForDashed) {
  const results = [];
  let previousWasMatch = false;
  
  // For every node we add, we need also the node just before,
  // so we can draw a line between them
  values.forEach((v, i) => {
    if(lookingForDashed === !!v.isDashed) {
      if (!previousWasMatch && i > 0) {
        results.push(values[i - 1]);
      }
      results.push(v);
      previousWasMatch = true;
    } else {
      results.push({ date: v.date, price: NaN });
      previousWasMatch = false;
    }
  });
  
  // console.log(results);
  return results;
}

lineGroups
  .append("path")
  .attr("class", "line")
  .attr("d", (d) => line(getDashedParts(d.values, false)));

lineGroups
  .append("path")
  .attr("class", "line line-dashed")
  .attr("d", (d) => line(getDashedParts(d.values, true)));

lineGroups.each(function(d, i) {
  const lineParts = d3.select(this).selectAll("path");
  lineParts
    .style("stroke", color(i))
    .style("opacity", lineOpacity)
    .on("mouseover", () => {
      d3.selectAll(".line").style("opacity", otherLinesOpacityHover);
      d3.selectAll(".circle").style("opacity", circleOpacityOnLineHover);
      lineParts
        .style("opacity", lineOpacityHover)
        .style("stroke-width", lineStrokeHover)
        .style("cursor", "pointer");
    })
    .on("mouseout", () => {
      d3.selectAll(".line").style("opacity", lineOpacity);
      d3.selectAll(".circle").style("opacity", circleOpacity);
      lineParts
        .style("stroke-width", lineStroke)
        .style("cursor", "none");
    });
});

/* Add circles in the line */
lines
  .selectAll("circle-group")
  .data(data)
  .enter()
  .append("g")
  .style("fill", (d, i) => color(i))
  .selectAll("circle")
  .data((d) => d.values)
  .enter()
  .append("g")
  .attr("class", "circle")
  .on("mouseover", function(d) {
    d3.select(this)
      .style("cursor", "pointer")
      .append("text")
      .attr("class", "text")
      .text(`${d.price}`)
      .attr("x", (d) => xScale(d.date) + 5)
      .attr("y", (d) => yScale(d.price) - 10);
  })
  .on("mouseout", function(d) {
    d3.select(this)
      .style("cursor", "none")
      .transition()
      .duration(duration)
      .selectAll(".text")
      .remove();
  })
  .append("circle")
  .attr("cx", (d) => xScale(d.date))
  .attr("cy", (d) => yScale(d.price))
  .attr("r", circleRadius)
  .style("opacity", circleOpacity)
  .on("mouseover", function(d) {
    d3.select(this)
      .transition()
      .duration(duration)
      .attr("r", circleRadiusHover);
  })
  .on("mouseout", function(d) {
    d3.select(this).transition().duration(duration).attr("r", circleRadius);
  });

/* Add Axis into SVG */
var xAxis = d3.axisBottom(xScale).ticks(5);
var yAxis = d3.axisLeft(yScale).ticks(5);

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

svg
  .append("g")
  .attr("class", "y axis")
  .call(yAxis)
  .append("text")
  .attr("y", 15)
  .attr("transform", "rotate(-90)")
  .attr("fill", "#000")
  .text("Total values");
.App {
  font-family: sans-serif;
  text-align: center;
}

svg {
  font-family: Sans-Serif, Arial;
}

.line {
  stroke-width: 2;
  fill: none;
}

.line-dashed {
  stroke-dasharray: 5,5;
}

.axis path {
  stroke: black;
}

.text {
  font-size: 12px;
}

.title-text {
  font-size: 12px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<div class="App">
  <div id="chart" />
</div>

我做了什么:

  1. 我将 isDefined() 添加到您对 d3.line() 的定义中,以便它可以处理 NaN;
  2. 我用 isDashed true 或 false 过滤了每个数组的值。当我找到一个匹配的元素而前一个不匹配时,我将这两个元素都添加到结果集中。这样,它们之间就画了一条线,就不会出现空隙了;
  3. 我稍微更改了鼠标悬停行为,这样如果将鼠标悬停在虚线和实线段上,它们就会同时突出显示。