javascriptecharts

Different formatting for min/max label on a time axis with echarts.js?


I'm building a Echart (5.5.1)'s chart with a time type xAxis and while I love the fact that it adapts the labels according to the range span, sometimes when you zoom for example it can be confusing to understand what is the time period you're actually looking at.

Therefore I added the max and min labels so I can have some visible landmarks like so:

xAxis: {
  type: 'time',
  data: [],
  axisLabel: {
    showMinLabel: true,
    showMaxLabel: true,
    rotate: 45
  }
},

But the labels of the min and max are, somehow, even more precise than the others when I actually want the opposite. It's showing the hour & time when I want something more like day & month.

enter image description here

Then I tried formatting specifically the min and max label

xAxis: {
  type: 'time',
  data: [],
  axisLabel: {
    showMinLabel: true,
    showMaxLabel: true,
    rotate: 45
  },
  min: function (value) {
    formatEdgeLabel(value.min)
  },
  max: function (value) {
    formatEdgeLabel(value.max)
  }
},

The function formatEdgeLabel() returns a string of the date formatted like 23/10, that's it. But somehow, it seems like echarts doesn't care and still show the hour time like in the screenshot above.

Does anyone know a solution to that issue or workaround maybe?


Solution

  • You are not using min / max correctly. These are for setting min and max boundaries on your axis. What you are looking for is the label formatter.

    Examples:

    // format string
    axisLabel: {
      formatter: '{d}/{M}/{yyyy}'
    }
    
    
    // function
    axisLabel: {
      formatter: function (value, index) {
        const date = new Date(value);
        if (date.getHours() === 0) {
          return '{d}/{M}/{yyyy}'
        }
        ...
        return 'anything you want';
      }
    }
    

    EDIT:

    Overwriting the formatter behaviour only for the min and max value is not supported by echarts and poses a few difficulties.

    1. It is difficult to determine the max value inside the formatter function.
    2. If the formatter is used for any value, all formatting has to be done by hand (its not possible to call the echarts default formatter from inside)

    To address the first issue I came up with a scrappy solution:

    let base = +new Date(1988, 9, 3);
    let oneDay = 24 * 3600 * 1000;
    let data = [[base, Math.random() * 300]];
    for (let i = 1; i < 20000; i++) {
      let now = new Date((base += oneDay));
      data.push([+now, Math.round((Math.random() - 0.5) * 20 + data[i - 1][1])]);
    }
    
    option = {
      xAxis: {
        type: 'time',
        boundaryGap: false,
        axisLabel: {
          formatter: function (value, index) {
            let boundaryLabel = false;
            if (index === 0 || value === endValue) {
              boundaryLabel = true;
              if (value === endValue) maxIndex = index;
            }
      
            if (tickSpan < 1000) {  // one second
              return boundaryLabel ? '{dd}-{MM}-{yyyy} {hh}:{mm}:{ss} {SSS}' : '{mm}:{ss} {SSS}';
            } else if (tickSpan < 1000 * 60) {  // one minute
              return boundaryLabel ? '{dd}-{MM}-{yyyy} {hh}:{mm}:{ss}' : '{mm}:{ss}';
            } else if (tickSpan < 1000 * 60 * 60) { // one hour
              return boundaryLabel ? '{HH}:{mm}' : '{dd}-{MM}-{yyyy} {hh}:{mm}'
            } else if (tickSpan < 1000 * 60 * 60 * 24) {  // one day
              if (new Date(value).getHours() === 0) return '{dd}/{MM}/{yyyy} {hh}:{mm}';
              return boundaryLabel ? '{dd}/{MM}/{yyyy} {hh}:{mm}' : '{HH}:{mm}';
            } else if (tickSpan < 1000 * 60 * 60 * 24 * 30) { // one month
              if (new Date(value).getDate() === 1) return '{MMM}';
              return boundaryLabel ? '{d}. {MMM} {yyyy}' : '{d}'
            } else if (tickSpan < 1000 * 60 * 60 * 24 * 365) {  // one year
              if (new Date(value).getMonth() === 0) return '{yyyy}';
              return boundaryLabel ? '{MMM} {yyyy}' : '{MMM}';
            } else {
              return boundaryLabel ? '{MMM} {yyyy}' : '{yyyy}';
            }
          },
          showMinLabel: true,
          showMaxLabel: true
        }
      },
      yAxis: {
        type: 'value',
        boundaryGap: [0, '100%']
      },
      dataZoom: [
        {
          type: 'inside',
        },
        {
          realtime: false
        }
      ],
      series: [
        {
          type: 'line',
          smooth: true,
          symbol: 'none',
          areaStyle: {},
          data: data
        }
      ]
    };
    
    
    const minX = data[0][0];
    const maxX = data[data.length-1][0];
    const dataSpan = maxX - minX;
    let endValue = maxX;
    let maxIndex = 6;
    let tickSpan = dataSpan / maxIndex;
    myChart.on('dataZoom', function (params) {
      if (params.from === undefined && params.batch === undefined) return;
      const end = params.end !== undefined ? params.end : params.batch[0].end;
      const start = params.start !== undefined ? params.start : params.batch[0].start;
      endValue = Math.round(dataSpan * end * 0.01 + minX);
      const zoomSpan = endValue - Math.round(dataSpan * start * 0.01 + minX);
      tickSpan = zoomSpan / maxIndex;
      myChart.dispatchAction({
        type: 'dataZoom',
        start: params.start,
        end: params.end
      });
    });
    

    The min value can be determined by the index 0. To determine the max value, I am listening to the dataZoom event and computing the expected maximum. Since the event is triggered only after the axis labels have been updated, I trigger the event again. Now the computed maximum can be used to update the labels.

    For hand formatting the other labels, the default formatting strategy of echarts is mentioned in the formatter documentation under 'Cascading Templates'. I included an example to give you an idea how it can be done that you can improve on.