scichart
Version:
Fast WebGL JavaScript Charting Library and Framework
601 lines (600 loc) • 30.8 kB
JavaScript
"use strict";
var __extends = (this && this.__extends) || (function () {
var extendStatics = function (d, b) {
extendStatics = Object.setPrototypeOf ||
({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; };
return extendStatics(d, b);
};
return function (d, b) {
if (typeof b !== "function" && b !== null)
throw new TypeError("Class extends value " + String(b) + " is not a constructor or null");
extendStatics(d, b);
function __() { this.constructor = d; }
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
};
})();
var __assign = (this && this.__assign) || function () {
__assign = Object.assign || function(t) {
for (var s, i = 1, n = arguments.length; i < n; i++) {
s = arguments[i];
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
t[p] = s[p];
}
return t;
};
return __assign.apply(this, arguments);
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.LabelProviderBase2D = exports.LabelInfo = void 0;
var app_1 = require("../../../../constants/app");
var guid_1 = require("../../../../utils/guid");
var text_1 = require("../../../../utils/text");
var NativeObject_1 = require("../../Helpers/NativeObject");
var SciChartDefaults_1 = require("../../SciChartDefaults");
var TextureManager_1 = require("../../TextureManager/TextureManager");
var LabelCache_1 = require("./LabelCache");
var LabelProvider_1 = require("./LabelProvider");
var defaultLineSpacing = 1.1;
var defaultNativeLineSpacing = 3;
var LabelInfo = /** @class */ (function () {
function LabelInfo(tick, text, bitmapTexture, textureHeight, textureWidth) {
this.tick = tick;
this.text = text;
this.bitmapTexture = bitmapTexture;
this.textureHeight = textureHeight;
this.textureWidth = textureWidth;
this.lastUsed = Date.now();
}
return LabelInfo;
}());
exports.LabelInfo = LabelInfo;
/**
* The {@link LabelProviderBase2D} provides base functionality for 2D label providers, including caching of label textures
*/
// tslint:disable-next-line: max-classes-per-file
var LabelProviderBase2D = /** @class */ (function (_super) {
__extends(LabelProviderBase2D, _super);
function LabelProviderBase2D(options) {
var _this = this;
var _a, _b, _c, _d, _e;
_this = _super.call(this, options) || this;
/**
* Used internally only for testing
*/
_this.useCache = !app_1.IS_TEST_ENV;
/**
* Whether to show the leftmost (or rightmost on axes with flippedCoordinates) label as long as its tick line is visible.
*
* If `true` and the label width is wider than the difference between majorTicks, it will prefer to push/hide the 2nd label instead of disappearing itself, as long as its tick-line is still in view.
*
* Valuable when used inside of a {@link SmartDateLabelProvider}, paired with its `showWiderDateOnFirstLabel: true`.
*
* Default `false` for all label providers, except for {@link SmartDateLabelProvider}.
*/
_this.alwaysShowFirstLabel = false;
/**
* After getLabels() is called, this contains the LabelInfo[] in 1:1 correspondence with the returned string[].
* Used to pass label info through the tickCache to measure and draw without repeated cache lookups.
*/
_this.labelInfos = [];
_this.tickToText = new Map();
/** Set this true if the format function could return different results for the same input (eg SmartDateLabelprovider) */
_this.textVariesForSameTick = false;
_this.rotationProperty = 0;
_this.lineSpacingProperty = defaultLineSpacing;
_this.providerId = (0, guid_1.generateGuid)();
_this.rotationProperty = (_a = options === null || options === void 0 ? void 0 : options.rotation) !== null && _a !== void 0 ? _a : _this.rotationProperty;
_this.lineSpacing = (_b = options === null || options === void 0 ? void 0 : options.lineSpacing) !== null && _b !== void 0 ? _b : _this.lineSpacing;
_this.alwaysShowFirstLabel = (_c = options === null || options === void 0 ? void 0 : options.alwaysShowFirstLabel) !== null && _c !== void 0 ? _c : _this.alwaysShowFirstLabel;
_this.useSharedCache = (_d = options === null || options === void 0 ? void 0 : options.useSharedCache) !== null && _d !== void 0 ? _d : SciChartDefaults_1.SciChartDefaults.useSharedCache;
_this.useNativeText = (_e = options === null || options === void 0 ? void 0 : options.useNativeText) !== null && _e !== void 0 ? _e : SciChartDefaults_1.SciChartDefaults.useNativeText;
return _this;
}
Object.defineProperty(LabelProviderBase2D.prototype, "useNativeText", {
/**
* Whether to use WebGL for rendering axis labels. Default true (was false before v4). These are much faster than rendering using canvas text, but do not have quite the same font and style support.
* Set {@link SciChartDefaults.useNativeText} to change the global default.
* Setting this at runtime also switches the {@link AxisBase2D.axisRenderer} to the matching renderer class.
*/
get: function () {
return this.useNativeTextProperty;
},
set: function (value) {
var _a, _b;
if (this.useNativeTextProperty !== value) {
this.useNativeTextProperty = value;
(_b = (_a = this.parentAxis) === null || _a === void 0 ? void 0 : _a.switchAxisRenderer) === null || _b === void 0 ? void 0 : _b.call(_a, value);
this.invalidateParent();
}
},
enumerable: false,
configurable: true
});
Object.defineProperty(LabelProviderBase2D.prototype, "rotation", {
get: function () {
return this.rotationProperty;
},
set: function (value) {
if (this.rotationProperty !== value) {
this.rotationProperty = value;
this.invalidateParent();
}
},
enumerable: false,
configurable: true
});
Object.defineProperty(LabelProviderBase2D.prototype, "lineSpacing", {
/**
* Line spacing to use if text is wrapped, as a multiple of the text height. Defaults to 1.1
*/
get: function () {
if (this.useNativeText) {
return this.lineSpacingProperty === defaultLineSpacing
? defaultNativeLineSpacing
: this.lineSpacingProperty;
}
else {
return this.lineSpacingProperty;
}
},
/**
* Line spacing to use if text is wrapped, as a multiple of the text height. Defaults to 1.1
*/
set: function (value) {
this.lineSpacingProperty = value;
this.invalidateParent();
},
enumerable: false,
configurable: true
});
/**
* Returns an array of label strings for an array of major tick numeric values
* @param majorTicks The major tick numeric values
*/
LabelProviderBase2D.prototype.getLabels = function (majorTicks) {
var _this = this;
var axis = this.parentAxis;
if (this.useCache) {
if (!this.styleId) {
this.styleId = LabelCache_1.labelCache.getStyleId(this.getCachedStyle());
}
else if (!LabelCache_1.labelCache.checkStyle(this.styleId, this.getCachedStyle())) {
this.resetCache();
this.styleId = LabelCache_1.labelCache.getStyleId(this.getCachedStyle());
}
}
var ticks = majorTicks;
if (this.parentAxis.isCategoryAxis) {
var categoryCoordCalc_1 = this.parentAxis.getCurrentCoordinateCalculator();
ticks = majorTicks.map(function (tick) { return categoryCoordCalc_1.transformIndexToData(tick); });
}
var labels = [];
this.labelInfos = [];
if (this.useCache) {
if (!this.useNativeText) {
for (var _i = 0, ticks_1 = ticks; _i < ticks_1.length; _i++) {
var tick = ticks_1[_i];
var cachedLabel = void 0;
var text = void 0;
var cachedLabelText = this.tickToText.get(tick);
if (this.textVariesForSameTick) {
text = this.formatLabel(tick);
if (cachedLabelText && cachedLabelText === text) {
cachedLabel = LabelCache_1.labelCache.getLabel(cachedLabelText, this.styleId);
if (cachedLabel) {
labels.push(cachedLabelText);
}
}
else {
this.tickToText.set(tick, text);
}
}
else {
if (cachedLabelText) {
text = cachedLabelText;
cachedLabel = LabelCache_1.labelCache.getLabel(cachedLabelText, this.styleId);
if (cachedLabel) {
labels.push(cachedLabelText);
}
}
else {
text = this.formatLabel(tick);
this.tickToText.set(tick, text);
cachedLabel = LabelCache_1.labelCache.getLabel(text, this.styleId);
if (cachedLabel) {
labels.push(text);
}
}
}
if (!cachedLabel) {
var texture = this.getLabelTexture(text, axis.axisRenderer.textureManager, axis.dpiAdjustedLabelStyle);
if (texture.textureWidth !== null) {
cachedLabel = new LabelInfo(tick, text, texture.bitmapTexture, texture.textureHeight, texture.textureWidth);
LabelCache_1.labelCache.setLabel(text, this.styleId, cachedLabel);
}
labels.push(text);
}
this.labelInfos.push(cachedLabel);
}
}
else {
if (this.currentNativeFontName === "SCRT_Loading") {
var renderContext = this.parentAxis.parentSurface.currentWebGlRenderContext;
var nativeFont = renderContext.getFont(this.getCachedStyle(), this.rotation !== 0, false);
if (nativeFont.GetFaceName() !== this.currentNativeFontName) {
this.invalidateCache();
this.styleId = LabelCache_1.labelCache.getStyleId(this.getCachedStyle());
}
}
var sizesToGet = [];
for (var _a = 0, ticks_2 = ticks; _a < ticks_2.length; _a++) {
var tick = ticks_2[_a];
var cachedLabelText = this.tickToText.get(tick);
if (this.textVariesForSameTick) {
var text = this.formatLabel(tick);
if (cachedLabelText && cachedLabelText === text) {
var cachedLabel = LabelCache_1.labelCache.getLabel(cachedLabelText, this.styleId);
labels.push(cachedLabelText);
if (cachedLabel) {
this.labelInfos.push(cachedLabel);
}
else {
this.labelInfos.push(undefined);
sizesToGet.push(cachedLabelText);
}
}
else {
this.tickToText.set(tick, text);
sizesToGet.push(text);
labels.push(text);
this.labelInfos.push(undefined);
}
}
else {
if (cachedLabelText) {
labels.push(cachedLabelText);
var cachedLabel = LabelCache_1.labelCache.getLabel(cachedLabelText, this.styleId);
if (cachedLabel) {
this.labelInfos.push(cachedLabel);
}
else {
this.labelInfos.push(undefined);
sizesToGet.push(cachedLabelText);
}
}
else {
var text = this.formatLabel(tick);
this.tickToText.set(tick, text);
sizesToGet.push(text);
labels.push(text);
this.labelInfos.push(undefined);
}
}
}
var results = this.getLabelSizesNative(sizesToGet, axis.dpiAdjustedLabelStyle);
// Fill in undefined labelInfos slots with results from getLabelSizesNative
if (results && results.length > 0) {
var resultIndex = 0;
for (var i = 0; i < this.labelInfos.length; i++) {
if (!this.labelInfos[i]) {
this.labelInfos[i] = results[resultIndex];
resultIndex++;
}
}
}
}
this.pruneTickTextCache();
}
else {
labels = ticks.map(function (t) { return _this.formatLabel(t); });
// No-cache path: push undefined for each label
for (var i = 0; i < labels.length; i++) {
this.labelInfos.push(undefined);
}
}
return labels;
};
/**
* Called during axis layout to get the height of the label
* @param ctx the CanvasRenderingContext2D which can be used to perform text measurment
* @param labelText the text of the label
* @param labelStyle the style of the label
* @returns the label height in pixels
*/
LabelProviderBase2D.prototype.getLabelHeight = function (ctx, labelText, labelStyle, labelInfo) {
if (this.rotationProperty % 90 === 0 || this.parentAxis.isHorizontalAxis) {
if (labelInfo === null || labelInfo === void 0 ? void 0 : labelInfo.textureHeight) {
return labelInfo.textureHeight;
}
var cachedLabel = this.useCache ? LabelCache_1.labelCache.getLabel(labelText, this.styleId) : undefined;
if (!cachedLabel || !cachedLabel.textureHeight) {
// This block is used when labelProvider.useCache = false and labelProvider.useNativeText = true; (TEST mode)
var fontSize = labelStyle.fontSize, padding = labelStyle.padding;
return (0, TextureManager_1.measureTextHeight)(fontSize) + (padding === null || padding === void 0 ? void 0 : padding.top) + (padding === null || padding === void 0 ? void 0 : padding.bottom);
}
return cachedLabel.textureHeight;
}
else {
return labelStyle.fontSize + labelStyle.padding.top + labelStyle.padding.bottom;
}
};
/**
* Called during axis layout to get the width of the label
* @param ctx the CanvasRenderingContext2D which can be used to perform text measurment
* @param labelText the text of the label
* @param labelStyle the style of the label
* @returns the label width in pixels
*/
LabelProviderBase2D.prototype.getLabelWidth = function (ctx, labelText, labelStyle, labelInfo) {
if (this.rotationProperty % 90 === 0 || !this.parentAxis.isHorizontalAxis) {
if (labelInfo === null || labelInfo === void 0 ? void 0 : labelInfo.textureWidth) {
return labelInfo.textureWidth;
}
var cachedlabel = this.useCache ? LabelCache_1.labelCache.getLabel(labelText, this.styleId) : undefined;
if (!cachedlabel || !cachedlabel.textureWidth) {
var padding = labelStyle.padding;
return (0, TextureManager_1.measureTextWidth)(ctx, labelText) + padding.left + padding.right;
}
return cachedlabel.textureWidth;
}
else {
return labelStyle.fontSize + labelStyle.padding.left + labelStyle.padding.right;
}
};
/**
* Called during axis layout to get the maximum height of labels on a horizontal axis.
* Normally this calls getLabelHeight for each label and returns the largest.
* @param majorTickLabels an array of text labels
* @param ctx the CanvasRenderingContext2D which can be used to perform text measurment
* @param labelStyle the style of the labels
* @returns the maximum label height in pixels
*/
LabelProviderBase2D.prototype.getMaxLabelHeightForHorizontalAxis = function (majorTickLabels, ctx, labelStyle, labelInfos) {
var _a;
// Fast path: when labelInfos are available and rotation doesn't require width-based measurement,
// scan textureHeight directly without per-label conditional checks
if (labelInfos && (this.rotationProperty % 90 === 0 || this.parentAxis.isHorizontalAxis)) {
var maxHeight_1 = 0;
for (var i = 0; i < labelInfos.length; i++) {
if (((_a = labelInfos[i]) === null || _a === void 0 ? void 0 : _a.textureHeight) > maxHeight_1) {
maxHeight_1 = labelInfos[i].textureHeight;
}
}
if (maxHeight_1 > 0)
return maxHeight_1;
}
var maxHeight = 0;
for (var i = 0; i < majorTickLabels.length; i++) {
var h = this.getLabelHeight(ctx, majorTickLabels[i], labelStyle, labelInfos === null || labelInfos === void 0 ? void 0 : labelInfos[i]);
if (h > maxHeight)
maxHeight = h;
}
return maxHeight;
};
/**
* Called during axis layout to get the maximum width of labels on a vertical axis.
* Normally this calls getLabelWidth for each label and returns the largest.
* @param majorTickLabels an array of text labels
* @param ctx the CanvasRenderingContext2D which can be used to perform text measurment
* @param labelStyle the style of the labels
* @returns the maximum label width in pixels
*/
LabelProviderBase2D.prototype.getMaxLabelWidthForVerticalAxis = function (majorTickLabels, ctx, labelStyle, labelInfos) {
var _a;
// Fast path: when labelInfos are available and rotation doesn't require height-based measurement,
// scan textureWidth directly without per-label conditional checks
if (labelInfos && (this.rotationProperty % 90 === 0 || !this.parentAxis.isHorizontalAxis)) {
var maxWidth_1 = 0;
for (var i = 0; i < labelInfos.length; i++) {
if (((_a = labelInfos[i]) === null || _a === void 0 ? void 0 : _a.textureWidth) > maxWidth_1) {
maxWidth_1 = labelInfos[i].textureWidth;
}
}
if (maxWidth_1 > 0)
return maxWidth_1;
}
var maxWidth = 0;
for (var i = 0; i < majorTickLabels.length; i++) {
var w = this.getLabelWidth(ctx, majorTickLabels[i], labelStyle, labelInfos === null || labelInfos === void 0 ? void 0 : labelInfos[i]);
if (w > maxWidth)
maxWidth = w;
}
return maxWidth;
};
/**
* Get a texture for the given label text. By default the textures are created first and then the resulting sizes are used by the layout functions
* @param labelText The required text
* @param textureManager A textureManager instance which contains methods for creating textures
* @param labelStyle The style for the text
* @returns A TTextureObject containing the bitmapTexture and the size
*/
LabelProviderBase2D.prototype.getCachedLabelTexture = function (labelText, textureManager, labelStyle) {
var _a;
var cachedLabel = LabelCache_1.labelCache.getLabel(labelText, this.styleId);
if (cachedLabel) {
return {
textureWidth: (_a = cachedLabel.textureWidth) !== null && _a !== void 0 ? _a : null,
textureHeight: cachedLabel.textureHeight,
bitmapTexture: cachedLabel.bitmapTexture
};
}
else {
return this.getLabelTexture(labelText, textureManager, labelStyle);
}
};
/**
* Create a texture for the given label text. This method is called if useNativeText is false.
* If overriding this method with useSharedCache = true, consider setting it false for this LabelProvider,
* otherwise other axes using the same style and text may see your custom texture. Alternatively you can override getCachedStyle or set styleId directly
*/
LabelProviderBase2D.prototype.getLabelTexture = function (labelText, textureManager, labelStyle) {
return textureManager.createTextTexture([labelText], labelStyle, this.rotationProperty);
};
LabelProviderBase2D.prototype.getNativeLabelInfo = function (labelText) {
return LabelCache_1.labelCache.getLabel(labelText, this.styleId);
};
/**
* Ensures all LabelInfo entries have sizes populated.
* Entries that are undefined or missing textureWidth are filled in via native or texture path.
*/
LabelProviderBase2D.prototype.ensureLabelInfoComplete = function (labelInfos, labels, labelStyle) {
var labelsToGet = [];
for (var i = 0; i < labelInfos.length; i++) {
if (!labelInfos[i] || !labelInfos[i].textureWidth) {
labelsToGet.push(labels[i]);
}
}
if (labelsToGet.length > 0) {
if (this.useNativeText) {
var results = this.getLabelSizesNative(labelsToGet, labelStyle);
if (results && results.length > 0) {
var resultIndex = 0;
for (var i = 0; i < labelInfos.length; i++) {
if (!labelInfos[i] || !labelInfos[i].textureWidth) {
labelInfos[i] = results[resultIndex];
resultIndex++;
}
}
}
}
else {
var axis = this.parentAxis;
for (var i = 0; i < labelInfos.length; i++) {
if (!labelInfos[i] || !labelInfos[i].textureWidth) {
var texture = this.getLabelTexture(labels[i], axis.axisRenderer.textureManager, labelStyle);
if (texture.textureWidth !== null) {
var cachedLabel = new LabelInfo(undefined, labels[i], texture.bitmapTexture, texture.textureHeight, texture.textureWidth);
LabelCache_1.labelCache.setLabel(labels[i], this.styleId, cachedLabel);
labelInfos[i] = cachedLabel;
}
}
}
}
}
return labelInfos;
};
LabelProviderBase2D.prototype.invalidateCache = function () {
if (this.styleId) {
LabelCache_1.labelCache.freeStyle(this.styleId);
}
this.tickToText.clear();
this.styleId = null;
};
LabelProviderBase2D.prototype.resetCache = function () {
this.invalidateCache();
};
LabelProviderBase2D.prototype.delete = function () {
this.resetCache();
};
/**
* This method is called for each label just before drawing and can be used to make final adjustments to the label
* Note that if native text is NOT used, only x and y of the return values will be used.
* Only color can be returned undefined to use label default
*/
LabelProviderBase2D.prototype.adjustLabel = function (index, text, width, height, x, y, rx, ry, rotationRadians) {
return { text: text, x: x, y: y, rx: rx, ry: ry, rotationRadians: rotationRadians, color: undefined };
};
LabelProviderBase2D.prototype.toJSON = function () {
var json = _super.prototype.toJSON.call(this);
var options = {
rotation: this.rotation,
lineSpacing: this.lineSpacing,
asyncLabels: false,
useSharedCache: this.useSharedCache,
useNativeText: this.useNativeText,
alwaysShowFirstLabel: this.alwaysShowFirstLabel
};
Object.assign(json.options, options);
return json;
};
LabelProviderBase2D.prototype.getLabelSizesNative = function (labels, textStyle) {
var _this = this;
if (!labels || labels.length === 0 || app_1.IS_TEST_ENV)
return [];
var allLabels = "";
var simpleLabels = [];
var renderContext = this.parentAxis.parentSurface.currentWebGlRenderContext;
if (!renderContext) {
// this can occur if layout is called before any render has taken place, eg when the chart is added to a vertical group,
// so this is not really an error condidtion - the labels will be re-calculated properly when the chart is rendered.
//console.error("No native context available when trying to get native label sizes");
return [];
}
var nativeFont = renderContext.getFont(this.getCachedStyle(), this.rotation !== 0, false);
var wasmContext = this.parentAxis.parentSurface.webAssemblyContext2D;
var textBounds = (0, NativeObject_1.getTextBounds)(wasmContext);
this.currentNativeFontName = nativeFont.GetFaceName();
var rotationRad = this.rotation * (Math.PI / 180);
var sin = Math.abs(Math.sin(rotationRad));
var cos = Math.abs(Math.cos(rotationRad));
var results = [];
var makeCacheEntry = function (width, height, label) {
var _a, _b;
var w = width;
var h = height;
if (_this.rotation !== 0) {
h = width * sin + height * cos;
w = width * cos + height * sin;
}
var cachedLabel = new LabelInfo(undefined, label, undefined, h + ((_a = textStyle.padding) === null || _a === void 0 ? void 0 : _a.top) + (textStyle === null || textStyle === void 0 ? void 0 : textStyle.padding.bottom), w + ((_b = textStyle.padding) === null || _b === void 0 ? void 0 : _b.left) + (textStyle === null || textStyle === void 0 ? void 0 : textStyle.padding.right));
cachedLabel.textWidth = width;
cachedLabel.textHeight = height;
results.push(cachedLabel);
LabelCache_1.labelCache.setLabel(label, _this.styleId, cachedLabel);
};
labels.forEach(function (labelText) {
if (labelText && !labelText.includes("\n")) {
simpleLabels.push(labelText);
allLabels += labelText + "\n";
}
else {
nativeFont.CalculateStringBounds(labelText !== null && labelText !== void 0 ? labelText : "", textBounds, _this.lineSpacing);
makeCacheEntry(textBounds.m_fWidth, (0, text_1.getMultilineTextHeight)(textBounds), labelText);
}
});
if (simpleLabels.length > 0) {
nativeFont.CalculateStringBounds(allLabels !== null && allLabels !== void 0 ? allLabels : "", textBounds, this.lineSpacing);
for (var i = 0; i < simpleLabels.length; i++) {
var label = simpleLabels[i];
var bounds = textBounds.GetLineBounds(i);
makeCacheEntry(bounds.m_fWidth, bounds.m_fHeight, label);
bounds.delete();
}
}
return results;
};
LabelProviderBase2D.prototype.pruneTickTextCache = function () {
if (this.tickToText.size > 1000) {
this.tickToText.clear();
}
};
/**
* This method creates the text style to be stored in the label cache.
* When useSharedCache = true, the label cache will generate a new styleId if this style does not match any existing style.
* Cached labels are accessed by text and styleId.
* If you are overriding getLabelTexture or getLabelTextureAsync and do not ensure the style is unique, you might not get the labels you expect.
* You can either set useSharedCache = false, override this and set the extras field in {@link TCachedLabelStyle}, or set styleId directly
*/
LabelProviderBase2D.prototype.getCachedStyle = function () {
var axis = this.parentAxis;
var wasmId = axis.parentSurface.getWasmId();
return __assign(__assign({}, axis.dpiAdjustedLabelStyle), { rotation: this.rotation, providerId: this.useSharedCache ? (this.useNativeText ? "native" : wasmId) : this.providerId });
};
LabelProviderBase2D.prototype.clearCache = function () {
if (this.useCache) {
// Clear cache if property changed
if (this.styleId) {
LabelCache_1.labelCache.freeStyle(this.styleId);
this.styleId = undefined;
}
}
this.tickToText.clear();
};
LabelProviderBase2D.prototype.invalidateParent = function () {
this.clearCache();
_super.prototype.invalidateParent.call(this);
};
return LabelProviderBase2D;
}(LabelProvider_1.LabelProvider));
exports.LabelProviderBase2D = LabelProviderBase2D;