wonu wns
wonu wns

Reputation: 3608

Sencha Ext JS 4 Chart: Customize xField (Category) label position

i am trying to port an application from Flex 3 to Sencha Ext JS 4. this application has a dashboard having a column chart (pleasee see first image below). the value for its xField is somewhat a long text.

enter image description here

as much as possible, i don't want my label to be rotated. well, i think it's kinda messy. if possible, i want my label to be positioned alternately if it doesn't fit.

label : {
    rotate : {degrees:45}
}

below image is my Sencha custom chart. most of the Category label are not shown.

enter image description here

i was thinking of customizing the onPlaceLabel function but i don't know how to do so. how am i gonna do this to achieve what i needed?

Upvotes: 1

Views: 4719

Answers (2)

TConere
TConere

Reputation: 61

I had a similar problem with xaxis label text being too long. If shortening the visible label text and using tooltips for axis labels is an option for you then you might find the following useful...

My solution was:

  1. Shorten the visible axis label text so that label will fit (and thus render)

  2. Add a tooltip for the axis label sprite and make it display the full label text

Code Snippets:

  1. Inside axis config...

    label : {
           renderer: function(v){
    
            //if v is 'Grilled Pizza Special' then you get something like 'Grilled ...'
             return Ext.String.ellipsis(v,12);
           }                 
        }
    
  2. Modified drawHorizontalLabels()...

    drawHorizontalLabels: function () {
    var me = this,
        labelConf = me.label,
        floor = Math.floor,
        max = Math.max,
        axes = me.chart.axes,
        insetPadding = me.chart.insetPadding,
        position = me.position,
        inflections = me.inflections,
        ln = inflections.length,
        labels = me.labels,
        maxHeight = 0,
        ratio,
        bbox, point, prevLabel, prevLabelId,
        adjustEnd = me.adjustEnd,
        hasLeft = axes.findIndex('position', 'left') != -1,
        hasRight = axes.findIndex('position', 'right') != -1,
        textLabel, text,tooltipText,
        last, x, y, i, firstLabel;
    
    last = ln - 1;
    //get a reference to the first text label dimensions
    point = inflections[0];
    firstLabel = me.getOrCreateLabel(0, me.label.renderer(labels[0]));
    ratio = Math.floor(Math.abs(Math.sin(labelConf.rotate && (labelConf.rotate.degrees * Math.PI / 180) || 0)));
    
    for (i = 0; i < ln; i++) {
        point = inflections[i];
        text = me.label.renderer(labels[i]);
        //not using text variable above for tooltip as tooltip would render the ellipsis text rather than the full label text.
        tooltipText = labels[i]; 
        textLabel = me.getOrCreateLabel(i, text);
        bbox = textLabel._bbox;
        maxHeight = max(maxHeight, bbox.height + me.dashSize + me.label.padding);
        x = floor(point[0] - (ratio ? bbox.height : bbox.width) / 2);
        if (adjustEnd && me.chart.maxGutter[0] == 0) {
            if (i == 0 && !hasLeft) {
                x = point[0];
            }
            else if (i == last && !hasRight) {
                x = Math.min(x, point[0] - bbox.width + insetPadding);
            }
        }
        if (position == 'top') {
            y = point[1] - (me.dashSize * 2) - me.label.padding - (bbox.height / 2);
        }
        else {
            y = point[1] + (me.dashSize * 2) + me.label.padding + (bbox.height / 2);
        }
    
        textLabel.setAttributes({
            hidden: false,
            x: x,
            y: y
        }, true);
    
              var labelTip = Ext.create('Ext.tip.ToolTip', {
            target : textLabel.el,
            html : tooltipText
        });
    
        // Skip label if there isn't available minimum space
        if (i != 0 && (me.intersect(textLabel, prevLabel)
            || me.intersect(textLabel, firstLabel))) {
            if (i === last && prevLabelId !== 0) {
                prevLabel.hide(true); 
            } else {
                textLabel.hide(true); 
                continue;
            }
        }
    
        prevLabel = textLabel;
        prevLabelId = i;
    }
    
    return maxHeight;
    }
    

Upvotes: 0

rixo
rixo

Reputation: 25001

That's Ext.chart.axis.Axis#drawHoriztontalLabels that you need to override. onPlaceLabel is for the stuff written in the chart itself. Like labels at the top of the bar, you see what I mean?

The clean way to do that is to override the class and implement some jolly options. I'll let you explore the full code I've put at the end of the post if you want to understand the implementation...

Ext.define('Ext.ux.chart.axis.Axis.OverlappingLabelOptions', {
    override: 'Ext.chart.axis.Axis'

    ,labelRows: 1

    ,hideOverlappingLabels: true

    ,drawHorizontalLabels: function() {
        // ... see at the end of the post for the full implementation
    }
});

Once you've included this override class using require, you can use these two options in your axis definition. For example:

{
    type: 'Category',
    position: 'bottom',
    fields: ['name'],
    title: 'Sample Metrics',

    labelRows: 5,
    hideOverlappingLabels: false
}

My first try was to automatically push overlapping labels one row lower. That's what you obtain by setting labelRows: 'auto'. It works well under certain conditions, and has the advantage of being automatic.

enter image description here

Unfortunately, it can get really messy:

enter image description here

So I resorted to implement an option that let you fix the number of label rows, and evenly distribute the labels among these rows. I also added the hideOverlappingLabels that ensure that no label will be lost even if we miss luck and some label ends overlapping.

Here's what you get with this configuration:

{
    // ...
    hideOverlappingLabels: false,
    labelRows: 5
}

enter image description here

I hope that will give you what you need, or at least the knowledge to bend Ext's code to your desires!

Full Implementation

/**
 * This override adds a {@link #labelRows} option to draw horizontal axis labels on multiple
 * rows, and also an {@link #hideOverlappingLabels} option.
 */
Ext.define('Ext.ux.chart.axis.Axis.OverlappingLabelOptions', {
    override: 'Ext.chart.axis.Axis'

    ,alternateClassName: 'Ext.ux.AxisOverlappingLabelOptions'

    /**
     * @cfg {Integer/String}
     *
     * Number of label rows. If this option is set to 'auto', then overlapping labels will
     * be drawn on the next row where they don't overlap. Which can give a messy result.
     */
    ,labelRows: 1

    /**
     * @cfg {Boolean}
     *
     * Set to false to prevent automatic hiding of overlapping labels.
     */
    ,hideOverlappingLabels: true

    ,drawHorizontalLabels: function() {
        var me = this,
            labelConf = me.label,
            floor = Math.floor,
            max = Math.max,
            axes = me.chart.axes,
            insetPadding = me.chart.insetPadding,
            gutters = me.chart.maxGutters,
            position = me.position,
            inflections = me.inflections,
            ln = inflections.length,
            labels = me.labels,
            maxHeight = 0,
            ratio,
            bbox, point, prevLabel, prevLabelId,
            adjustEnd = me.adjustEnd,
            hasLeft = axes.findIndex('position', 'left') != -1,
            hasRight = axes.findIndex('position', 'right') != -1,
            textLabel, text,
            last, x, y, i, firstLabel;

        var labelRows = Ext.num(this.labelRows, 1),
            autoOffsetLabels = this.labelRows === 'auto',
            hideLabels = this.hideOverlappingLabels;

        var lastLabelOnRow = [],
            row, j;

        last = ln - 1;
        //get a reference to the first text label dimensions
        point = inflections[0];
        firstLabel = me.getOrCreateLabel(0, me.label.renderer(labels[0]));
        ratio = Math.floor(Math.abs(Math.sin(labelConf.rotate && (labelConf.rotate.degrees * Math.PI / 180) || 0)));

        for (i = 0; i < ln; i++) {
            row = 0; // rx: start at first row
            point = inflections[i];
            text = me.label.renderer(labels[i]);
            textLabel = me.getOrCreateLabel(i, text);
            bbox = textLabel._bbox;
            //maxHeight = max(maxHeight, bbox.height + me.dashSize + me.label.padding);
            x = floor(point[0] - (ratio ? bbox.height : bbox.width) / 2);
            if (adjustEnd && gutters.left == 0 && gutters.right == 0) {
                if (i == 0 && !hasLeft) {
                    x = point[0];
                }
                else if (i == last && !hasRight) {
                    x = Math.min(x, point[0] - bbox.width + insetPadding);
                }
            }
            if (position == 'top') {
                y = point[1] - (me.dashSize * 2) - me.label.padding - (bbox.height / 2);
            }
            else {
                y = point[1] + (me.dashSize * 2) + me.label.padding + (bbox.height / 2);
            }

            // rx: vertical offset
            y += (i % labelRows) * bbox.height;

            textLabel.setAttributes({
                hidden: false,
                x: x,
                y: y
            }, true);

            if (autoOffsetLabels) {
                // rx: find the row on which we can draw the label without overlapping
                for (j=0; j<lastLabelOnRow.length; j++) {
                    if (me.intersect(textLabel, lastLabelOnRow[j])) {
                        row++;
                        textLabel.setAttributes({
                            y: y + row * bbox.height
                        }, true);
                    }
                }

                // rx: calc maxHeight knowing the row
                maxHeight = max(maxHeight, bbox.height + me.dashSize + me.label.padding + (bbox.height * row));

                // rx: keep reference to know where we can place the next label
                lastLabelOnRow[row] = textLabel;
            } else {

                if (hideLabels) {
                    // Skip label if there isn't available minimum space
                    if (i != 0 && (me.intersect(textLabel, prevLabel)
                        || me.intersect(textLabel, firstLabel))) {
                        if (i === last && prevLabelId !== 0) {
                            prevLabel.hide(true);
                        } else {
                            textLabel.hide(true);
                            continue;
                        }
                    }
                }

                maxHeight = max(maxHeight, bbox.height + me.dashSize + me.label.padding + bbox.height * (i % labelRows));
            }

            prevLabel = textLabel;
            prevLabelId = i;
        }

        return maxHeight;
    }
});

Upvotes: 5

Related Questions