Scrollable CategoryAxis
В последнее время приходится плотно работать с графиками, и в один "прекрасный" момент, встала задача обеспечить прокрутку и зумирование для графика у которого одна из осей ось категорий (CategoryAxis).
На просторах гугла обнаружились лишь два подхода к организации прокрутки категорий (как и три года назад). Первый вариант, заключается в изменении dataProvider графика с целью убрать значения не удовлетворяющие условиям. Второй вариант, заключается в подмене CategoryAxis на LinearAxis и установки для него labelFunction для маскировки под категории.
Первый вариант, несет дополнительные накладные расходы (при умелом подходе не слишком большие). Второй собственно не похож на оригинал и требует в качестве категории данных которые способна отобразить LinearAxis.
Отсюда следует вывод, что либо мало кто разобрался как работают оси в графиках, что сомнительно, ибо странно. Либо те, кто все-таки разобрался, делиться не хотят.
К сожалению, ни наследованием, ни композицией решить поставленную задачу не получится, поэтому итоговый класс на 98% состоит из кода оригинального CategoryAxis.
Для начала добавим пару свойств minimum и maximum и пару переменных adjustedMin и adjustedMax. CategoryAxis работает только с индексами, поэтому использовать будем целые числа.
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(); }
minimum это минимальный отображаемый индекс элемента в dataProvider, maximum — соответственно максимальный, т.е. максимальное валидное значение dataProvider.length - 1.
minimum и maximum используются только для хранения пользовательских настроек, т.е. значения у них могут быть и не валидные. Отрицательное значение означает, что значение не задано. adjustedMin и adjustedMax содержат скорректированные значения и используются в дальнейших расчетах. Теперь добавим функцию которая будет вычислять валидные значения.
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; } }
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(); } }
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; } } }
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; } }
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.
Всего комментариев 3
Комментарии
10.01.2012 23:20 | |
Экскурс небольшой бы в axis. И чем обособлена именно ось категорий, например, от "ос" -- полосатый мух.
Мне-то все равно, но Платон беспокоится. |
11.01.2012 00:06 | |
Последние записи от alatar
- Трансформации вокруг произвольной точки (05.02.2015)
- Декомпозиция матрицы. (25.12.2014)
- Баги TextField в iOS (29.07.2013)
- [Spark] Стрелочный индикатор. (06.12.2012)
- RSLs Monkey Patching (23.01.2012)