Timeline chart with highcharts using x-range with multiple stacks

Viewed 1671

I am using Highcharts to create a timeline chart which shows the flow of different "states" over time. The current implementation is at http://jsfiddle.net/hq1kdpmo/8/ and it looks like state chart.

The current code is as follows:

Highcharts.chart('container', {
  "chart": {
    "type": "xrange"
  },
  "title": {
    "text": "State Periods"
  },
  "xAxis": {
    "type": "datetime"
  },
  "yAxis": [{
    "title": {
      "text": "Factions"
    },
    "categories": ["A", "B", "C", "D"],
    "reversed": true
  }],
  "plotOptions": {
    "xrange": {
      "borderRadius": 0,
      "borderWidth": 0,
      "grouping": false,
      "dataLabels": {
        "align": "center",
        "enabled": true,
        "format": "{point.name}"
      },
      "colorByPoint": false
    }
  },
  "tooltip": {
    "headerFormat": "<span style=\"font-size: 0.85em\">{point.x} - {point.x2}</span><br/>",
    "pointFormat": "<span style=\"color:{series.color}\">●</span> {series.name}: <b>{point.yCategory}</b><br/>"
  },
  "series": [{
    "name": "State A",
    "pointWidth": 20,
    "data": [{
      "x": 1540430613000,
      "x2": 1540633768100,
      "y": 0
    }, {
      "x": 1540191009000,
      "x2": 1540633768100,
      "y": 1
    }, {
      "x": 1540191009000,
      "x2": 1540530613000,
      "y": 2
    }, {
      "x": 1540530613000,
      "x2": 1540633768100,
      "y": 3
    }]
  }, {
    "name": "State B",
    "pointWidth": 20,
    "data": [{
      "x": 1540191009000,
      "x2": 1540430613000,
      "y": 0
    }, {
      "x": 1540530613000,
      "x2": 1540633768100,
      "y": 2
    }, {
      "x": 1540191009000,
      "x2": 1540330613000,
      "y": 3
    }]
  }, {
    "name": "State C",
    "pointWidth": 20,
    "data": [{
      "x": 1540330613000,
      "x2": 1540530613000,
      "y": 3
    }]
  }],
  "exporting": {
    "enabled": true,
    "sourceWidth": 1200
  }
});

Now what I am looking forward to is create something of this sort (pardon my paint skills)chart to be.

Here the categories A to D are the only axis on y. But I would like to group a variable number of parallel ranges. The use case is that there can be multiple states at any point of time and the number of states at any point of time is variable. How do I go on about doing this?

3 Answers

To create such a chart you will have to add 12 yAxis (0-11) and set proper ticks and labels so that only A-D categories will be plotted. Additionally, adjust plotOptions.pointPadding and plotOptions.groupPadding properties to set points width automatically (series.pointWidth should be undefined then).

yAxis options:

  yAxis: [{
    title: {
      text: "Factions"
    },
    categories: ["A", "B", "C", "D"],
    tickPositions: [-1, 2, 5, 8, 11],
    lineWidth: 0,
    labels: {
      y: -20,
      formatter: function() {
        var chart = this.chart,
          axis = this.axis,
          label;

        if (!chart.yaxisLabelIndex) {
          chart.yaxisLabelIndex = 0;
        }

        if (this.value !== -1) {

          label = axis.categories[chart.yaxisLabelIndex];
          chart.yaxisLabelIndex++;

          if (chart.yaxisLabelIndex === 4) {
            chart.yaxisLabelIndex = 0;
          }

          return label;
        }
      },
    },
    reversed: true
  }]

Demo: https://jsfiddle.net/wchmiel/s9qefg7t/1/

I have managed to solve this thanks to support from Highcharts themselves. The idea is to set the tick position on the load event and use the labels.formatter for formatting each individual label.

events: {
  load() {
    let labelGroup = document.querySelectorAll('.highcharts-yaxis-labels');
    // nodeValue is distance from top
    let ticks = document.querySelectorAll('.highcharts-yaxis-grid');
    let tickPositions = Array.from(ticks[0].childNodes).map(
        function(node){
            return +node.attributes.d.nodeValue.split(" ")[2];
        }
    );
    let labelPositions = [];
    for(let i =1 ;i<tickPositions.length;i++){
        labelPositions.push((tickPositions[i] + tickPositions[i-1])/2);
    }
    labelGroup[0].childNodes[0].attributes.y.nodeValue = labelPositions[0] + parseFloat(labelGroup[0].childNodes[0].style["font-size"], 10) / 2;
    labelGroup[0].childNodes[1].attributes.y.nodeValue = labelPositions[1] + parseFloat(labelGroup[0].childNodes[1].style["font-size"], 10) / 2;
    labelGroup[0].childNodes[2].attributes.y.nodeValue = labelPositions[2] + parseFloat(labelGroup[0].childNodes[2].style["font-size"], 10) / 2;
    labelGroup[0].childNodes[3].attributes.y.nodeValue = labelPositions[3] + parseFloat(labelGroup[0].childNodes[3].style["font-size"], 10) / 2;
    labelGroup[0].childNodes[4].attributes.y.nodeValue = labelPositions[4] + parseFloat(labelGroup[0].childNodes[4].style["font-size"], 10) / 2;
  }
}

And the labels are formatted as:

labels: {
  formatter: function() {
    var chart = this.chart,
      axis = this.axis,
      label;

    if (!chart.yaxisLabelIndex) {
      chart.yaxisLabelIndex = 0;
    }
    if (this.value !== -1) {

      label = axis.categories[chart.yaxisLabelIndex];
      chart.yaxisLabelIndex++;

      if (chart.yaxisLabelIndex === groups.length) {
        chart.yaxisLabelIndex = 0;
      }

      return label;
    }
  },
}

Fiddle at https://jsfiddle.net/yvnp4su0/42/

As I suggested in the comment above it is a better idea to use Highcharts renderer and add custom labels than manipulate Dom elements as you did in the previous answer, because it is a much cleaner solution.

Disable default labels:

  yAxis: [{
    title: {
      text: "Factions",
      margin: 35
    },
    categories: ["A", "B", "C", "D", "E"],
    tickPositions: tickPositions,
    lineWidth: 0,
    labels: {
      enabled: false
    },
    reversed: true
  }]

Add custom labels in proper positions using renderer:

  chart: {
    type: 'xrange',
    height: 500,
    marginLeft: 60,
    events: {
        load: function() {
        this.customLabels = [];
      },
      render: function() {
        var chart = this,
          yAxis = chart.yAxis[0],
          categories = yAxis.categories,
          xOffset = 15,
          yOffset = 20,
          xPos = yAxis.left - xOffset,
          tickPositions = yAxis.tickPositions,
          text,
          label,
          yPos,
          tick1Y,
          tick2Y,
          i;

        for (i = 0; i < tickPositions.length - 1; i++) {
            if (chart.customLabels[i]) {
            chart.customLabels[i].destroy();
          }

          tick1Y = yAxis.toPixels(tickPositions[i]);
          tick2Y = yAxis.toPixels(tickPositions[i + 1]);
          yPos = (tick1Y + tick2Y) / 2 + yOffset;
          text = categories[i];

          label = chart.renderer.text(text, xPos, yPos)
            .css({
              color: '#ccc',
              fontSize: '14px'
            })
            .add();

          chart.customLabels[i] = label;
        }
      }
    }
  }

Demo: https://jsfiddle.net/wchmiel/vkz7o1hw/

Related