@amcharts/amcharts5
Version:
amCharts 5
460 lines • 16.7 kB
JavaScript
import { __awaiter } from "tslib";
import { Container } from "../../../core/render/Container";
import { LineSeries } from "../../xy/series/LineSeries";
import { BaseColumnSeries } from "../../xy/series/BaseColumnSeries";
import { MultiDisposer } from "../../../core/util/Disposer";
import * as $array from "../../../core/util/Array";
import * as $math from "../../../core/util/Math";
import * as $type from "../../../core/util/Type";
/**
* Base class for [[StockChart]] indicators.
*
* @see {@link https://www.amcharts.com/docs/v5/charts/stock/indicators/} for more info
*/
export class Indicator extends Container {
constructor() {
super(...arguments);
this._editableSettings = [];
this._dataDirty = false;
}
/**
* Clamps numeric settings that carry `minValue`/`maxValue`/`step` limits in
* `_editableSettings` back into their allowed range, so out-of-range values
* (from the settings modal, deserialized configs, or the API) can never reach
* the heavy `prepareData()` — which, for e.g. Volume Profile, would otherwise
* freeze the page. A `step` of `1` marks an integer setting, so typed decimals
* are rounded. Called with `force` on add/deserialize (nothing is "dirty"
* yet); otherwise only dirty keys are checked.
*/
_clampSettings(force = false) {
$array.each(this._editableSettings, (setting) => {
if (setting.type != "number") {
return;
}
const key = setting.key;
if (!force && !this.isDirty(key)) {
return;
}
let value = this.get(key);
if (!$type.isNumber(value)) {
return;
}
let clamped = value;
if (setting.step != null && setting.step >= 1) {
clamped = Math.round(clamped);
}
clamped = $math.fitToRange(clamped, setting.minValue != null ? setting.minValue : -Infinity, setting.maxValue != null ? setting.maxValue : Infinity);
if (clamped !== value) {
// setRaw (not set): writes the value without marking the key dirty, so
// clamping here can't re-enter the dirty cycle. (The per-setting
// `on(key)` event still fires; that's harmless and the clamp converges.)
this.setRaw(key, clamped);
}
});
}
/**
* Assigns numeric limits to an editable setting by key. Used for entries that
* are inherited from a base indicator (e.g. overbought/oversold) whose range
* differs per subclass.
*/
_setEditableSettingBounds(key, minValue, maxValue, step) {
$array.each(this._editableSettings, (setting) => {
if (setting.type == "number" && setting.key == key) {
setting.minValue = minValue;
setting.maxValue = maxValue;
setting.step = step;
}
});
}
_prepareChildren() {
super._prepareChildren();
this._clampSettings();
if (this.isDirty("stockSeries") || this.isDirty("volumeSeries")) {
this.markDataDirty();
const stockSeries = this.get("stockSeries");
const previousS = this._prevSettings.stockSeries;
if (previousS && this._sDP) {
this._sDP.dispose();
}
if (stockSeries) {
this._sDP = new MultiDisposer([
stockSeries.events.on("datavalidated", () => {
this._markDataDirty();
}),
stockSeries.events.on("datasetchanged", () => {
this._markDataDirty();
})
]);
}
const previousV = this._prevSettings.volumeSeries;
if (previousV && this._vDP) {
this._vDP.dispose();
}
const volumeSeries = this.get("volumeSeries");
if (volumeSeries) {
this._vDP = new MultiDisposer([
volumeSeries.events.on("datavalidated", () => {
this._markDataDirty();
}),
volumeSeries.events.on("datasetchanged", () => {
this._markDataDirty();
})
]);
}
}
if (this.isDirty("field")) {
if (this.get("field")) {
this.markDataDirty();
}
}
if (this.isDirty("period")) {
this.markDataDirty();
this.setCustomData("period", this.get("period"));
}
if (this._dataDirty) {
this.prepareData();
this._dataDirty = false;
}
}
_markDataDirty() {
this._dataDirty = true;
this.markDirty();
}
markDataDirty() {
this._root.events.once("frameended", () => {
this._markDataDirty();
});
}
_updateChildren() {
super._updateChildren();
if (this.isDirty("seriesColor")) {
this._updateSeriesColor(this.series, this.get("seriesColor"), "seriesColor");
}
this.setCustomData("period", this.get("period"));
this.setCustomData("field", this.get("field"));
this.setCustomData("name", this.get("name"));
this.setCustomData("shortName", this.get("shortName"));
}
_dispose() {
super._dispose();
if (this._sDP) {
this._sDP.dispose();
}
if (this._vDP) {
this._vDP.dispose();
}
const series = this.series;
if (series) {
series.dispose();
const yAxis = series.get("yAxis");
if (yAxis) {
yAxis.markDirtySelectionExtremes();
}
}
const stockChart = this.get("stockChart");
if (stockChart) {
const legend = this.get("legend");
if (legend) {
const legendDataItem = series.get("legendDataItem");
if (legendDataItem) {
legend.disposeDataItem(legendDataItem);
}
}
stockChart.indicators.removeValue(this);
}
}
hide(duration) {
const _super = Object.create(null, {
hide: { get: () => super.hide }
});
return __awaiter(this, void 0, void 0, function* () {
return Promise.all([
_super.hide.call(this, duration),
this.series.hide(duration)
]);
});
}
show(duration) {
const _super = Object.create(null, {
show: { get: () => super.show }
});
return __awaiter(this, void 0, void 0, function* () {
return Promise.all([
_super.show.call(this, duration),
this.series.show(duration)
]);
});
}
_handleLegend(series) {
const legend = this.get("legend");
if (legend) {
legend.data.push(series);
const legendDataItem = legend.dataItems[legend.dataItems.length - 1];
legendDataItem.get("marker").set("forceHidden", true);
const closeButton = legendDataItem.get("closeButton");
closeButton.set("forceHidden", false);
closeButton.events.on("click", () => {
this.dispose();
});
const settingsButton = legendDataItem.get("settingsButton");
settingsButton.setPrivate("customData", this);
const editableSettings = this._editableSettings;
if (!editableSettings || editableSettings.length == 0) {
settingsButton.set("forceHidden", true);
}
}
}
_updateSeriesColor(series, color, contextName) {
if (series) {
series.set("stroke", color);
series.set("fill", color);
if (series instanceof LineSeries) {
series.strokes.template.set("stroke", color);
series.fills.template.set("fill", color);
}
if (series instanceof BaseColumnSeries) {
series.columns.template.setAll({ stroke: color, fill: color });
}
if (contextName && color) {
this.setCustomData(contextName, color.toCSSHex());
}
}
}
setCustomData(name, value) {
const customData = this.series.getPrivate("customData");
if (customData) {
customData[name] = value;
}
}
/**
* @ignore
*/
prepareData() {
}
_getValue(dataItem) {
const field = this.get("field");
let o = dataItem.get("openValueY");
let h = dataItem.get("highValueY");
let l = dataItem.get("lowValueY");
let c = dataItem.get("valueY");
switch (field) {
case "close":
return c;
break;
case "open":
return o;
break;
case "high":
return h;
break;
case "low":
return l;
break;
case "hl/2":
return (h + l) / 2;
break;
case "hlc/3":
return (h + l + c) / 3;
break;
case "hlcc/4":
return (h + l + c + c) / 4;
break;
case "ohlc/4":
return (o + h + l + c) / 4;
break;
}
}
/**
* @ignore
*/
_getDataArray(dataItems) {
const data = [];
$array.each(dataItems, (dataItem) => {
data.push({ valueX: dataItem.get("valueX"), value_y: this._getValue(dataItem) });
});
return data;
}
/**
* @ignore
*/
_getTypicalPrice(dataItems) {
const data = [];
$array.each(dataItems, (dataItem) => {
data.push({ valueX: dataItem.get("valueX"), value_y: (dataItem.get("valueY", 0) + dataItem.get("highValueY", 0) + dataItem.get("lowValueY", 0)) / 3 });
});
return data;
}
_sma(data, period, field, toField) {
if (!data || data.length === 0 || period <= 0) {
return;
}
const invPeriod = 1 / period;
const len = data.length;
let count = 0; // number of non-null values encountered
let sum = 0; // sum of values in the current window
for (let i = 0; i < len; i++) {
const dataItem = data[i];
const value = dataItem[field];
if (value != null) {
count++;
sum += value;
// remove value that goes out of window (by index, to preserve original behavior)
if (count > period) {
const valueToRemove = data[i - period][field];
if (valueToRemove != null) {
sum -= valueToRemove;
}
}
if (count >= period) {
dataItem[toField] = sum * invPeriod;
}
}
}
}
_wma(data, period, field, toField) {
const denom = period * (period + 1) / 2;
const buffer = new Array(period);
let start = 0;
let len = 0;
let sum = 0; // sum of values in the window
let weighted = 0; // weighted sum (1*v1 + 2*v2 + ... + p*vp)
for (let i = 0; i < data.length; i++) {
const dataItem = data[i];
const value = dataItem[field];
if (value == null) {
continue;
}
if (len < period) {
const pos = (start + len) % period;
buffer[pos] = value;
len++;
sum += value;
weighted += len * value;
if (len === period) {
dataItem[toField] = weighted / denom;
}
}
else {
const oldest = buffer[start];
weighted = weighted - sum + period * value;
sum = sum - oldest + value;
buffer[start] = value;
start = (start + 1) % period;
dataItem[toField] = weighted / denom;
}
}
}
_ema(data, period, field, toField) {
const len = data.length;
if (len === 0 || period <= 0) {
return;
}
const invPeriod = 1 / period;
const multiplier = 2 / (1 + period);
const oneMinus = 1 - multiplier;
let ma = 0;
let count = 0;
for (let i = 0; i < len; i++) {
const dataItem = data[i];
const value = dataItem[field];
if (value == null) {
continue;
}
count++;
if (count > period) {
ma = value * multiplier + ma * oneMinus;
dataItem[toField] = ma;
}
else {
ma += value * invPeriod;
if (count === period) {
dataItem[toField] = ma;
}
}
}
}
_dema(data, period, field, toField) {
if (period <= 0 || data.length === 0) {
return;
}
const multiplier = 2 / (1 + period);
const oneMinus = 1 - multiplier;
const invPeriod = 1 / period;
let ema = 0;
let emaCount = 0;
let ema2 = 0;
let ema2Count = 0;
for (let i = 0, len = data.length; i < len; i++) {
const dataItem = data[i];
const value = dataItem[field];
if (value == null) {
continue;
}
// compute EMA (same logic as _ema)
emaCount++;
if (emaCount > period) {
ema = value * multiplier + ema * oneMinus;
dataItem.ema = ema;
}
else {
ema += value * invPeriod;
if (emaCount === period) {
dataItem.ema = ema;
}
}
// if EMA was produced for this item, update EMA of EMA (ema2) and DEMA
const emaVal = dataItem.ema;
if (emaVal != null) {
ema2Count++;
if (ema2Count > period) {
ema2 = emaVal * multiplier + ema2 * oneMinus;
dataItem[toField] = 2 * emaVal - ema2;
dataItem.ema2 = ema2;
}
else {
ema2 += emaVal / period;
if (ema2Count === period) {
dataItem[toField] = 2 * emaVal - ema2;
dataItem.ema2 = ema2;
}
}
}
}
}
_tema(data, period, field, toField) {
if (period <= 0 || data.length === 0) {
return;
}
const multiplier = 2 / (1 + period);
const oneMinus = 1 - multiplier;
// Ensure ema and ema2 are present
this._dema(data, period, field, "dema");
let ema3 = 0;
let count = 0;
for (let i = 0, len = data.length; i < len; i++) {
const dataItem = data[i];
const ema = dataItem.ema;
const ema2 = dataItem.ema2;
if (ema2 == null) {
continue;
}
count++;
if (count > period) {
ema3 = ema2 * multiplier + ema3 * oneMinus;
// guard ema just in case (should be present when ema2 is)
const emaVal = (ema == null) ? 0 : ema;
dataItem[toField] = 3 * emaVal - 3 * ema2 + ema3;
}
else {
ema3 += ema2 / period;
if (count === period) {
const emaVal = (ema == null) ? 0 : ema;
dataItem[toField] = 3 * emaVal - 3 * ema2 + ema3;
}
}
}
}
}
Indicator.className = "Indicator";
Indicator.classNames = Container.classNames.concat([Indicator.className]);
//# sourceMappingURL=Indicator.js.map