@amcharts/amcharts5
Version:
amCharts 5
689 lines • 27.3 kB
JavaScript
import { Axis } from "./Axis";
import { populateString } from "../../../core/util/PopulateString";
import { ValueAxis } from "./ValueAxis";
import * as $array from "../../../core/util/Array";
import * as $type from "../../../core/util/Type";
import * as $math from "../../../core/util/Math";
import * as $utils from "../../../core/util/Utils";
/**
* Creates a category axis.
*
* @see {@link https://www.amcharts.com/docs/v5/charts/xy-chart/axes/category-axis/} for more info
* @important
*/
export class CategoryAxis extends Axis {
constructor() {
super(...arguments);
this._frequency = 1;
this._itemMap = {};
}
_afterNew() {
this._settings.themeTags = $utils.mergeTags(this._settings.themeTags, ["axis"]);
this.fields.push("category", "id", "cellSize");
this.setPrivateRaw("name", "category");
this.addTag("category");
super._afterNew();
}
_afterDataChange() {
super._afterDataChange();
const len = this.dataItems.length;
if (len > 0) {
this.setPrivateRaw("maxZoomFactor", len);
}
// fix final indexes
this.setPrivateRaw("startIndex", Math.min(this.getPrivate("startIndex", 0), len));
this.setPrivateRaw("endIndex", Math.min(this.getPrivate("endIndex", len), 1));
}
_prepareChildren() {
super._prepareChildren();
if (this.isDirty("startLocation") || this.isDirty("endLocation")) {
this._sizeDirty = true;
}
const len = this.dataItems.length;
let i = 0;
if (this._valuesDirty) {
this._itemMap = {};
$array.each(this.dataItems, (dataItem) => {
dataItem.setRaw("index", i);
this._itemMap[dataItem.get("category")] = dataItem;
i++;
});
this.setPrivateRaw("maxZoomFactor", len);
}
let start = this.get("start", 0);
let end = this.get("end", 1);
let indices = this._getIndices(start, end);
this.setPrivateRaw("startIndex", indices.startIndex);
this.setPrivateRaw("endIndex", indices.endIndex);
if (this._sizeDirty || this._valuesDirty || (this.isDirty("start") || this.isDirty("end") || this.isPrivateDirty("endIndex") || this.isPrivateDirty("startIndex") || this.isPrivateDirty("width") || this.isPrivateDirty("height"))) {
if (this.dataItems.length > 0) {
this._handleRangeChange();
this._prepareAxisItems();
this._updateAxisRanges();
}
}
}
/**
* @ignore
*/
adjustZoom() {
const len = this.dataItems.length;
if (len > 1) {
let maxZoomCount = this.get("maxZoomCount", this.dataItems.length);
let minZoomCount = this.get("minZoomCount", 1);
let count = 0;
let startIndex = this.getPrivate("startIndex", 0);
let endIndex = this.getPrivate("endIndex", 0);
const sAnimation = this._sAnimation;
const eAnimation = this._eAnimation;
let start = this.get("start", 0);
let getIndices = false;
if (sAnimation && !sAnimation.stopped) {
start = Number(sAnimation.to);
getIndices = true;
}
let end = this.get("end", 1);
if (eAnimation && !eAnimation.stopped) {
end = Number(eAnimation.to);
getIndices = true;
}
if (getIndices) {
const indices = this._getIndices(start, end);
startIndex = indices.startIndex;
endIndex = indices.endIndex;
}
for (let i = startIndex; i < endIndex; i++) {
const dataItem = this.dataItems[i];
count += dataItem.get("finalCellSize", 1);
}
;
if (count <= minZoomCount) {
// need to zoom out
let c = 0;
for (let i = startIndex; i < len; i++) {
const dataItem = this.dataItems[i];
c += dataItem.get("finalCellSize", 1);
endIndex = i + 1;
if (c >= minZoomCount) {
break;
}
}
if (c < minZoomCount) {
// still not enough, try to extend at the start
for (let i = startIndex - 1; i >= 0; i--) {
const dataItem = this.dataItems[i];
c += dataItem.get("finalCellSize", 1);
startIndex = i;
if (c >= minZoomCount) {
break;
}
}
}
this.zoomToIndexes(startIndex, endIndex);
}
else if (count >= maxZoomCount) {
let c = count;
for (let i = endIndex - 1; i >= startIndex; i--) {
const dataItem = this.dataItems[i];
c -= dataItem.get("finalCellSize", 1);
if (c <= maxZoomCount) {
endIndex = i + 1;
break;
}
}
this.zoomToIndexes(startIndex, endIndex);
}
}
}
_handleRangeChange() {
$array.each(this.series, (series) => {
let baseAxis = series.get("baseAxis");
let xAxis = series.get("xAxis");
let yAxis = series.get("yAxis");
if (xAxis instanceof CategoryAxis && yAxis instanceof CategoryAxis) {
series._markDirtyAxes();
}
else if (baseAxis === this) {
let key;
let openKey;
let otherAxis = yAxis;
if (xAxis === baseAxis) {
if (series.get("categoryXField")) {
key = "categoryX";
}
if (series.get("openCategoryXField")) {
openKey = "openCategoryX";
}
}
else if (yAxis === baseAxis) {
if (series.get("categoryYField")) {
key = "categoryY";
}
if (series.get("openCategoryYField")) {
openKey = "openCategoryY";
}
otherAxis = xAxis;
}
if (otherAxis instanceof ValueAxis) {
if (key || openKey) {
let firstIndex = this.startIndex();
let lastIndex = this.endIndex() - 1;
let startIndex = -1;
let endIndex = 0;
// Series data is not necessarily in the same order as axis
// categories, and a category can hold more than one item, so
// find the outermost items that touch the selected categories
// instead of relying on the order.
for (let i = 0, len = series.dataItems.length; i < len; i++) {
let dataItem = series.dataItems[i];
let low = NaN;
let high = NaN;
if (key) {
low = high = this.categoryToIndex(dataItem.get(key));
}
if (openKey) {
let openIndex = this.categoryToIndex(dataItem.get(openKey));
if ($type.isNumber(openIndex)) {
low = $type.isNumber(low) ? Math.min(low, openIndex) : openIndex;
high = $type.isNumber(high) ? Math.max(high, openIndex) : openIndex;
}
}
if ($type.isNumber(low) && low <= lastIndex && high >= firstIndex) {
if (startIndex == -1) {
startIndex = i;
}
endIndex = i + 1;
}
}
if (startIndex == -1) {
startIndex = 0;
}
series.setPrivate("startIndex", startIndex);
series.setPrivate("endIndex", endIndex);
let hasValue = false;
for (let i = startIndex; i < endIndex; i++) {
const dataItem = series.dataItems[i];
$array.each(series.__valueXShowFields, (key) => {
let value = dataItem.get(key);
if (value != null) {
hasValue = true;
}
});
$array.each(series.__valueYShowFields, (key) => {
let value = dataItem.get(key);
if (value != null) {
hasValue = true;
}
});
if (hasValue) {
break;
}
}
series.setPrivate("outOfSelection", !hasValue);
}
}
series._markDirtyAxes(); // must be outside
}
});
}
_prepareAxisItems() {
var _a;
const renderer = this.get("renderer");
const len = this.dataItems.length;
let startIndex = this.startIndex();
if (startIndex > 0) {
startIndex--;
}
let endIndex = this.endIndex();
if (endIndex < len) {
endIndex++;
}
const minorLabelsEnabled = renderer.get("minorLabelsEnabled");
const minorGridEnabled = renderer.get("minorGridEnabled", minorLabelsEnabled);
let maxCount = renderer.axisLength() / Math.max(renderer.get("minGridDistance"), 1);
let frequency = Math.max(1, Math.min(len, Math.ceil((endIndex - startIndex) / maxCount)));
// A degenerate axis length (e.g. a NaN/Infinity angle span on a radar
// chart) makes `maxCount` non-finite, which cascades into a NaN
// `frequency` and a NaN `startIndex` — and `this.dataItems[NaN]` is
// undefined, throwing on `.get()` below. Fall back to a valid frequency.
if (!$type.isNumber(frequency)) {
frequency = 1;
}
startIndex = Math.floor(startIndex / frequency) * frequency;
this._frequency = frequency;
for (let j = 0; j < len; j++) {
this._toggleDataItem(this.dataItems[j], false);
}
let f = this.dataItems[startIndex].get("index", 0);
for (let i = startIndex; i < endIndex; i = i + frequency) {
let dataItem = this.dataItems[i];
this._createAssets(dataItem, []);
this._toggleDataItem(dataItem, true);
let count = frequency;
if (minorGridEnabled) {
count = 1;
}
this._prepareDataItem(dataItem, f, count);
f++;
}
if (renderer.get("minorGridEnabled")) {
for (let i = startIndex; i < endIndex; i++) {
let dataItem = this.dataItems[i];
if (i % frequency != 0) {
this._createAssets(dataItem, ["minor"], true);
this._toggleDataItem(dataItem, true);
this._prepareDataItem(dataItem, 0, 1);
if (!minorLabelsEnabled) {
(_a = dataItem.get("label")) === null || _a === void 0 ? void 0 : _a.setPrivate("visible", false);
}
}
}
}
this._updateGhost();
}
_prepareDataItem(dataItem, fillIndex, count) {
let renderer = this.get("renderer");
let categoryLocation = dataItem.get("categoryLocation", 0);
let endCategoryLocation = dataItem.get("endCategoryLocation", 1);
let index = dataItem.get("index");
if (!$type.isNumber(index)) {
index = this.categoryToIndex(dataItem.get("category"));
}
let position = this.indexToPosition(index, categoryLocation);
let endCategory = dataItem.get("endCategory");
let endIndex;
if (endCategory) {
endIndex = this.categoryToIndex(endCategory);
if (!$type.isNumber(endIndex)) {
endIndex = index;
}
}
else {
endIndex = index;
}
let endPosition = this.indexToPosition(endIndex, endCategoryLocation);
let fillEndIndex;
let fillEndPosition;
if (dataItem.get("isRange")) {
fillEndIndex = endIndex;
if (!$type.isNumber(index)) {
this._toggleDataItem(dataItem, false);
return;
}
}
else {
fillEndIndex = index + this._frequency - 1;
}
fillEndPosition = this.indexToPosition(fillEndIndex, endCategoryLocation);
renderer.updateLabel(dataItem.get("label"), position, endPosition, count);
renderer.updateGrid(dataItem.get("grid"), position, endPosition);
renderer.updateTick(dataItem.get("tick"), position, endPosition, count);
renderer.updateFill(dataItem.get("axisFill"), position, fillEndPosition);
this._processBullet(dataItem);
renderer.updateBullet(dataItem.get("bullet"), position, endPosition);
const fillRule = this.get("fillRule");
if (fillRule) {
fillRule(dataItem, fillIndex);
}
}
startIndex() {
let len = this.dataItems.length;
return Math.min(Math.max(this.getPrivate("startIndex", 0), 0), len - 1);
}
endIndex() {
let len = this.dataItems.length;
return Math.max(1, Math.min(this.getPrivate("endIndex", len), len));
}
/**
* @ignore
*/
baseValue() {
}
/**
* @ignore
*/
basePosition() {
return 0;
}
/**
* Returns X coordinate in pixels corresponding to specific category index.
*
* @param value Index
* @return X coordinate
*/
getX(value) {
let axisDataItem = this._itemMap[value];
if (axisDataItem) {
return this._settings.renderer.positionToCoordinate(this.indexToPosition(axisDataItem.get("index", 0)));
}
return NaN;
}
/**
* Returns Y coordinate in pixels corresponding to specific category index.
*
* @param value Index
* @return Y coordinate
*/
getY(value) {
let axisDataItem = this._itemMap[value];
if (axisDataItem) {
return this._settings.renderer.positionToCoordinate(this.indexToPosition(axisDataItem.get("index", 0)));
}
return NaN;
}
/**
* @ignore
*/
getDataItemPositionX(dataItem, field, cellLocation, _axisLocation) {
const category = dataItem.get(field);
const axisDataItem = this._itemMap[category];
if (axisDataItem) {
return this.indexToPosition(axisDataItem.get("index", 0), cellLocation);
}
return NaN;
}
/**
* @ignore
*/
getDataItemCoordinateX(dataItem, field, cellLocation, _axisLocation) {
return this._settings.renderer.positionToCoordinate(this.getDataItemPositionX(dataItem, field, cellLocation, _axisLocation));
}
/**
* @ignore
*/
getDataItemPositionY(dataItem, field, cellLocation, _axisLocation) {
const category = dataItem.get(field);
const axisDataItem = this._itemMap[category];
if (axisDataItem) {
return this.indexToPosition(axisDataItem.get("index", 0), cellLocation);
}
return NaN;
}
/**
* @ignore
*/
getDataItemCoordinateY(dataItem, field, cellLocation, _axisLocation) {
return this._settings.renderer.positionToCoordinate(this.getDataItemPositionY(dataItem, field, cellLocation, _axisLocation));
}
/**
* Converts category index to a relative position.
*
* `location` indicates relative position within category: 0 - start, 1 - end.
*
* If not set, will use middle (0.5) of the category.
*
* @param index Index
* @param location Location
* @return Index
*/
indexToPosition(index, location, final) {
let len = this.dataItems.length;
let position = 0;
if (len == 0) {
return 0;
}
if (index >= len) {
index = len - 1;
location = 1;
}
if (!$type.isNumber(location)) {
location = 0.5;
}
if (!$type.isNumber(index)) {
return 0;
}
let startLocation = this.get("startLocation", 0);
let endLocation = this.get("endLocation", 1);
if (!this.get("cellSizeField")) {
len -= startLocation;
len -= (1 - endLocation);
position = (index + location - startLocation) / len;
}
else {
let name = final ? "finalCellSize" : "cellSize";
const dataItems = this.dataItems;
// Calculate total modified count (sum of cell sizes)
let modCount = 0;
$array.each(this.dataItems, (dataItem) => {
modCount += dataItem.get(name, 1);
});
modCount -= startLocation * dataItems[0].get(name, 1);
modCount -= (1 - endLocation) * dataItems[dataItems.length - 1].get(name, 1);
// Calculate the position based on cell sizes
let acc = 0;
for (let i = 0; i < index; i++) {
acc += dataItems[i].get(name, 1);
}
let cellSizeAtIndex = dataItems[index].get(name, 1);
position = (acc + location * cellSizeAtIndex - startLocation * dataItems[0].get(name, 1)) / modCount;
}
if (!final) {
let dataItem = this.dataItems[index];
if (dataItem) {
position += dataItem.get("deltaPosition", 0);
}
}
return position;
}
/**
* Returns a position of a category.
*
* @param category Category to look up
* @return Position
*/
categoryToPosition(category) {
let dataItem = this._itemMap[category];
if (dataItem) {
return this.indexToPosition(dataItem.get("index"));
}
return NaN;
}
/**
* Returns an index of a category.
*
* @param category Category to look up
* @return Index
*/
categoryToIndex(category) {
let dataItem = this._itemMap[category];
if (dataItem) {
return dataItem.get("index");
}
return NaN;
}
/**
* @ignore
*/
dataItemToPosition(dataItem) {
return this.indexToPosition(dataItem.get("index"));
}
/**
* @ignore
*/
roundAxisPosition(position, location) {
position += (0.5 - location) / this.dataItems.length;
return this.indexToPosition(this.axisPositionToIndex(position), location);
}
/**
* Returns an index of the category that corresponds to specific pixel
* position within axis.
*
* @param position Position (px)
* @return Category index
*/
axisPositionToIndex(position) {
let len = this.dataItems.length;
if (len === 0) {
return 0;
}
// Calculate total modified length (sum of cell sizes)
if (this.get("cellSizeField")) {
let modifiedLen = 0;
let cellSizes = [];
$array.each(this.dataItems, (dataItem) => {
const cellSize = dataItem.get("cellSize", 1);
cellSizes.push(cellSize);
modifiedLen += cellSize;
});
// Adjust for startLocation and endLocation
let startLocation = this.get("startLocation", 0);
let endLocation = this.get("endLocation", 1);
modifiedLen -= startLocation;
modifiedLen -= (1 - endLocation);
// Find which cell the position falls into
let rel = position * modifiedLen + startLocation;
let acc = 0;
for (let i = 0; i < len; i++) {
const cellSize = cellSizes[i];
if (rel < acc + cellSize) {
return i;
}
acc += cellSize;
}
return len - 1;
}
else {
return $math.fitToRange(Math.floor(position * len), 0, len - 1);
}
}
/**
* Returns text to be used in an axis tooltip for specific relative position.
*
* @param position Position
* @return Tooltip text
*/
getTooltipText(position, _adjustPosition) {
//@todo number formatter + tag
const dataItem = this.dataItems[this.axisPositionToIndex(position)];
if (dataItem) {
const label = dataItem.get("label");
if (label) {
return populateString(label, this.get("tooltipText", ""));
}
}
}
_updateTooltipText(tooltip, position) {
tooltip._setDataItem(this.dataItems[this.axisPositionToIndex(position)]);
tooltip.label.text.markDirtyText();
}
/**
* Returns a data item from series that is closest to the `position`.
*
* @param series Series
* @param position Relative position
* @return Data item
*/
getSeriesItem(series, position) {
if (this.dataItems.length > 0) {
let fieldName = (this.getPrivate("name") + this.get("renderer").getPrivate("letter"));
let index = this.axisPositionToIndex(position);
// try simple first
let seriesDataItem = series.dataItems[index];
let axisDataItem = this.dataItems[index];
let category = axisDataItem.get("category");
if (seriesDataItem && axisDataItem) {
if (seriesDataItem.get(fieldName) === category) {
return seriesDataItem;
}
}
// if not found, try looking
for (let i = 0, len = series.dataItems.length; i < len; i++) {
let dataItem = series.dataItems[i];
if (dataItem.get(fieldName) === category) {
return dataItem;
}
}
}
}
_getIndices(start, end) {
let len = this.dataItems.length;
let startIndex = 0;
let endIndex = len;
let name = "cellSize";
if (this.get("cellSizeField")) {
let count = 0;
const dataItems = this.dataItems;
if (dataItems.length == 0) {
return { startIndex: 0, endIndex: 0 };
}
$array.each(dataItems, (dataItem) => {
count += dataItem.get(name, 1);
});
count -= this.get("startLocation", 0) * dataItems[0].get(name, 1);
count -= (1 - this.get("endLocation", 1)) * dataItems[dataItems.length - 1].get(name, 1);
let c = 0;
for (let i = 0; i < len; i++) {
c += this.dataItems[i].get(name, 1);
if (Math.round(c) > Math.round(start * count)) {
startIndex = i;
break;
}
}
for (let i = startIndex + 1; i < len; i++) {
c += this.dataItems[i].get(name, 1);
if (Math.round(c) >= Math.round(end * count)) {
endIndex = i + 1;
break;
}
}
startIndex = Math.max(startIndex, 0);
endIndex = Math.min(endIndex, len);
}
else {
startIndex = Math.max(Math.round(this.get("start", 0) * len), 0);
endIndex = Math.min(Math.round(this.get("end", 1) * len), len);
}
return { startIndex, endIndex };
}
/**
* Zooms the axis to specific `start` and `end` indexes.
*
* Optional `duration` specifies duration of zoom animation in milliseconds.
*
* @param start Start index
* @param end End index
* @param duration Duration in milliseconds
*/
zoomToIndexes(start, end, duration) {
let len = this.dataItems.length;
if (this.get("cellSizeField")) {
start = Math.min(Math.max(start, 0), len);
end = Math.max(Math.min(end, len), 1);
this.setPrivateRaw("startIndex", start);
this.setPrivateRaw("endIndex", end);
this.zoom(this.indexToPosition(start, 0, true), this.indexToPosition(end, 0, true), duration);
}
else {
this.zoom(start / len, end / len, duration);
}
}
zoomToCategories(startCategory, endCategory, duration) {
this.zoomToIndexes(this.categoryToIndex(startCategory), this.categoryToIndex(endCategory) + 1, duration);
}
/**
* Returns position span between start and end of a single cell in axis.
*
* @since 5.2.30
* @return Position
*/
getCellWidthPosition() {
return this._frequency / this.dataItems.length / (this.get("end", 1) - this.get("start", 0));
}
/**
* @ignore
*/
nextPosition(count) {
if (count == null) {
count = 1;
}
if (this.get("renderer").getPrivate("letter") == "Y") {
count *= -1;
}
const position = this.getPrivate("tooltipPosition", 0);
const index = $math.fitToRange(this.axisPositionToIndex(position) + count, 0, this.dataItems.length - 1);
return this.toGlobalPosition(this.indexToPosition(index));
}
}
CategoryAxis.className = "CategoryAxis";
CategoryAxis.classNames = Axis.classNames.concat([CategoryAxis.className]);
//# sourceMappingURL=CategoryAxis.js.map