Home > Blockchain >  D3 apply conditional formatting to Line element
D3 apply conditional formatting to Line element

Time:07-03

My dataset looks like this:

var data = [{
    "Date": "31/12/2019",
    "Value": 23,
    "Type": "Actual",
    "Plan": 100,
    "Over": ""
  },
  {
    "Date": "31/01/2020",
    "Value": 49,
    "Type": "Actual",
    "Plan": 100,
    "Over": ""
  },
  {
    "Date": "29/02/2020",
    "Value": 23,
    "Type": "Actual",
    "Plan": 100,
    "Over": ""
  },
  {
    "Date": "31/03/2020",
    "Value": 12,
    "Type": "Actual",
    "Plan": 100,
    "Over": ""
  },
  {
    "Date": "30/04/2020",
    "Value": 33,
    "Type": "Actual",
    "Plan": 100,
    "Over": ""
  },
  {
    "Date": "31/05/2020",
    "Value": 62,
    "Type": "Actual",
    "Plan": 100,
    "Over": ""
  },
  {
    "Date": "30/06/2020",
    "Value": 65,
    "Type": "Actual",
    "Plan": 100,
    "Over": ""
  },
  {
    "Date": "31/07/2020",
    "Value": 77,
    "Type": "Actual",
    "Plan": 100,
    "Over": ""
  },
  {
    "Date": "31/08/2020",
    "Value": 65,
    "Type": "Actual",
    "Plan": 100,
    "Over": ""
  },
  {
    "Date": "30/09/2020",
    "Value": 58,
    "Type": "Actual",
    "Plan": 100,
    "Over": ""
  },
  {
    "Date": "31/10/2020",
    "Value": 87,
    "Type": "Actual",
    "Plan": 100,
    "Over": ""
  },
  {
    "Date": "30/11/2020",
    "Value": 120,
    "Type": "Actual",
    "Plan": 100,
    "Over": "Y"
  },
  {
    "Date": "31/12/2020",
    "Value": 103,
    "Type": "Fcst",
    "Plan": 100,
    "Over": "Y"
  },
  {
    "Date": "31/01/2021",
    "Value": 110,
    "Type": "Fcst",
    "Plan": 100,
    "Over": "Y"
  },
  {
    "Date": "28/02/2021",
    "Value": 117,
    "Type": "Fcst",
    "Plan": 100,
    "Over": "Y"
  }
]

I'm trying to plot a line chart that has the following formatting:

When d.Type === "Actual" then show the line as SOLID and BLUE

When d.Type === "Fcst" show the line as dashed (stroke-dasharray) and GREEN

Essential this is what I'm trying to achieve:

enter image description here

...and this is what I have so far:

enter image description here

This is what I tried doing to add the conditional fomatting:

//add Trend Line
svg.append('path')
  .datum(data)
  .attr('class', 'data-line glowed')
  .style('stroke', function(d, i) {
    if (d.Type == "Fcst")
      return "#00D4B2"
    else {
      return "#3087CD"
    }
  })
  .style('stroke-width', 2)
  .style("stroke-dasharray",function(d, i) {
    if (d.Type == "Fcst")
      return ("3, 1"); else {
      return ("0, 0")
    }
  })
  .style('fill', 'none')
  .attr('d', line)

Here is my JSFIDDLE

CodePudding user response:

Because you use datum you don't iterate over each point of the line - instead the entire path is generated by line and is a single element and can only accept one set of styles.

The approach below draws 3 lines (one transparent) overlaid to one another. The shorter 'actual' line is on top of the 'forecast' line. You need to do it this way to get the nice curvature (I use d3.curveMonotoneX).

The steps:

  1. Draw the line for 'actual' data (per filter on Type) but make it transparent
  2. Calculate the length of this 'actual' line from step (1) with getTotalLength()
  3. Draw the 'forecast' line for the full length of line (e.g. dashed green)
  4. Draw the 'actual' line again, this time in blue, and also set stroke-dasharray such that it has one dash (from start for length calculated in step (2)) and no repeating dash so the end of the original 'forecast' line shows through this single 'gap' of the dashed line.

See working example below and check the comments:

// get actual data points only
const actualData = data.filter(d => d.Type === "Actual");

// draw actual line but transparent 
const actual = svg.append("g")
  .append("path")
  .datum(actualData)
  .attr("d", line)
  .style("fill", "none")
  .style("opacity", 0)
  .style("stroke-width", lineWidth);

// get length of actual curve
const actualLength = actual.node().getTotalLength();

// draw all data points in dashed green - this will be forecast
const allPoints = svg.append("g")
  .append("path")
  .datum(data)
  .attr("d", line)
  .style("fill", "none")
  .style("stroke", "#00ee22")
  .style("stroke-width", lineWidth)
  .style("stroke-dasharray", "4, 2");

// draw full line in blue but only to actual end point with stroke-dasharray
const forecast = svg.append("g")
  .append("path")
  .datum(data)
  .attr("d", line)
  .style("fill", "none")
  .style("stroke", "#2222ee")
  .style("stroke-width", lineWidth)
  .style("stroke-dasharray", `${actualLength}`);
  
// add points on line etc
const points = svg.selectAll(".label")
  .data(data)
  .enter()
  .append("circle")
  .attr("cx", d => xScale(parseTime(d.Date)))
  .attr("cy", d => yScale(d.Value))
  .attr("r", 4)
  .attr("fill", "#2288ee")
  .attr("stroke", "#22ccee")  
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<script>
var data = [{
    "Date": "31/12/2019",
    "Value": 23,
    "Type": "Actual",
    "Plan": 100,
    "Over": ""
  },
  {
    "Date": "31/01/2020",
    "Value": 49,
    "Type": "Actual",
    "Plan": 100,
    "Over": ""
  },
  {
    "Date": "29/02/2020",
    "Value": 23,
    "Type": "Actual",
    "Plan": 100,
    "Over": ""
  },
  {
    "Date": "31/03/2020",
    "Value": 12,
    "Type": "Actual",
    "Plan": 100,
    "Over": ""
  },
  {
    "Date": "30/04/2020",
    "Value": 33,
    "Type": "Actual",
    "Plan": 100,
    "Over": ""
  },
  {
    "Date": "31/05/2020",
    "Value": 62,
    "Type": "Actual",
    "Plan": 100,
    "Over": ""
  },
  {
    "Date": "30/06/2020",
    "Value": 65,
    "Type": "Actual",
    "Plan": 100,
    "Over": ""
  },
  {
    "Date": "31/07/2020",
    "Value": 77,
    "Type": "Actual",
    "Plan": 100,
    "Over": ""
  },
  {
    "Date": "31/08/2020",
    "Value": 65,
    "Type": "Actual",
    "Plan": 100,
    "Over": ""
  },
  {
    "Date": "30/09/2020",
    "Value": 58,
    "Type": "Actual",
    "Plan": 100,
    "Over": ""
  },
  {
    "Date": "31/10/2020",
    "Value": 87,
    "Type": "Actual",
    "Plan": 100,
    "Over": ""
  },
  {
    "Date": "30/11/2020",
    "Value": 120,
    "Type": "Actual",
    "Plan": 100,
    "Over": "Y"
  },
  {
    "Date": "31/12/2020",
    "Value": 103,
    "Type": "Fcst",
    "Plan": 100,
    "Over": "Y"
  },
  {
    "Date": "31/01/2021",
    "Value": 110,
    "Type": "Fcst",
    "Plan": 100,
    "Over": "Y"
  },
  {
    "Date": "28/02/2021",
    "Value": 117,
    "Type": "Fcst",
    "Plan": 100,
    "Over": "Y"
  }
];

// basic line chart setup
const innerWidth = 520;
const innerHeight = 160;
const margin = {top: 5, bottom: 25, left: 15, right: 5}
const lineWidth = 3;
const svg = d3.select("body")
  .append("svg")
  .attr("width", innerWidth   margin.left   margin.right)
  .attr("height", innerHeight   margin.top   margin.bottom)
  .append("g")
  .attr("transform", `translate(${margin.left}, ${margin.top})`);
  
const yScale = d3.scaleLinear()
  .range([innerHeight, 0])
  .domain([0, d3.max(data, d => d.Value)   20]);

const yAxis = svg.append("g")
  .attr("transform", `translate(${margin.left},0)`)
  .call(d3.axisLeft(yScale).ticks(5));
  
const parseTime = d3.timeParse("%d/%m/%Y")
const dates = data.map(d => parseTime(d.Date));
const xScale = d3.scaleTime()
  .range([0, innerWidth])
  .domain(d3.extent(dates))
  .nice();
  
const xAxis = svg.append("g")
  .attr("transform", `translate(${margin.left}, ${innerHeight})`)
  .call(d3.axisBottom(xScale)
    .tickFormat(d3.timeFormat('%b'))
  );
  
const line = d3.line()
  .x(d => xScale(parseTime(d.Date)))
  .y(d => yScale(d.Value))
  .curve(d3.curveMonotoneX);

</script>

  • Related