Форум Flasher.ru
Ближайшие курсы в Школе RealTime
Список интенсивных курсов: [см.]  
  
Специальные предложения: [см.]  
  
 
Блоги Правила Справка Пользователи Календарь Поиск рулит! Сообщения за день Все разделы прочитаны
 

Вернуться   Форум Flasher.ru > Блоги > alatar

Оценить эту запись

Scrollable CategoryAxis

Запись от alatar размещена 10.01.2012 в 15:59
Обновил(-а) alatar 22.03.2012 в 20:37

В последнее время приходится плотно работать с графиками, и в один "прекрасный" момент, встала задача обеспечить прокрутку и зумирование для графика у которого одна из осей ось категорий (CategoryAxis).

На просторах гугла обнаружились лишь два подхода к организации прокрутки категорий (как и три года назад). Первый вариант, заключается в изменении dataProvider графика с целью убрать значения не удовлетворяющие условиям. Второй вариант, заключается в подмене CategoryAxis на LinearAxis и установки для него labelFunction для маскировки под категории.

Первый вариант, несет дополнительные накладные расходы (при умелом подходе не слишком большие). Второй собственно не похож на оригинал и требует в качестве категории данных которые способна отобразить LinearAxis.

Отсюда следует вывод, что либо мало кто разобрался как работают оси в графиках, что сомнительно, ибо странно. Либо те, кто все-таки разобрался, делиться не хотят.

К сожалению, ни наследованием, ни композицией решить поставленную задачу не получится, поэтому итоговый класс на 98% состоит из кода оригинального CategoryAxis.

Для начала добавим пару свойств minimum и maximum и пару переменных adjustedMin и adjustedMax. CategoryAxis работает только с индексами, поэтому использовать будем целые числа.

Код AS3:
private var adjustedMin:int;
private var adjustedMax:int;
...
//----------------------------------
//  minimum
//----------------------------------
 
/**
 *  @private
 */
private var _minimum:int = -1;
 
/**
 *  Specifies the minimum index for displayed items.
 *  If <code>minimum < 0</code>, Flex determines the minimum value
 *  as 0. 
 *  The default value is <code>-1</code>.
 *  
 *  @langversion 3.0
 *  @playerversion Flash 9
 *  @playerversion AIR 1.1
 *  @productversion Flex 3
 */
 
 
public function get minimum():int
{
    return _minimum;
}
 
/**
 *  @private
 */
public function set minimum(value:int):void
{
    if (value == _minimum)
        return;
 
    _minimum = value;
 
    invalidateCategories();
}
 
//----------------------------------
//  maximum
//----------------------------------
 
/**
 *  @private
 */
private var _maximum:int = -1;
 
/**
 *  Specifies the maximum index for displayed items.
 *  If <code>maximum > dataProvider.length</code> or <code>maximum < 0</code>, Flex determines the maximum value
 *  from the dataProvider. 
 *  The default value is <code>-1</code>.
 *  
 *  @langversion 3.0
 *  @playerversion Flash 9
 *  @playerversion AIR 1.1
 *  @productversion Flex 3
 */
 
public function get maximum():int
{
    return _maximum;
}
 
/**
 *  @private
 */
public function set maximum(value:int):void
{
    if (value == _maximum)
        return;
 
    _maximum = value;
 
    invalidateCategories();
}
Вот из-за вызова приватной функции invalidateCategories(), которая обнуляет не менее приватные кеши и пришлось заниматься копипастой вместо наследования.

minimum это минимальный отображаемый индекс элемента в dataProvider, maximum — соответственно максимальный, т.е. максимальное валидное значение dataProvider.length - 1.
minimum и maximum используются только для хранения пользовательских настроек, т.е. значения у них могут быть и не валидные. Отрицательное значение означает, что значение не задано. adjustedMin и adjustedMax содержат скорректированные значения и используются в дальнейших расчетах. Теперь добавим функцию которая будет вычислять валидные значения.
Код AS3:
private function adjustMinMax():void
{
    if (minimum < 0)
    {
        adjustedMin = 0;
    }
    else if (minimum > _dataProvider.length)
    {
        adjustedMin = _dataProvider.length;
    }
    else
    {
        adjustedMin = minimum;
    }
 
    if (maximum < 0 || maximum >= _dataProvider.length)
    {
        adjustedMax = _dataProvider.length;
    }
    else if (maximum < adjustedMin)
    {
        adjustedMax = adjustedMin;
    }
    else
    {
        adjustedMax = maximum + 1;
    }
}
Теперь собственно займемся расчетами. По большому счету, нас интересую четыре функции update, mapCache, filterCache и transformCache. Изменения по возможности минимальны.

Код AS3:
public function update():void
{
    if (!_labelSet)
    {
        adjustMinMax();
 
        var prop:Object;
 
        _catMap = {};
        _categoryValues = [];
        _labelsMatchToCategoryValuesByIndex = [];
 
        var categoryItems:Array = [];
        var i:int;
 
        var min:Number = adjustedMin;
        var max:Number = adjustedMax - 1;
        var alen:Number = max - min;
 
        if (dataFunction != null)
        {
            _cursor.seek(CursorBookmark.FIRST, adjustedMin);
            i = 0;
            while (!_cursor.afterLast)
            {
                if (i > alen)
                    break;
 
                categoryItems[i] = _cursor.current;
                prop = dataFunction(this,categoryItems[i]);
                if (prop)
                    _catMap[prop.toString()] = i;                   
                _categoryValues[i] = prop;
 
 
                i++;
                _cursor.moveNext()
            }
        }
 
        else if (categoryField == "")
        {
            _cursor.seek(CursorBookmark.FIRST, adjustedMin);
            i = 0;
            while (!_cursor.afterLast)
            {
                if (i > alen)
                    break;
 
                prop = _cursor.current;
                if (prop != null)
                    _catMap[prop.toString()] = i;                   
                _categoryValues[i] = categoryItems[i] = prop;
                _cursor.moveNext();
                i++;
            }
        }
        else
        {
            _cursor.seek(CursorBookmark.FIRST, adjustedMin);
            i = 0;
            while (!_cursor.afterLast)
            {
                if (i > alen)
                    break;
 
                categoryItems[i] = _cursor.current;
                if (categoryItems[i] && categoryField in categoryItems[i])
                {
                    prop = categoryItems[i][categoryField];
                    if (prop != null)
                        _catMap[prop.toString()] = i;                   
                    _categoryValues[i] = prop;
                }
                else
                {
                    _categoryValues[i] = null;
                }
                i++;
                _cursor.moveNext()
            }
        }
 
        var axisLabels:Array /* of AxisLabel */ = [];
 
        min = -padding;
        max = _categoryValues.length - 1 + padding;
        alen = max - min;
        var label:AxisLabel;
 
        var n:int = _categoryValues.length;
        if (labelFunction != null)
        {
            var previousValue:Object = null;
            for (i = 0; i < n; i++)
            {
                if (_categoryValues[i] == null)
                    continue;
 
                label = new AxisLabel((i - min) / alen, _categoryValues[i],
                    labelFunction(_categoryValues[i], previousValue,
                        this, categoryItems[i]));
                _labelsMatchToCategoryValuesByIndex[i] = label;
                axisLabels.push(label);
 
                previousValue = _categoryValues[i];
            }
        }
        else
        {
            for (i = 0; i < n; i++)
            {
                if (!_categoryValues[i])
                    continue;
 
                label = new AxisLabel((i - min) / alen, _categoryValues[i],
                    _categoryValues[i].toString());
                _labelsMatchToCategoryValuesByIndex[i] = label;
                axisLabels.push(label);
            }               
        }
 
        _labelSet = new AxisLabelSet();
        _labelSet.labels = axisLabels;
        _labelSet.accurate = true;
        _labelSet.minorTicks = minorTicks;
        _labelSet.ticks = generateTicks();          
    }
}
Код AS3:
public function mapCache(cache:Array /* of ChartItem */, field:String,
                         convertedField:String,
                         indexValues:Boolean = false):void
{
    update();
 
    var n:int = cache.length;
 
    // Find the first non null item in the cache so we can determine type.
    // Since these initial values are null,
    // we can safely skip assigning values for them.
    for (var i:int = 0; i < n; i++)
    {
        if (cache[i][field] != null)
            break;
    }
 
    if (i < adjustedMin)
        i = adjustedMin;
 
    if (n > adjustedMax)
        n = adjustedMax;
 
    if (i == n)
        return;
 
    var value:Object = cache[i][field]
    if (value is XML ||
        value is XMLList)
    {
        for (; i < n; i++)
        {
            cache[i][convertedField] = _catMap[cache[i][field].toString()] = i;             
        }
    }
    else if ((value is Number || value is int || value is uint) &&
        indexValues == true)
    {
        for (i = 0; i < n; i++)
        {
            var v:Object = cache[i];
            v[convertedField] = v[field];
        }
    }
    else
    {
        for (; i < n; i++)
        {
            cache[i][convertedField] = _catMap[cache[i][field]] = i;                
        }
    }
}
Код AS3:
public function filterCache(cache:Array /* of ChartItem */, field:String,
                            filteredField:String):void
{
    update();
 
    // Our bounds are the categories, plus/minus padding,
    // plus a little fudge factor to account for floating point errors.
    var computedMinimum:Number = adjustedMin;
    var computedMaximum:Number = adjustedMax;
 
    var n:int = cache.length;
    for (var i:int = 0; i < n; i++)
    {
        var v:Number =  cache[i][field];
        cache[i][filteredField] = v >= computedMinimum &&
            v < computedMaximum ?
            v :
            NaN;
    }
}
Код AS3:
public function transformCache(cache:Array /* of ChartItem */, field:String,
                               convertedField:String):void
{
    update();
 
    var min:Number = adjustedMin - padding;
    var max:Number = adjustedMax - 1 + padding;
    var alen:Number = max - min;
 
    var n:int = cache.length;
    for (var i:int = 0; i < n; i++)
    {
        cache[i][convertedField] = (cache[i][field] - min) / alen;
    }
}
Вот собственно и все, будут баги (а в специфических ситуациях они будут, т.к. я не проводил всесторонней проверки) пишите. Во вложениях сам класс и флешка для поиграться.

UPD: Исправлен небольшой баг в методе mapCache при cache.length == 0. Спасибо nicky75.
Вложения
Тип файла: zip SubRangeCategoryAxis.zip (387.7 Кб, 210 просмотров)
Размещено в Flex , Flex 4 , Flex 3
Комментарии 3 Отправить другу ссылку на эту запись
Всего комментариев 3

Комментарии

Старый 10.01.2012 23:20 dimarik вне форума
dimarik
 
Аватар для dimarik
Экскурс небольшой бы в axis. И чем обособлена именно ось категорий, например, от "ос" -- полосатый мух.
Мне-то все равно, но Платон беспокоится.
Старый 11.01.2012 00:06 alatar вне форума
alatar
 
Аватар для alatar
Старый 11.01.2012 21:17 PainKiller вне форума
PainKiller
 
Аватар для PainKiller
Неплохо, к сожалению я не на флексе пишу. Стояла у меня задача строить гистограммы такого же типа как у вас, при этом входные данные могли быть какой угодно размерности. Сначала пытался рассчитывать расположение столбиков через формулы, но все равно на разных данных косметические косяки возникали. В конечном итоге пришел к выводу, что в эксемельнике надо атрибутами задавать местоположение и размер каждого элемента, иначе унифицировать графики никак не получалось. Но это сильно усложняет создание XML файлов для графика.
 

 


Часовой пояс GMT +4, время: 19:25.


Copyright © 1999-2008 Flasher.ru. All rights reserved.
Работает на vBulletin®. Copyright ©2000 - 2024, Jelsoft Enterprises Ltd. Перевод: zCarot
Администрация сайта не несёт ответственности за любую предоставленную посетителями информацию. Подробнее см. Правила.