"""
Implementation of a standard financial plot visualization using Chaco
renderers and scales.

In the main price plot area, mouse wheel zooms and mouse drag pans (if
the plot is not at the edge of the time series data).  In the bottom
overview plot area, right-click-drag selects a range of times to display
on the top two plots.  Once a region is selected, it can be moved
around by left-dragging or resized by left-dragging one of its
edges.
"""

# Major library imports
from numpy import abs, cumprod, linspace, random
import time

from enable.example_support import DemoFrame, demo_main

# Enthought library imports
from enable.api import Window

# Chaco imports
from chaco.api import ArrayDataSource, BarPlot, DataRange1D, \
        LinePlot, LinearMapper, VPlotContainer, PlotAxis, \
        FilledLinePlot, add_default_grids
from chaco.tools.api import PanTool, ZoomTool, RangeSelection, \
        RangeSelectionOverlay

from chaco.scales.api import CalendarScaleSystem
from chaco.scales_tick_generator import ScalesTickGenerator


def create_dates(numpoints, units="days"):
    """ Returns **numpoints** number of dates that evenly bracket the current
    date and time.  **units** should be one of "weeks", "days", "hours"
    "minutes", or "seconds".
    """
    units_map = { "weeks" : 7*24*3600,
                  "days" : 24*3600,
                  "hours" : 3600,
                  "minutes" : 60,
                  "seconds" : 1 }
    now = time.time()
    dt = units_map[units]
    dates = linspace(now, now+numpoints*dt, numpoints)
    return dates


class PlotFrame(DemoFrame):

    def _create_price_plots(self, times, prices, mini_height=75):
        """ Creates the two plots of prices and returns them.  One of the
        plots can be zoomed and panned, and the other plot (smaller) always
        shows the full data.

        *dates* and *prices* are two data sources.
        """

        # Create the price plot
        price_plot = FilledLinePlot(index = times, value = prices,
                        index_mapper = LinearMapper(range=DataRange1D(times)),
                        value_mapper = LinearMapper(range=DataRange1D(prices)),
                        edge_color = "blue",
                        face_color = "paleturquoise",
                        bgcolor = "white",
                        border_visible = True)

        # Add pan and zoom
        price_plot.tools.append(PanTool(price_plot, constrain=True,
                                        constrain_direction="x"))
        price_plot.overlays.append(ZoomTool(price_plot, drag_button="right",
                                              always_on=True,
                                              tool_mode="range",
                                              axis="index",
                                              max_zoom_out_factor=1.0,
                                             ))

        # Create the miniplot
        miniplot = LinePlot(index = times, value = prices,
                        index_mapper = LinearMapper(range=DataRange1D(times)),
                        value_mapper = LinearMapper(range=DataRange1D(prices)),
                        color = "black",
                        border_visible = True,
                        bgcolor = "white",
                        height = mini_height,
                        resizable = "h")

        # Add a range overlay to the miniplot that is hooked up to the range
        # of the main price_plot
        range_tool = RangeSelection(miniplot)
        miniplot.tools.append(range_tool)
        range_overlay = RangeSelectionOverlay(miniplot, metadata_name="selections")
        miniplot.overlays.append(range_overlay)
        range_tool.on_trait_change(self._range_selection_handler, "selection")

        # Attach a handler that sets the tool when the plot's index range changes
        self.range_tool = range_tool
        price_plot.index_range.on_trait_change(self._plot_range_handler, "updated")

        return price_plot, miniplot


    def _range_selection_handler(self, event):
        # The event obj should be a tuple (low, high) in data space
        if event is not None:
            low, high = event
            self.price_plot.index_range.low = low
            self.price_plot.index_range.high = high
        else:
            self.price_plot.index_range.set_bounds("auto", "auto")

    def _plot_range_handler(self, event):
        if event is not None:
            low, high = event
            if "auto" not in (low, high):
                self.range_tool.selection = (low, high)

    def _create_vol_plot(self, times, volumes, height=100):
        "Creates and returns the volume plot"
        index_range = self.price_plot.index_range
        vol_plot = BarPlot(index = times, value = volumes,
                       index_mapper = LinearMapper(range=index_range),
                       value_mapper = LinearMapper(range=DataRange1D(volumes)),
                       line_color = "transparent",
                       fill_color = "black",
                       bar_width = 1.0,
                       bar_width_type = "screen",
                       antialias = False,
                       height = 100,
                       resizable = "h",
                       bgcolor = "white",
                       border_visible = True)
        vol_plot.tools.append(PanTool(vol_plot, constrain=True,
                                      constrain_direction="x"))
        return vol_plot

    def _create_window(self):

        # Create the data and datasource objects
        # In order for the date axis to work, the index data points need to
        # be in units of seconds since the epoch.  This is because we are using
        # the CalendarScaleSystem, whose formatters interpret the numerical values
        # as seconds since the epoch.
        numpoints = 500
        index = create_dates(numpoints)
        returns = random.lognormal(0.01, 0.1, size=numpoints)
        price = 100.0 * cumprod(returns)
        volume = abs(random.normal(1000.0, 1500.0, size=numpoints) + 2000.0)

        time_ds = ArrayDataSource(index)
        vol_ds = ArrayDataSource(volume, sort_order="none")
        price_ds = ArrayDataSource(price, sort_order="none")

        # Create the price plots
        price_plot, mini_plot = self._create_price_plots(time_ds, price_ds)
        price_plot.index_mapper.domain_limits = (index[0], index[-1])
        self.price_plot = price_plot
        self.mini_plot = mini_plot

        # Create the volume plot
        vol_plot = self._create_vol_plot(time_ds, vol_ds)
        vol_plot.index_mapper.domain_limits = (index[0], index[-1])

        # Set the plot's bottom axis to use the Scales ticking system
        ticker = ScalesTickGenerator(scale=CalendarScaleSystem())
        for plot in price_plot, mini_plot, vol_plot:
            bottom_axis = PlotAxis(plot, orientation="bottom",
                                   tick_generator = ticker)
            plot.overlays.append(bottom_axis)
            plot.overlays.append(PlotAxis(plot, orientation="left"))
            hgrid, vgrid = add_default_grids(plot)
            vgrid.tick_generator = bottom_axis.tick_generator

        container = VPlotContainer(bgcolor = "lightgray",
                                   spacing = 40,
                                   padding = 50,
                                   fill_padding=False)
        container.add(mini_plot, vol_plot, price_plot)

        return Window(self, -1, component=container)

if __name__ == "__main__":
    # Save demo so that it doesn't get garbage collected when run within
    # existing event loop (i.e. from ipython).
    demo = demo_main(PlotFrame, size=(800,600),
                     title="Stock price and volume")
