FunctionPatternDecomposition
// This source code is subject to the terms of the Mozilla Public License 2.0 at https://mozilla.org/MPL/2.0/
// © RicardoSantos

//@version=5

// @description Methods for decomposing price into common grid/matrix patterns.
library(title='FunctionPatternDecomposition')

// reference:
//      https://jackschaedler.github.io/handwriting-recognition/

import RicardoSantos/FunctionPeakDetection/2 as peakdetection
import RicardoSantos/ArrayExtension/2 as ae

// Helper function to rescale the 500 limit to the right of current bar_index
X(points)=>int(500/points)

// @function Helper for converting series to array.
// @param source float, data series.
// @param length int, size.
// @returns float array.
export series_to_array (float source, int length) =>
    float[] _data = array.new_float(length, float(na))
    for _i = (length-1) to 0
        if bar_index[_i] >= 0
            array.set(_data, _i, source[_i])
    _data

// @function Smooth data sample into 2d points.
// @param data float array, source data.
// @param rate float, default=0.25, the rate of smoothness to apply.
// @returns tuple with 2 float arrays.
export smooth_data_2d (float[] data, float rate=0.25) => //{
    int _size = array.size(data)
    //
    float _ir = 1.0 - rate
    float[] _X = array.new_float(_size, 0.0)
    float[] _Y = array.copy(data)
    //
    if _size > 1
        for _i = 1 to (_size - 1)
            float _yi1 = array.get(_Y, _i - 1)
            float _yi0 = array.get(_Y, _i)
            array.set(_X, _i, rate * (_i - 1) + _ir * _i)
            array.set(_Y, _i, rate * _yi1 + _ir * _yi0)
    [_X, _Y]
//{ usage
// data = array.from(100.0, 300.0, 500.0, 200.0, 100.0, 200.0, 300.0, 600.0, 100.0)
length = input(10)
var data = array.new_float(length, close), array.push(data, close), array.shift(data)
[x, y] = smooth_data_2d(data, 0.7)
if barstate.islastconfirmedhistory
    for _i = 1 to array.size(data)-1
        line.new(int((_i-1)*X(length)), array.get(data, _i-1), int(_i*X(length)), array.get(data, _i), color=color.gray)
        line.new(int(array.get(x, _i-1)*X(length)), array.get(y, _i-1), int(array.get(x, _i)*X(length)), array.get(y, _i), color=color.silver)
//}}

// @function Thin the number of points.
// @param data_x float array, points x value.
// @param data_y float array, points y value.
// @param rate float, default=2.0, minimum threshold rate of sample stdev to accept points.
// @returns tuple with 2 float arrays.
export thin_points (float[] data_x, float[] data_y, float rate=2.0) => //{
    // modified rate to be a multiplier of the data standard deviation as to accomodate any values
    int _size = array.size(data_x)
    float _xt = array.get(data_x, 0)
    float _yt = array.get(data_y, 0)
    float[] _X = array.new_float(1, _xt)
    float[] _Y = array.new_float(1, _yt)
    if _size > 1
        float _xd = array.stdev(data_x)
        float _yd = array.stdev(data_y)
        for _i = 1 to (_size - 1)
            float _xi = array.get(data_x, _i)
            float _yi = array.get(data_y, _i)
            if ((_xi-_xt) >= ( _xd * rate)) or ((_yi-_yt) >= ( _yd * rate))
                array.push(_X, _xi), _xt := _xi
                array.push(_Y, _yi), _yt := _yi
    [_X, _Y]
//{ usage:
[tx, ty] = thin_points(x, y, input(defval=1.0, title='thin'))
if barstate.islastconfirmedhistory
    for _i = 1 to array.size(tx)-1
        line.new(int(array.get(tx, _i-1)*X(length)), array.get(ty, _i-1), int(array.get(tx, _i)*X(length)), array.get(ty, _i), color=color.yellow)
//}}

// @function Extract the direction each point faces.
// @param data_x float array, points x value.
// @param data_y float array, points y value.
// @returns float array.
export extract_point_direction (float[] data_x, float[] data_y) => //{
    // modified rate to be a multiplier of the data standard deviation as to normalize value discrepancy
    // directional data is stored as (-1: down, 0: right, 1:up)
    int _size = array.size(data_x)
    float[] _direction = array.new_float(_size, na)
    if _size > 1
        float _xd = array.stdev(data_x)
        float _yd = array.stdev(data_y)
        for _i = 1 to (_size - 1)
            float _xir = (array.get(data_x, _i) - array.get(data_x, _i - 1)) / _xd
            float _yir = (array.get(data_y, _i) - array.get(data_y, _i - 1)) / _yd
            if _xir >= math.abs(_yir)
                array.set(_direction, _i, 0.0)
            else
                if _yir >= 0
                    array.set(_direction, _i, +1.0)
                else
                    array.set(_direction, _i, -1.0)
    _direction
//{ usage
_d = extract_point_direction(tx, ty)
if barstate.islastconfirmedhistory
    for _i = 1 to array.size(_d)-1
        _y = array.get(ty, _i)
        _style = switch (array.get(_d, _i)) 
            (+1.0) => label.style_triangleup
            (-1.0) => label.style_triangledown
            => label.style_diamond
        label.new(int(array.get(tx, _i)*X(length)), _y, str.tostring(_y), style=_style)
//}}

// @function ...
// @param data_x float array, points x value.
// @param data_y float array, points y value.
// @param rate float, minimum threshold rate of data y stdev.
// @returns tuple with 2 float arrays.
export find_corners (float[] data_x, float[] data_y, float rate) => //{
    float _delta = array.stdev(data_y) * rate
    [ph, pl] = peakdetection.function(data_x, data_y, _delta)
//{ usage:
[ph, pl] = find_corners(tx, ty, input(defval=1.0, title='corners'))
if barstate.islastconfirmedhistory
    label.new(bar_index, 400.0, str.format('top: {0}\nbot: {1}', str.tostring(ph), str.tostring(pl)))
//}}

// @function transforms points data to a constrained sized matrix format.
// @param data_x float array, points x value.
// @param data_y float array, points y value.
// @param m_size int, default=10, size of the matrix.
// @returns flat 2d pseudo matrix.
export grid_coordinates (float[] data_x, float[] data_y, int m_size=10) => //{
    int _size_x = array.size(data_x)
    int _size_y = array.size(data_y)
    // pattern size properties:
    float _min_x = array.min(data_x)
    float _max_x = array.max(data_x)
    float _min_y = array.min(data_y)
    float _max_y = array.max(data_y)
    // need to add some extra space on the range edges to accomodate the high/low value
    float _rx = (1.01 * _max_x - _min_x * 0.99)
    float _ry = (1.01 * _max_y - _min_y * 0.99)
    float[] _m = array.new_float(m_size * m_size, 0.0)
    for _i = 0 to (_size_x - 1)
        float _xi = array.get(data_x, _i)
        float _yi = array.get(data_y, _i)
        int _x_idx = math.floor(((_xi - _min_x) / _rx) * m_size)
        int _y_idx = math.floor(((_yi - _min_y) / _ry) * m_size)
        int _2d1 = ae.index_2d_to_1d(m_size, m_size, _x_idx, m_size-_y_idx-1)
        _mxy = array.get(_m, _2d1)
        array.set(_m, _2d1, _mxy + 1)
    _m
// { usage
msize = input(10)
m = grid_coordinates(tx, ty, msize)
tab = table.new(position.bottom_left, msize, msize)
for _i = 0 to msize-1
    for _j = 0 to msize-1
        _v = array.get(m, ae.index_2d_to_1d(msize, msize, _i, _j))
        _c = _v > 0 ? color.silver : color.gray
        table.cell(tab, _i, _j, str.tostring(_v), text_color=_c)
//}
        