Spline path - 3D effect

Viewed 222

I'm trying to achieve the desired result (left) using d3, svg, and css for a spline chart to give it a '3d' effect. Overlaying two splines with an offset results in something close however with certain data or zooming you can see the bottom 'shadow' (right) does not follow the main spline due to interpolation.

I've tried with a gradient however this gets applied over the entire svg area and not just horizontally on the path. Code snippet follows d3's example (https://bl.ocks.org/gordlea/27370d1eea8464b04538e6d8ced39e89).

    // 2. Use the margin convention practice
    var margin = { top: 50, right: 50, bottom: 50, left: 50 }
        , width = window.innerWidth - margin.left - margin.right // Use the window's width
        , height = window.innerHeight - margin.top - margin.bottom; // Use the window's height

    // The number of datapoints
    var n = 21;

    // 5. X scale will use the index of our data
    var xScale = d3.scaleLinear()
        .domain([0, n - 1]) // input
        .range([0, width]); // output

    // 6. Y scale will use the randomly generate number
    var yScale = d3.scaleLinear()
        .domain([0, 1]) // input
        .range([height, 0]); // output

    // 7. d3's line generator
    var line = d3.line()
        .x(function (d, i) { return xScale(i); }) // set the x values for the line generator
        .y(function (d) { return yScale(d.y); }) // set the y values for the line generator
        .curve(d3.curveBasisOpen) // apply smoothing to the line

    // 8. An array of objects of length N. Each object has key -> value pair, the key being "y" and the value is a random number
    var dataset = d3.range(n).map(function (d) { return { "y": d3.randomUniform(1)() } })

    // 1. Add the SVG to the page and employ #2
    var svg = d3.select("body").append("svg")
        .attr("width", width + margin.left + margin.right)
        .attr("height", height + margin.top + margin.bottom)
        .append("g")
        .attr("transform", "translate(" + margin.left + "," + margin.top + ")");

    // 3. Call the x axis in a group tag
    svg.append("g")
        .attr("class", "x axis")
        .attr("transform", "translate(0," + height + ")")
        .call(d3.axisBottom(xScale)); // Create an axis component with d3.axisBottom

    // 4. Call the y axis in a group tag
    svg.append("g")
        .attr("class", "y axis")
        .call(d3.axisLeft(yScale)); // Create an axis component with d3.axisLeft

    // 9. Append the path, bind the data, and call the line generator
    svg.append("path")
        .datum(dataset) // 10. Binds data to the line
        .attr("class", "line") // Assign a class for styling      
        .attr("stroke", "url(#gradID)")
        //.attr("stroke", "red")        
        .attr("d", line); // 11. Calls the line generator


    var defs = svg.append('defs');
    const linearGradient = defs.append("linearGradient");
        
    linearGradient
        .attr("id", "gradID")
        .attr("x1", "0%")
        .attr("y1", "0%")
        .attr("x2", "0%")
        .attr("y2", "100%");
    linearGradient
        .append("stop")
        .attr("offset", "0")
        .attr("style", "stop-color:blue;stop-opacity:1");
    linearGradient
        .append("stop")
        .attr("offset", "1")
        .attr("style", "stop-color:red;stop-opacity:1");
.line {
    fill: none;
    stroke-width: 10;
    stroke-linecap: round;
}
<!DOCTYPE html>
<meta charset="utf-8">

<body>
</body>

<!-- Load in the d3 library -->
<script src="https://d3js.org/d3.v5.min.js"></script>

1 Answers

Thank you @grantD with the link was able to achieve the desired result: result

Updated code snippet:

  // 2. Use the margin convention practice
  var margin = { top: 50, right: 50, bottom: 50, left: 50 },
    width = window.innerWidth - margin.left - margin.right, // Use the window's width
    height = window.innerHeight - margin.top - margin.bottom; // Use the window's height

  // The number of datapoints
  var n = 10;

  // 5. X scale will use the index of our data
  var xScale = d3
    .scaleLinear()
    .domain([0, n - 1]) // input
    .range([0, width]); // output

  // 6. Y scale will use the randomly generate number
  var yScale = d3
    .scaleLinear()
    .domain([0, 1]) // input
    .range([height, 0]); // output

  // 7. d3's line generator
  var line = d3
    .line()
    .x(function (d, i) {
      return xScale(i);
    }) // set the x values for the line generator
    .y(function (d) {
      return yScale(d.y);
    }) // set the y values for the line generator
    .curve(d3.curveBasisOpen); // apply smoothing to the line

  // 8. An array of objects of length N. Each object has key -> value pair, the key being "y" and the value is a random number
  var dataset = d3.range(n).map(function (d) {
    return { y: d3.randomUniform(1)() };
  });

  // 1. Add the SVG to the page and employ #2
  var svg = d3
    .select("body")
    .append("svg")
    .attr("width", width + margin.left + margin.right)
    .attr("height", height + margin.top + margin.bottom)
    .append("g")
    .attr("transform", "translate(" + margin.left + "," + margin.top + ")");

  var defs = svg.append("defs");

  // create filter with id #drop-shadow
  // height=130% so that the shadow is not clipped
  var filter = defs.append("filter").attr("id", "inner-shadow");

  filter.append("feOffset").attr("dx", "0").attr("dy", "-4");
  filter
    .append("feGaussianBlur")
    .attr("stdDeviation", "0.2")
    .attr("result", "offset-blur");
  filter
    .append("feComposite")
    .attr("operator", "out")
    .attr("in", "SourceGraphic")
    .attr("in2", "offset-blur")
    .attr("result", "inverse");
  filter
    .append("feFlood")
    .attr("flood-color", "orange")
    .attr("flood-opacity", "1")
    .attr("result", "color");
  filter
    .append("feComposite")
    .attr("operator", "in")
    .attr("in", "color")
    .attr("in2", "inverse")
    .attr("result", "shadow");
  const comp = filter
    .append("feComponentTransfer")
    .attr("in", "shadow")
    .attr("result", "shadow");
  comp.append("feFuncA").attr("type", "linear").attr("slope", "100.10");
  filter
    .append("feComposite")
    .attr("operator", "over")
    .attr("in", "shadow")
    .attr("in2", "SourceGraphic");

  // 3. Call the x axis in a group tag
  svg
    .append("g")
    .attr("class", "x axis")
    .attr("transform", "translate(0," + height + ")")
    .call(d3.axisBottom(xScale)); // Create an axis component with d3.axisBottom

  // 4. Call the y axis in a group tag
  svg.append("g").attr("class", "y axis").call(d3.axisLeft(yScale)); // Create an axis component with d3.axisLeft

  // 9. Append the path, bind the data, and call the line generator
  svg
    .append("path")
    .datum(dataset) // 10. Binds data to the line
    .attr("class", "line") // Assign a class for styling
    .attr("filter", "url(#inner-shadow)")    
    .attr("d", line); // 11. Calls the line generator

  var defs = svg.append("defs");

  var filter = defs.append("filter").attr("id", "dropshadow"); /// !!! important - define id to reference it later

  // append gaussian blur to filter
  filter
    .append("feGaussianBlur")
    .attr("in", "SourceAlpha")
    .attr("stdDeviation", 10)
    .attr("result", "blur");

  // append offset filter to result of gaussion blur filter
  filter
    .append("feOffset")
    .attr("in", "blur")
    .attr("dx", 20)
    .attr("dy", 30)
    .attr("result", "offsetBlur");

  // merge result with original image
  var feMerge = filter.append("feMerge");

  // first layer result of blur and offset
  feMerge.append("feMergeNode").attr('in", "offsetBlur');

  // original image on top
  feMerge.append("feMergeNode").attr("in", "SourceGraphic");
  
.line {
    fill: none;
    stroke: #ffb653;
    stroke-width: 10;
    stroke-linecap: round;
  }
<body></body>

<!-- Load in the d3 library -->
<script src="https://d3js.org/d3.v5.min.js"></script>

Related