From b64cd8498460d5b7f638a4c73c68d02b419bc4a4 Mon Sep 17 00:00:00 2001 From: kMutagene Date: Sun, 7 Jun 2020 15:04:12 +0200 Subject: [PATCH 1/2] #38 WIP: add support for grid charts --- docsrc/tools/generate.fsx | 1 + src/FSharp.Plotly.WPF/AssemblyInfo.fs | 8 +- src/FSharp.Plotly/AssemblyInfo.fs | 8 +- src/FSharp.Plotly/ChartExtensions.fs | 86 +++++++++++++ src/FSharp.Plotly/FSharp.Plotly.fsproj | 2 + src/FSharp.Plotly/Layout.fs | 9 ++ src/FSharp.Plotly/LayoutGrid.fs | 123 +++++++++++++++++++ src/FSharp.Plotly/Playground.fsx | 161 +++++++++++++++++++++++++ src/FSharp.Plotly/StyleParams.fs | 66 ++++++++++ 9 files changed, 456 insertions(+), 8 deletions(-) create mode 100644 src/FSharp.Plotly/LayoutGrid.fs create mode 100644 src/FSharp.Plotly/Playground.fsx diff --git a/docsrc/tools/generate.fsx b/docsrc/tools/generate.fsx index 256a30e9c..334849f67 100644 --- a/docsrc/tools/generate.fsx +++ b/docsrc/tools/generate.fsx @@ -33,6 +33,7 @@ let rec copyRecursive dir1 dir2 = copyRecursive subdir1 subdir2 for file in Directory.EnumerateFiles dir1 do File.Copy(file, file.Replace(dir1, dir2), true) + // Web site location for the generated documentation let website = "https://muehlhaus.github.io/FSharp.Plotly/" diff --git a/src/FSharp.Plotly.WPF/AssemblyInfo.fs b/src/FSharp.Plotly.WPF/AssemblyInfo.fs index 84e091890..464e44d0d 100644 --- a/src/FSharp.Plotly.WPF/AssemblyInfo.fs +++ b/src/FSharp.Plotly.WPF/AssemblyInfo.fs @@ -5,13 +5,13 @@ open System.Reflection [] [] [] -[] -[] +[] +[] do () module internal AssemblyVersionInformation = let [] AssemblyTitle = "FSharp.Plotly.WPF" let [] AssemblyProduct = "FSharp.Plotly" let [] AssemblyDescription = "A F# interactive charting library using plotly.js" - let [] AssemblyVersion = "1.2.2" - let [] AssemblyFileVersion = "1.2.2" + let [] AssemblyVersion = "1.2.3" + let [] AssemblyFileVersion = "1.2.3" diff --git a/src/FSharp.Plotly/AssemblyInfo.fs b/src/FSharp.Plotly/AssemblyInfo.fs index d69454d22..12d669962 100644 --- a/src/FSharp.Plotly/AssemblyInfo.fs +++ b/src/FSharp.Plotly/AssemblyInfo.fs @@ -5,13 +5,13 @@ open System.Reflection [] [] [] -[] -[] +[] +[] do () module internal AssemblyVersionInformation = let [] AssemblyTitle = "FSharp.Plotly" let [] AssemblyProduct = "FSharp.Plotly" let [] AssemblyDescription = "A F# interactive charting library using plotly.js" - let [] AssemblyVersion = "1.2.2" - let [] AssemblyFileVersion = "1.2.2" + let [] AssemblyVersion = "1.2.3" + let [] AssemblyFileVersion = "1.2.3" diff --git a/src/FSharp.Plotly/ChartExtensions.fs b/src/FSharp.Plotly/ChartExtensions.fs index 8c06dfc4c..ea59ded5d 100644 --- a/src/FSharp.Plotly/ChartExtensions.fs +++ b/src/FSharp.Plotly/ChartExtensions.fs @@ -343,6 +343,15 @@ module ChartExtensions = (fun (ch:GenericChart) -> GenericChart.addLayout layout ch) + // Set the LayoutGrid options of a Chart + [] + static member withLayoutGrid(layoutGrid:LayoutGrid) = + (fun (ch:GenericChart) -> + let layout = + GenericChart.getLayout ch + |> Layout.SetLayoutGrid layoutGrid + GenericChart.setLayout layout ch) + [] static member withConfig (config:Config) = (fun (ch:GenericChart) -> @@ -442,6 +451,83 @@ module ChartExtensions = GenericChart.combine gCharts + [] + static member Grid ((gCharts:seq<#seq>), + sharedAxes:bool + ) = + + let nRows = Seq.length gCharts + let rowWidth = 1. / float nRows + + let nCols = gCharts |> Seq.maxBy Seq.length |> Seq.length + let colWidth = 1. / float nCols + + let pattern = if sharedAxes then StyleParam.LayoutGridPattern.Coupled else StyleParam.LayoutGridPattern.Independent + let grid = + LayoutGrid.init( + Rows=nRows,Columns=nCols,Pattern=pattern + ) + gCharts + |> Seq.mapi (fun rowIndex row -> + row |> Seq.mapi (fun colIndex gChart -> + let xdomain = (colWidth * float (colIndex-1), (colWidth * float colIndex)) + let ydomain = (1. - ((rowWidth * float rowIndex)),1. - (rowWidth * float (rowIndex-1))) + + let newXIndex, newYIndex = + (if sharedAxes then colIndex + 1 else (rowIndex + colIndex + 1)), + (if sharedAxes then rowIndex + 1 else (rowIndex + colIndex + 1)) + + + let xaxis,yaxis,layout = + let layout = GenericChart.getLayout gChart + let xAxisName, yAxisName = StyleParam.AxisId.X 1 |> StyleParam.AxisId.toString, StyleParam.AxisId.Y 1 |> StyleParam.AxisId.toString + + let updateXAxis index domain axis = + axis |> Axis.LinearAxis.style(Anchor=StyleParam.AxisAnchorId.X index,Domain=StyleParam.Range.MinMax domain) + + let updateYAxis index domain axis = + axis |> Axis.LinearAxis.style(Anchor=StyleParam.AxisAnchorId.Y index,Domain=StyleParam.Range.MinMax domain) + match (layout.TryGetTypedValue xAxisName),(layout.TryGetTypedValue yAxisName) with + | Some x, Some y -> + // remove axis + DynObj.remove layout xAxisName + DynObj.remove layout yAxisName + + x |> updateXAxis newXIndex xdomain, + y |> updateYAxis newYIndex ydomain, + layout + + | Some x, None -> + // remove x - axis + DynObj.remove layout xAxisName + + x |> updateXAxis newXIndex xdomain, + Axis.LinearAxis.init(Anchor=StyleParam.AxisAnchorId.Y newYIndex ,Domain=StyleParam.Range.MinMax ydomain), + layout + + | None, Some y -> + // remove y - axis + DynObj.remove layout yAxisName + + Axis.LinearAxis.init(Anchor=StyleParam.AxisAnchorId.X newXIndex,Domain=StyleParam.Range.MinMax xdomain), + y |> updateYAxis newYIndex ydomain, + layout + | None, None -> + Axis.LinearAxis.init(Anchor=StyleParam.AxisAnchorId.X newXIndex,Domain=StyleParam.Range.MinMax xdomain), + Axis.LinearAxis.init(Anchor=StyleParam.AxisAnchorId.Y newYIndex,Domain=StyleParam.Range.MinMax ydomain), + layout + + gChart + |> GenericChart.setLayout layout + |> Chart.withAxisAnchor(X=newXIndex,Y=newYIndex) + |> Chart.withX_Axis(xaxis,newXIndex) + |> Chart.withY_Axis(yaxis,newYIndex) + ) + ) + |> Seq.map Chart.Combine + |> Chart.Combine + |> Chart.withLayoutGrid grid + /// Create a combined chart with the given charts merged [] static member Stack ( [] ?Columns:int, diff --git a/src/FSharp.Plotly/FSharp.Plotly.fsproj b/src/FSharp.Plotly/FSharp.Plotly.fsproj index 792e5ab1b..1813ee51f 100644 --- a/src/FSharp.Plotly/FSharp.Plotly.fsproj +++ b/src/FSharp.Plotly/FSharp.Plotly.fsproj @@ -49,6 +49,7 @@ + @@ -57,6 +58,7 @@ + diff --git a/src/FSharp.Plotly/Layout.fs b/src/FSharp.Plotly/Layout.fs index 1785db525..cc9a2bf4a 100644 --- a/src/FSharp.Plotly/Layout.fs +++ b/src/FSharp.Plotly/Layout.fs @@ -388,4 +388,13 @@ type Layout() = layout ) + static member SetLayoutGrid + ( + grid: LayoutGrid + ) = + (fun (layout:Layout) -> + grid |> DynObj.setValue layout "grid" + layout + ) + diff --git a/src/FSharp.Plotly/LayoutGrid.fs b/src/FSharp.Plotly/LayoutGrid.fs new file mode 100644 index 000000000..22b8f4a74 --- /dev/null +++ b/src/FSharp.Plotly/LayoutGrid.fs @@ -0,0 +1,123 @@ +namespace FSharp.Plotly + +open System + +/// A plot grid that can contain subplots with shared axes. +type LayoutGrid () = + inherit DynamicObj () + + /// Initializes LayoutGrid object + /// + /// + ///SubPlots : Used for freeform grids, where some axes may be shared across subplots but others are not. Each entry should be a cartesian subplot id, like "xy" or "x3y2", or "" to leave that cell empty. You may reuse x axes within the same column, and y axes within the same row. Non-cartesian subplots and traces that support `domain` can place themselves in this grid separately using the `gridcell` attribute. + /// + ///XAxes : Used with `yaxes` when the x and y axes are shared across columns and rows. Each entry should be an y axis id like "y", "y2", etc., or "" to not put a y axis in that row. Entries other than "" must be unique. Ignored if `subplots` is present. If missing but `xaxes` is present, will generate consecutive IDs. + /// + ///YAxes : Used with `yaxes` when the x and y axes are shared across columns and rows. Each entry should be an x axis id like "x", "x2", etc., or "" to not put an x axis in that column. Entries other than "" must be unique. Ignored if `subplots` is present. If missing but `yaxes` is present, will generate consecutive IDs. + /// + ///Rows : The number of rows in the grid. If you provide a 2D `subplots` array or a `yaxes` array, its length is used as the default. But it's also possible to have a different length, if you want to leave a row at the end for non-cartesian subplots. + /// + ///Columns : The number of columns in the grid. If you provide a 2D `subplots` array, the length of its longest row is used as the default. If you give an `xaxes` array, its length is used as the default. But it's also possible to have a different length, if you want to leave a row at the end for non-cartesian subplots. + /// + ///RowOrder : Is the first row the top or the bottom? Note that columns are always enumerated from left to right. + /// + ///Pattern : If no `subplots`, `xaxes`, or `yaxes` are given but we do have `rows` and `columns`, we can generate defaults using consecutive axis IDs, in two ways: "coupled" gives one x axis per column and one y axis per row. "independent" uses a new xy pair for each cell, left-to-right across each row then iterating rows according to `roworder`. + /// + ///XGap : Horizontal space between grid cells, expressed as a fraction of the total width available to one cell. Defaults to 0.1 for coupled-axes grids and 0.2 for independent grids. + /// + ///YGap : Vertical space between grid cells, expressed as a fraction of the total height available to one cell. Defaults to 0.1 for coupled-axes grids and 0.3 for independent grids. + /// + ///Domain : Sets the domains of this grid subplot (in plot fraction). The first and last cells end exactly at the domain edges, with no grout around the edges. + /// + ///XSide : Sets where the x axis labels and titles go. "bottom" means the very bottom of the grid. "bottom plot" is the lowest plot that each x axis is used in. "top" and "top plot" are similar. + /// + ///YSide : Sets where the y axis labels and titles go. "left" means the very left edge of the grid. "left plot" is the leftmost plot that each y axis is used in. "right" and "right plot" are similar. + static member init + ( + ?SubPlots : StyleParam.AxisId [] [], + ?XAxes : StyleParam.AxisId [], + ?YAxes : StyleParam.AxisId [], + ?Rows : int, + ?Columns : int, + ?RowOrder : StyleParam.LayoutGridRowOrder, + ?Pattern : StyleParam.LayoutGridPattern, + ?XGap : float, + ?YGap : float, + ?Domain : Domain, + ?XSide : StyleParam.LayoutGridXSide, + ?YSide : StyleParam.LayoutGridYSide + ) = + LayoutGrid () + |> LayoutGrid.style + ( + ?SubPlots = SubPlots, + ?XAxes = XAxes , + ?YAxes = YAxes , + ?Rows = Rows , + ?Columns = Columns , + ?RowOrder = RowOrder, + ?Pattern = Pattern , + ?XGap = XGap , + ?YGap = YGap , + ?Domain = Domain , + ?XSide = XSide , + ?YSide = YSide + + ) + + // Applies the styles to LayoutGrid() + /// + ///SubPlots : Used for freeform grids, where some axes may be shared across subplots but others are not. Each entry should be a cartesian subplot id, like "xy" or "x3y2", or "" to leave that cell empty. You may reuse x axes within the same column, and y axes within the same row. Non-cartesian subplots and traces that support `domain` can place themselves in this grid separately using the `gridcell` attribute. + /// + ///XAxes : Used with `yaxes` when the x and y axes are shared across columns and rows. Each entry should be an y axis id like "y", "y2", etc., or "" to not put a y axis in that row. Entries other than "" must be unique. Ignored if `subplots` is present. If missing but `xaxes` is present, will generate consecutive IDs. + /// + ///YAxes : Used with `yaxes` when the x and y axes are shared across columns and rows. Each entry should be an x axis id like "x", "x2", etc., or "" to not put an x axis in that column. Entries other than "" must be unique. Ignored if `subplots` is present. If missing but `yaxes` is present, will generate consecutive IDs. + /// + ///Rows : The number of rows in the grid. If you provide a 2D `subplots` array or a `yaxes` array, its length is used as the default. But it's also possible to have a different length, if you want to leave a row at the end for non-cartesian subplots. + /// + ///Columns : The number of columns in the grid. If you provide a 2D `subplots` array, the length of its longest row is used as the default. If you give an `xaxes` array, its length is used as the default. But it's also possible to have a different length, if you want to leave a row at the end for non-cartesian subplots. + /// + ///RowOrder : Is the first row the top or the bottom? Note that columns are always enumerated from left to right. + /// + ///Pattern : If no `subplots`, `xaxes`, or `yaxes` are given but we do have `rows` and `columns`, we can generate defaults using consecutive axis IDs, in two ways: "coupled" gives one x axis per column and one y axis per row. "independent" uses a new xy pair for each cell, left-to-right across each row then iterating rows according to `roworder`. + /// + ///XGap : Horizontal space between grid cells, expressed as a fraction of the total width available to one cell. Defaults to 0.1 for coupled-axes grids and 0.2 for independent grids. + /// + ///YGap : Vertical space between grid cells, expressed as a fraction of the total height available to one cell. Defaults to 0.1 for coupled-axes grids and 0.3 for independent grids. + /// + ///Domain : Sets the domains of this grid subplot (in plot fraction). The first and last cells end exactly at the domain edges, with no grout around the edges. + /// + ///XSide : Sets where the x axis labels and titles go. "bottom" means the very bottom of the grid. "bottom plot" is the lowest plot that each x axis is used in. "top" and "top plot" are similar. + /// + ///YSide : Sets where the y axis labels and titles go. "left" means the very left edge of the grid. "left plot" is the leftmost plot that each y axis is used in. "right" and "right plot" are similar. + static member style + ( + ?SubPlots : StyleParam.AxisId [] [], + ?XAxes : StyleParam.AxisId [], + ?YAxes : StyleParam.AxisId [], + ?Rows : int, + ?Columns : int, + ?RowOrder : StyleParam.LayoutGridRowOrder, + ?Pattern : StyleParam.LayoutGridPattern, + ?XGap : float, + ?YGap : float, + ?Domain : Domain, + ?XSide : StyleParam.LayoutGridXSide, + ?YSide : StyleParam.LayoutGridYSide + ) = + (fun (layoutGrid: LayoutGrid) -> + SubPlots |> DynObj.setValueOptBy layoutGrid "subplots" (Array.map (Array.map StyleParam.AxisId.toString)) + XAxes |> DynObj.setValueOptBy layoutGrid "xaxes" (Array.map StyleParam.AxisId.toString) + YAxes |> DynObj.setValueOptBy layoutGrid "yaxes" (Array.map StyleParam.AxisId.toString) + Rows |> DynObj.setValueOpt layoutGrid "rows" + Columns |> DynObj.setValueOpt layoutGrid "columns" + RowOrder |> DynObj.setValueOptBy layoutGrid "roworder" StyleParam.LayoutGridRowOrder.toString + Pattern |> DynObj.setValueOptBy layoutGrid "pattern" StyleParam.LayoutGridPattern.toString + XGap |> DynObj.setValueOpt layoutGrid "xgap" + YGap |> DynObj.setValueOpt layoutGrid "ygap" + Domain |> DynObj.setValueOpt layoutGrid "domain" + XSide |> DynObj.setValueOptBy layoutGrid "xside" StyleParam.LayoutGridXSide.toString + YSide |> DynObj.setValueOptBy layoutGrid "yside" StyleParam.LayoutGridYSide.toString + + layoutGrid + ) diff --git a/src/FSharp.Plotly/Playground.fsx b/src/FSharp.Plotly/Playground.fsx new file mode 100644 index 000000000..663863bad --- /dev/null +++ b/src/FSharp.Plotly/Playground.fsx @@ -0,0 +1,161 @@ +#load "StyleParams.fs" +#load "DynamicObj.fs" +#load "Colors.fs" +#load "Colorbar.fs" +#load "RangeSlider.fs" +#load "Light.fs" +#load "Contours.fs" +#load "Dimensions.fs" +#load "Domain.fs" +#load "Line.fs" +#load "Box.fs" +#load "Meanline.fs" +#load "Marker.fs" +#load "Font.fs" +#load "Hoverlabel.fs" +#load "Axis.fs" +#load "Bins.fs" +#load "Cumulative.fs" +#load "Scene.fs" +#load "Selected.fs" +#load "Shape.fs" +#load "Error.fs" +#load "Table.fs" +#load "Trace.fs" +#load "Trace3d.fs" +#load "LayoutGrid.fs" +#load "Layout.fs" +#load "Config.fs" +#r @"..\..\packages\Newtonsoft.Json\lib\netstandard2.0\Newtonsoft.Json.dll" +#load "GenericChart.fs" +#load "Chart.fs" +#load "ChartExtensions.fs" +#load "CandelstickExtension.fs" +#load "SankeyExtension.fs" +#load "Templates.fs" + +open FSharp.Plotly +open GenericChart + + +let grid ((gCharts:seq<#seq>),sharedXAxes:bool,sharedYAxes:bool) = + let nRows = Seq.length gCharts + let rowWidth = 1. / float nRows + + let nCols = gCharts |> Seq.maxBy Seq.length |> Seq.length + let colWidth = 1. / float nCols + + let xGap = 0.1 + let yGap = 0.1 + + let pattern = StyleParam.LayoutGridPattern.Independent + + let xSide = StyleParam.LayoutGridXSide.Bottom + let ySide = StyleParam.LayoutGridYSide.Left + + let grid = + LayoutGrid.init( + Rows=nRows,Columns=nCols,XGap= 0.05,YGap= 0.05 + ) + + let generateDomainRanges (count:int) (gap:float) = + [|0. .. (1. / (float count)) .. 1.|] + |> fun doms -> + doms + |> Array.windowed 2 + |> Array.mapi (fun i x -> + if i = 0 then + x.[0], (x.[1] - (gap / 2.)) + elif i = (doms.Length - 1) then + (x.[0] + (gap / 2.)),x.[1] + else + (x.[0] + (gap / 2.)) , (x.[1] - (gap / 2.)) + ) + + let yDomains = generateDomainRanges nRows yGap + let xDomains = generateDomainRanges nCols xGap + + gCharts + |> Seq.mapi (fun rowIndex row -> + row |> Seq.mapi (fun colIndex gChart -> + let xdomain = xDomains.[colIndex] + let ydomain = yDomains.[rowIndex] + + let newXIndex, newYIndex = + (if sharedXAxes then colIndex + 1 else ((nRows * rowIndex) + (colIndex + 1))), + (if sharedYAxes then rowIndex + 1 else ((nRows * rowIndex) + (colIndex + 1))) + + + let xaxis,yaxis,layout = + let layout = GenericChart.getLayout gChart + let xAxisName, yAxisName = StyleParam.AxisId.X 1 |> StyleParam.AxisId.toString, StyleParam.AxisId.Y 1 |> StyleParam.AxisId.toString + + let updateXAxis index domain axis = + axis |> Axis.LinearAxis.style(Anchor=StyleParam.AxisAnchorId.X index,Domain=StyleParam.Range.MinMax domain) + + let updateYAxis index domain axis = + axis |> Axis.LinearAxis.style(Anchor=StyleParam.AxisAnchorId.Y index,Domain=StyleParam.Range.MinMax domain) + match (layout.TryGetTypedValue xAxisName),(layout.TryGetTypedValue yAxisName) with + | Some x, Some y -> + // remove axis + DynObj.remove layout xAxisName + DynObj.remove layout yAxisName + + x |> updateXAxis newXIndex xdomain, + y |> updateYAxis newYIndex ydomain, + layout + + | Some x, None -> + // remove x - axis + DynObj.remove layout xAxisName + + x |> updateXAxis newXIndex xdomain, + Axis.LinearAxis.init(Anchor=StyleParam.AxisAnchorId.Y newYIndex ,Domain=StyleParam.Range.MinMax ydomain), + layout + + | None, Some y -> + // remove y - axis + DynObj.remove layout yAxisName + + Axis.LinearAxis.init(Anchor=StyleParam.AxisAnchorId.X newXIndex,Domain=StyleParam.Range.MinMax xdomain), + y |> updateYAxis newYIndex ydomain, + layout + | None, None -> + Axis.LinearAxis.init(Anchor=StyleParam.AxisAnchorId.X newXIndex,Domain=StyleParam.Range.MinMax xdomain), + Axis.LinearAxis.init(Anchor=StyleParam.AxisAnchorId.Y newYIndex,Domain=StyleParam.Range.MinMax ydomain), + layout + + gChart + |> GenericChart.setLayout layout + |> Chart.withAxisAnchor(X=newXIndex,Y=newYIndex) + |> Chart.withX_Axis(xaxis,newXIndex) + |> Chart.withY_Axis(yaxis,newYIndex) + ) + ) + |> Seq.map Chart.Combine + |> Chart.Combine + |> Chart.withLayoutGrid grid + + +grid ([ + [Chart.Point([(0,1)]);Chart.Point([(1,1)])] + [Chart.Point([(0,1)]);Chart.Point([(1,1)])] +],false,true) +|> Chart.Show + + +let generateDomainRanges nRows nCols = + + if nCols > 0 && nRows > 0 then + + [0. .. (1. / (float nRows)) .. 1.] + |> List.windowed 2 + |> List.map (fun x -> x.[0], x.[1]) + , + [0. .. (1. / (float nCols)) .. 1.] + |> List.windowed 2 + |> List.map (fun x -> x.[0], x.[1]) + + else failwith "negative amount of rows or columns is stupid." + +generateDomainRanges 8 1 \ No newline at end of file diff --git a/src/FSharp.Plotly/StyleParams.fs b/src/FSharp.Plotly/StyleParams.fs index be9e3f90c..4847bedf3 100644 --- a/src/FSharp.Plotly/StyleParams.fs +++ b/src/FSharp.Plotly/StyleParams.fs @@ -485,6 +485,70 @@ module StyleParam = static member convert = LocationFormat.toString >> box + + /// Determines wether the rows of a LayoutGrid are enumerated from the top or the bottom. + [] + type LayoutGridRowOrder = + |TopToBottom + |BottomToTop + + static member toString = function + |TopToBottom -> "top to bottom" + |BottomToTop -> "bottom to top" + + static member convert = + LayoutGridRowOrder.toString >> box + + /// Pattern to use for autogenerating Axis Ids when not specifically specifying subplot axes IDs in LayoutGrids + [] + type LayoutGridPattern = + /// Uses a new xy pair for each cell, left-to-right across each row then iterating rows according to `roworder` + | Independent + /// Gives one x axis per column and one y axis per row + | Coupled + + static member toString = function + | Independent -> "independent" + | Coupled -> "coupled" + + static member convert = + LayoutGridPattern.toString >> box + + + /// Sets where the x axis labels and titles go on a layout grid. + [] + type LayoutGridXSide = + | Bottom + | BottomPlot + | Top + | TopPlot + + static member toString = function + | Bottom -> "bottom" + | BottomPlot -> "bottom plot" + | Top -> "top" + | TopPlot -> "top plot" + + static member convert = LayoutGridXSide.toString >> box + + /// Sets where the y axis labels and titles go on a layout grid. + [] + type LayoutGridYSide = + | Left + | LeftPlot + | Right + | RightPlot + + static member toString = function + | Left -> "left" + | LeftPlot -> "left plot" + | Right -> "right" + | RightPlot -> "right plot" + + static member convert = LayoutGridYSide.toString >> box + + + //-------------------------- // #M# //-------------------------- @@ -627,6 +691,8 @@ module StyleParam = | MinMax (min,max) -> box [|min;max|] | Values arr -> box arr + + //-------------------------- // #S# //-------------------------- From 0a661560f77c0aa8fc0bbf5ae9760e75beee9915 Mon Sep 17 00:00:00 2001 From: Kevin Schneider Date: Mon, 8 Jun 2020 23:49:54 +0200 Subject: [PATCH 2/2] Add Chart.Grid and Chart.SingleStack --- src/FSharp.Plotly/ChartExtensions.fs | 125 +++++++++++++++++++++++---- src/FSharp.Plotly/Layout.fs | 10 +++ src/FSharp.Plotly/Playground.fsx | 81 +++++++++++++---- 3 files changed, 180 insertions(+), 36 deletions(-) diff --git a/src/FSharp.Plotly/ChartExtensions.fs b/src/FSharp.Plotly/ChartExtensions.fs index ea59ded5d..7e62cf26c 100644 --- a/src/FSharp.Plotly/ChartExtensions.fs +++ b/src/FSharp.Plotly/ChartExtensions.fs @@ -352,6 +352,46 @@ module ChartExtensions = |> Layout.SetLayoutGrid layoutGrid GenericChart.setLayout layout ch) + // Set the LayoutGrid options of a Chart + [] + static member withLayoutGridStyle([]?SubPlots : StyleParam.AxisId [] [], + []?XAxes : StyleParam.AxisId [], + []?YAxes : StyleParam.AxisId [], + []?Rows : int, + []?Columns : int, + []?RowOrder : StyleParam.LayoutGridRowOrder, + []?Pattern : StyleParam.LayoutGridPattern, + []?XGap : float, + []?YGap : float, + []?Domain : Domain, + []?XSide : StyleParam.LayoutGridXSide, + []?YSide : StyleParam.LayoutGridYSide + ) = + (fun (ch:GenericChart) -> + let layout = GenericChart.getLayout ch + let updatedGrid = + let currentGrid = + match layout.TryGetTypedValue "grid" with + | Some grid -> grid + | None -> LayoutGrid() + currentGrid + |> LayoutGrid.style( + ?SubPlots = SubPlots, + ?XAxes = XAxes , + ?YAxes = YAxes , + ?Rows = Rows , + ?Columns = Columns , + ?RowOrder = RowOrder, + ?Pattern = Pattern , + ?XGap = XGap , + ?YGap = YGap , + ?Domain = Domain , + ?XSide = XSide , + ?YSide = YSide + ) + let updatedLayout = layout |> Layout.SetLayoutGrid updatedGrid + GenericChart.setLayout updatedLayout ch) + [] static member withConfig (config:Config) = (fun (ch:GenericChart) -> @@ -450,41 +490,71 @@ module ChartExtensions = static member Combine(gCharts:seq) = GenericChart.combine gCharts - + ///Creates a Grid containing the given plots as subplots with the dimensions of the input (amount of columns equal to the largest inner sequence). + /// + ///Parameters: + /// + ///sharedAxes : Wether the subplots share one xAxis per column and one yAxis per row or not. (default:TopToBottom) + /// + ///rowOrder : the order in which the rows of the grid will be rendered (default:false) + /// + ///xGap : The space between columns of the grid relative to the x dimension of the grid + /// + ///yGap : The space between rows of the grid relative to the y dimension of the grid + /// + ///Use Chart.withLayoutGridStyle to further style the grid object contained in the returned chart. [] static member Grid ((gCharts:seq<#seq>), - sharedAxes:bool + []?sharedAxes:bool, + []?rowOrder:StyleParam.LayoutGridRowOrder, + [] ?xGap, + [] ?yGap ) = - let nRows = Seq.length gCharts - let rowWidth = 1. / float nRows + let sharedAxes = defaultArg sharedAxes false + let rowOrder = defaultArg rowOrder StyleParam.LayoutGridRowOrder.TopToBottom + let xGap = defaultArg xGap 0.05 + let yGap = defaultArg yGap 0.05 + let nRows = Seq.length gCharts let nCols = gCharts |> Seq.maxBy Seq.length |> Seq.length - let colWidth = 1. / float nCols - let pattern = if sharedAxes then StyleParam.LayoutGridPattern.Coupled else StyleParam.LayoutGridPattern.Independent - let grid = - LayoutGrid.init( - Rows=nRows,Columns=nCols,Pattern=pattern - ) + + let generateDomainRanges (count:int) (gap:float) = + [|0. .. (1. / (float count)) .. 1.|] + |> fun doms -> + doms + |> Array.windowed 2 + |> Array.mapi (fun i x -> + if i = 0 then + x.[0], (x.[1] - (gap / 2.)) + elif i = (doms.Length - 1) then + (x.[0] + (gap / 2.)),x.[1] + else + (x.[0] + (gap / 2.)) , (x.[1] - (gap / 2.)) + ) + + let yDomains = generateDomainRanges nRows yGap + let xDomains = generateDomainRanges nCols xGap + gCharts |> Seq.mapi (fun rowIndex row -> row |> Seq.mapi (fun colIndex gChart -> - let xdomain = (colWidth * float (colIndex-1), (colWidth * float colIndex)) - let ydomain = (1. - ((rowWidth * float rowIndex)),1. - (rowWidth * float (rowIndex-1))) + let xdomain = xDomains.[colIndex] + let ydomain = yDomains.[rowIndex] let newXIndex, newYIndex = - (if sharedAxes then colIndex + 1 else (rowIndex + colIndex + 1)), - (if sharedAxes then rowIndex + 1 else (rowIndex + colIndex + 1)) + (if sharedAxes then colIndex + 1 else ((nRows * rowIndex) + (colIndex + 1))), + (if sharedAxes then rowIndex + 1 else ((nRows * rowIndex) + (colIndex + 1))) let xaxis,yaxis,layout = let layout = GenericChart.getLayout gChart let xAxisName, yAxisName = StyleParam.AxisId.X 1 |> StyleParam.AxisId.toString, StyleParam.AxisId.Y 1 |> StyleParam.AxisId.toString - + let updateXAxis index domain axis = axis |> Axis.LinearAxis.style(Anchor=StyleParam.AxisAnchorId.X index,Domain=StyleParam.Range.MinMax domain) - + let updateYAxis index domain axis = axis |> Axis.LinearAxis.style(Anchor=StyleParam.AxisAnchorId.Y index,Domain=StyleParam.Range.MinMax domain) match (layout.TryGetTypedValue xAxisName),(layout.TryGetTypedValue yAxisName) with @@ -526,9 +596,30 @@ module ChartExtensions = ) |> Seq.map Chart.Combine |> Chart.Combine - |> Chart.withLayoutGrid grid + |> Chart.withLayoutGrid( + LayoutGrid.init( + Rows=nRows,Columns=nCols,XGap= xGap,YGap= yGap,Pattern=pattern,RowOrder=rowOrder + ) + ) + + ///Creates a chart stack from the input charts by stacking them on top of each other starting from the first chart. + /// + ///Parameters: + /// + ///sharedAxis : wether the stack has a shared x axis (default:true) + [] + static member SingleStack (charts:#seq, + [] ?sharedXAxis:bool) = + + let sharedAxis = defaultArg sharedXAxis true + let singleCol = seq { + for i = 0 to ((Seq.length charts) - 1) do + yield seq {Seq.item i charts} + } + Chart.Grid(gCharts = singleCol, sharedAxes = sharedAxis, rowOrder = StyleParam.LayoutGridRowOrder.BottomToTop) /// Create a combined chart with the given charts merged + [] [] static member Stack ( [] ?Columns:int, [] ?Space) = diff --git a/src/FSharp.Plotly/Layout.fs b/src/FSharp.Plotly/Layout.fs index cc9a2bf4a..9b29a9b8e 100644 --- a/src/FSharp.Plotly/Layout.fs +++ b/src/FSharp.Plotly/Layout.fs @@ -397,4 +397,14 @@ type Layout() = layout ) + + static member GetLayoutGrid + ( + grid: LayoutGrid + ) = + (fun (layout:Layout) -> + grid |> DynObj.setValue layout "grid" + layout + ) + diff --git a/src/FSharp.Plotly/Playground.fsx b/src/FSharp.Plotly/Playground.fsx index 663863bad..b83cf7eb4 100644 --- a/src/FSharp.Plotly/Playground.fsx +++ b/src/FSharp.Plotly/Playground.fsx @@ -38,24 +38,15 @@ open FSharp.Plotly open GenericChart -let grid ((gCharts:seq<#seq>),sharedXAxes:bool,sharedYAxes:bool) = - let nRows = Seq.length gCharts - let rowWidth = 1. / float nRows +let grid ((gCharts:seq<#seq>),sharedAxes:bool,xGap,yGap) = + let nRows = Seq.length gCharts let nCols = gCharts |> Seq.maxBy Seq.length |> Seq.length - let colWidth = 1. / float nCols - - let xGap = 0.1 - let yGap = 0.1 - - let pattern = StyleParam.LayoutGridPattern.Independent - - let xSide = StyleParam.LayoutGridXSide.Bottom - let ySide = StyleParam.LayoutGridYSide.Left + let pattern = if sharedAxes then StyleParam.LayoutGridPattern.Coupled else StyleParam.LayoutGridPattern.Independent let grid = LayoutGrid.init( - Rows=nRows,Columns=nCols,XGap= 0.05,YGap= 0.05 + Rows=nRows,Columns=nCols,XGap= xGap,YGap= yGap,Pattern=pattern ) let generateDomainRanges (count:int) (gap:float) = @@ -82,8 +73,8 @@ let grid ((gCharts:seq<#seq>),sharedXAxes:bool,sharedYAxes:bool) = let ydomain = yDomains.[rowIndex] let newXIndex, newYIndex = - (if sharedXAxes then colIndex + 1 else ((nRows * rowIndex) + (colIndex + 1))), - (if sharedYAxes then rowIndex + 1 else ((nRows * rowIndex) + (colIndex + 1))) + (if sharedAxes then colIndex + 1 else ((nRows * rowIndex) + (colIndex + 1))), + (if sharedAxes then rowIndex + 1 else ((nRows * rowIndex) + (colIndex + 1))) let xaxis,yaxis,layout = @@ -138,11 +129,47 @@ let grid ((gCharts:seq<#seq>),sharedXAxes:bool,sharedYAxes:bool) = grid ([ - [Chart.Point([(0,1)]);Chart.Point([(1,1)])] - [Chart.Point([(0,1)]);Chart.Point([(1,1)])] -],false,true) + [Chart.Point([(0,1)]);Chart.Point([(0,1)]);Chart.Point([(0,1)]);] + [Chart.Point([(0,1)]);Chart.Point([(0,1)]);Chart.Point([(0,1)]);] + [Chart.Point([(0,1)]);Chart.Point([(0,1)]);Chart.Point([(0,1)]);] +],true, 0.05,0.05) |> Chart.Show +let stack ( columns:int, space) = + (fun (charts:#seq) -> + + let col = columns + let len = charts |> Seq.length + let colWidth = 1. / float col + let rowWidth = + let tmp = float len / float col |> ceil + 1. / tmp + let space = + let s = defaultArg space 0.05 + if s < 0. || s > 1. then + printfn "Space should be between 0.0 - 1.0. Automaticaly set to default (0.05)" + 0.05 + else + s + + let contains3d ch = + ch + |> existsTrace (fun t -> + match t with + | :? Trace3d -> true + | _ -> false) + + charts + |> Seq.mapi (fun i ch -> + let colI,rowI,index = (i%col+1), (i/col+1),(i+1) + let xdomain = (colWidth * float (colI-1), (colWidth * float colI) - space ) + let ydomain = (1. - ((rowWidth * float rowI) - space ),1. - (rowWidth * float (rowI-1))) + xdomain) + ) + +let a = + stack (2, None) [Chart.Point([0,1]);Chart.Point([0,1])] + |> Array.ofSeq let generateDomainRanges nRows nCols = @@ -158,4 +185,20 @@ let generateDomainRanges nRows nCols = else failwith "negative amount of rows or columns is stupid." -generateDomainRanges 8 1 \ No newline at end of file +generateDomainRanges 8 1 + + + + +[ + Chart.Point([(0,1)]) |> Chart.withY_AxisStyle("This title") + Chart.Point([(0,1)]) + |> Chart.withY_AxisStyle("Must be set",Zeroline=false) + Chart.Point([(0,1)]) + |> Chart.withY_AxisStyle("on the respective charts",Zeroline=false) +] +|> Chart.SingleStack +|> Chart.withLayoutGridStyle(XSide=StyleParam.LayoutGridXSide.Bottom) +|> Chart.withTitle("Hi i am the new SingleStackChart") +|> Chart.withX_AxisStyle("im the shared xAxis") +|> Chart.Show \ No newline at end of file