Results 1 to 9 of 9

Thread: Column charts cannot handle negative values

    Success! Looks like we've fixed this one. According to our records the fix was applied for CHARTS-201 in a recent build.
  1. #1
    Ext JS Premium Member seth.horrigan's Avatar
    Join Date
    Mar 2011
    Location
    San Mateo, CA
    Posts
    17

    Default Column charts cannot handle negative values

    REQUIRED INFORMATION

    Ext version tested:

    • Ext 4.0.6
    Browser versions tested against:
    • Chrome 13.0.782.220
    Description:
    • Column charts will not render correctly if negative y-values are specified
    Steps to reproduce the problem:
    • Create a column chart with all negative y-values, or both negative and positive y-values
    The result that was expected:
    • The base would always be zero.
    • Negative values would chart down from zero like positive chart up from zero.
    • With both positive and negative y-values, they would chart up and down.
    The result that occurs instead:
    • See screenshots below
    Test Case:
    Code:
     
    window.generateDataNegative = function(n, floor){
            var data = [],
                p = (Math.random() *  11) + 1,
                i;
                
            floor = (!floor && floor !== 0)? 20 : floor;
                
            for (i = 0; i < (n || 12); i++) {
                data.push({
                    name: Ext.Date.monthNames[i % 12],
                    data1: Math.floor(((Math.random() - 0.5) * 100), floor),
                    data2: Math.floor(((Math.random() - 0.5) * 100), floor),
                    data3: Math.floor(((Math.random() - 0.5) * 100), floor),
                    data4: Math.floor(((Math.random() - 0.5) * 100), floor),
                    data5: Math.floor(((Math.random() - 0.5) * 100), floor),
                    data6: Math.floor(((Math.random() - 0.5) * 100), floor),
                    data7: Math.floor(((Math.random() - 0.5) * 100), floor),
                    data8: Math.floor(((Math.random() - 0.5) * 100), floor),
                    data9: Math.floor(((Math.random() - 0.5) * 100), floor)
                });
            }
            return data;
        };
          
    window.storeNegatives = Ext.create('Ext.data.JsonStore', {
            fields: ['name', 'data1', 'data2', 'data3', 'data4', 'data5', 'data6', 'data7', 'data9', 'data9'],
            data: generateDataNegative()
        });
    
    
    Ext.create("widget.window", {
                   title: "Negatives broken",
                   modal: true, 
                   width: 600,
                   height: 600,
                   layout: 'fit',
                   items: [{
                xtype: 'chart',
                animate: true,
                shadow: true,
                store: storeNegatives,
                axes: [{
                    type: 'Numeric',
                    position: 'left',
                    fields: ['data1'],
                    title: 'Hits',
                    //minimum: 0,
                    maximum: 0,
                    grid: true
                }, {
                    type: 'Category',
                    position: 'bottom',
                    fields: ['name'],
                    title: 'Months',
                    label: {
                        rotate: {
                            degrees: 270
                        }
                    }
                }],
                series: [{
                    type: 'column',
                    axis: 'left',
                    gutter: 80,
                    xField: 'name',
                    yField: ['data1'],
                    tips: {
                        trackMouse: true,
                        width: 74,
                        height: 38,
                        renderer: function(storeItem, item) {
                            this.setTitle(storeItem.get('name') + '<br />' + storeItem.get('data1'));
                        }
                    },
                    style: {
                        fill: '#38B8BF'
                    }
                }]
            }]
    }).show()

    HELPFUL INFORMATION

    Screenshot or Video:

    All negative values, no maximum or minimum set, y-axis goes from -400 to -100, columns render "up" from -400 to -100
    all-negative.jpg
    All negative, 0 maximum set, axis goes from 0 to -400, columns do not render correctly
    negative-0-max.jpg
    All negative, 0 minimum set, y-axis goes from 0 to -100, columns do not render correctly
    negative-0-minimum.jpg
    Mixed positive and negative, no minimum or maximum set, y-axis goes from -4000 to 4000, but negative column doed not render correctly
    mixed.jpg
    Mixed, maximum set to 0, y-axis goes from 0 to -4000, but columns do not render correctly
    mixed-0-maximum.jpg
    Mixed, minimum set to 0, y-axis goes from 0 to -4000, but columns do not render correctly
    File uploader will not allow me to attach image

    See this URL for live test case: http://

    Debugging already done:

    • none
    Possible fix:
    • not provided
    Additional CSS used:
    • only default ext-all.css (setting styles in code dynamically)
    Operating System:
    • Ubuntu 10.04
    Last edited by seth.horrigan; 21 Sep 2011 at 4:55 PM. Reason: Adding test code in hopes of speeding this along.
    Not all who wander are lost

  2. #2
    Ext JS Premium Member seth.horrigan's Avatar
    Join Date
    Mar 2011
    Location
    San Mateo, CA
    Posts
    17

    Default

    Attachment 28223
    Final image (when I tried again using the "Basic Uploader" instead, it gave me an error letting me know I could only attach five images, so I just added it to a reply)
    Not all who wander are lost

  3. #3
    Ext JS Premium Member
    Join Date
    Apr 2010
    Location
    Montreal, Canada
    Posts
    265

    Default

    I'm having the same problem and can't figure out why. but..

    When you go to : http://docs.sencha.com/ext-js/4-0/#!....series.Column and modify the example with some negative values in the data store, the chart display properly.

  4. #4
    Ext JS Premium Member
    Join Date
    Jan 2008
    Location
    Germany, Berlin
    Posts
    135

    Default

    I can confirm that behaviour for Ext 4.0.6, the bug is not present in 4.0.2a that's why the example at the sencha docs page is still working (they are using 4.0.2a there).

    I digged into the series-bar-chart-code and found a little change in the way the column height and maxY, minY gets calculated.

    in 4.0.2a it is
    Code:
    # line 240 Bar.js
            if (me.axis) {
                axis = chart.axes.get(me.axis);
                if (axis) {
                    out = axis.calcEnds();
                    minY = out.from || axis.prevMin;
                    maxY = mmax(out.to || axis.prevMax, 0);
                }
            }
    
    
            if (me.yField && !Ext.isNumber(minY)) {
                axis = Ext.create('Ext.chart.axis.Axis', {
                    chart: chart,
                    fields: [].concat(me.yField)
                });
                out = axis.calcEnds();
                minY = out.from || axis.prevMin;
                maxY = mmax(out.to || axis.prevMax, 0);
            }
    
    # line 351 Bar.js
    height = Math.round((yValue - ((bounds.minY < 0) ? 0 : bounds.minY)) * bounds.scale);
    In 4.0.6. it is
    Code:
    # line 231 Bar.js
    
            if (me.axis) {
                axis = chart.axes.get(me.axis);
                if (axis) {
                    out = axis.calcEnds();
                    minY = out.from;
                    maxY = out.to;
                }
            }
    
    
            if (me.yField && !Ext.isNumber(minY)) {
                axis = Ext.create('Ext.chart.axis.Axis', {
                    chart: chart,
                    fields: [].concat(me.yField)
                });
                out = axis.calcEnds();
                minY = out.from;
                maxY = out.to;
            }
    
    # line 342 Bar.js
    height = Math.round((yValue - bounds.minY) * bounds.scale);
    I'm using a store which sends varying data, on some days the store contains negative values mixed with positive ones, on other days there might be only positive values. and the values also range from -300 up to 5000 and then will be filtered locally so I can't (and won't) use minimum and maximum.

    The chart should be able to figure the scale and size of the chart by itself.

    PS: when I use 4.0.2a the charts show up perfectly displaying negative and positive values as they should be. So I quite don't get the changes made in 4.0.6....
    Last edited by fschaeffer; 29 Sep 2011 at 10:30 PM. Reason: changed minValue to minimum to avoid confusion

  5. #5
    Ext JS Premium Member
    Join Date
    Apr 2010
    Location
    Montreal, Canada
    Posts
    265

    Default

    god, I had a test case in a sandbox which was working perfectly and the same chart that had trouble running in my app. I tried to reproduced the same environment in my sandbox and the chart always rendered perfectly. My sandbox was running 4.05 and my app is under 4.06.. broken under 4.06. Thanks Fschaeffer, I will stop wasting time trying to figuring this out!

    Code:
    Ext.application({
        name: 'ExtjJS Chart Test',
        launch: function() {
        
            Ext.define('testModel', {
                extend: 'Ext.data.Model',
                fields: [
                    {name: 'VOS', type: 'string'},
                    {name: 'DESC',type: 'string'},
                    {name: 'COST', type: 'float'}
                ]
            });
            
            var store = Ext.create('Ext.data.Store', {
                model: 'testModel',
                data: [            
                    {"VOS": "CAS", "DESC": "CASE", "COST": 20896.17, "LIST": 76096.17},
                    {"VOS": "CHR", "DESC": "CHRYSLER", "COST": 138203.93, "LIST": 29096.17},
                    {"VOS": "DIV", "DESC": "DIVERS", "COST": 19064.31, "LIST": 1089.17},
                    {"VOS": "ENV", "DESC": "ENVIRONNEMENT", "COST": -33720.80, "LIST": 2096.17},
                    {"VOS": "FOR", "DESC": "FORD",  "COST": 157577.64, "LIST": 138744.17},
                    {"VOS": "FTL", "DESC": "FREIGHT",  "COST": 4.62, "LIST": -25336.17},
                    {"VOS": "GMC", "DESC": "GMC", "COST": 38363.97, "LIST": 16.17},
                    {"VOS": "HON", "DESC": "HONDA", "COST": 48891.65, "LIST": 35778.17},
                    {"VOS": "MIT", "DESC": "MITSUBISHI", "COST": 61735.00, "LIST": 56998.17},
                    {"VOS": "NAV", "DESC": "NAVISTAR",  "COST": 211176.33, "LIST": 1356.17},
                    {"VOS": "POR", "DESC": "PORSCHE",  "COST": 647.93, "LIST": 11526.17},
                    {"VOS": "UAP", "DESC": "UAP", "COST": 16.00, "LIST": 6585.17},
                    {"VOS": "VOL", "DESC": "VOLVO", "COST": 467762.71, "LIST": 896.17},
                    {"VOS": "VWC", "DESC": "VOLKSWAGEN",  "COST": 86843.94, "LIST": 20896.17},
                    {"VOS": "YAM", "DESC": "GH", "COST": -123.00, "LIST": -6555.45}          
                ]
            });        
                    
            var win = Ext.create('Ext.window.Window', {
                layout: 'fit',                        
                title: 'test',            
                items: [
                {                
                    xtype: 'chart',
                    animate: true,
                    shadow: true,
                    height: 500,
                    width: 500,
                    legend: {
                        position: 'right'  
                    },
                    store: store,
                    axes: [
                    {
                        type: 'Numeric',
                        position: 'left',                    
                        fields: ['COST', 'LIST'],                    
                        label: {
                            renderer: Ext.util.Format.numberRenderer('0')
                        },
                        title: 'Price',
                        grid: true
                    },{
                        type: 'Category',
                        position: 'bottom',
                        fields: ['VOS'],
                        title: 'Value'
                    }
                    ],
                    series: [
                    {
                        type: 'column',
                        axis: 'left',
                        highlight: true,
                        xField: 'VOS',                    
                        yField: ['COST', 'LIST'],
                        tips: { 
                            trackMouse: true,
                            width: 250,
                            height: 40,
                            renderer: function(rec, item){
                                this.setTitle(rec.get('VOS') + ' = ' + rec.get('COST'));
                            }
                        }
                    }
                        
                    ]
                }
                ]
            });        
            win.show();        
        }
    });

  6. #6
    Ext JS Premium Member
    Join Date
    Jan 2008
    Location
    Germany, Berlin
    Posts
    135

    Default

    I just made an override and replaced the 4.0.6. methods by the ones from 4.0.2a and the negative values are showing up as expected.

    The problem is that there might be situations where one would not provide minimum and maximum. E.g. if I use a store providing data with varying values (negative and positive ones) I would expect that the chart configures itself to match all of my values and then draws the series.

    If I provide minimum and maximum I sort of cut off the range of values the store could be holding for me.

    In other situations it might be appropriate to just define a minimum value (lets say -100 to always display the negative part of an chart) without defininig a maximum (as I simply don't know how much revenue we will make on that day ....).

  7. #7
    Ext JS Premium Member
    Join Date
    Apr 2010
    Location
    Montreal, Canada
    Posts
    265

    Default

    big thanks ! work perfectly with your override. Now, I hope sencha will notice this thread and fix for next release!

    btw, if anyone wants the full override code
    Code:
    Ext.override(Ext.chart.series.Bar, {
            getBounds: function() {
            var me = this,
                chart = me.chart,
                store = chart.substore || chart.store,
                bars = [].concat(me.yField),
                barsLen = bars.length,
                groupBarsLen = barsLen,
                groupGutter = me.groupGutter / 100,
                column = me.column,
                xPadding = me.xPadding,
                yPadding = me.yPadding,
                stacked = me.stacked,
                barWidth = me.getBarGirth(),
                math = Math,
                mmax = math.max,
                mabs = math.abs,
                groupBarWidth, bbox, minY, maxY, axis, out,
                scale, zero, total, rec, j, plus, minus;
    
    
            me.setBBox(true);
            bbox = me.bbox;
    
    
            //Skip excluded series
            if (me.__excludes) {
                for (j = 0, total = me.__excludes.length; j < total; j++) {
                    if (me.__excludes[j]) {
                        groupBarsLen--;
                    }
                }
            }
    
    
            if (me.axis) {
                axis = chart.axes.get(me.axis);
                if (axis) {
                    out = axis.calcEnds();
    //             from 4.06
    //             minY = out.from;
    //             maxY = out.to;
    
    
    //             new override
                    minY = out.from || axis.prevMin;
                    maxY = mmax(out.to || axis.prevMax, 0);
                }
            }
    
    
            if (me.yField && !Ext.isNumber(minY)) {
                axis = Ext.create('Ext.chart.axis.Axis', {
                    chart: chart,
                    fields: [].concat(me.yField)
                });
                out = axis.calcEnds();
    //         from 4.06
    //         minY = out.from;
    //         maxY = out.to;
    
    
    //             new override
                minY = out.from || axis.prevMin;
                maxY = mmax(out.to || axis.prevMax, 0);
            }
    
    
            if (!Ext.isNumber(minY)) {
                minY = 0;
            }
            if (!Ext.isNumber(maxY)) {
                maxY = 0;
            }
            scale = (column ? bbox.height - yPadding * 2 : bbox.width - xPadding * 2) / (maxY - minY);
            groupBarWidth = barWidth / ((stacked ? 1 : groupBarsLen) * (groupGutter + 1) - groupGutter);
            zero = (column) ? bbox.y + bbox.height - yPadding : bbox.x + xPadding;
    
    
            if (stacked) {
                total = [[], []];
                store.each(function(record, i) {
                    total[0][i] = total[0][i] || 0;
                    total[1][i] = total[1][i] || 0;
                    for (j = 0; j < barsLen; j++) {
                        if (me.__excludes && me.__excludes[j]) {
                            continue;
                        }
                        rec = record.get(bars[j]);
                        total[+(rec > 0)][i] += mabs(rec);
                    }
                });
                total[+(maxY > 0)].push(mabs(maxY));
                total[+(minY > 0)].push(mabs(minY));
                minus = mmax.apply(math, total[0]);
                plus = mmax.apply(math, total[1]);
                scale = (column ? bbox.height - yPadding * 2 : bbox.width - xPadding * 2) / (plus + minus);
                zero = zero + minus * scale * (column ? -1 : 1);
            }
            else if (minY / maxY < 0) {
                zero = zero - minY * scale * (column ? -1 : 1);
            }
            return {
                bars: bars,
                bbox: bbox,
                barsLen: barsLen,
                groupBarsLen: groupBarsLen,
                barWidth: barWidth,
                groupBarWidth: groupBarWidth,
                scale: scale,
                zero: zero,
                xPadding: xPadding,
                yPadding: yPadding,
                signed: minY / maxY < 0,
                minY: minY,
                maxY: maxY
            };
        },
    
    
        getPaths: function() {
            var me = this,
                chart = me.chart,
                store = chart.substore || chart.store,
                bounds = me.bounds = me.getBounds(),
                items = me.items = [],
                gutter = me.gutter / 100,
                groupGutter = me.groupGutter / 100,
                animate = chart.animate,
                column = me.column,
                group = me.group,
                enableShadows = chart.shadow,
                shadowGroups = me.shadowGroups,
                shadowAttributes = me.shadowAttributes,
                shadowGroupsLn = shadowGroups.length,
                bbox = bounds.bbox,
                xPadding = me.xPadding,
                yPadding = me.yPadding,
                stacked = me.stacked,
                barsLen = bounds.barsLen,
                colors = me.colorArrayStyle,
                colorLength = colors && colors.length || 0,
                math = Math,
                mmax = math.max,
                mmin = math.min,
                mabs = math.abs,
                j, yValue, height, totalDim, totalNegDim, bottom, top, hasShadow, barAttr, attrs, counter,
                shadowIndex, shadow, sprite, offset, floorY;
    
    
            store.each(function(record, i, total) {
                bottom = bounds.zero;
                top = bounds.zero;
                totalDim = 0;
                totalNegDim = 0;
                hasShadow = false;
                for (j = 0, counter = 0; j < barsLen; j++) {
                    // Excluded series
                    if (me.__excludes && me.__excludes[j]) {
                        continue;
                    }
                    yValue = record.get(bounds.bars[j]);
    //                from 4.06
    //                height = Math.round((yValue - bounds.minY) * bounds.scale);
    
    
    //             new override
                    height = Math.round((yValue - ((bounds.minY < 0) ? 0 : bounds.minY)) * bounds.scale);
                    barAttr = {
                        fill: colors[(barsLen > 1 ? j : 0) % colorLength]
                    };
                    if (column) {
                        Ext.apply(barAttr, {
                            height: height,
                            width: mmax(bounds.groupBarWidth, 0),
                            x: (bbox.x + xPadding + i * bounds.barWidth * (1 + gutter) + counter * bounds.groupBarWidth * (1 + groupGutter) * !stacked),
                            y: bottom - height
                        });
                    }
                    else {
                        // draw in reverse order
                        offset = (total - 1) - i;
                        Ext.apply(barAttr, {
                            height: mmax(bounds.groupBarWidth, 0),
                            width: height + (bottom == bounds.zero),
                            x: bottom + (bottom != bounds.zero),
                            y: (bbox.y + yPadding + offset * bounds.barWidth * (1 + gutter) + counter * bounds.groupBarWidth * (1 + groupGutter) * !stacked + 1)
                        });
                    }
                    if (height < 0) {
                        if (column) {
                            barAttr.y = top;
                            barAttr.height = mabs(height);
                        } else {
                            barAttr.x = top + height;
                            barAttr.width = mabs(height);
                        }
                    }
                    if (stacked) {
                        if (height < 0) {
                            top += height * (column ? -1 : 1);
                        } else {
                            bottom += height * (column ? -1 : 1);
                        }
                        totalDim += mabs(height);
                        if (height < 0) {
                            totalNegDim += mabs(height);
                        }
                    }
                    barAttr.x = Math.floor(barAttr.x) + 1;
                    floorY = Math.floor(barAttr.y);
                    if (!Ext.isIE9 && barAttr.y > floorY) {
                        floorY--;
                    }
                    barAttr.y = floorY;
                    barAttr.width = Math.floor(barAttr.width);
                    barAttr.height = Math.floor(barAttr.height);
                    items.push({
                        series: me,
                        storeItem: record,
                        value: [record.get(me.xField), yValue],
                        attr: barAttr,
                        point: column ? [barAttr.x + barAttr.width / 2, yValue >= 0 ? barAttr.y : barAttr.y + barAttr.height] :
                                        [yValue >= 0 ? barAttr.x + barAttr.width : barAttr.x, barAttr.y + barAttr.height / 2]
                    });
                    // When resizing, reset before animating
                    if (animate && chart.resizing) {
                        attrs = column ? {
                            x: barAttr.x,
                            y: bounds.zero,
                            width: barAttr.width,
                            height: 0
                        } : {
                            x: bounds.zero,
                            y: barAttr.y,
                            width: 0,
                            height: barAttr.height
                        };
                        if (enableShadows && (stacked && !hasShadow || !stacked)) {
                            hasShadow = true;
                            //update shadows
                            for (shadowIndex = 0; shadowIndex < shadowGroupsLn; shadowIndex++) {
                                shadow = shadowGroups[shadowIndex].getAt(stacked ? i : (i * barsLen + j));
                                if (shadow) {
                                    shadow.setAttributes(attrs, true);
                                }
                            }
                        }
                        //update sprite position and width/height
                        sprite = group.getAt(i * barsLen + j);
                        if (sprite) {
                            sprite.setAttributes(attrs, true);
                        }
                    }
                    counter++;
                }
                if (stacked && items.length) {
                    items[i * counter].totalDim = totalDim;
                    items[i * counter].totalNegDim = totalNegDim;
                }
            }, me);
        }
    });

  8. #8
    Ext JS Premium Member
    Join Date
    Apr 2010
    Location
    Montreal, Canada
    Posts
    265

    Default

    bump, still a problem under 4.07

    Proposed overwrite by fschaeffer still works.

  9. #9
    Ext JS Premium Member
    Join Date
    Apr 2010
    Location
    Montreal, Canada
    Posts
    265

    Default

    thread marked as fixed, uner touch 2.0 ?!

    posting a new thread.

Posting Permissions

  • You may not post new threads
  • You may not post replies
  • You may not post attachments
  • You may not edit your posts
  •