2

I want to draw a curve with interpolation for some given points. Here, the points represent how much solar energy can be generated by solar panels, so there is one point per hour of the day with sun. The number of points may vary depending on the month of the year (for example, 10 points in December and 16 points in June, because there are respectively 10 and 16 hours of sun a day in those months).

Until here everything fine, but now we want to add a sun image at the hour of the day you're seeing the graphics. For this, I have created 2 lines : one before the current hour and one after, and put the sun image in the current hour position. It looks like this in June with 16 points at 1PM : Curve in June

This looks fine. The problem is when there are less points, the space between the point before and after the current hour is bigger, and becomes graphically too big. This is for January at 9AM with 10 points (wrong graphical rendering) :

Curve in January

(in both images, the ending / beginning time at the bottom are static)

I want the blank space that is left for the sun to be always the same.

I have tried various things :

  • adding some points "closer to the sun" in the data : doesn't work because it messes up the scale, and even with a scale updated after adding the points, the top part of the curve is not centered anymore
  • putting a background on the sun image : the graph must be integrated in a transparent container
  • using stroke-dasharray : i couldn't manage to understand the percentage / pixels values enough to calculate it. For example, with a distance to dash of 100%, it would dash before the end of the line. For the pixels unit, I haven't found any way to calculate the number of pixels generated by the curve drawing so it isn't possible to calculate the exact position of the dash
  • using a linearGradiant : I can't get to scale a proper percentage positioning. Anyway, the render is ugly because it cuts the line color vertically, which is not nice graphically

If anyone has an idea of how to properly accomplish this, it would be great. Also I may probably have missed something obvious or think a wrong way for this problem, but after 3 days of thinking about it I'm a bit overloading haha. Thank you for reading

Littletime
  • 527
  • 6
  • 12
  • There are tons of documentation about clipping and masking SVGs (e. g. https://getflywheel.com/layout/css-svg-clipping-and-masking-techniques/). Give it try! If you make the effort to set up a working demo, surely someone will come up with an elegant solution rather quickly. – altocumulus Nov 25 '16 at 15:52
  • Thank you, I'm gonna look into it ! Best – Littletime Nov 25 '16 at 15:53
  • It works fine with clipping! Only there is a little graphical rendering "problem": being cut by 2 rectangles, the curve is cut vertically so not in the way the line is going. Here is how it gets rendered [link](http://imgur.com/a/5bKTq). I was wondering, is there a way to do the opposite of clipping ? Meaning for example putting a circle at the sun place, that would NOT render the line when passing through it ? If this is not possible the actual rendering would be acceptable, so thank you for it – Littletime Nov 25 '16 at 16:22
  • It's cumbersome to do this on a theoretical basis. Like I said: set up a little demo and while still working on a proper solution myself, someone will probably beat me to it. I reckon there will even be more than only one elegant solution. – altocumulus Nov 25 '16 at 16:30
  • You are not restricted to rectangles when clipping. A clipPath may be arbitrarily shaped: https://sarasoueidan.com/blog/css-svg-clipping/. – altocumulus Nov 25 '16 at 16:32
  • For an inverted clipping you are better off with masking: [*"SVG clipPath to clip the *outer* content out"*](/q/4817999). – altocumulus Nov 25 '16 at 16:35
  • Thanks a lot for all those indications, I'll dig into this and prepare a demo. Best – Littletime Nov 25 '16 at 16:44
  • IT WORKED with an inverted mask !!!!!! Thanks a lot ! – Littletime Nov 25 '16 at 16:54
  • It would be nice if you could sum this up in a [self-answer](http://stackoverflow.com/help/self-answer) for the rest of us to learn from it. – altocumulus Nov 25 '16 at 16:56
  • Done! Thanks a lot altocumulus – Littletime Nov 25 '16 at 17:27

4 Answers4

1

One solution to this problem was to use a svg mask.

Like explained here : "SVG clipPath to clip the *outer content out", you can create masks that will create a zone of "non-display" of the element that you apply it to. In other terms, I created a mask with a circle that is at the sun position all the time, which hides the part of the curve that is inside the circle.

Community
  • 1
  • 1
Littletime
  • 527
  • 6
  • 12
1

Sounds like you have your answer but I'll propose a different approach. This sounds very solvable using stroke-dasharray. Here a quick demo:

<!DOCTYPE html>
<html>

<head>
  <script data-require="d3@4.0.0" data-semver="4.0.0" src="https://d3js.org/d3.v4.min.js"></script>
</head>

<body>
  <script>
    var svg = d3.select("body")
      .append("svg")
      .attr("width", 500)
      .attr("height", 500);

    var line = d3.line()
      .curve(d3.curveCardinal)
      .x(function(d) {
        return d[0];
      })
      .y(function(d) {
        return d[1];
      });
      
    var data = [[10,450], [250, 50], [490, 450]];
    
    var p = svg.append("path")
      .datum(data)
      .attr("d", line)
      .style("stroke", "orange")
      .style("stroke-width", "5px")
      .style("fill", "none");
    
    var l = p.node().getTotalLength(),
        sunSpace = l  / 12;
    
    function createSpace(){
      var sunPos = Math.random() * l;
      p.attr("stroke-dasharray", (sunPos - sunSpace/2) + "," + sunSpace + "," + l);
    }
    
    createSpace();
    setInterval(createSpace, 1000);    
  </script>
</body>

</html>

EDITS FOR COMMENTS

<!DOCTYPE html>
<html>

<head>
  <script data-require="d3@4.0.0" data-semver="4.0.0" src="https://d3js.org/d3.v4.min.js"></script>
</head>

<body>
  <svg width="500" height="500"></svg>
  <script>
    var svg = d3.select("svg"),
      margin = {
        top: 20,
        right: 20,
        bottom: 30,
        left: 50
      },
      width = +svg.attr("width") - margin.left - margin.right,
      height = +svg.attr("height") - margin.top - margin.bottom,
      g = svg.append("g").attr("transform", "translate(" + margin.left + "," + margin.top + ")");

    var y = d3.scaleLinear()
      .rangeRound([height, 0])
      .domain([0, 10]);

    var x = d3.scaleLinear()
      .rangeRound([0, width])
      .domain([0, 10]);

    var line = d3.line()
      .curve(d3.curveCardinal)
      .x(function(d) {
        return x(d[0]);
      })
      .y(function(d) {
        return y(d[1]);
      });

    var data = [
      [1, 1],
      [5, 9],
      [9, 1]
    ];

    g.append("g")
      .attr("class", "axis axis--x")
      .attr("transform", "translate(0," + height + ")")
      .call(d3.axisBottom(x));

    g.append("g")
      .attr("class", "axis axis--y")
      .call(d3.axisLeft(y))

    var p = g.append("path")
      .datum(data)
      .attr("d", line)
      .style("stroke", "orange")
      .style("stroke-width", "5px")
      .style("fill", "none");



    var pathLength = p.node().getTotalLength(),
      sunSpace = pathLength / 12;

    function createSpace() {

      var sunPos = x(3);

      var beginning = 0,
        end = pathLength,
        target;

      while (true) {
        target = Math.floor((beginning + end) / 2);
        pos = p.node().getPointAtLength(target);
        if ((target === end || target === beginning) && pos.x !== sunPos) {
          break;
        }
        if (pos.x > sunPos) end = target;
        else if (pos.x < sunPos) beginning = target;
        else break; //position found
      }
      p.attr("stroke-dasharray", (target - sunSpace/2) + "," + sunSpace + "," + pathLength);
    }

    createSpace();
  </script>
</body>

</html>
Mark
  • 106,305
  • 20
  • 172
  • 230
  • Hey, good idea. I didn't know about the node.getTotalLength function. Although here it would not work either. The sun is placed in a horizontal x scale, with the same y that is calculated for drawing the curve (so the space between each x is constant). Trying your method, I created a scale with the number of horizontal x range and a domain from 0 to L (L being the curve total length). It doesn't work because the distance between two X within the curve may vary, depending on the inclination of the curve. So the dash isn't always properly placed. – Littletime Nov 25 '16 at 23:54
  • I don't know if I'm clear in my explanation, feel free to ask more details – Littletime Nov 25 '16 at 23:54
  • @Littletime, excellent point. Luckily there's a [little trick](http://bl.ocks.org/duopixel/3824661) for translating x position into path length. See edited answer above. – Mark Nov 26 '16 at 02:25
  • very nice technique ! It works like a charm with your edit. Thanks for your indications, getting to know better d3 every day :) – Littletime Nov 26 '16 at 08:44
-1

You may be overthinking it. Have you considered drawing the full line and including a white circle around your sun icon? As long as you draw the icon after the line, it would leave just the right amount of space.

Brandon Gano
  • 6,430
  • 1
  • 25
  • 25
-1

You should be able to separate your data from your rendering model. You didn't include code, so your specific solution will vary. But the general idea is that you convert the actual data into something that better suits your rendering needs. For example:

// If this is your actual data ...
var numberOfHours = 10;
var showSunAt = 3;

// ... and this is your desired "resolution" for the path ...
var resolution = 16;

// ... map your data to work with this resolution.
var mapped = showSunAt / numberOfHours * resolution;

// Define the "space" to leave between segments.
var spaceBetweenSegments = 1;

// Then, dynamically set the start/end points for each segment.
var firstSegmentEndsAt = mapped - (spaceBetweenSegmenets / 2);
var secondSegmenetStartsAt = mapped + (spaceBetweenSegmenets / 2);

You now have the exact points along the path where the first segment should end, the icon should be rendered, and the second segment should begin.

Brandon Gano
  • 6,430
  • 1
  • 25
  • 25
  • Thanks for your answer. This is the idea, but this would mean adding a point before and after the sun in the data for the curve to take it into account, and this would mess up the scale. – Littletime Nov 25 '16 at 16:01