Incubator-echarts: min/max on yAxis for bar charts, makes the bars go outside limits

Created on 9 Aug 2018  ·  17Comments  ·  Source: apache/incubator-echarts

One-line summary [问题简述]

Setting custom min/max values on the yAxis for bar charts, makes the bars go below the lowest limit of x axis and above the highest limit of y axis.

Version & Environment [版本及环境]

  • ECharts version [ECharts 版本]: 4.1.0
  • Browser version [浏览器类型和版本]: 68.0.3440.84
  • OS Version [操作系统类型和版本]: macOS High Sierra 10.13.3

Expected behaviour [期望结果]

Without min/max on bar chart I get,
bar_without_min_max

When I set min/max on bar chart I get,
bar_with_min_max

I do not face this issue if I change the chart type to line,

Line without min/max:
line_without_min_max

Line with min/max:
line_with_min_max

What can I do to make min/max for bar behave like line ? Is there some way I can prevent the bars from going outside limits ?

ECharts option [ECharts配置项]


option = {
    xAxis: {
        type: 'category',
        data: ['Foo', 'Bar', 'Baz']
    },
    yAxis: {
        type: 'value',
        min: 200,
        max: 250
    },
    series: [{
        data: [100, 200, 300],
        type: 'line' // 'bar'
    }]
};

Most helpful comment

@suremicro No need to go the custom chart route. I've solved this using a similar solution to yours but instead of creating a custom chart, I update each bar's layout properties. You can update each bar's layout by registering a layout function before initializing your chart.

Example:

echarts.registerLayout(myCustomLayoutFunction);
echarts.init(myChartContainer);

function myCustomLayoutFunction(ecModel) {
  ecModel.eachSeries(seriesComponent => {
    const data = seriesComponent.getData();
    data.each(idx => {
      const layout = data.getItemLayout(idx);

      if (layout) {
        const newLayout = echarts.graphic.clipRectByRect(
          layout,
          seriesComponent.coordinateSystem.grid.getRect(),
        );

        if (newLayout) {
          ({ x: layout.x, y: layout.y, width: layout.width, height: layout.height } = newLayout);
        }  
      }
    });
  }
}

I use this technique of registering layout functions for my charts quite often to do things like dynamically showing or hiding series labels depending if they fit.

All 17 comments

Related:

If I specify min/max values outside min/max value of data, I see bars for 'bar' type graph.

negative_min_max_bar

option = {
    xAxis: {
        type: 'category',
        data: ['Foo', 'Bar', 'Baz']
    },
    yAxis: {
        type: 'value',
        min: -250,
        max: -150
    },
    series: [{
        data: [100, 200, 300],
        type: 'bar'
    }]
};

This doesn't happen for line,
negative_min_max_line

option = {
    xAxis: {
        type: 'category',
        data: ['Foo', 'Bar', 'Baz']
    },
    yAxis: {
        type: 'value',
        min: -250,
        max: -150
    },
    series: [{
        data: [100, 200, 300],
        type: 'line'
    }]
};

Is there any news on this issue? For lines some sort of clipping is performed but not for bar types.

@suremicro, I am still facing the same issue. No new developments.

I know this is not a solution for everybody, but in my case is ok. What I did is hide the bar below the axis line by using the axis ticks. So I have some exaggerated axis ticks like:

        axisTick: {
          show: true,
          alignWithLabel: true,
          length: 100,
          lineStyle: {
            width: 500,
            color: '#f3f3f3'
          }
        },

Axis ticks are positioned by default below the axis labels and do not mess with grid positioning which is nice. Just replace the color with the background color of your choice so it mimics the background.

I hope this issue continues opened and can have a real solution in the future!

Nice try at fixing this ;) I can't believe this hasn't been fixed as it makes an otheriwse excellent charting package almost unusable in a product.

I have a workaround to this that uses the custom chart type. Using this custom type, a bar chart style can be re-created and the in-built view clipping function used to ensure the chart doesn't exceed the chart area.

It is just a test currently and doesn't support horizontal bar styles or stacking but does partially resolve this annoying issue.

chart-clipping

@suremicro No need to go the custom chart route. I've solved this using a similar solution to yours but instead of creating a custom chart, I update each bar's layout properties. You can update each bar's layout by registering a layout function before initializing your chart.

Example:

echarts.registerLayout(myCustomLayoutFunction);
echarts.init(myChartContainer);

function myCustomLayoutFunction(ecModel) {
  ecModel.eachSeries(seriesComponent => {
    const data = seriesComponent.getData();
    data.each(idx => {
      const layout = data.getItemLayout(idx);

      if (layout) {
        const newLayout = echarts.graphic.clipRectByRect(
          layout,
          seriesComponent.coordinateSystem.grid.getRect(),
        );

        if (newLayout) {
          ({ x: layout.x, y: layout.y, width: layout.width, height: layout.height } = newLayout);
        }  
      }
    });
  }
}

I use this technique of registering layout functions for my charts quite often to do things like dynamically showing or hiding series labels depending if they fit.

Thanks Jonathan this is exactly what I was looking for, however I can't find any documentation about registerLayout. Do you need to return the new layout?

Thanks Jonathan this is exactly what I was looking for, however I can't find any documentation about registerLayout. Do you need to return the new layout?

No need to return the new layout. Just overwrite the properties of the one returned by:
data.getItemLayout(idx);

I'm using ES2015 code, so I did it above using object destructuring.
({ x: layout.x, y: layout.y, width: layout.width, height: layout.height } = newLayout)

As far as I know, the team hasn't documented all of the API. I often read the code of the existing charts to get familiar with the undocumented portions of the API.

That's what I assumed from the code, the I only asked as it doesn't seem to clip. BTW I found there's a
data.setItemLayout(index, layout);
function as well (which does them same as your line).

I expected to give clipRectByRect the data item layout along with the min/max area and it returned a clipped layout which I then apply, but no luck

I see now what I was missing - the layout seems to be pixel co-ordinates with the height neing a negative value, which is why the clipRectByRect doesn't work (the grid is all positive).

I've added a simple co-ordinate limiting logic so the bars do keep within the grid boundary when zooming and scrolling, however at the moment it does leave the labels 'stuck' to the axis lines.

Although not perfect it does look better than at present - thanks to Jonathan for the info.

            function chartLayout(ec) {

                ec.eachSeries(seriesComponent => {

                    var data = seriesComponent.getData();

                    data.each(idx => {

                        var layout = data.getItemLayout(idx);

                        if (layout) {

                            var rect = seriesComponent.coordinateSystem.grid.getRect();

                            if (layout.x < rect.x) {
                                layout.width = layout.width - (rect.x - layout.x);
                                layout.x = rect.x;
                            }

                            if (layout.x + layout.width > rect.x + rect.width) {
                                layout.width = rect.x + rect.width - layout.x;
                            }

                            if (layout.width < 0) layout.width = 0;

                            if (layout.y > rect.y + rect.height) {
                                layout.height = layout.height - (rect.y + rect.height - layout.y);
                                layout.y = rect.y + rect.height;
                            }

                            var absY = (rect.y + rect.height) - layout.y;

                            if (absY + Math.abs(layout.height) + 65 > rect.y + rect.height) {
                                layout.height = -(rect.y + rect.height - 65 - absY);
                            }

                            if (layout.height > 0) layout.height = 0;

                        }
                    });
                });
            }

@suremicro Can you post an example of the 'stuck' labels in a codepen or some other online code playground to check it out?

@jonavila Could you point me in the right direction to hide labels in bar charts? Thank you

@suremicro Here you go: https://codesandbox.io/s/echarts-showhide-labels-in-bars-jr1ks

I've adapted an example from one of my use cases, but I've simplified it a bit to only account for vertical bars with 90 rotated labels.

Here's the gist of it:

  1. For each series, we loop through the data and get the bar layout.
  2. For each of the bars, we define the label text. In my case, it's a combination of series name and value.
  3. We then grab the itemLabelModel. This is the object with label options. I wanted to show/hide labels for each bars as opposed to doing it at the series level.
  4. We merge seriesLabel options into itemLabel options just in case you have defined some of the config at the series level.
  5. We obtain text rectangle for each label (the bounding box of the text)
  6. Finally, we compare it against the bar layout. When rotated 90 degress, you want to compare the label height with the bar width and the label width with the bar height.

In the demo, I'm showing the labels on hover so that you can see them overflow the bars.
Please resize the window in the sandbox so that you can see lables show/hide

@jonavila That's very helpful and will also allow me to investiagate the API further myself.
Thank your for your time on this.

I'm noticing the same issue but in the X axis when I use time type.
image

https://jsfiddle.net/jsilversun/10wm6dny/23/

I tried what you shared for the Y axis (the layout function) but it didn't work for me, do you have any insights about how I could fix this?

You example doesn't include the layout clipping function??? You need to add the function and register it using the global echarts function. If you try the code below in your jsfiddle example it will clip the bars ;)

echarts.registerLayout(chartLayout);
var container = document.getElementById('main');
var chart = echarts.init(container);

const min = "2020-11-25T06:10";
const max = "2020-11-25T08:10";
function getData() {
      const data = [];
      let now = +new Date(2020, 10, 25);
      const oneDay = 10 * 60 * 1000;
      const randomData = ()=> {
        now = new Date(+now + oneDay);
        return {
          value: [
            now.toISOString().substring(0,16),
            Math.floor(Math.random() * 100),
          ],
        };
      };
      for (let i = 0; i < 72; i++) {
        data.push(randomData());
      }
      return data;
}

            function chartLayout(ec) {

                ec.eachSeries(seriesComponent => {

                    var data = seriesComponent.getData();

                    data.each(idx => {

                        var layout = data.getItemLayout(idx);

                        if (layout) {

                            var rect = seriesComponent.coordinateSystem.grid.getRect();

                            if (layout.x < rect.x) {
                                layout.width = layout.width - (rect.x - layout.x);
                                layout.x = rect.x;
                            }

                            if (layout.x + layout.width > rect.x + rect.width) {
                                layout.width = rect.x + rect.width - layout.x;
                            }

                            if (layout.width < 0) layout.width = 0;

                            if (layout.y > rect.y + rect.height) {
                                layout.height = layout.height - (rect.y + rect.height - layout.y);
                                layout.y = rect.y + rect.height;
                            }

                            var absY = (rect.y + rect.height) - layout.y;

                            if (absY + Math.abs(layout.height) + 65 > rect.y + rect.height) {
                                layout.height = -(rect.y + rect.height - 65 - absY);
                            }

                            if (layout.height > 0) layout.height = 0;

                        }
                    });
                });
            }

const data = getData();
chart.setOption({
    xAxis: [
      {
        splitNumber: 24,
        scale: false,
        min, 
        max,
        type: "time",
        axisLabel: {
          formatter(value) {
            return moment(value).format("HH");
          }
        }
      }
    ],
    yAxis: [
      {
        type: "value"
      }
    ],
    series: [
      {
        data,
        type:'bar',
      }
    ]
  });
Was this page helpful?
0 / 5 - 0 ratings