Tuesday, April 21, 2015

Micro-solution: Automatic Value Axes Offset in amCharts

Working with amCharts library for visualizing various report's data in I-Plan Supply Chart Planning software, I came across a difficulty configuring multiple value axes for our specific case.

amCharts is very flexible, allowing to set several axes, define their position (“left” or “right”), title, colour, min/max values, tick formatting etc. It even allows defining an offset (in pixels) to avoid overlap of axes.

Yet in our case the reports are built by the users, various values assigned to different axes based on units, and can be aligned left or right at will. This means that a constant offset value simply doesn't cut it—if you have report values that are too wide they'll overlap, and if they're narrow—they will leave a gap.

Life is like a box of axes with fixed offsets.
Neither look good.
To test various permutations I created this fiddle based on amChart's own demo. In the demo the value range is known for all axes, so it is simple to hand-pick the required offset. Yet if the values are outside of your control, you could get the picture on the right.

Not nice.

So, with the aid of extremely helpful Martynas Majeris from amCharts support team, I set out to identify actual axis widths after they have been rendered, and automatically update the axes' offsets.

You can jump straight into the code:
// Render chart (your code may vary)
chart.write(sChartID);

var oAxesOffsetPerPosition = {};

// After chart is rendered, find value axes in the SVG
// Using class names from http://www.amcharts.com/tutorials/css-class-names/
chart.valueAxes.forEach(function (oAxis, nAxisIndex) {
    chartElem.find('.value-axis-' + oAxis.id).each(function () {
        // amCharts has three elements per .value-axis-[axisId], find the one with labels
        if ($(this).find('.amcharts-axis-label').length > 0) {
            if (!oAxesOffsetPerPosition.hasOwnProperty(oAxis.position))
                oAxesOffsetPerPosition[oAxis.position] = 0;

            chart.valueAxes[nAxisIndex].offset = oAxesOffsetPerPosition[oAxis.position];

            oAxesOffsetPerPosition[oAxis.position] += $(this)[0].getBBox().width + 20;
         }
    });
});

// Redraw chart with updated .valueAxes data
chart.validateData();

The idea is very simple, though has some finer points:

  • Identifying the axes: Thankfully, since v3.12 amCharts support including class names into the generated SVG—otherwise it would be near impossible to find the value axes. Make sure to enable these. The next hurdle is that for a given axis ID there are three (!) SVG elements with the corresponding class—this includes the grid lines etc. The element we need is the one that contains “[prefix]-axis-label” elements.
  • Determine axis width: My go-to function $.outerWidth() doesn't work on SVG elements, so using .getBBox().width instead. Thank you, StackOverflow :)
  • Accumulate width per axis position: This is straightforward, I created an object with properties for “left” and “right” positions, and increment them by each axis.
  • Update the chart: After a property of the amCharts' “chart” object is updated, I call .validateData(). Interestingly, calling .validateNow(true, true) causes the chart to visibly jump around—so .validateData() is empirically better.

After all that, our mission is accomplished:


And even weirdo users can get their way now. At least they are not playing with real axes—that could be dangerous:


Take care!

No comments:

Post a Comment