''' Show a streaming, updating representation of Fourier Series.

The example was inspired by `this video`_.

Use the ``bokeh serve`` command to run the example by executing:

    bokeh serve fourier_animated.py

at your command prompt. Then navigate to the URL

    http://localhost:5006/fourier_animated

in your browser.

.. _this video: https://www.youtube.com/watch?v=LznjC4Lo7lE

'''
from collections import OrderedDict

import numpy as np

from bokeh.driving import repeat
from bokeh.io import curdoc
from bokeh.layouts import column
from bokeh.models import ColumnDataSource
from bokeh.plotting import figure

pi = np.pi
N = 100
newx = x = np.linspace(0, 2*pi, N)
shift = 2.2
base_x = x + shift

period = pi/2
palette = ['#08519c', '#3182bd', '#6baed6', '#bdd7e7']

def new_source():
    return dict(
        curve=ColumnDataSource(dict(x=[], base_x=[], y=[])),
        lines=ColumnDataSource(dict(line_x=[], line_y=[], radius_x=[], radius_y=[])),
        circle_point=ColumnDataSource(dict(x=[], y=[], r=[])),
        circleds=ColumnDataSource(dict(x=[], y=[]))
    )

def create_circle_glyphs(p, color, sources):
    p.circle('x', 'y', size=1., line_color=color, color=None, source=sources['circleds'])
    p.circle('x', 'y', size=5, line_color=color, color=color, source=sources['circle_point'])
    p.line('radius_x', 'radius_y', line_color=color, color=color, alpha=0.5, source=sources['lines'])

def create_plot(foos, title='', r = 1, y_range=None, period = pi/2, cfoos=None):
    if y_range is None:
        y_range=[-2, 2]

    # create new figure
    p = figure(title=title, plot_width=800, plot_height=300, x_range=[-2, 9], y_range=y_range)
    p.xgrid.bounds = (-2, 2)
    p.xaxis.bounds = (-2, 2)

    _sources = []
    cx, cy = 0, 0
    for i, foo in enumerate(foos):
        sources = new_source()
        get_new_sources(x, foo, sources, cfoos[i], cx, cy, i==0)
        cp = sources['circle_point'].data
        cx, cy = cp['x'][0], cp['y'][0]

        if i==0:
            # compute the full fourier eq
            full_y = sum(foo(x) for foo in foos)
            # replace the foo curve with the full fourier eq
            sources['curve'] = ColumnDataSource(dict(x=x, base_x=base_x, y=full_y))
            # draw the line
            p.line('base_x','y', color="orange", line_width=2, source=sources['curve'])

        if i==len(foos)-1:
            # if it's the last foo let's draw a circle on the head of the curve
            sources['floating_point'] = ColumnDataSource({'x':[shift], 'y': [cy]})
            p.line('line_x', 'line_y', color=palette[i], line_width=2, source=sources['lines'])
            p.circle('x', 'y', size=10, line_color=palette[i], color=palette[i], source=sources['floating_point'])

        # draw the circle, radius and circle point related to foo domain
        create_circle_glyphs(p, palette[i], sources)
        _sources.append(sources)

    return p, _sources


def get_new_sources(xs, foo, sources, cfoo, cx=0, cy=0, compute_curve = True):
    if compute_curve:
        ys = foo(xs)
        sources['curve'].data = dict(x=xs, base_x=base_x, y=ys)

    r = foo(period)
    y = foo(xs[0]) + cy
    x = cfoo(xs[0]) + cx

    sources['lines'].data = {
        'line_x': [x, shift], 'line_y': [y, y],
        'radius_x': [0, x], 'radius_y': [0, y]
    }
    sources['circle_point'].data = {'x': [x], 'y': [y], 'r': [r]}
    sources['circleds'].data=dict(
        x = cx + np.cos(np.linspace(0, 2*pi, N)) * r,
        y = cy + np.sin(np.linspace(0, 2*pi, N)) * r,
    )

def update_sources(sources, foos, newx, ind, cfoos):
    cx, cy = 0, 0

    for i, foo in enumerate(foos):
        get_new_sources(newx, foo, sources[i], cfoos[i], cx, cy,
                        compute_curve = i != 0)

        if i == 0:
            full_y = sum(foo(newx) for foo in foos)
            sources[i]['curve'].data = dict(x=newx, base_x=base_x, y=full_y)

        cp = sources[i]['circle_point'].data
        cx, cy = cp['x'][0], cp['y'][0]

        if i == len(foos)-1:
            sources[i]['floating_point'].data['x'] = [shift]
            sources[i]['floating_point'].data['y'] = [cy]

def update_centric_sources(sources, foos, newx, ind, cfoos):
    for i, foo in enumerate(foos):
        get_new_sources(newx, foo, sources[i], cfoos[i])

def create_centric_plot(foos, title='', r = 1, y_range=(-2, 2), period = pi/2, cfoos=None):
    p = figure(title=title, plot_width=800, plot_height=300, x_range=[-2, 9], y_range=y_range)
    p.xgrid.bounds = (-2, 2)
    p.xaxis.bounds = (-2, 2)

    _sources = []
    for i, foo in enumerate(foos):
        sources = new_source()
        get_new_sources(x, foo, sources, cfoos[i])
        _sources.append(sources)

        if i:
            legend_label = "4sin(%(c)sx)/%(c)spi" % {'c': i*2+1}
        else:
            legend_label = "4sin(x)/pi"

        p.line('base_x','y', color=palette[i], line_width=2, source=sources['curve'])
        p.line('line_x', 'line_y', color=palette[i], line_width=2,
                source=sources['lines'], legend_label=legend_label)

        create_circle_glyphs(p, palette[i], sources)

    p.legend.location = "top_right"
    p.legend.orientation = "horizontal"
    p.legend.padding = 6
    p.legend.margin = 6
    p.legend.spacing = 6

    return p, _sources

# create the series partials
f1 = lambda x: (4*np.sin(x))/pi
f2 = lambda x: (4*np.sin(3*x))/(3*pi)
f3 = lambda x: (4*np.sin(5*x))/(5*pi)
f4 = lambda x: (4*np.sin(7*x))/(7*pi)
cf1 = lambda x: (4*np.cos(x))/pi
cf2 = lambda x: (4*np.cos(3*x))/(3*pi)
cf3 = lambda x: (4*np.cos(5*x))/(5*pi)
cf4 = lambda x: (4*np.cos(7*x))/(7*pi)
fourier = OrderedDict(
    fourier_4 = {
        'f': lambda x: f1(x) + f2(x) + f3(x) + f4(x),
        'fs': [f1, f2, f3, f4],
        'cfs': [cf1, cf2, cf3, cf4]
    },
)

for k, p in fourier.items():
    p['plot'], p['sources'] = create_plot(
        p['fs'], 'Fourier (Sum of the first 4 Harmonic Circles)', r = p['f'](period), cfoos = p['cfs']
    )

for k, p in fourier.items():
    p['cplot'], p['csources'] = create_centric_plot(
        p['fs'], 'Fourier First 4 Harmonics & Harmonic Circles', r = p['f'](period), cfoos = p['cfs']
    )

layout = column(*[f['plot'] for f in fourier.values()] + [f['cplot'] for f in fourier.values()])

@repeat(range(N))
def cb(gind):
    global newx
    oldx = np.delete(newx, 0)
    newx = np.hstack([oldx, [oldx[-1] + 2*pi/N]])

    for k, p in fourier.items():
        update_sources(p['sources'], p['fs'], newx, gind, p['cfs'])
        update_centric_sources(p['csources'], p['fs'], newx, gind, p['cfs'])

curdoc().add_periodic_callback(cb, 100)
curdoc().add_root(layout)
curdoc().title = "Fourier Animated"
