{-# 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 | ModeText deriving Show instance {-# OVERLAPS #-} ToJSON [Mode] where toJSON = toJSON . intercalate "+" . map (map toLower . dropInitial "Mode" . show) -- | What kind of plot type are we building - scatter (inluding line plots) or bars? data TraceType = Scatter | Scatter3D | Bar | Box | Mesh3D | Pie | Contour 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 Eq Color where (ColRGBA r0 g0 b0 a0) == (ColRGBA r1 g1 b1 a1) = (r0,g0,b0,a0) == (r1,g1,b1,a1) (ColRGB r0 g0 b0) == (ColRGB r1 g1 b1) = (r0,g0,b0) == (r1,g1,b1) (ColIx i0) == (ColIx i1) = i0 == i1 (ColRGBA r0 g0 b0 1) == (ColRGB r1 g1 b1) = (r0,g0,b0) == (r1,g1,b1) (ColRGB r0 g0 b0) == (ColRGBA r1 g1 b1 1) = (r0,g0,b0) == (r1,g1,b1) _ == _ = False 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 | CustomSymbol Text deriving (Show, Eq) instance ToJSON Symbol where toJSON (CustomSymbol t) = toJSON t toJSON s = toJSON . map toLower . show $ s 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 data Sizemode = Diameter | Area deriving (Show, Eq) instance ToJSON Sizemode where toJSON = toJSON . map toLower . show -- | Marker line specification data MarkerLine = MarkerLine { _markerlinewidth :: Maybe (ListOrElem Double) , _markerlinecolor :: Maybe (ListOrElem Value) } deriving (Generic, Eq) makeLenses ''MarkerLine instance ToJSON MarkerLine where toJSON = genericToJSON jsonOptions {fieldLabelModifier = dropInitial "markerline" . unLens} -- | default marker line specification defMarkerLine :: MarkerLine defMarkerLine = MarkerLine Nothing Nothing -- | Marker specification data Marker = Marker { _size :: Maybe (ListOrElem Value) , _sizeref :: Maybe Value , _sizeMode :: Maybe Sizemode , _markercolor :: Maybe (ListOrElem Value) , _markercolors :: Maybe (ListOrElem Value) -- for pie charts , _symbol :: Maybe (ListOrElem Symbol) , _opacity :: Maybe Double , _markerline :: Maybe MarkerLine } deriving (Generic, Eq) makeLenses ''Marker instance ToJSON Marker where toJSON = genericToJSON jsonOptions {fieldLabelModifier = dropInitial "marker" . unLens} -- | default marker specification defMarker :: Marker defMarker = Marker Nothing Nothing Nothing Nothing 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 = FillNone | ToZeroY | ToNextY | ToZeroX | ToNextX | ToSelf | ToNext deriving Show instance ToJSON Fill where toJSON = toJSON . map toLower . dropInitial "Fill" . show data LineShape = Linear | Spline | Hv | Hvh | Vh | Vhv deriving Show instance ToJSON LineShape where toJSON = toJSON . map toLower . show -- | line specification data Line = Line { _linewidth :: Maybe Double , _linecolor :: Maybe Color , _lineshape :: Maybe LineShape , _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 Nothing data HoverElem = HoverX | HoverY | HoverZ | HoverText | HoverName deriving (Generic, Show) data HoverInfo = HoverPlus [HoverElem] | HoverAll | HoverNone | HoverSkip deriving (Generic, Show) instance ToJSON HoverInfo where toJSON (HoverPlus elems) = toJSON . intercalate "+" $ (map toLower . dropInitial "Hover" . show) <$> elems toJSON x = toJSON . map toLower . dropInitial "Hover" $ show x data HoverMode = X | Y | Closest | False | XUnified | YUnified instance ToJSON HoverMode where toJSON mode = toJSON ( showMode mode ) where showMode :: HoverMode -> Value showMode m = case m of X -> "x" Y -> "y" Closest -> "closest" Graphics.Plotly.Base.False -> "False" XUnified -> "x unified" YUnified -> "y unified" data HoverOn = HoverPoints | HoverFills deriving (Generic, Show) instance {-# OVERLAPS #-} ToJSON [HoverOn] where toJSON = toJSON . intercalate "+" . map (map toLower . dropInitial "Hover" . show) data TextPosition = TopLeft | TopCenter | TopRight | MiddleLeft | MiddleCenter | MiddleRight | BottomLeft | BottomCenter | BottomRight deriving (Generic, Show) instance ToJSON TextPosition where toJSON = toJSON . camelTo2 ' ' . show -- | 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 , _z :: Maybe [Value] -- ^ z values, as numbers , _values :: Maybe [Value] -- values for pie chart , _labels :: Maybe [Text] -- labels for pie chart , _hole :: Maybe Value -- pie chart hole property , _mode :: Maybe [Mode] -- ^ select one or two modes. , _name :: Maybe Text -- ^ name of this trace, for legend , _text :: Maybe [Text] , _textposition :: Maybe TextPosition , _tracetype :: TraceType , _marker :: Maybe Marker , _line :: Maybe Line , _fill :: Maybe Fill , _orientation :: Maybe Orientation , _visible :: Maybe Value , _traceshowlegend :: Maybe Bool , _legendgroup :: Maybe Text , _customdata :: Maybe [Value] , _hoverinfo :: Maybe HoverInfo , _hovertext :: Maybe (ListOrElem Text) , _hoveron :: Maybe [HoverOn] , _connectgaps :: Maybe Bool -- Stacked Area Charts , _stackgroup :: Maybe Text , _fillcolor :: Maybe Text -- Pie , _sort :: Maybe Bool -- 3D mesh , _i :: Maybe [Int] -- ^ i values, as ints , _j :: Maybe [Int] -- ^ j values, as ints , _k :: Maybe [Int] -- ^ k values, as ints , _tracecolor :: Maybe Color , _traceopacity :: Maybe Double -- Sub-plots , _tracexaxis :: Maybe Text -- ^ X-axis name , _traceyaxis :: Maybe Text -- ^ Y-axis name } deriving Generic makeLenses ''Trace mkTrace :: TraceType -> Trace mkTrace tt = Trace Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing tt Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing -- TODO: there must be a way to avoid all that nothing. Something like this? -- mkTrace :: TraceType -> Trace -- mkTrace tt = Trace { _tracetype = tt -- |an empty scatter plot scatter :: Trace scatter = mkTrace Scatter -- |an empty 3D scatter plot scatter3d :: Trace scatter3d = mkTrace Scatter3D -- |an empty bar plot bars :: Trace bars = mkTrace Bar -- |an empty box plot box :: Trace box = mkTrace Box -- |an empty 3D mesh plot mesh3d :: Trace mesh3d = mkTrace Mesh3D -- |an empty 3D mesh plot contour :: Trace contour = mkTrace Contour -- | an empty pie chart pie :: Trace pie = mkTrace Pie instance ToJSON Trace where toJSON = genericToJSON jsonOptions {fieldLabelModifier = renamer} where renamer = dropInitial "trace" . unLens data AxisType = Log | Date | Category deriving Show instance ToJSON AxisType where toJSON = toJSON . map toLower . show -- |Options for axes data Axis = Axis { _range :: Maybe (Double,Double) , _axistype :: Maybe AxisType , _axistitle :: Maybe Text , _showgrid :: Maybe Bool , _zeroline :: Maybe Bool , _axisvisible :: Maybe Bool , _tickvals :: Maybe [Value] , _ticktext :: Maybe [Text] , _domain :: Maybe (Double,Double) } deriving Generic makeLenses ''Axis instance ToJSON Axis where toJSON = genericToJSON jsonOptions {fieldLabelModifier = dropInitial "axis" . unLens} defAxis :: Axis defAxis = Axis Nothing Nothing Nothing Nothing Nothing 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 Fonts. data Font = Font { _fontfamily :: Maybe Text , _fontsize :: Maybe Double , _fontcolor :: Maybe Color } deriving Generic makeLenses ''Font instance ToJSON Font where toJSON = genericToJSON jsonOptions { fieldLabelModifier = dropInitial "font" . unLens} defFont :: Font defFont = Font Nothing Nothing Nothing data Align = AlignLeft | AlignCenter | AlignRight deriving (Generic, Show) instance ToJSON Align where toJSON = toJSON . map toLower . dropInitial "Align" . show -- | Options for annotations data Annotation = Annotation { _annotationvisible :: Maybe Bool , _annotationtext :: Maybe Text , _annotationfont :: Maybe Font , _annotationwidth :: Maybe Double , _annotationheight :: Maybe Double , _annotationopacity :: Maybe Double , _annotationalign :: Maybe Align , _annotataonbgcolor :: Maybe Color , _annotationbordercolor :: Maybe Color , _annotationshowarrow :: Maybe Bool , _annotationx :: Maybe Value , _annotationxref :: Maybe Text -- ^ "paper" or X-axis name , _annotationxshift :: Maybe Double , _annotationy :: Maybe Value , _annotationyref :: Maybe Text -- ^ "paper" or Y-axis name , _annotationyshift :: Maybe Double } deriving Generic makeLenses ''Annotation defAnnotation :: Annotation defAnnotation = Annotation Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing instance ToJSON Annotation where toJSON = genericToJSON jsonOptions {fieldLabelModifier = dropInitial "annotation" . unLens} -- |options for the layout of the whole plot data Layout = Layout { _xaxis :: Maybe Axis , _xaxis2 :: Maybe Axis , _xaxis3 :: Maybe Axis , _xaxis4 :: Maybe Axis , _yaxis :: Maybe Axis , _yaxis2 :: Maybe Axis , _yaxis3 :: Maybe Axis , _yaxis4 :: Maybe Axis , _zaxis :: Maybe Axis , _title :: Maybe Text , _titlefont :: Maybe Font , _showlegend :: Maybe Bool , _height :: Maybe Int , _width :: Maybe Int , _barmode :: Maybe Barmode , _hovermode :: Maybe HoverMode , _margin :: Maybe Margin , _font :: Maybe Font , _annotations :: Maybe [Annotation] } deriving Generic makeLenses ''Layout -- -- |a defaultlayout defLayout :: Layout defLayout = Layout Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing 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 } deriving Generic instance ToJSON Plotly where toJSON = genericToJSON jsonOptions makeLenses ''Plotly -- | helper function for building the plot. plotly :: Text -> [Trace] -> Plotly plotly idnm trs = Plotly idnm trs defLayout