@progress/kendo-charts
Version:
Kendo UI platform-independent Charts library
616 lines (494 loc) • 17.6 kB
JavaScript
import Axis from './axis';
import AxisLabel from './axis-label';
import { BLACK, COORD_PRECISION, DEFAULT_PRECISION, X, Y } from '../common/constants';
import { isNumber, last, limitValue, round, setDefaultOptions, valueOrDefault, HashMap } from '../common';
import { dateEquals } from '../date-utils';
const MIN_CATEGORY_POINTS_RANGE = 0.01;
const MIN_CATEGORY_RANGE = 0.1;
function indexOf(value, arr) {
if (value instanceof Date) {
return arr.findIndex((item) => dateEquals(item, value));
}
return arr.indexOf(value);
}
class CategoryAxis extends Axis {
initFields() {
this._ticks = {};
}
categoriesHash() {
return "";
}
clone() {
const copy = new CategoryAxis(Object.assign({}, this.options, {
categories: this.options.srcCategories
}), this.chartService);
copy.createLabels();
return copy;
}
initUserOptions(options) {
const categories = options.categories || [];
const definedMin = options.min !== undefined;
const definedMax = options.max !== undefined;
options.srcCategories = options.categories = categories;
if ((definedMin || definedMax) && categories.length) {
const min = definedMin ? Math.floor(options.min) : 0;
let max;
if (definedMax) {
max = options.justified ? Math.floor(options.max) + 1 : Math.ceil(options.max);
} else {
max = categories.length;
}
options.categories = options.categories.slice(min, max);
}
return options;
}
rangeIndices() {
const options = this.options;
const length = options.categories.length || 1;
const min = isNumber(options.min) ? options.min % 1 : 0;
let max;
if (isNumber(options.max) && options.max % 1 !== 0 && options.max < this.totalRange().max) {
max = length - (1 - options.max % 1);
} else {
max = length - (options.justified ? 1 : 0);
}
return {
min: min,
max: max
};
}
range() {
const options = this.options;
const min = isNumber(options.min) ? options.min : 0;
const max = isNumber(options.max) ? options.max : this.totalRange().max;
return {
min: min,
max: max
};
}
roundedRange() {
return this.range();
}
totalRange() {
const options = this.options;
return { min: 0, max: Math.max(this._seriesMax || 0, options.srcCategories.length) - (options.justified ? 1 : 0) };
}
scaleOptions() {
const { min, max } = this.rangeIndices();
const lineBox = this.lineBox();
const size = this.options.vertical ? lineBox.height() : lineBox.width();
const scale = size / ((max - min) || 1);
return {
scale: scale * (this.options.reverse ? -1 : 1),
box: lineBox,
min: min,
max: max
};
}
arrangeLabels() {
super.arrangeLabels();
this.hideOutOfRangeLabels();
}
hideOutOfRangeLabels() {
const { box, labels } = this;
if (labels.length > 0) {
const valueAxis = this.options.vertical ? Y : X;
const start = box[valueAxis + 1];
const end = box[valueAxis + 2];
const firstLabel = labels[0];
const lastLabel = last(labels);
if (firstLabel.box[valueAxis + 1] > end || firstLabel.box[valueAxis + 2] < start) {
firstLabel.options.visible = false;
}
if (lastLabel.box[valueAxis + 1] > end || lastLabel.box[valueAxis + 2] < start) {
lastLabel.options.visible = false;
}
}
}
getMajorTickPositions() {
return this.getTicks().majorTicks;
}
getMinorTickPositions() {
return this.getTicks().minorTicks;
}
getLabelsTickPositions() {
return this.getTicks().labelTicks;
}
tickIndices(stepSize) {
const { min, max } = this.rangeIndices();
const limit = Math.ceil(max);
let current = Math.floor(min);
const indices = [];
while (current <= limit) {
indices.push(current);
current += stepSize;
}
return indices;
}
getTickPositions(stepSize) {
const { vertical, reverse } = this.options;
const { scale, box, min } = this.scaleOptions();
const pos = box[(vertical ? Y : X) + (reverse ? 2 : 1)];
const indices = this.tickIndices(stepSize);
const positions = [];
for (let idx = 0; idx < indices.length; idx++) {
positions.push(pos + round(scale * (indices[idx] - min), COORD_PRECISION));
}
return positions;
}
getTicks() {
const options = this.options;
const cache = this._ticks;
const range = this.rangeIndices();
const lineBox = this.lineBox();
const hash = lineBox.getHash() + range.min + "," + range.max + options.reverse + options.justified;
if (cache._hash !== hash) {
const hasMinor = options.minorTicks.visible || options.minorGridLines.visible;
cache._hash = hash;
cache.labelTicks = this.getTickPositions(1);
cache.majorTicks = this.filterOutOfRangePositions(cache.labelTicks, lineBox);
cache.minorTicks = hasMinor ? this.filterOutOfRangePositions(this.getTickPositions(0.5), lineBox) : [];
}
return cache;
}
filterOutOfRangePositions(positions, lineBox) {
if (!positions.length) {
return positions;
}
const axis = this.options.vertical ? Y : X;
const inRange = (position) => lineBox[axis + 1] <= position && position <= lineBox[axis + 2];
const end = positions.length - 1;
let startIndex = 0;
while (!inRange(positions[startIndex]) && startIndex <= end) {
startIndex++;
}
let endIndex = end;
while (!inRange(positions[endIndex]) && endIndex >= 0) {
endIndex--;
}
return positions.slice(startIndex, endIndex + 1);
}
lineInfo() {
const { vertical, reverse } = this.options;
const lineBox = this.lineBox();
const lineSize = vertical ? lineBox.height() : lineBox.width();
const axis = vertical ? Y : X;
const axisDir = reverse ? -1 : 1;
const startEdge = axisDir === 1 ? 1 : 2;
const axisOrigin = axis + startEdge.toString();
const lineStart = lineBox[axisOrigin];
return {
axis,
axisOrigin,
axisDir,
lineBox,
lineSize,
lineStart
};
}
lineDir() {
/*
* Category axis line direction:
* * Vertical: down.
* * Horizontal: right.
*/
const { reverse } = this.options;
return reverse ? -1 : 1;
}
// TODO: Rename to slotBox, valueSlot, slotByIndex?
getSlot(from, to, limit) {
const options = this.options;
const { reverse, justified } = options;
const { scale, box, min } = this.scaleOptions();
const { axis: valueAxis, lineStart } = this.lineInfo();
const slotBox = box.clone();
const singleSlot = to === undefined;
const start = valueOrDefault(from, 0);
let end = valueOrDefault(to, start);
end = Math.max(end - 1, start);
// Fixes transient bug caused by iOS 6.0 JIT
// (one can never be too sure)
end = Math.max(start, end);
let p1 = lineStart + (start - min) * scale;
let p2 = lineStart + (end + 1 - min) * scale;
if (singleSlot && justified) {
p2 = p1;
}
if (limit) {
p1 = limitValue(p1, box[valueAxis + 1], box[valueAxis + 2]);
p2 = limitValue(p2, box[valueAxis + 1], box[valueAxis + 2]);
}
slotBox[valueAxis + 1] = reverse ? p2 : p1;
slotBox[valueAxis + 2] = reverse ? p1 : p2;
return slotBox;
}
limitSlot(slot) {
const vertical = this.options.vertical;
const valueAxis = vertical ? Y : X;
const lineBox = this.lineBox();
const limittedSlot = slot.clone();
limittedSlot[valueAxis + 1] = limitValue(slot[valueAxis + 1], lineBox[valueAxis + 1], lineBox[valueAxis + 2]);
limittedSlot[valueAxis + 2] = limitValue(slot[valueAxis + 2], lineBox[valueAxis + 1], lineBox[valueAxis + 2]);
return limittedSlot;
}
slot(from, to, limit) {
const min = Math.floor(this.options.min || 0);
let start = from;
let end = to;
if (typeof start === "string") {
start = this.categoryIndex(start);
} else if (isNumber(start)) {
start -= min;
}
if (typeof end === "string") {
end = this.categoryIndex(end);
} else if (isNumber(end)) {
end -= min;
}
return super.slot(start, end, limit);
}
pointCategoryIndex(point) {
const { reverse, justified, vertical } = this.options;
const valueAxis = vertical ? Y : X;
const { scale, box, min, max } = this.scaleOptions();
const startValue = reverse ? max : min;
const lineStart = box[valueAxis + 1];
const lineEnd = box[valueAxis + 2];
const pos = point[valueAxis];
if (pos < lineStart || pos > lineEnd) {
return null;
}
let value = startValue + (pos - lineStart) / scale;
const diff = value % 1;
if (justified) {
value = Math.round(value);
} else if (diff === 0 && value > 0) {
value--;
}
return Math.floor(value);
}
getCategory(point) {
const index = this.pointCategoryIndex(point);
if (index === null) {
return null;
}
return this.options.categories[index];
}
categoryIndex(value) {
return this.totalIndex(value) - Math.floor(this.options.min || 0);
}
categoryAt(index, total) {
const options = this.options;
return (total ? options.srcCategories : options.categories)[index];
}
categoriesCount() {
return (this.options.categories || []).length;
}
translateRange(delta) {
const options = this.options;
const lineBox = this.lineBox();
const size = options.vertical ? lineBox.height() : lineBox.width();
const range = options.categories.length;
const scale = size / range;
const offset = round(delta / scale, DEFAULT_PRECISION);
return {
min: offset,
max: range + offset
};
}
scaleRange(scale, cursor) {
const position = Math.abs(this.pointOffset(cursor));
const rangeIndices = this.limitedRangeIndices();
const range = rangeIndices.max - rangeIndices.min;
const delta = this.scaleToDelta(scale, range);
const minDelta = position * delta;
const maxDelta = (1 - position) * delta;
const min = rangeIndices.min + minDelta;
let max = rangeIndices.max - maxDelta;
if (max - min < MIN_CATEGORY_RANGE) {
max = min + MIN_CATEGORY_RANGE;
}
return {
min: min,
max: max
};
}
zoomRange(scale, cursor) {
const { min: totalMin, max: totalMax } = this.totalRange();
const range = this.scaleRange(scale, cursor);
return {
min: limitValue(range.min, totalMin, totalMax),
max: limitValue(range.max, totalMin, totalMax)
};
}
labelsCount() {
const labelsRange = this.labelsRange();
return labelsRange.max - labelsRange.min;
}
labelsRange() {
const options = this.options;
const { justified, labels: labelOptions } = options;
let { min, max } = this.limitedRangeIndices(true);
const start = Math.floor(min);
if (!justified) {
min = Math.floor(min);
max = Math.ceil(max);
} else {
min = Math.ceil(min);
max = Math.floor(max);
}
let skip;
if (min > labelOptions.skip) {
skip = labelOptions.skip + labelOptions.step * Math.ceil((min - labelOptions.skip) / labelOptions.step);
} else {
skip = labelOptions.skip;
}
return {
min: skip - start,
max: (options.categories.length ? max + (justified ? 1 : 0) : 0) - start
};
}
createAxisLabel(index, labelOptions, labelContext) {
const options = this.options;
const dataItem = options.dataItems ? options.dataItems[index] : null;
const category = valueOrDefault(options.categories[index], "");
labelContext.dataItem = dataItem;
const text = this.axisLabelText(category, labelOptions, labelContext);
return new AxisLabel(category, text, index, dataItem, labelOptions);
}
shouldRenderNote(value) {
const range = this.limitedRangeIndices();
return Math.floor(range.min) <= value && value <= Math.ceil(range.max);
}
noteSlot(value) {
const options = this.options;
const index = value - Math.floor(options.min || 0);
return this.getSlot(index);
}
arrangeNotes() {
super.arrangeNotes();
this.hideOutOfRangeNotes();
}
hideOutOfRangeNotes() {
const { notes, box } = this;
if (notes && notes.length) {
const valueAxis = this.options.vertical ? Y : X;
const start = box[valueAxis + 1];
const end = box[valueAxis + 2];
for (let idx = 0; idx < notes.length; idx++) {
const note = notes[idx];
if (note.box && (end < note.box[valueAxis + 1] || note.box[valueAxis + 2] < start)) {
note.hide();
}
}
}
}
pan(delta) {
const range = this.limitedRangeIndices(true);
const { scale } = this.scaleOptions();
const offset = round(delta / scale, DEFAULT_PRECISION);
const totalRange = this.totalRange();
const min = range.min + offset;
const max = range.max + offset;
return this.limitRange(min, max, 0, totalRange.max, offset);
}
pointsRange(start, end) {
const { reverse, vertical } = this.options;
const valueAxis = vertical ? Y : X;
const range = this.limitedRangeIndices(true);
const { scale, box } = this.scaleOptions();
const lineStart = box[valueAxis + (reverse ? 2 : 1)];
const diffStart = start[valueAxis] - lineStart;
const diffEnd = end[valueAxis] - lineStart;
const min = range.min + diffStart / scale;
const max = range.min + diffEnd / scale;
const rangeMin = Math.min(min, max);
const rangeMax = Math.max(min, max);
if (rangeMax - rangeMin >= MIN_CATEGORY_POINTS_RANGE) {
return {
min: rangeMin,
max: rangeMax
};
}
}
valueRange() {
return this.range();
}
totalIndex(value) {
const options = this.options;
const index = this._categoriesMap ?
this._categoriesMap.get(value) : indexOf(value, options.srcCategories);
return index;
}
currentRangeIndices() {
const options = this.options;
let min = 0;
if (isNumber(options.min)) {
min = Math.floor(options.min);
}
let max;
if (isNumber(options.max)) {
max = options.justified ? Math.floor(options.max) : Math.ceil(options.max) - 1;
} else {
max = this.totalCount() - 1;
}
return {
min: min,
max: max
};
}
limitedRangeIndices(totalLimit) {
const options = this.options;
let min = isNumber(options.min) ? options.min : 0;
let max;
if (isNumber(options.max)) {
max = options.max;
} else if (isNumber(options.min)) {
max = min + options.categories.length;
} else {
max = this.totalRange().max || 1;
}
if (totalLimit) {
const totalRange = this.totalRange();
min = limitValue(min, 0, totalRange.max);
max = limitValue(max, 0, totalRange.max);
}
return {
min: min,
max: max
};
}
totalRangeIndices() {
return {
min: 0,
max: this.totalRange().max || 1
};
}
indexCategories() {
if (!this._categoriesMap) {
const map = this._categoriesMap = new HashMap();
const srcCategories = this.options.srcCategories;
for (let idx = 0; idx < srcCategories.length; idx++) {
map.set(srcCategories[idx], idx);
}
}
}
totalCount() {
return Math.max(this.options.srcCategories.length, this._seriesMax || 0);
}
}
setDefaultOptions(CategoryAxis, {
type: "category",
vertical: false,
majorGridLines: {
visible: false,
width: 1,
color: BLACK
},
labels: {
zIndex: 1
},
justified: false,
_deferLabels: true
});
export default CategoryAxis;