Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Feature Request] spanGaps with different style, eg. dashed line #5956

Closed
MikeTeare opened this issue Jan 5, 2019 · 12 comments · Fixed by #8844
Closed

[Feature Request] spanGaps with different style, eg. dashed line #5956

MikeTeare opened this issue Jan 5, 2019 · 12 comments · Fixed by #8844

Comments

@MikeTeare
Copy link

I'm looking to replace a home brewed charting solution in a PHP based web app but really, really need to span gaps in line charts with a dotted or dashed line. I want to keep the line running over gaps to show the trend but change its style to make it obvious there is a gap.

I thought I might be able to do what I want by duplicating the data series with the gaps - display it once with a solid line and spanGaps: false and again dashed with spanGaps: true but then the two interpolate differently - I'd like to use monotone interpolation.

Any ideas? I've seen SO questions about this but no answers.

@darless
Copy link
Contributor

darless commented Jan 10, 2019

I don't believe there is any support for this at the moment.
I didn't find anything in https://www.npmjs.com/search?q=chartjs-plugin-%20 as far as plugins.
I coded something up just to see if that's what you're looking for.

Line Gap Dash Image
The JS for the chart line above: https://jsfiddle.net/cbh5k637/

If that's what's being looked for then I'd like members to suggest what the options for the line would be in this case. This would be a setting in the main options. A thought:

  • gapLineDash ([Number]): This would be the primary setting for what the dash looks like during gaps. Default: false.

The way I did this was to move the logic within element.line.js draw function into the loop so that I could modify each section as need be.

@simonbrunel
Copy link
Member

@Madrussian still not sure how I would implement it but an idea come to my mind: I would allow the user to configure every line segment via scriptable options (not fully implemented yet). I think it's a much better approach because it would provide a generic and flexible solution instead of a rigid set of new options.

For example (not tested but this is the idea):

function hasGap(ctx) {
        var idx = ctx.dataset.data[ctx.dataIndex]
        var d0 = ctx.dataset.data[idx]
        var d1 = ctx.dataset.data[idx + 1]
        return d0 === null || d1 === null;
}

datasets: [{
    // draw gap line dashed and in red
    borderColor: function(ctx) {
        return hasGap(ctx) ? 'red' : 'green';
    },
    borderDash: function(ctx) {
        return hasGap(ctx) ? [1, 4] : false;
    }
}]

And a totally different use case:

datasets: [{
    // draw increasing line in green, decreasing in red
    borderColor: function(ctx) {
        var idx = ctx.dataset.data[ctx.dataIndex]
        var d0 = ctx.dataset.data[idx]
        var d1 = ctx.dataset.data[idx + 1]
        return d0 < d1 ? 'red' : 'green';
    }
}]

@koprivajakub
Copy link

koprivajakub commented Jan 10, 2019

I am missing this feature right now as well, I am able to provide two datasets with the calculated value for the missing point. But it is an overhead that would not be needed if I just can describe what segment should have different style.

The proposed callback function from @simonbrunel make most sense to me. Also the user might want to have different borderWidth, pointBackgroundColor and so on, but for now the borderDash callback would make my day.

@koprivajakub
Copy link

koprivajakub commented Jan 10, 2019

I was more thinking about this and I might have a bit better proposal for the interface than @simonbrunel proposed. And right now I am pretty sure that this behaviour might be needed and useful also for the point styles.

var points = [10, 30, null, null, null, null, 10, null, 15, 25]

// Since the line is defined by two points the developer will be more interested into the interface like this.
// From my point it make the developer life easier that he does not care about all the undefined checks.
function hasGap(lineSourceIndex, lineDestinationIndex, ctx) {
  return hasMissingValue(lineSourceIndex) || hasMissingValue(lineDestinationIndex) === null;
}

function hasMissingValue(datasetIndex) {
  return points[datasetIndex] === null;
}

datasets: [{
  data: points,

  borderColor: function(lineSourceIndex, lineDestinationIndex, ctx) {
    return hasGap(lineSourceIndex, lineDestinationIndex, ctx) ? 'red' : 'green';
  },
  borderDash: function(lineSourceIndex, lineDestinationIndex, ctx) {
    return hasGap(lineSourceIndex, lineDestinationIndex, ctx) ? [1, 4] : false;
  },
  backgroundColor: function(lineSourceIndex, lineDestinationIndex, ctx) {
    return hasGap(lineSourceIndex, lineDestinationIndex, ctx) ? 'red' : 'green';
  },
  borderWidth: function(lineSourceIndex, lineDestinationIndex, ctx) {
    return hasGap(lineSourceIndex, lineDestinationIndex, ctx) ? 3 : 1;
  },
  pointRadius: function(datasetIndex, ctx) {
    return hasMissingValue(datasetIndex) ? 3 : 1;
  },
  pointBorderWidth: function(datasetIndex, ctx) {
    return hasMissingValue(datasetIndex) ? 1 : 2;
  },
  pointHoverBorderWidth: function(datasetIndex, ctx) {
    return hasMissingValue(datasetIndex) ? 0 : 2;
  },
  pointHoverRadius: function(datasetIndex, ctx) {
    return hasMissingValue(datasetIndex) ? 0 : 2;
  },
  pointHoverRadius: function(datasetIndex, ctx) {
    return hasMissingValue(datasetIndex) ? 0 : 15;
  },
  pointBackgroundColor: function(datasetIndex, ctx) {
    return hasMissingValue(datasetIndex) ? 'red' : 'green';
  },
  pointHoverBackgroundColor: function(datasetIndex, ctx) {
    return hasMissingValue(datasetIndex) ? 'red' : 'green';
  },
  pointHoverBorderColor: function(datasetIndex, ctx) {
    return hasMissingValue(datasetIndex) ? 'white' : 'black';
  },
  pointBorderColor: function(datasetIndex, ctx) {
    return hasMissingValue(datasetIndex) ? 'white' : 'black';
  },
}]

I am not 100% sure that I did not miss a some option, but those the options I am using right now.

@etimberg
Copy link
Member

@simonbrunel scriptable options is what I thought of first. Right now, #5973 has the start of scriptable options for line charts. Perhaps @Madrussian you could fork from that branch and implement it for the line dash settings.

@darless
Copy link
Contributor

darless commented Jan 13, 2019

@etimberg Sure, although as discussed in chat with @simonbrunel the PR will wait until #5973 is merged since its a requirement.

Example:
https://codepen.io/darless/pen/maaOzp

@koprivajakub
Copy link

Thanks for the effort. Looking forward to use it, so far I have graph like this image link and there was needed not complex but pretty long algorithm to get it work for all possible values.

@koprivajakub
Copy link

@etimberg Sure, although as discussed in chat with @simonbrunel the PR will wait until #5973 is merged since its a requirement.

Example:
https://codepen.io/darless/pen/maaOzp

@Madrussian Just looking into the codepen now, would be hard to make the lines curved based on tension? Also it would be nice if there would be an option to fill the missing points with interpolated value so you can hover on top of those.

@KimmoHop
Copy link

Just an idea, why not use spanGaps to define dash (or color, line thickness, complete alternate border definition)? Unless something more complex - eh, universal - is needed ;)
spanGaps: false -> no line over missing points
spanGaps: true -> similar line over missing points
spanGaps: [5,5] -> dashed line over missing points

Example (dummy data): https://pasteboard.co/HYKmwH1.png

I'm not sure how well it works with tension, but at least it renders. I don't intend to use tension in my charts.
With Google Charts I currently have two charts, one with skip nulls/solid over another with interpolate nulls/dash :/
https://pasteboard.co/HYKtr6u.png

@darless
Copy link
Contributor

darless commented Mar 24, 2019

Unfortunately I have not had time to work on this. If someone wants to take over this or has already started then go for it, I'm not sure when I'll have availability at the present moment. What I have for this is linked below. Since its not complete and there are tests failing I did not create a PR for it.
https://github.com/Darless/Chart.js/tree/scriptable-line-segments

@dgoal
Copy link

dgoal commented Oct 23, 2019

Any luck to have it in near feature?

@alvarotrigo
Copy link

alvarotrigo commented Nov 18, 2020

Here's a great implementation that might work for others:
#7523 (comment)
https://jsfiddle.net/haeydc7m/4/

And here's an implementation of this for Chart.js v1 that I found on this answer.
https://jsfiddle.net/3gxjfndm/3/

And here's the code:

Chart.types.Line.extend({
  name: "LineAlt",
  initialize: function (data) {
    var strokeColors = [];
    data.datasets.forEach(function (dataset, i) {
      if (dataset.dottedFromLabel) {
        strokeColors.push(dataset.strokeColor);
        dataset.strokeColor = "rgba(0,0,0,0)"
      }
    })

    Chart.types.Line.prototype.initialize.apply(this, arguments);

    var self = this;
    data.datasets.forEach(function (dataset, i) {
      if (dataset.dottedFromLabel) {
        self.datasets[i].dottedFromIndex = data.labels.indexOf(dataset.dottedFromLabel) + 1;
        self.datasets[i]._saved = {
          strokeColor: strokeColors.shift()
        }
      }
    })
  },
  draw: function () {
    Chart.types.Line.prototype.draw.apply(this, arguments);

    // from Chart.js library code
    var hasValue = function (item) {
      return item.value !== null;
    },
        nextPoint = function (point, collection, index) {
          return Chart.helpers.findNextWhere(collection, hasValue, index) || point;
        },
        previousPoint = function (point, collection, index) {
          return Chart.helpers.findPreviousWhere(collection, hasValue, index) || point;
        };

    var ctx = this.chart.ctx;
    var self = this;
    ctx.save();
    this.datasets.forEach(function (dataset) {
      if (dataset.dottedFromIndex) {
        ctx.lineWidth = self.options.datasetStrokeWidth;
        ctx.strokeStyle = dataset._saved.strokeColor;

        // adapted from Chart.js library code
        var pointsWithValues = Chart.helpers.where(dataset.points, hasValue);
        Chart.helpers.each(pointsWithValues, function (point, index) {
          if (index >= dataset.dottedFromIndex)
            ctx.setLineDash([3, 3]);
          else
            ctx.setLineDash([]);

          if (index === 0) {
            ctx.moveTo(point.x, point.y);
          }
          else {
            if (self.options.bezierCurve) {
              var previous = previousPoint(point, pointsWithValues, index);
              ctx.bezierCurveTo(
                previous.controlPoints.outer.x,
                previous.controlPoints.outer.y,
                point.controlPoints.inner.x,
                point.controlPoints.inner.y,
                point.x,
                point.y
              );
            }
            else {
              ctx.lineTo(point.x, point.y);
            }
          }

          ctx.stroke();
        }, this);
      }
    })
    ctx.restore();
  }
});

Unfortunately it doesn't seem to be working for the latest version.

I've seen other suggesting using duplicated datasets with empty values except the last one, but that's far from an acceptable solution, specially when having multiple lines in a chart and dynamically dealing with them.

Any chance of making this code work for the latest version?

marusak added a commit to marusak/bots that referenced this issue Feb 8, 2021
If we don't merge anything on some day, graphs are becoming rather ugly.
With this option lines are drawn between points. No point is visually
drawn in the missing day and also we indicate in label that there was no
run.

It would be nice if chartjs/Chart.js#5956
would be implemented, but in the meantime I guess we can just use this.
(Other option is to create for each graph two data sets, but that is rather busy work)
marusak added a commit to cockpit-project/bots that referenced this issue Feb 8, 2021
If we don't merge anything on some day, graphs are becoming rather ugly.
With this option lines are drawn between points. No point is visually
drawn in the missing day and also we indicate in label that there was no
run.

It would be nice if chartjs/Chart.js#5956
would be implemented, but in the meantime I guess we can just use this.
(Other option is to create for each graph two data sets, but that is rather busy work)
@etimberg etimberg added this to the Version 3.1 milestone Mar 18, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging a pull request may close this issue.

8 participants