{-# LANGUAGE DeriveGeneric, OverloadedStrings,FlexibleInstances, TemplateHaskell #-}

{-|

This module defines datatypes that can be used to generate [Plotly.js](https://plot.ly/javascript/)
plots via their JSON values. The interface encourages the use of
lenses. Every trace on a plot is defined by a `Trace` type value, the
construction of which is the central goal of this module.

Example scatter plot of the Iris dataset:

@
import Graphics.Plotly
import Numeric.Dataset.Iris

tr :: Trace
tr = scatter & x ?~ map sepalLength iris
             & y ?~ map sepalWidth iris
             & marker ?~ (defMarker & markercolor ?~ catColors (map irisClass irisd))
             & mode ?~ [Markers]
@

Horizontal bars:

@
hbarData :: [(Text, Double)]
hbarData = [(\"Simon\", 14.5), (\"Joe\", 18.9), (\"Dorothy\", 16.2)]

hbarsTrace :: Trace
hbarsTrace = bars & ytext ?~ map fst hbarData
                  & x ?~ map snd hbarData
                  & orientation ?~ Horizontal
@

see Graphics.Plotly.Lucid for helper functions that turn traces into HTML.

-}

module Graphics.Plotly.Base where

import Data.Aeson
import Data.Aeson.Types
import Data.Char (toLower)
import Data.List (intercalate, nub, findIndex)
import Data.Monoid ((<>))
import Data.Maybe (fromJust)
import Data.Text (Text)

import GHC.Generics
import Lens.Micro.TH

import Graphics.Plotly.Utils

-- * Traces

-- |How should traces be drawn? (lines or markers)
data Mode = Markers | Lines deriving Show

instance {-# OVERLAPS #-} ToJSON [Mode] where
  toJSON = toJSON . intercalate "+" . map (map toLower . show)

-- | What kind of plot type are we building - scatter (inluding line plots) or bars?
data TraceType = Scatter | Bar deriving Show

instance ToJSON TraceType where
  toJSON = toJSON . map toLower . show


-- | A color specification, either as a concrete RGB/RGBA value or a color per point.
data Color = ColRGBA Int Int Int Int -- ^ use this RGBA color for every point in the trace
           | ColRGB Int Int Int -- ^ use this RGB color for every point in the trace
           | ColIx Int  -- ^ use a different color index for each point

instance ToJSON Color where
  toJSON (ColRGB r g b) = toJSON $ "rgb("<>show r<>","<>show g<>","<>show b<>")"
  toJSON (ColRGBA r g b a) = toJSON $ "rgba("<>show r<>","<>show g<>","<>show b<>","<> show a<>")"
  toJSON (ColIx cs) = toJSON cs

-- | Assign colors based on any categorical value
catColors :: Eq a => [a] -> ListOrElem Value
catColors xs =
  let vals = nub xs
      f x = fromJust $ findIndex (==x) vals
  in List $ map (toJSON . ColIx . f) xs

-- | Different types of markers
data Symbol = Circle | Square | Diamond | Cross deriving (Show, Eq)

instance ToJSON Symbol where
  toJSON = toJSON . map toLower . show

data ListOrElem a = List [a] | All a deriving Eq

instance ToJSON a => ToJSON (ListOrElem a) where
  toJSON (List xs) = toJSON xs
  toJSON (All x) = toJSON x

-- | Marker specification
data Marker = Marker
  { _size :: Maybe (ListOrElem Value)
  , _markercolor :: Maybe (ListOrElem Value)
  , _symbol :: Maybe Symbol
  , _opacity :: Maybe Double
  } deriving (Generic, Eq)

makeLenses ''Marker

instance ToJSON Marker where
  toJSON = genericToJSON jsonOptions {fieldLabelModifier = rename "markercolor" "color" . unLens}

-- | default marker specification
defMarker :: Marker
defMarker  = Marker Nothing Nothing Nothing Nothing


-- | Dash type specification
data Dash = Solid | Dashdot | Dot deriving Show

instance ToJSON Dash where
  toJSON = toJSON . map toLower . show

-- | Horizontal or Vertical orientation of bars
data Orientation = Horizontal | Vertical

instance ToJSON Orientation where
  toJSON Horizontal = "h"
  toJSON Vertical = "v"

-- | Are we filling area plots from the zero line or to the next Y value?
data Fill = ToZeroY | ToNextY deriving Show

instance ToJSON Fill where
  toJSON = toJSON . map toLower . show

-- | line specification
data Line = Line
  { _linewidth :: Maybe Double
  , _linecolor :: Maybe Color
  , _dash :: Maybe Dash
  } deriving Generic

makeLenses ''Line

instance ToJSON Line where
  toJSON = genericToJSON jsonOptions { fieldLabelModifier = dropInitial "line" . unLens}

defLine :: Line
defLine = Line Nothing Nothing Nothing

-- | A `Trace` is the component of a plot. Multiple traces can be superimposed.
data Trace = Trace
  { _x :: Maybe [Value] -- ^ x values, as numbers
  , _y :: Maybe [Value] -- ^ y values, as numbers
  , _mode :: Maybe [Mode] -- ^ select one or two modes.
  , _name :: Maybe Text -- ^ name of this trace, for legend
  , _text :: Maybe [Text]
  , _tracetype :: TraceType
  , _marker :: Maybe Marker
  , _line :: Maybe Line
  , _fill :: Maybe Fill
  , _orientation :: Maybe Orientation
  } deriving Generic

makeLenses ''Trace

-- |an empty scatter plot
scatter :: Trace
scatter = Trace Nothing Nothing Nothing Nothing Nothing Scatter Nothing Nothing Nothing Nothing

-- |an empty bar plot
bars :: Trace
bars = Trace Nothing Nothing Nothing Nothing Nothing Bar Nothing Nothing Nothing Nothing


instance ToJSON Trace where
  toJSON = genericToJSON jsonOptions {fieldLabelModifier = rename "tracetype" "type" . unLens}

-- |Options for axes
data Axis = Axis
  { _range :: Maybe (Double,Double)
  , _axistitle :: Maybe Text
  , _showgrid :: Maybe Bool
  , _zeroline :: Maybe Bool
  } deriving Generic

makeLenses ''Axis

instance ToJSON Axis where
  toJSON = genericToJSON jsonOptions {fieldLabelModifier = rename "axistitle" "axis" . unLens}

defAxis :: Axis
defAxis = Axis Nothing Nothing Nothing Nothing

-- * Layouts

-- | How different bar traces be superimposed? By grouping or by stacking?
data Barmode = Stack | Group deriving Show

instance ToJSON Barmode where
  toJSON = toJSON . map toLower . show

-- |Options for Margins.
data Margin = Margin
  { _marginl :: Int
  , _marginr :: Int
  , _marginb :: Int
  , _margint :: Int
  , _marginpad :: Int
  } deriving Generic

makeLenses ''Margin

instance ToJSON Margin where
  toJSON = genericToJSON jsonOptions { fieldLabelModifier = dropInitial "margin" . unLens}

-- | some good values for margins
thinMargins, titleMargins :: Margin
thinMargins = Margin 50 25 30 10 4
titleMargins = Margin 50 25 30 40 4


-- |options for the layout of the whole plot
data Layout = Layout
  { _xaxis :: Maybe Axis
  , _yaxis :: Maybe Axis
  , _title :: Maybe Text
  , _showlegend :: Maybe Bool
  , _height :: Maybe Int
  , _width :: Maybe Int
  , _barmode :: Maybe Barmode
  , _margin :: Maybe Margin
  } deriving Generic

makeLenses ''Layout

-- |a defaultlayout
defLayout :: Layout
defLayout = Layout Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing

instance ToJSON Layout where
  toJSON = genericToJSON jsonOptions

-- * Plotly

-- | A helper record which represents the whole plot
data Plotly = Plotly
  { _elemid :: Text
  , _traces :: [Trace]
  , _layout :: Layout
  }

makeLenses ''Plotly

-- | helper function for building the plot.
plotly :: Text -> [Trace] -> Plotly
plotly idnm trs = Plotly idnm trs defLayout