Creating custom indicators for a Stock Chart

This tutorial will show how we can create custom indicators for a stock chart.

Indicator class

To begin creating a custom indicator, we need to define a new class that extends a built-in Indicator class.

Our class definition at the very least should contain three things:

  • _afterNew() method - it is used to set everything up, such as creating indicator series.
  • prepareData() method - whenever data needs to be updated, e.g. on first load or target stock series data updates.
  • An instance of one of the XYSeries, e.g. LineSeries, which needs to be set on indicator's series property.
class MyIndicator extends am5stock.Indicator {
  declare className: string = "MyIndicator";
  declare series: am5xy.LineSeries;

  public _afterNew() {
    // Setting up indicator elements
    // ...
  }

  public prepareData() {
    // Setting up data
    // ...
  }
}
class MyIndicator extends am5stock.Indicator {
  className = "MyIndicator";

  _afterNew() {
    // Setting up indicator elements
    // ...
  }

  prepareData() {
    // Setting up data
    // ...
  }
}

NOTESetting className property of the indicator class is a good practice, as it is used when serializing and restoring indicators.

To test our new indicator, we will invoke it programmatically.

let myIndicator = stockChart.indicators.push(MyIndicator.new(root, {
  stockChart: stockChart,
  stockSeries: stockChart.get("stockSeries"),
  legend: valueLegend
}));
var myIndicator = stockChart.indicators.push(MyIndicator.new(root, {
  stockChart: stockChart,
  stockSeries: stockChart.get("stockSeries"),
  legend: valueLegend
}));

NOTEThe stockChart and stockSeries parameters are mandatory, whereas legend is optional, but we'll be using it anyway, since we'll need to create custom legend for our indicator.

Registering indicator class

IMPORTANTThis step is important if you allow users to save/serialize indicators.

Since the new indicator class we created is not bundled with amCharts, the serializer/parser routines cannot process it.

This means that indicators created using custom class will not be saved and restored when using Stock Chart's serialization techniques.

To fix that we need to register our class:

am5stock.registerClass("MyIndicator", MyIndicator);
am5stock.registerClass("MyIndicator", MyIndicator);

Main functionality

Setting up

Whenever indicator is instantiated, it will run its _afterNew() method, which is supposed to set everything up.

At the very least it should create a series and set it to own series property.

class MyIndicator extends am5stock.Indicator {

  declare series: am5xy.LineSeries;

  public _afterNew() {
    // Setting up indicator elements
    const stockSeries = this.get("stockSeries");
    const chart = stockSeries.chart;

    if (chart) {
      const series = chart.series.push(am5xy.LineSeries.new(this._root, {
        valueXField: "valueX",
        valueYField: "valueY1",
        openValueYField: "valueY2",
        groupDataDisabled: true,
        xAxis: stockSeries.get("xAxis"),
        yAxis: stockSeries.get("yAxis"),
        themeTags: ["indicator"],
        name: "My indicator"
      }));

      this.series = series;
    }

    // Don't forget inherited stuff
    super._afterNew();
  }

  public prepareData() {
    // Setting up data
    // ...
  }
}
class MyIndicator extends am5stock.Indicator {

  _afterNew() {
    // Setting up indicator elements
    var stockSeries = this.get("stockSeries");
    var chart = stockSeries.chart;

    if (chart) {
      var series = chart.series.push(am5xy.LineSeries.new(this._root, {
        valueXField: "valueX",
        valueYField: "valueY1",
        openValueYField: "valueY2",
        groupDataDisabled: true,
        xAxis: stockSeries.get("xAxis"),
        yAxis: stockSeries.get("yAxis"),
        themeTags: ["indicator"],
        name: "My indicator"
      }));

      this.series = series;
    }

    // Don't forget inherited stuff
    super._afterNew();
  }

  prepareData() {
    // Setting up data
    // ...
  }
}

NOTEDo not forget to call super._afterNew(). We want stuff from original _afterNew() to run, too.

Populating data

We now have a series set up, but for it to be plotted, we need to populate its data.

We'll use indicator's prepareData() method, to grab source data from target series, and create our own derivative data set.

In most cases, we will use stock chart's main series as a source of the data.

class MyIndicator extends am5stock.Indicator {

  declare series: am5xy.LineSeries;

  public _afterNew() {
    // Setting up indicator elements
    // ...

    // Don't forget inherited stuff
    super._afterNew();
  }

  public prepareData() {
    // Setting up data
    const stockSeries = this.get("stockSeries");
    const dataItems = stockSeries.dataItems;
    let data = this._getDataArray(dataItems);

    let margin = 100;

    am5.array.each(data, (item, i) => {
      let baseValue = dataItems[i].get("valueY", 0);
      item.valueY1 = baseValue + Math.round(Math.random() * margin);
      item.valueY2 = baseValue - Math.round(Math.random() * margin);
    });

    this.series.data.setAll(data);
  }
}
class MyIndicator extends am5stock.Indicator {

  _afterNew() {
    // Setting up indicator elements
    // ...

    // Don't forget inherited stuff
    super._afterNew();
  }

  prepareData() {
    // Setting up data
    var stockSeries = this.get("stockSeries");
    var dataItems = stockSeries.dataItems;
    var data = this._getDataArray(dataItems);

    var margin = 100;

    am5.array.each(data, function(item, i) {
      var baseValue = dataItems[i].get("valueY", 0);
      item.valueY1 = baseValue + Math.round(Math.random() * margin);
      item.valueY2 = baseValue - Math.round(Math.random() * margin);
    });

    this.series.data.setAll(data);
  }
}

NOTEIndicator has a utility method _getDataArray() which accepts a list of data items from series, and returns a new array, that has the same number of items with dates of the data items pre-set, but no values. We are using this array as a starting point for indicator data.

Legend

Adding to legend

If we want our indicator to appear in legend, we need to pass in legend instance, when instantiating it.

let myIndicator = stockChart.indicators.push(MyIndicator.new(root, {
  stockChart: stockChart,
  stockSeries: stockChart.get("stockSeries"),
  legend: valueLegend
}));
var myIndicator = stockChart.indicators.push(MyIndicator.new(root, {
  stockChart: stockChart,
  stockSeries: stockChart.get("stockSeries"),
  legend: valueLegend
}));

To make indicator create a legend item for itself, all we need to do is call its _handleLegend() method in _afterNew(), passing in the indicator series as a parameter.

class MyIndicator extends am5stock.Indicator {

  declare series: am5xy.LineSeries;

  public _afterNew() {
    // Setting up indicator elements
    // ...

    // Create a legend item
    this._handleLegend(series);

    // Don't forget inherited stuff
    super._afterNew();
  }

  public prepareData() {
    // Setting up data
    // ...
  }
}
class MyIndicator extends am5stock.Indicator {

  _afterNew() {
    // Setting up indicator elements
    // ...

    // Create a legend item
    this._handleLegend(series);

    // Don't forget inherited stuff
    super._afterNew();
  }

  prepareData() {
    // Setting up data
    // ...
  }
}

Legend content

A legend will show only indicator's (series) name by default.

We can use indicator series' legendLabelText and legendValueText to specify what we need to see there.

class MyIndicator extends am5stock.Indicator {

  declare series: am5xy.LineSeries;

  public _afterNew() {
    // Setting up indicator elements
    const stockSeries = this.get("stockSeries");
    const chart = stockSeries.chart;

    if (chart) {
      const series = chart.series.push(am5xy.LineSeries.new(this._root, {
        valueXField: "valueX",
        valueYField: "valueY1",
        openValueYField: "valueY2",
        groupDataDisabled: true,
        xAxis: stockSeries.get("xAxis"),
        yAxis: stockSeries.get("yAxis"),
        themeTags: ["indicator"],
        name: "My indicator",
        legendLabelText: "{name}",
        legendValueText: "high: [bold]{valueY}[/] - low: [bold]{openValueY}[/]",
        legendRangeValueText: ""
      }));

      this.series = series;

      // Create a legend item
      this._handleLegend(series);
    }

    // Don't forget inherited stuff
    super._afterNew();
  }

  public prepareData() {
    // Setting up data
    // ...
  }
}
class MyIndicator extends am5stock.Indicator {

  public _afterNew() {
    // Setting up indicator elements
    var stockSeries = this.get("stockSeries");
    var chart = stockSeries.chart;

    if (chart) {
      var series = chart.series.push(am5xy.LineSeries.new(this._root, {
        valueXField: "valueX",
        valueYField: "valueY1",
        openValueYField: "valueY2",
        groupDataDisabled: true,
        xAxis: stockSeries.get("xAxis"),
        yAxis: stockSeries.get("yAxis"),
        themeTags: ["indicator"],
        name: "My indicator",
        legendLabelText: "{name}",
        legendValueText: "high: [bold]{valueY}[/] - low: [bold]{openValueY}[/]",
        legendRangeValueText: ""
      }));

      this.series = series;

      // Create a legend item
      this._handleLegend(series);
    }

    // Don't forget inherited stuff
    super._afterNew();
  }

  prepareData() {
    // Setting up data
    // ...
  }
}

MORE INFOFor more information about the relation between a series and a legend, refer to "Legend label content".

Editable settings

Most indicators can be configured using modal popup.

For example, users might want to check colors, functionality, or other factors.

We can define those for custom indicators as well. Let's look at how it works.

Defining indicator settings

Before we can implement setting editor, we need to set up custom settings for our indicator.

It usually involves these tasks:

  • Defining settings interface (strongly typed languages only, e.g. TypeScript).
  • Setting defaults.
  • Handling changes.

Defining settings interface

Skip on to "Setting defaults", if you are using vanilla JavaScript.

Just so compiler knows we are adding new settings, we need to implement a settings interface, as well as declare its use via classe's _settings:

// Defining custom settings
export interface MyIndicatorSettings extends am5stock.IIndicatorSettings {
  margin?: number;
  seriesStyle?: "Solid" | "Dashed";
  showFill?: boolean;
}

class MyIndicator extends am5stock.Indicator {

  // Declaring custom settings use
  declare public _settings: MyIndicatorSettings;

  declare series: am5xy.LineSeries;

  public _afterNew() {
    // Setting up indicator elements
    // ...
  }

  public prepareData() {
    // Setting up data
    // ...
  }
}

Setting defaults

We will use indicator's _setDefault() method for, well, setting default values for the custom settings:

class MyIndicator extends am5stock.Indicator {

  // Declaring custom settings use
  declare public _settings: MyIndicatorSettings;

  declare series: am5xy.LineSeries;

  public _afterNew() {
    // Set defaults
    this._setDefault("name", "My Indicator");
    this._setDefault("margin", 100);
    this._setDefault("seriesColor", am5.color(0x045153));
    this._setDefault("seriesStyle", "Solid");
    this._setDefault("showFill", true);

    // Setting up indicator elements
    // ...

    // Don't forget inherited stuff
    super._afterNew();
  }

  public prepareData() {
    // Setting up data
    // ...
  }
}
class MyIndicator extends am5stock.Indicator {

  _afterNew() {
    // Set defaults
    this._setDefault("name", "My Indicator");
    this._setDefault("margin", 100);
    this._setDefault("seriesColor", am5.color(0x045153));
    this._setDefault("seriesStyle", "Solid");
    this._setDefault("showFill", true);

    // Setting up indicator elements
    // ...

    // Don't forget inherited stuff
    super._afterNew();
  }

  prepareData() {
    // Setting up data
    // ...
  }
}

Handling changes

Indicator needs to be set up to handle all changes to its custom settings - both through API and via settings modal.

For that we will need to employ another method: _beforeChanged().

class MyIndicator extends am5stock.Indicator {

  // Declaring custom settings use
  declare public _settings: MyIndicatorSettings;

  declare series: am5xy.LineSeries;

  public _afterNew() {
    // Setting up indicator settings
    // ...
  }

  public _beforeChanged() {

    if (this.isDirty("margin")) {
      this.markDataDirty();
    }

    if (this.isDirty("seriesStyle")) {
      const style = this.get("seriesStyle");
      if (style == "Dashed") {
        this.series.strokes.template.set("strokeDasharray", [4, 4]);
      }
      else {
        this.series.strokes.template.remove("strokeDasharray");
      }
    }

    if (this.isDirty("showFill")) {
      this.series.fills.template.set("visible", this.get("showFill"));
    }

    // Don't forget inherited stuff
    super._beforeChanged();
  }

  public prepareData() {
    // Setting up data
    // ...
  }
}
class MyIndicator extends am5stock.Indicator {

  _afterNew() {
    // Setting up indicator settings
    // ...
  }

  _beforeChanged() {

    if (this.isDirty("margin")) {
      this.markDataDirty();
    }

    if (this.isDirty("seriesStyle")) {
      var style = this.get("seriesStyle");
      if (style == "Dashed") {
        this.series.strokes.template.set("strokeDasharray", [4, 4]);
      }
      else {
        this.series.strokes.template.remove("strokeDasharray");
      }
    }

    if (this.isDirty("showFill")) {
      this.series.fills.template.set("visible", this.get("showFill"));
    }

    // Don't forget inherited stuff
    super._beforeChanged();
  }

  prepareData() {
    // Setting up data
    // ...
  }
}

A few notes about the above code:

  • isDirty() method is used to check whether value of the specific setting changed. No need to update anything if there was no change.
  • We don't need to worry about seriesColor, because it's a setting for parent Indicator class, and is handled by its own change handling mechanism.

Setting up the modal

To enable settings modal, we need to set indicator's _editableSettings property, which is an array of IIndicatorEditableSetting objects.

Each object has the following properties:

KeyTypeComment
keyStringA key (name) of the indicator setting.
nameStringA setting name to display next to input field in modal.
type"color" | "number" | "dropdown" | "checkbox"Type of the input field.
optionsArrayAn array of options to choose from if type is set to "dropdown"

Let's set up am interface for editing the three custom settings we added (margin, seriesStyle, showFill) plus the seriesColor which is an inherited setting from Indicator.

class MyIndicator extends am5stock.Indicator {

  // Declaring custom settings use
  declare public _settings: MyIndicatorSettings;

  public _editableSettings: am5stock.IIndicatorEditableSetting[] = [{
    key: "margin",
    name: "Margin",
    type: "number"
  }, {
    key: "seriesColor",
    name: "Color",
    type: "color"
  }, {
    key: "seriesStyle",
    name: "Line Style",
    type: "dropdown",
    options: ["Solid", "Dashed"]
  }, {
    key: "showFill",
    name: "Show fill",
    type: "checkbox"
  }];

  declare series: am5xy.LineSeries;

  public _afterNew() {
    // Setting up indicator settings
    // ...
  }

  public _beforeChanged() {
    // Handling changes
    // ...
  }

  public prepareData() {
    // Setting up data
    // ...
  }
}
class MyIndicator extends am5stock.Indicator {

  _editableSettings = [{
    key: "margin",
    name: "Margin",
    type: "number"
  }, {
    key: "seriesColor",
    name: "Color",
    type: "color"
  }, {
    key: "seriesStyle",
    name: "Line Style",
    type: "dropdown",
    options: ["Solid", "Dashed"]
  }, {
    key: "showFill",
    name: "Show fill",
    type: "checkbox"
  }];

  _afterNew() {
    // Setting up indicator settings
    // ...
  }

  _beforeChanged() {
    // Handling changes
    // ...
  }

  prepareData() {
    // Setting up data
    // ...
  }
}

Cleaning up

When indicator instance is destroyed, it cleans after itself, including disposing its series (one stored in the series property).

If indicator did not create any other objects, like additional series, we're all set.

If, however, indicator has created other elements, that need to be destroyed together with the indicator, we will need to define a _dispose() method, which does that.

class MyIndicator extends am5stock.Indicator {

  public series: am5xy.LineSeries;
  public anotherSeries: am5xy.LineSeries;

  public _afterNew() {
    // Setting up indicator elements
    // ...
  }

  public prepareData() {
    // Setting up data
    // ...
  }

  public _dispose() {
    // this.series will be disposed automatically by parent class
    this.anotherSeries.dispose();
    super._dispose();
  }
}
class MyIndicator extends am5stock.Indicator {

  _afterNew() {
    // Setting up indicator elements
    // ...
  }

  prepareData() {
    // Setting up data
    // ...
  }

  _dispose() {
    // this.series will be disposed automatically by parent class
    this.anotherSeries.dispose();
    super._dispose();
  }
}

NOTEDo not forget to call super._dispose(), which ensures that everything is cleaned up properly.

Adding to toolbar

To enable users to add our new indicator via UI, we need to add it to the Indicator control.

To do that we need to use indicators setting, which is an array of names of built-in indicators, or objects implementing IIndicator interface.

We'll be using the latter to add our custom indicator.

// Create indicator control
let indicatorControl = am5stock.IndicatorControl.new(root, {
  stockChart: stockChart,
  legend: valueLegend
});

// Get current indicators
let indicators = indicatorControl.get("indicators", []);

// Add custom indicator to the top of the list
indicators.unshift({
  id: "myIndicator",
  name: "My indicator",
  callback: () => {
    const myIndicator = stockChart.indicators.push(MyIndicator.new(root, {
      stockChart: stockChart,
      stockSeries: stockChart.get("stockSeries"),
      legend: valueLegend
    }));
    return myIndicator;
  }
});

// Set indicator list back
indicatorControl.set("indicators", indicators);

// Use indicator control in the toolbar
let toolbar = am5stock.StockToolbar.new(root, {
  container: document.getElementById("chartcontrols")!,
  stockChart: stockChart,
  controls: [
    indicatorControl,
    am5stock.ResetControl.new(root, {
      stockChart: stockChart
    }),
    am5stock.SettingsControl.new(root, {
      stockChart: stockChart
    })
  ]
});
// Create indicator control
var indicatorControl = am5stock.IndicatorControl.new(root, {
  stockChart: stockChart,
  legend: valueLegend
});

// Get current indicators
var indicators = indicatorControl.get("indicators", []);

// Add custom indicator to the top of the list
indicators.unshift({
  id: "myIndicator",
  name: "My indicator",
  callback: function() {
    const myIndicator = stockChart.indicators.push(MyIndicator.new(root, {
      stockChart: stockChart,
      stockSeries: stockChart.get("stockSeries"),
      legend: valueLegend
    }));
    return myIndicator;
  }
});

// Set indicator list back
indicatorControl.set("indicators", indicators);

// Use indicator control in the toolbar
var toolbar = am5stock.StockToolbar.new(root, {
  container: document.getElementById("chartcontrols"),
  stockChart: stockChart,
  controls: [
    indicatorControl,
    am5stock.ResetControl.new(root, {
      stockChart: stockChart
    }),
    am5stock.SettingsControl.new(root, {
      stockChart: stockChart
    })
  ]
});

The object, identifying custom indicator consists of these properties:

  • id - a unique ID of the indicator. It must not be the same as built-in indicators.
  • name - a name that will be shown in the dropdown.
  • callback - a function that will be called when user selects this indicator from the list. It must return an indicator object.

Full code and example

// Defining custom settings
export interface MyIndicatorSettings extends am5stock.IIndicatorSettings {
  margin?: number;
  seriesStyle?: "Solid" | "Dashed";
  showFill?: boolean;
}

// Define indicator class
class MyIndicator extends am5stock.Indicator {

  // Declaring custom settings use
  declare public _settings: MyIndicatorSettings;

  public _editableSettings: am5stock.IIndicatorEditableSetting[] = [{
    key: "margin",
    name: "Margin",
    type: "number"
  }, {
    key: "seriesColor",
    name: "Color",
    type: "color"
  }, {
    key: "seriesStyle",
    name: "Line Style",
    type: "dropdown",
    options: ["Solid", "Dashed"]
  }, {
    key: "showFill",
    name: "Show fill",
    type: "checkbox"
  }];

  declare series: am5xy.SmoothedXLineSeries;

  public _afterNew() {

    // Set default indicator name
    this._setDefault("name", "My Indicator");
    this._setDefault("margin", 100);
    this._setDefault("seriesColor", am5.color(0x045153));
    this._setDefault("seriesStyle", "Solid");
    this._setDefault("showFill", true);

    // Setting up indicator elements
    const stockSeries = this.get("stockSeries");
    const chart = stockSeries.chart;

    if (chart) {
      const series = chart.series.push(am5xy.SmoothedXLineSeries.new(this._root, {
        valueXField: "valueX",
        valueYField: "valueY1",
        openValueYField: "valueY2",
        groupDataDisabled: true,
        calculateAggregates: true,
        xAxis: stockSeries.get("xAxis"),
        yAxis: stockSeries.get("yAxis"),
        themeTags: ["indicator"],
        name: "My indicator",
        legendLabelText: "{name}",
        legendValueText: "high: [bold]{valueY}[/] - low: [bold]{openValueY}[/]",
        legendRangeValueText: "",
        stroke: this.get("seriesColor"),
        fill: this.get("seriesColor")
      }));

      series.fills.template.setAll({
        fillOpacity: 0.3,
        visible: true
      });

      this.series = series;
      this._handleLegend(series);
    }

    // Don't forget inherited stuff
    super._afterNew();
  }

  public _beforeChanged() {

    if (this.isDirty("margin")) {
      this.markDataDirty();
    }

    if (this.isDirty("seriesStyle")) {
      const style = this.get("seriesStyle");
      if (style == "Dashed") {
        this.series.strokes.template.set("strokeDasharray", [4, 4]);
      }
      else {
        this.series.strokes.template.remove("strokeDasharray");
      }
    }

    if (this.isDirty("showFill")) {
      this.series.fills.template.set("visible", this.get("showFill"));
    }

    // Don't forget inherited stuff
    super._beforeChanged();
  }

  public prepareData() {
    // Setting up data
    const stockSeries = this.get("stockSeries");
    const dataItems = stockSeries.dataItems;
    let data = this._getDataArray(dataItems);

    const margin = this.get("margin", 0);

    am5.array.each(data, function(item, i) {
      let baseValue = dataItems[i].get("valueY", 0);
      item.valueY1 = baseValue + Math.round(Math.random() * margin);
      item.valueY2 = baseValue - Math.round(Math.random() * margin);
    });

    this.series.data.setAll(data);
  }

}

// Add custom indicator
let myIndicator = stockChart.indicators.push(MyIndicator.new(root, {
  stockChart: stockChart,
  stockSeries: stockChart.get("stockSeries"),
  legend: valueLegend
}));

// Create indicator control
let indicatorControl = am5stock.IndicatorControl.new(root, {
  stockChart: stockChart,
  legend: valueLegend
});

// Get current indicators
let indicators = indicatorControl.get("indicators", []);

// Add custom indicator to the top of the list
indicators.unshift({
  id: "myIndicator",
  name: "My indicator",
  callback: function() {
    const myIndicator = stockChart.indicators.push(MyIndicator.new(root, {
      stockChart: stockChart,
      stockSeries: stockChart.get("stockSeries"),
      legend: valueLegend
    }));
    return myIndicator;
  }
});

// Set indicator list back
indicatorControl.set("indicators", indicators);


// Stock toolbar
let toolbar = am5stock.StockToolbar.new(root, {
  container: document.getElementById("chartcontrols")!,
  stockChart: stockChart,
  controls: [
    indicatorControl,
    am5stock.DateRangeSelector.new(root, {
      stockChart: stockChart
    }),
    am5stock.PeriodSelector.new(root, {
      stockChart: stockChart
    }),
    am5stock.DrawingControl.new(root, {
      stockChart: stockChart
    }),
    am5stock.ResetControl.new(root, {
      stockChart: stockChart
    }),
    am5stock.SettingsControl.new(root, {
      stockChart: stockChart
    })
  ]
});
// Define indicator class
class MyIndicator extends am5stock.Indicator {

  _editableSettings = [{
    key: "margin",
    name: "Margin",
    type: "number"
  }, {
    key: "seriesColor",
    name: "Color",
    type: "color"
  }, {
    key: "seriesStyle",
    name: "Line Style",
    type: "dropdown",
    options: ["Solid", "Dashed"]
  }, {
    key: "showFill",
    name: "Show fill",
    type: "checkbox"
  }];

  _afterNew() {

    // Set default indicator name
    this._setDefault("name", "My Indicator");
    this._setDefault("margin", 100);
    this._setDefault("seriesColor", am5.color(0x045153));
    this._setDefault("seriesStyle", "Solid");
    this._setDefault("showFill", true);

    // Setting up indicator elements
    var stockSeries = this.get("stockSeries");
    var chart = stockSeries.chart;

    if (chart) {
      var series = chart.series.push(am5xy.SmoothedXLineSeries.new(this._root, {
        valueXField: "valueX",
        valueYField: "valueY1",
        openValueYField: "valueY2",
        groupDataDisabled: true,
        calculateAggregates: true,
        xAxis: stockSeries.get("xAxis"),
        yAxis: stockSeries.get("yAxis"),
        themeTags: ["indicator"],
        name: "My indicator",
        legendLabelText: "{name}",
        legendValueText: "high: [bold]{valueY}[/] - low: [bold]{openValueY}[/]",
        legendRangeValueText: "",
        stroke: this.get("seriesColor"),
        fill: this.get("seriesColor")
      }));

      series.fills.template.setAll({
        fillOpacity: 0.3,
        visible: true
      });

      this.series = series;
      this._handleLegend(series);
    }

    // Don't forget inherited stuff
    super._afterNew();
  }

  _beforeChanged() {

    if (this.isDirty("margin")) {
      this.markDataDirty();
    }

    if (this.isDirty("seriesStyle")) {
      var style = this.get("seriesStyle");
      if (style == "Dashed") {
        this.series.strokes.template.set("strokeDasharray", [4, 4]);
      }
      else {
        this.series.strokes.template.remove("strokeDasharray");
      }
    }

    if (this.isDirty("showFill")) {
      this.series.fills.template.set("visible", this.get("showFill"));
    }

    // Don't forget inherited stuff
    super._beforeChanged();
  }

  prepareData() {
    // Setting up data
    var stockSeries = this.get("stockSeries");
    var dataItems = stockSeries.dataItems;
    var data = this._getDataArray(dataItems);

    var margin = this.get("margin", 0);

    am5.array.each(data, function(item, i) {
      let baseValue = dataItems[i].get("valueY", 0);
      item.valueY1 = baseValue + Math.round(Math.random() * margin);
      item.valueY2 = baseValue - Math.round(Math.random() * margin);
    });

    this.series.data.setAll(data);
  }

}

// Add indicator
var myIndicator = stockChart.indicators.push(MyIndicator.new(root, {
  stockChart: stockChart,
  stockSeries: stockChart.get("stockSeries"),
  legend: valueLegend
}));

// Create indicator control
var indicatorControl = am5stock.IndicatorControl.new(root, {
  stockChart: stockChart,
  legend: valueLegend
});

// Get current indicators
var indicators = indicatorControl.get("indicators", []);

// Add custom indicator to the top of the list
indicators.unshift({
  id: "myIndicator",
  name: "My indicator",
  callback: function() {
    var myIndicator = stockChart.indicators.push(MyIndicator.new(root, {
      stockChart: stockChart,
      stockSeries: stockChart.get("stockSeries"),
      legend: valueLegend
    }));
    return myIndicator;
  }
});

// Set indicator list back
indicatorControl.set("indicators", indicators);


// Stock toolbar
var toolbar = am5stock.StockToolbar.new(root, {
  container: document.getElementById("chartcontrols"),
  stockChart: stockChart,
  controls: [
    indicatorControl,
    am5stock.DateRangeSelector.new(root, {
      stockChart: stockChart
    }),
    am5stock.PeriodSelector.new(root, {
      stockChart: stockChart
    }),
    am5stock.DrawingControl.new(root, {
      stockChart: stockChart
    }),
    am5stock.ResetControl.new(root, {
      stockChart: stockChart
    }),
    am5stock.SettingsControl.new(root, {
      stockChart: stockChart
    })
  ]
});

See the Pen Stock Chart with custom indicator by amCharts team (@amcharts) on CodePen.

Dedicated panel

An indicator can be made to create and open in its own panel.

The process of defining such indicator is similar to regular indicators, with three differences:

  • It needs to extend ChartIndicator class (instead of Indicator).
  • It needs to have a _createSeries() method. We need a separate method for creating series, as we can't create them in _afterNew() like regular indicators, because new panel is not ready at that point, yet.
  • For indicator series, we need to use indicator panel's own axes accessible via its xAxis and yAxis properties.

Here's how updated code would look like:

// Defining custom settings
export interface MyIndicatorSettings extends am5stock.IChartIndicatorSettings {
  margin?: number;
  seriesStyle?: "Solid" | "Dashed";
  showFill?: boolean;
}

// Define indicator class
class MyIndicator extends am5stock.ChartIndicator {

  // Declaring custom settings use
  declare public _settings: MyIndicatorSettings;

  public _editableSettings: am5stock.IIndicatorEditableSetting[] = [{
    key: "margin",
    name: "Margin",
    type: "number"
  }, {
    key: "seriesColor",
    name: "Color",
    type: "color"
  }, {
    key: "seriesStyle",
    name: "Line Style",
    type: "dropdown",
    options: ["Solid", "Dashed"]
  }, {
    key: "showFill",
    name: "Show fill",
    type: "checkbox"
  }];

  declare series: am5xy.SmoothedXLineSeries;

  public _createSeries(): am5xy.SmoothedXLineSeries {
    const stockSeries = this.get("stockSeries");

    const series = this.panel.series.push(am5xy.SmoothedXLineSeries.new(this._root, {
      valueXField: "valueX",
      valueYField: "valueY1",
      openValueYField: "valueY2",
      groupDataDisabled: true,
      calculateAggregates: true,
      xAxis: this.xAxis,
      yAxis: this.yAxis,
      themeTags: ["indicator"],
      name: "My indicator",
      legendLabelText: "{name}",
      legendValueText: "high: [bold]{valueY}[/] - low: [bold]{openValueY}[/]",
      legendRangeValueText: "",
      stroke: this.get("seriesColor"),
      fill: this.get("seriesColor")
    }));

    series.fills.template.setAll({
      fillOpacity: 0.3,
      visible: true
    });

    return series;
  }

  public _afterNew() {
    // Set default indicator name
    this._setDefault("name", "My Indicator");
    this._setDefault("margin", 100);
    this._setDefault("seriesColor", am5.color(0x045153));
    this._setDefault("seriesStyle", "Solid");
    this._setDefault("showFill", true);

    // Don't forget inherited stuff
    super._afterNew();
  }

  public _beforeChanged() {

    if (this.isDirty("margin")) {
      this.markDataDirty();
    }

    if (this.isDirty("seriesStyle")) {
      const style = this.get("seriesStyle");
      if (style == "Dashed") {
        this.series.strokes.template.set("strokeDasharray", [4, 4]);
      }
      else {
        this.series.strokes.template.remove("strokeDasharray");
      }
    }

    if (this.isDirty("showFill")) {
      this.series.fills.template.set("visible", this.get("showFill"));
    }

    // Don't forget inherited stuff
    super._beforeChanged();
  }

  public prepareData() {
    // Setting up data
    const stockSeries = this.get("stockSeries");
    const dataItems = stockSeries.dataItems;
    let data = this._getDataArray(dataItems);

    const margin = this.get("margin", 0);

    am5.array.each(data, function(item, i) {
      let baseValue = dataItems[i].get("valueY", 0);
      item.valueY1 = baseValue + Math.round(Math.random() * margin);
      item.valueY2 = baseValue - Math.round(Math.random() * margin);
    });

    this.series.data.setAll(data);
  }

}

// Add custom indicator
let myIndicator = stockChart.indicators.push(MyIndicator.new(root, {
  stockChart: stockChart,
  stockSeries: stockChart.get("stockSeries"),
  legend: valueLegend
}));

// Create indicator control
let indicatorControl = am5stock.IndicatorControl.new(root, {
  stockChart: stockChart,
  legend: valueLegend
});

// Get current indicators
let indicators = indicatorControl.get("indicators", []);

// Add custom indicator to the top of the list
indicators.unshift({
  id: "myIndicator",
  name: "My indicator",
  callback: function() {
    const myIndicator = stockChart.indicators.push(MyIndicator.new(root, {
      stockChart: stockChart,
      stockSeries: stockChart.get("stockSeries"),
      legend: valueLegend
    }));
    return myIndicator;
  }
});

// Set indicator list back
indicatorControl.set("indicators", indicators);


// Stock toolbar
let toolbar = am5stock.StockToolbar.new(root, {
  container: document.getElementById("chartcontrols")!,
  stockChart: stockChart,
  controls: [
    indicatorControl,
    am5stock.DateRangeSelector.new(root, {
      stockChart: stockChart
    }),
    am5stock.PeriodSelector.new(root, {
      stockChart: stockChart
    }),
    am5stock.DrawingControl.new(root, {
      stockChart: stockChart
    }),
    am5stock.ResetControl.new(root, {
      stockChart: stockChart
    }),
    am5stock.SettingsControl.new(root, {
      stockChart: stockChart
    })
  ]
});
// Define indicator class
class MyIndicator extends am5stock.ChartIndicator {

  _editableSettings = [{
    key: "margin",
    name: "Margin",
    type: "number"
  }, {
    key: "seriesColor",
    name: "Color",
    type: "color"
  }, {
    key: "seriesStyle",
    name: "Line Style",
    type: "dropdown",
    options: ["Solid", "Dashed"]
  }, {
    key: "showFill",
    name: "Show fill",
    type: "checkbox"
  }];

  _createSeries() {
    var stockSeries = this.get("stockSeries");

    var series = this.panel.series.push(am5xy.SmoothedXLineSeries.new(this._root, {
      valueXField: "valueX",
      valueYField: "valueY1",
      openValueYField: "valueY2",
      groupDataDisabled: true,
      calculateAggregates: true,
      xAxis: this.xAxis,
      yAxis: this.yAxis,
      themeTags: ["indicator"],
      name: "My indicator",
      legendLabelText: "{name}",
      legendValueText: "high: [bold]{valueY}[/] - low: [bold]{openValueY}[/]",
      legendRangeValueText: "",
      stroke: this.get("seriesColor"),
      fill: this.get("seriesColor")
    }));

    series.fills.template.setAll({
      fillOpacity: 0.3,
      visible: true
    });

    return series;
  }

  _afterNew() {
    // Set default indicator name
    this._setDefault("name", "My Indicator");
    this._setDefault("margin", 100);
    this._setDefault("seriesColor", am5.color(0x045153));
    this._setDefault("seriesStyle", "Solid");
    this._setDefault("showFill", true);

    // Don't forget inherited stuff
    super._afterNew();
  }

  _beforeChanged() {

    if (this.isDirty("margin")) {
      this.markDataDirty();
    }

    if (this.isDirty("seriesStyle")) {
      var style = this.get("seriesStyle");
      if (style == "Dashed") {
        this.series.strokes.template.set("strokeDasharray", [4, 4]);
      }
      else {
        this.series.strokes.template.remove("strokeDasharray");
      }
    }

    if (this.isDirty("showFill")) {
      this.series.fills.template.set("visible", this.get("showFill"));
    }

    // Don't forget inherited stuff
    super._beforeChanged();
  }

  prepareData() {
    // Setting up data
    var stockSeries = this.get("stockSeries");
    var dataItems = stockSeries.dataItems;
    var data = this._getDataArray(dataItems);

    var margin = this.get("margin", 0);

    am5.array.each(data, function(item, i) {
      let baseValue = dataItems[i].get("valueY", 0);
      item.valueY1 = baseValue + Math.round(Math.random() * margin);
      item.valueY2 = baseValue - Math.round(Math.random() * margin);
    });

    this.series.data.setAll(data);
  }

  _dispose() {
    this.series.dispose();
    super._dispose();
  }
}

// Add indicator
var myIndicator = stockChart.indicators.push(MyIndicator.new(root, {
  stockChart: stockChart,
  stockSeries: stockChart.get("stockSeries"),
  legend: valueLegend
}));

// Create indicator control
var indicatorControl = am5stock.IndicatorControl.new(root, {
  stockChart: stockChart,
  legend: valueLegend
});

// Get current indicators
var indicators = indicatorControl.get("indicators", []);

// Add custom indicator to the top of the list
indicators.unshift({
  id: "myIndicator",
  name: "My indicator",
  callback: function() {
    var myIndicator = stockChart.indicators.push(MyIndicator.new(root, {
      stockChart: stockChart,
      stockSeries: stockChart.get("stockSeries"),
      legend: valueLegend
    }));
    return myIndicator;
  }
});

// Set indicator list back
indicatorControl.set("indicators", indicators);


// Stock toolbar
var toolbar = am5stock.StockToolbar.new(root, {
  container: document.getElementById("chartcontrols"),
  stockChart: stockChart,
  controls: [
    indicatorControl,
    am5stock.DateRangeSelector.new(root, {
      stockChart: stockChart
    }),
    am5stock.PeriodSelector.new(root, {
      stockChart: stockChart
    }),
    am5stock.DrawingControl.new(root, {
      stockChart: stockChart
    }),
    am5stock.ResetControl.new(root, {
      stockChart: stockChart
    }),
    am5stock.SettingsControl.new(root, {
      stockChart: stockChart
    })
  ]
});

See the Pen Stock Chart with custom indicator by amCharts team (@amcharts) on CodePen.

Related tutorials