Spectrogram

View as Markdown

Indicator Tutorial 10

Our Spectrogram has a lot in common with our previous indicator. Let’s move out such pieces to a new helper module:

1const FFT = require("fft");
2
3function initialize(instance) {
4 const period = instance.props.period;
5 instance.fft = FFT(period);
6 instance.signal = new Array(period);
7 instance.zero = new Array(period);
8 for(let i=0; i<period; ++i) {
9 instance.zero[i] = 0.0;
10 }
11 instance.lastIndex = -1;
12}
13
14function updateSeries(instance, value, index) {
15 const period = instance.props.period;
16 if (index < period) {
17 instance.signal[period - index - 1] = value;
18 }
19 else {
20 if (instance.lastIndex < index) {
21 instance.signal.pop();
22 instance.signal.unshift(value);
23 }
24 else {
25 instance.signal[0] = value;
26 }
27 }
28
29 instance.lastIndex = index;
30
31 if (index >= period) {
32 const re = [].concat(instance.signal);
33 const im = [].concat(instance.zero);
34 instance.fft.fft1d(re, im);
35 return { re, im };
36 }
37}
38
39module.exports = {
40 initialize,
41 updateSeries,
42 tag: "Fourier Analysis"
43};

Let’s save it with the fourierCommon.js name.

Refactored fourierMA will be reduced up to the next version:

1const predef = require("./tools/predef");
2const FFT = require("fft");
3const fourierCommon = require("./fourierCommon");
4
5class fourierMA {
6 init() {
7 fourierCommon.initialize(this);
8 }
9
10 map(d, index) {
11 const period = this.props.period;
12 const value = d.value();
13
14 const transform = fourierCommon.updateSeries(this, value, index);
15
16 if (transform) {
17 const re = transform.re;
18 const im = transform.im;
19
20 const middle = period / 2 + 1;
21 const startFreq = this.props.filterFreqStart;
22 for(let i=startFreq; i<middle; ++i) {
23 re[i] = im[i] = 0.0;
24 re[period - i] = im[period - i] = 0.0;
25 }
26
27 this.fft.ifft1d(re, im);
28
29 return re[0];
30 }
31 }
32}
33
34module.exports = {
35 name: "fourierMA",
36 description: "Fourier MA",
37 calculator: fourierMA,
38 params: {
39 period: predef.paramSpecs.period(64),
40 filterFreqStart: predef.paramSpecs.period(16),
41 },
42 tags: [fourierCommon.tag],
43};

The Spectrogram indicator will use the same approach to calculate Fourier coefficients. Then, we will build a custom plotter that will show these coefficients as 2D map of frequencies and their amplitudes.

1const predef = require("./tools/predef");
2const meta = require("./tools/meta");
3const FFT = require("fft");
4const fourierCommon = require("./fourierCommon");
5const p = require("./tools/plotting");
6
7class spectrogram {
8 init() {
9 fourierCommon.initialize(this);
10 this.peakValue = 0;
11 }
12
13 map(d, index) {
14 const period = this.props.period;
15 const value = d.value();
16
17 const transform = fourierCommon.updateSeries(this, value, index);
18
19 if (transform) {
20 const amplitudes = [];
21 const n = period / 2 + 1;
22 for(let i=1; i<n; ++i) {
23 const re = transform.re[i]/period;
24 const im = transform.im[i]/period;
25 const amplitude = 2 * Math.sqrt(re * re + im * im);
26 amplitudes.push(amplitude);
27 this.peakValue = Math.max(this.peakValue, amplitude);
28 }
29 return {
30 amplitudes,
31 lower: 1,
32 upper: period / 2 + 1
33 };
34 }
35 else {
36 return {};
37 }
38 }
39}
40
41function hexhex(d) {
42 return (d < 16 ? "0" : "") + d.toString(16);
43}
44
45function toRgb(r, g, b) {
46 return "#" + hexhex(r) + hexhex(g) + hexhex(b) + "80";
47}
48
49function heatmapPlotter(canvas, instance, history) {
50 const period = instance.props.period;
51 const heatmap = p.createHeatmap(1, period / 2 + 1);
52 for(let i=0; i<history.data.length; ++i) {
53 const item = history.get(i);
54 if (item.amplitudes) {
55 const colors = item.amplitudes.map(
56 (amp) => {
57 const colorValue = Math.round(255 * amp / instance.peakValue);
58 return toRgb(colorValue, colorValue, colorValue)
59 });
60 heatmap.addColumn(p.x.get(item), colors);
61 }
62 }
63 canvas.drawHeatmap(heatmap.end());
64}
65
66module.exports = {
67 name: "spectrogram",
68 description: "Spectrogram",
69 calculator: spectrogram,
70 params: {
71 period: predef.paramSpecs.period(64)
72 },
73 tags: [fourierCommon.tag],
74 areaChoice: meta.AreaChoice.NEW,
75 plotter: [
76 predef.plotters.custom(heatmapPlotter)
77 ],
78 scaler: predef.scalers.multiPath(["lower", "upper"])
79};

Because there are no regular plots and the app can struggle to evaluate min/max values of indicator to properly auto-scale it in the area, we implemented Indicator.scaler field in the module’s export. It will tell the app to use two fields from the output for scaling, even if they are not plotted.

The same lower and upper fields are used as domain boundaries for the heatmap object. Each column of the heatmap is a list of colors that divides a space between lower and upper to equal pieces.

Spectrogram

Note: vertical axis shows frequency as a divider of the period of the indicator. For example, 1 corresponds to the whole period, 2 - twice faster than indicator’s period, etc.