Finish the plotting example

This commit is contained in:
Sumit Sahrawat 2015-08-01 17:03:52 +05:30
parent 2ce44db1dc
commit 0862981867

View File

@ -4,13 +4,8 @@
"cell_type": "markdown",
"metadata": {},
"source": [
"## A complete plotting example"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## A complete plotting example\n",
"\n",
"This example is inspired from the [plot-gtk-ui](https://github.com/sumitsahrawat/plot-gtk-ui) package. Our goal will be to create an interface similar to the screenshot below.\n",
"\n",
"![Screenshot](https://raw.githubusercontent.com/sumitsahrawat/plot-gtk-ui/master/screenshots/sinax.png)"
@ -46,52 +41,44 @@
"import Data.IORef\n",
"import Data.Monoid (mempty)\n",
"import Data.Text (Text)\n",
"import qualified Data.Map as M\n",
"\n",
"data PlotInfo = PlotInfo {\n",
" plotTitle :: Text,\n",
" plotTitleSize :: Integer,\n",
" subTitle :: Text,\n",
" subTitleSize :: Integer,\n",
" xLabel :: Text,\n",
" xLabelSize :: Integer,\n",
" yLabel :: Text,\n",
" yLabelSize :: Integer,\n",
" showXAxis :: Bool,\n",
" showYAxis :: Bool,\n",
" plotTitle :: String,\n",
" plotTitleSize :: Double,\n",
" xLabel :: String,\n",
" xLabelSize :: Double,\n",
" yLabel :: String,\n",
" yLabelSize :: Double,\n",
" showXGrid :: Bool,\n",
" showYGrid :: Bool,\n",
" xRange :: (Double, Double),\n",
" yRange :: (Double, Double)\n",
" yRange :: (Double, Double),\n",
" sampling :: Double,\n",
" functions :: M.Map String (Double -> Double)\n",
" }\n",
"\n",
"defaultPlotInfo = PlotInfo {\n",
" plotTitle = mempty,\n",
" plotTitleSize = 10,\n",
" subTitle = mempty,\n",
" subTitleSize = 10,\n",
" xLabel = mempty,\n",
" xLabelSize = 10,\n",
" yLabel = mempty,\n",
" yLabelSize = 10,\n",
" showXAxis = True,\n",
" showYAxis = True,\n",
" showXGrid = True,\n",
" showYGrid = True,\n",
" xRange = (-5, 5),\n",
" yRange = (-5, 5)\n",
" }\n",
"\n",
"plotData <- newIORef defaultPlotInfo"
" yRange = (-5, 5),\n",
" sampling = 50,\n",
" functions = mempty\n",
" }"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# TODO: Plotting Implementation"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"The first required element is a box, to implement a vertical division between the plotting region and the input widgets."
"Now, we implement the plotting logic. We also create an `ImageWidget` here, which will be used to display the plot."
]
},
{
@ -109,6 +96,87 @@
"output_type": "display_data"
}
],
"source": [
"import Data.IORef\n",
"import Graphics.Rendering.Chart.Easy hiding (tan)\n",
"import Graphics.Rendering.Chart.Backend.Cairo\n",
"import qualified Data.ByteString as B\n",
"import IHaskell.Display (base64)\n",
"import Control.Applicative ((<$>))\n",
"\n",
"tempImgWidget <- mkImageWidget\n",
"\n",
"setField tempImgWidget Width 400\n",
"setField tempImgWidget Height 400\n",
"\n",
"plotState <- newIORef defaultPlotInfo\n",
"\n",
"-- Update and redraw.\n",
"update :: (PlotInfo -> IO PlotInfo) -> IO ()\n",
"update modifier = readIORef plotState >>= modifier >>= writeIORef plotState >> redraw\n",
"\n",
"redraw :: IO ()\n",
"redraw = readIORef plotState >>= mkPlot >>= setField tempImgWidget B64Value . base64\n",
"\n",
"mkDset :: PlotInfo -> [(String, [(Double, Double)])]\n",
"mkDset info = let funcs = M.toList $ functions info\n",
" (xLow, xHigh) = xRange info\n",
" period = 1 / sampling info\n",
" xs = [xLow, xLow + period .. xHigh]\n",
" in map (\\(s, f) -> (s, zip xs $ map f xs)) funcs\n",
"\n",
"axisSetter :: Bool -> Bool -> AxisData t -> AxisData t\n",
"axisSetter axis grid ad =\n",
" ad { _axis_grid = if grid then _axis_grid ad else []\n",
" , _axis_visibility = if axis\n",
" then AxisVisibility True True True\n",
" else AxisVisibility False False False\n",
" }\n",
"\n",
"mkPlot :: PlotInfo -> IO B.ByteString\n",
"mkPlot info = do\n",
" let dset = mkDset info\n",
" opts = def { _fo_size = (400, 400) }\n",
" toFile opts \".chart\" $ do\n",
" layout_title .= plotTitle info\n",
" layout_title_style . font_size .= plotTitleSize info\n",
" layout_x_axis . laxis_title .= xLabel info\n",
" layout_x_axis . laxis_title_style . font_size .= xLabelSize info\n",
" layout_x_axis . laxis_generate .= scaledAxis def (xRange info)\n",
" layout_x_axis . laxis_override .= if showXGrid info then id else axisGridHide\n",
" layout_y_axis . laxis_title .= yLabel info\n",
" layout_y_axis . laxis_title_style . font_size .= yLabelSize info\n",
" layout_y_axis . laxis_generate .= scaledAxis def (yRange info)\n",
" layout_y_axis . laxis_override .= if showYGrid info then id else axisGridHide\n",
"\n",
" mapM_ (\\(s, ps) -> plot (line s [ps])) dset\n",
" B.readFile \".chart\""
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"All that's left now is to create an interface and hook widget events accordingly.\n",
"\n",
"The first required element is a box, to create a vertical division between the plotting region and the input widgets."
]
},
{
"cell_type": "code",
"execution_count": 4,
"metadata": {
"collapsed": false
},
"outputs": [
{
"data": {
"text/plain": []
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"divBox <- mkFlexBox\n",
"setField divBox Orientation HorizontalOrientation\n",
@ -136,7 +204,7 @@
},
{
"cell_type": "code",
"execution_count": 4,
"execution_count": 5,
"metadata": {
"collapsed": false,
"scrolled": true
@ -152,7 +220,9 @@
],
"source": [
"slBox <- mkFlexBox\n",
"plImg <- mkImageWidget\n",
"\n",
"-- Reusing the image widget created before\n",
"let plImg = tempImgWidget\n",
"\n",
"-- Sliders need to be laid out vertically.\n",
"setField slBox Orientation VerticalOrientation\n",
@ -167,63 +237,11 @@
"source": [
"Now, we fill the other half with the following:\n",
"\n",
"+ Four `FlexBox` widgets (title, sub-title, x-label, y-label), containing a `TextWidget` for title and a `BoundedIntText` for the font size.\n",
"+ A `FlexBox` with two selection widgets for toggling axis visibility. We'll go with `ToggleButton` just for fun.\n",
"+ Four `FlexBox` widgets (title, sub-title, x-label, y-label), containing a `TextWidget` for title and a `BoundedFloatText` for the font size.\n",
"+ A `FlexBox` with two selection widgets for toggling visibility for different elements. We'll go with `ToggleButton` just for fun.\n",
"+ Two more `FlexBox`, with `FloatText` widgets for deciding the plot range."
]
},
{
"cell_type": "code",
"execution_count": 5,
"metadata": {
"collapsed": false
},
"outputs": [
{
"data": {
"text/plain": []
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"-- The four FlexBox widgets.\n",
"import Control.Monad (replicateM, forM_)\n",
"import Data.List (zip4)\n",
"import Text.Printf (printf)\n",
"import Data.Text (pack)\n",
"\n",
"-- pl : plotTitle\n",
"-- sb : subTitle\n",
"-- x : xLabel\n",
"-- y : yLabel\n",
"boxes <- replicateM 4 mkFlexBox\n",
"texts@[plTxt,sbTxt,xTxt,yTxt] <- replicateM 4 mkTextWidget\n",
"inpts@[plInp,sbInp,xInp,yInp] <- replicateM 4 mkBoundedIntText\n",
"\n",
"-- Adding event handlers. This is a clumsy way to emulate first-class record fields.\n",
"let setHandler widget fieldSetter = setField widget SubmitHandler $ do\n",
" oldVal <- readIORef plotData\n",
" newStr <- getField widget StringValue\n",
" writeIORef plotData (fieldSetter oldVal newStr)\n",
" in do\n",
" setHandler plTxt $ \\struct val -> struct { plotTitle = val }\n",
" setHandler sbTxt $ \\struct val -> struct { subTitle = val }\n",
" setHandler xTxt $ \\struct val -> struct { xLabel = val }\n",
" setHandler yTxt $ \\struct val -> struct { yLabel = val }\n",
"\n",
"let boxInfo = zip4 boxes texts inpts [\"plot title\", \"sub-title\", \"X-Label\", \"Y-Label\"]\n",
"\n",
"forM_ boxInfo $ \\(box,text,input,placeholder) -> do\n",
" setField box Orientation HorizontalOrientation\n",
" setField box Children [ChildWidget text, ChildWidget input]\n",
" setField text Placeholder $ pack $ printf \"Enter %s here ...\" placeholder\n",
" setField input MinInt 1\n",
" setField input MaxInt 72\n",
" setField input IntValue 10"
]
},
{
"cell_type": "code",
"execution_count": 6,
@ -240,26 +258,46 @@
}
],
"source": [
"-- A FlexBox with ToggleButtons\n",
"buttonBox <- mkFlexBox\n",
"setField buttonBox Orientation HorizontalOrientation\n",
"tButtons@[xBut,yBut] <- replicateM 2 mkToggleButton\n",
"-- The four FlexBox widgets.\n",
"import Control.Monad (replicateM, forM_)\n",
"import Data.List (zip4)\n",
"import Text.Printf (printf)\n",
"import Data.Text (unpack, pack)\n",
"\n",
"let tgButtonInfo = zip tButtons [\"X-Axis\", \"Y-Axis\"]\n",
"-- pl : plotTitle\n",
"-- x : xLabel\n",
"-- y : yLabel\n",
"boxes <- replicateM 3 mkFlexBox\n",
"texts@[plTxt,xTxt,yTxt] <- replicateM 3 mkTextWidget\n",
"inpts@[plInp,xInp,yInp] <- replicateM 3 mkBoundedFloatText\n",
"\n",
"let setHandler widget fieldSetter = setField widget ChangeHandler $ do\n",
" oldVal <- readIORef plotData\n",
" newStr <- getField widget BoolValue\n",
" writeIORef plotData (fieldSetter oldVal newStr)\n",
"-- Adding event handlers for text widgets. This is a clumsy way to emulate first-class record fields.\n",
"let setHandler widget field = setField widget ChangeHandler $ update $ \\info -> do\n",
" newStr <- getField widget StringValue\n",
" return $ field info newStr\n",
" in do\n",
" setHandler xBut $ \\struct val -> struct { showXAxis = val }\n",
" setHandler yBut $ \\struct val -> struct { showYAxis = val }\n",
" setHandler plTxt $ \\struct val -> struct { plotTitle = unpack val }\n",
" setHandler xTxt $ \\struct val -> struct { xLabel = unpack val }\n",
" setHandler yTxt $ \\struct val -> struct { yLabel = unpack val }\n",
"\n",
"forM_ tgButtonInfo $ \\(widget, description) -> do\n",
" setField widget Description description\n",
" setField widget BoolValue True\n",
"-- Adding events for the numeric input widgets.\n",
"let setHandler widget field = setField widget ChangeHandler $ update $ \\info -> do\n",
" newNum <- getField widget FloatValue\n",
" return $ field info newNum\n",
" in do\n",
" setHandler plInp $ \\struct val -> struct { plotTitleSize = val }\n",
" setHandler xInp $ \\struct val -> struct { xLabelSize = val }\n",
" setHandler yInp $ \\struct val -> struct { yLabelSize = val }\n",
"\n",
"setField buttonBox Children (map ChildWidget tButtons)"
"let boxInfo = zip4 boxes texts inpts [\"plot title\", \"X-Label\", \"Y-Label\"]\n",
"\n",
"forM_ boxInfo $ \\(box,text,input,placeholder) -> do\n",
" setField box Orientation HorizontalOrientation\n",
" setField box Children [ChildWidget text, ChildWidget input]\n",
" setField text Placeholder $ pack $ printf \"Enter %s here ...\" placeholder\n",
" setField input MinFloat 1\n",
" setField input MaxFloat 72\n",
" setField input FloatValue 10"
]
},
{
@ -278,6 +316,45 @@
}
],
"source": [
"-- A FlexBox with ToggleButtons\n",
"buttonBox <- mkFlexBox\n",
"setField buttonBox Orientation HorizontalOrientation\n",
"tButtons@[xGrid,yGrid] <- replicateM 2 mkToggleButton\n",
"\n",
"let tgButtonInfo = zip tButtons [\"X-Grid\", \"Y-Grid\"]\n",
"\n",
"let setHandler widget fieldSetter = setField widget ChangeHandler $ update $ \\info -> do\n",
" newStr <- getField widget BoolValue\n",
" return $ fieldSetter info newStr\n",
" in do\n",
" setHandler xGrid $ \\struct val -> struct { showXGrid = val }\n",
" setHandler yGrid $ \\struct val -> struct { showYGrid = val }\n",
"\n",
"forM_ tgButtonInfo $ \\(widget, description) -> do\n",
" setField widget Description description\n",
" setField widget BoolValue True\n",
"\n",
"setField buttonBox Children (map ChildWidget tButtons)"
]
},
{
"cell_type": "code",
"execution_count": 8,
"metadata": {
"collapsed": false
},
"outputs": [
{
"data": {
"text/plain": []
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"import Control.Arrow (first, second)\n",
"\n",
"-- Finally, the ranges\n",
"rangeBoxes <- replicateM 2 mkFlexBox\n",
"fTxts@[xLow,xHigh,yLow,yHigh] <- replicateM 4 mkFloatText\n",
@ -286,7 +363,16 @@
"\n",
"forM_ rangeInfo $ \\(box, (lowTxt, highTxt)) -> do\n",
" setField box Orientation HorizontalOrientation\n",
" setField box Children (map ChildWidget [lowTxt, highTxt])"
" setField box Children (map ChildWidget [lowTxt, highTxt])\n",
"\n",
"let setHandler widget modifier = setField widget ChangeHandler $ update $ \\info -> do\n",
" val <- getField widget FloatValue\n",
" return $ modifier val info\n",
" in do\n",
" setHandler xLow $ \\v p -> p { xRange = first (const v) (xRange p) }\n",
" setHandler xHigh $ \\v p -> p { xRange = second (const v) (xRange p) }\n",
" setHandler yLow $ \\v p -> p { yRange = first (const v) (yRange p) }\n",
" setHandler yHigh $ \\v p -> p { yRange = second (const v) (yRange p) }"
]
},
{
@ -298,7 +384,7 @@
},
{
"cell_type": "code",
"execution_count": 8,
"execution_count": 9,
"metadata": {
"collapsed": false
},
@ -322,32 +408,6 @@
"We also need to give a title to each page in the `Accordion` widget."
]
},
{
"cell_type": "code",
"execution_count": 9,
"metadata": {
"collapsed": false
},
"outputs": [
{
"data": {
"text/plain": []
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"setField tlBox Titles [\"Plot title\", \"Subtitle\", \"X-Label\", \"Y-Label\", \"Axis visibility\", \"X-range\", \"Y-range\"]"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Then we sync the values from out `plotData :: IORef PlotData` to the widgets."
]
},
{
"cell_type": "code",
"execution_count": 10,
@ -364,54 +424,87 @@
}
],
"source": [
"let syncVal widget value fieldGetter = readIORef plotData >>= setField widget value . fieldGetter\n",
"setField tlBox Titles [\"Plot title\", \"X-Label\", \"Y-Label\", \"Grid\", \"X-range\", \"Y-range\"]"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Then we sync the initial values from the `plotData` to the widgets."
]
},
{
"cell_type": "code",
"execution_count": 11,
"metadata": {
"collapsed": false
},
"outputs": [
{
"data": {
"text/plain": []
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"let syncVal widget value fieldGetter = readIORef plotState >>= setField widget value . fieldGetter\n",
" in do\n",
" syncVal plTxt StringValue plotTitle\n",
" syncVal plInp IntValue plotTitleSize\n",
" syncVal sbTxt StringValue subTitle\n",
" syncVal sbInp IntValue subTitleSize\n",
" \n",
" syncVal xTxt StringValue xLabel\n",
" syncVal xInp IntValue xLabelSize\n",
" syncVal yTxt StringValue yLabel\n",
" syncVal yInp IntValue yLabelSize\n",
" syncVal plTxt StringValue (pack . plotTitle)\n",
" syncVal plInp FloatValue plotTitleSize\n",
"\n",
" syncVal xTxt StringValue (pack . xLabel)\n",
" syncVal xInp FloatValue xLabelSize\n",
" syncVal yTxt StringValue (pack . yLabel)\n",
" syncVal yInp FloatValue yLabelSize\n",
"\n",
" syncVal xGrid BoolValue showXGrid\n",
" syncVal yGrid BoolValue showYGrid\n",
"\n",
" syncVal xBut BoolValue showXAxis\n",
" syncVal yBut BoolValue showYAxis\n",
" \n",
" syncVal xLow FloatValue (fst . xRange)\n",
" syncVal xHigh FloatValue (snd . xRange)\n",
" syncVal yLow FloatValue (fst . yRange)\n",
" syncVal yHigh FloatValue (snd . yRange)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Now that everything is set, we also need to provide a way for the user to add or remove plots from the interface."
]
},
{
"cell_type": "code",
"execution_count": 11,
"execution_count": 12,
"metadata": {
"collapsed": false
},
"outputs": [],
"source": [
"addFunction :: String -> (Double -> Double) -> IO ()\n",
"addFunction name func = update $ \\p -> return p { functions = M.insert name func $ functions p }\n",
"\n",
"removeFunction :: String -> IO ()\n",
"removeFunction name = update $ \\p -> return p { functions = M.delete name $ functions p }"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"And now we display the complete interface, ready to use."
]
},
{
"cell_type": "code",
"execution_count": 13,
"metadata": {
"collapsed": false
},
"outputs": [
{
"data": {},
"metadata": {},
"output_type": "display_data"
},
{
"data": {},
"metadata": {},
"output_type": "display_data"
},
{
"data": {},
"metadata": {},
"output_type": "display_data"
},
{
"data": {},
"metadata": {},
"output_type": "display_data"
},
{
"data": {},
"metadata": {},
@ -424,8 +517,51 @@
}
],
"source": [
"-- Spurious update to display empty plot instead of empty image initially\n",
"update return\n",
"\n",
"divBox"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Now, we can use `addFunction` and `removeFunction` to add and remove functions respectively."
]
},
{
"cell_type": "code",
"execution_count": 14,
"metadata": {
"collapsed": true
},
"outputs": [],
"source": [
"addFunction \"sin\" sin"
]
},
{
"cell_type": "code",
"execution_count": 15,
"metadata": {
"collapsed": false
},
"outputs": [],
"source": [
"addFunction \"cos\" cos"
]
},
{
"cell_type": "code",
"execution_count": 16,
"metadata": {
"collapsed": true
},
"outputs": [],
"source": [
"addFunction \"x^2\" (\\x -> x * x)"
]
}
],
"metadata": {