diff --git a/src/Plotly.NET.CSharp/ChartAPI/Chart.cs b/src/Plotly.NET.CSharp/ChartAPI/Chart.cs index 0ce232c5..13db2dac 100644 --- a/src/Plotly.NET.CSharp/ChartAPI/Chart.cs +++ b/src/Plotly.NET.CSharp/ChartAPI/Chart.cs @@ -23,6 +23,9 @@ public static partial class Chart /// The charts to display on the grid. /// 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. /// 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. + /// A collection of titles for the individual subplots. + /// The font of the subplot titles + /// A vertical offset applied to each subplot title, moving it upwards if positive and vice versa /// 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. /// 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. /// 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. @@ -37,6 +40,9 @@ public static GenericChart Grid( IEnumerable gCharts, int nRows, int nCols, + Optional> SubPlotTitles = default, + Optional SubPlotTitleFont = default, + Optional SubPlotTitleOffset = default, Optional[][]> SubPlots = default, Optional XAxes = default, Optional YAxes = default, @@ -48,9 +54,12 @@ public static GenericChart Grid( Optional XSide = default, Optional YSide = default ) => - Plotly.NET.Chart.Grid>( + Plotly.NET.Chart.Grid,IEnumerable>( nRows: nRows, nCols: nCols, + SubPlotTitles: SubPlotTitles.ToOption(), + SubPlotTitleFont: SubPlotTitleFont.ToOption(), + SubPlotTitleOffset: SubPlotTitleOffset.ToOption(), SubPlots: SubPlots.ToOption(), XAxes: XAxes.ToOption(), YAxes: YAxes.ToOption(), diff --git a/src/Plotly.NET/ChartAPI/Chart.fs b/src/Plotly.NET/ChartAPI/Chart.fs index 5d302a7a..151c896e 100644 --- a/src/Plotly.NET/ChartAPI/Chart.fs +++ b/src/Plotly.NET/ChartAPI/Chart.fs @@ -1128,7 +1128,7 @@ type Chart = match sceneAxisId with | StyleParam.SubPlotId.XAxis _ -> scene |> Scene.getXAxis | StyleParam.SubPlotId.YAxis _ -> scene |> Scene.getYAxis - | StyleParam.SubPlotId.ZAxis _ -> scene |> Scene.getZAxis + | StyleParam.SubPlotId.ZAxis -> scene |> Scene.getZAxis | _ -> failwith "invalid scene axis id" let updatedAxis = @@ -1140,7 +1140,7 @@ type Chart = match sceneAxisId with | StyleParam.SubPlotId.XAxis _ -> s |> Scene.setXAxis axis | StyleParam.SubPlotId.YAxis _ -> s |> Scene.setYAxis axis - | StyleParam.SubPlotId.ZAxis _ -> s |> Scene.setZAxis axis + | StyleParam.SubPlotId.ZAxis -> s |> Scene.setZAxis axis | _ -> failwith "invalid scene axis id" layout |> Layout.updateSceneById (id, updatedScene) @@ -1153,7 +1153,7 @@ type Chart = match sceneAxisId with | StyleParam.SubPlotId.XAxis _ -> s |> Scene.setXAxis axis | StyleParam.SubPlotId.YAxis _ -> s |> Scene.setYAxis axis - | StyleParam.SubPlotId.ZAxis _ -> s |> Scene.setZAxis axis + | StyleParam.SubPlotId.ZAxis -> s |> Scene.setZAxis axis | _ -> failwith "invalid scene axis id" layout |> Layout.updateSceneById (id, updatedScene)) @@ -3072,18 +3072,32 @@ type Chart = ch |> Chart.withConfig config) - - //============================================================================================================== //================================= More complicated composite methods ========================================= //============================================================================================================== /// - /// Creates a subplot grid with the given dimensions (nRows x nCols) for the input charts. + /// Creates a subplot grid with the given dimensions (nRows x nCols) for the input charts. The default row order is from top to bottom. + /// + /// For each input chart, a corresponding subplot cell is created in the grid. The following limitations apply to the individual grid cells: + /// + /// - only one pair of 2D cartesian axes is allowed per cell. If there are multiple x or y axes on an input chart, the first one is used, and the rest is discarded (meaning, it is removed from the combined layout). + /// if you need multiple axes per grid cell, create a custom grid by manually creating axes with custom domains instead. + /// The new id of the axes corresponds to the number of the grid cell, e.g. the third grid cell will contain xaxis3 and yaxis3 + /// + /// - For other subplot layouts (Cartesian3D, Polar, Ternary, Geo, Mapbox, Smith), the same rule applies: only one subplot per grid cell, the first one is used, the rest is discarded. + /// The new id of the subplot layout corresponds to the number of the grid cell, e.g. the third grid cell will contain scene3 etc. + /// + /// - The Domain of traces that calculate their position by domain only (e.g. Pie traces) are replaced by a domain pointing to the new grid position. + /// + /// - If SubPlotTitles are provided, they are used as the titles of the individual cells in ascending order. If the number of titles is less than the number of subplots, the remaining subplots are left without a title. /// /// 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. /// 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. + /// A collection of titles for the individual subplots. + /// The font of the subplot titles + /// A vertical offset applied to each subplot title, moving it upwards if positive and vice versa /// 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. /// 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. /// 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. @@ -3099,6 +3113,9 @@ type Chart = ( nRows: int, nCols: int, + [] ?SubPlotTitles: #seq, + [] ?SubPlotTitleFont: Font, + [] ?SubPlotTitleOffset: float, [] ?SubPlots: (StyleParam.LinearAxisId * StyleParam.LinearAxisId)[][], [] ?XAxes: StyleParam.LinearAxisId[], [] ?YAxes: StyleParam.LinearAxisId[], @@ -3112,12 +3129,76 @@ type Chart = ) = fun (gCharts: #seq) -> + // calculates the grid cell dimensions (in fractions of paper size), that is, the start and end points of each cell in a row or column + let getGridCellDimensions (gridDimensionStart: float) (gridDimensionEnd: float) (gap: float) (length: int) (reversed: bool) = + // adapted from grid cell layout logic directly in plotly.js source code: https://github.com/plotly/plotly.js/blob/5d6d45758f485ca309691bc7f33e799ef80f2cd5/src/components/grid/index.js#L224-L238 + + let step = (gridDimensionEnd - gridDimensionStart) / (float length - gap) + let cellDomain = step * (1. - gap) + + Array.init length (fun i -> + let cellStart = gridDimensionStart + (step * float i) + (cellStart, cellStart + cellDomain) + ) + |> fun p -> if reversed then p else Array.rev p + + // calculates the positions of the subplot titles + // titles are placed in the middle of the top edge of each cell in a layout grid as annotations with paper copordinates. + let calculateSubplotTitlePositions (gridDimensionStart: float) (gridDimensionEnd: float) (xgap: float) (ygap: float) (nrows: int) (ncols: int) (reversed:bool) = + + let subPlotTitleOffset = defaultArg SubPlotTitleOffset 0. + + let xDomains = getGridCellDimensions gridDimensionStart gridDimensionEnd xgap ncols true + let yDomains = getGridCellDimensions gridDimensionStart gridDimensionEnd ygap nrows reversed + + Array.init nrows (fun r -> + Array.init ncols (fun c -> + let xStart = fst xDomains.[c] + let xEnd = snd xDomains.[c] + let yEnd = snd yDomains.[r] + (r,c), ((xStart + xEnd) / 2., yEnd + subPlotTitleOffset) + ) + ) + |> Array.concat + let pattern = defaultArg Pattern StyleParam.LayoutGridPattern.Independent + let rowOrder = defaultArg RowOrder StyleParam.LayoutGridRowOrder.TopToBottom + + let xGap = defaultArg XGap (if pattern = StyleParam.LayoutGridPattern.Coupled then 0.1 else 0.2) + let yGap = defaultArg YGap (if pattern = StyleParam.LayoutGridPattern.Coupled then 0.1 else 0.3) + + let hasSharedAxes = pattern = StyleParam.LayoutGridPattern.Coupled + let subPlotTitleAnnotations = + match SubPlotTitles with + | Some titles -> + + let reversed = rowOrder = StyleParam.LayoutGridRowOrder.BottomToTop + + let positions = + calculateSubplotTitlePositions 0. 1. xGap yGap nRows nCols reversed + + titles + |> Seq.zip positions[0 .. (Seq.length titles) - 1] + |> Seq.map (fun (((rowIndex, colIndex), (x, y)), title) -> + Annotation.init( + X = x, + XRef = "paper", + XAnchor = StyleParam.XAnchorPosition.Center, + Y = y, + YRef = "paper", + YAnchor = StyleParam.YAnchorPosition.Bottom, + Text = title, + ShowArrow = false, + ?Font = SubPlotTitleFont + ) + ) + | None -> [||] + // rows x cols coordinate grid let gridCoordinates = Array.init nRows (fun rowIndex -> Array.init nCols (fun colIndex -> rowIndex + 1, colIndex + 1)) @@ -3143,6 +3224,13 @@ type Chart = let yAxis = layout.TryGetTypedValue "yaxis" |> Option.defaultValue (LinearAxis.init ()) + let allXAxes = Layout.getXAxes layout |> Seq.map fst + let allYAxes = Layout.getYAxes layout |> Seq.map fst + + // remove all axes from layout. Only cartesian axis in each dimension is supported per grid cell, and leaving anything else on this layout may lead to property name clashes on combine. + allXAxes |> Seq.iter (fun propName -> layout.Remove(propName) |> ignore) + allYAxes |> Seq.iter (fun propName -> layout.Remove(propName) |> ignore) + let xAnchor, yAnchor = if hasSharedAxes then colIndex, rowIndex //set axis anchors according to grid coordinates @@ -3153,13 +3241,7 @@ type Chart = |> Chart.withAxisAnchor (xAnchor, yAnchor) // set adapted axis anchors |> Chart.withXAxis (xAxis, (StyleParam.SubPlotId.XAxis(i + 1))) // set previous axis with adapted id (one individual axis for each subplot, whether or not they will be used later) |> Chart.withYAxis (yAxis, (StyleParam.SubPlotId.YAxis(i + 1))) // set previous axis with adapted id (one individual axis for each subplot, whether or not they will be used later) - |> GenericChart.mapLayout (fun l -> - if i > 0 then - // remove default axes from consecutive charts, otherwise they will override the first one - l.Remove("xaxis") |> ignore - l.Remove("yaxis") |> ignore - l) | TraceID.Cartesian3D -> let scene = @@ -3167,6 +3249,11 @@ type Chart = |> Option.defaultValue (Scene.init ()) |> Scene.style (Domain = LayoutObjects.Domain.init (Row = rowIndex - 1, Column = colIndex - 1)) + let allScenes = Layout.getScenes layout |> Seq.map fst + + // remove all scenes from layout. Only one scene is supported per grid cell, and leaving anything else on this layout may lead to property name clashes on combine. + allScenes |> Seq.iter (fun propName -> layout.Remove(propName) |> ignore) + let sceneAnchor = StyleParam.SubPlotId.Scene(i + 1) @@ -3180,6 +3267,11 @@ type Chart = |> Option.defaultValue (Polar.init ()) |> Polar.style (Domain = LayoutObjects.Domain.init (Row = rowIndex - 1, Column = colIndex - 1)) + let allPolars = Layout.getPolars layout |> Seq.map fst + + // remove all polar subplots from layout. Only one polar subplot is supported per grid cell, and leaving anything else on this layout may lead to property name clashes on combine. + allPolars |> Seq.iter (fun propName -> layout.Remove(propName) |> ignore) + let polarAnchor = StyleParam.SubPlotId.Polar(i + 1) @@ -3194,6 +3286,11 @@ type Chart = layout.TryGetTypedValue "smith" |> Option.defaultValue (Smith.init ()) |> Smith.style (Domain = LayoutObjects.Domain.init (Row = rowIndex - 1, Column = colIndex - 1)) + + let allSmiths = Layout.getSmiths layout |> Seq.map fst + + // remove all smith subplots from layout. Only one smith subplot is supported per grid cell, and leaving anything else on this layout may lead to property name clashes on combine. + allSmiths |> Seq.iter (fun propName -> layout.Remove(propName) |> ignore) let polarAnchor = StyleParam.SubPlotId.Smith(i + 1) @@ -3209,12 +3306,18 @@ type Chart = |> Option.defaultValue (Geo.init ()) |> Geo.style (Domain = LayoutObjects.Domain.init (Row = rowIndex - 1, Column = colIndex - 1)) + let allGeos = Layout.getGeos layout |> Seq.map fst + + // remove all geo subplots from layout. Only one geo subplot is supported per grid cell, and leaving anything else on this layout may lead to property name clashes on combine. + allGeos |> Seq.iter (fun propName -> layout.Remove(propName) |> ignore) + let geoAnchor = StyleParam.SubPlotId.Geo(i + 1) gChart |> GenericChart.mapTrace (fun t -> t :?> TraceGeo |> TraceGeoStyle.SetGeo geoAnchor :> Trace) |> Chart.withGeo (geo, (i + 1)) + | TraceID.Mapbox -> let mapbox = layout.TryGetTypedValue "mapbox" @@ -3223,6 +3326,14 @@ type Chart = Domain = LayoutObjects.Domain.init (Row = rowIndex - 1, Column = colIndex - 1) ) + let allMapboxes = Layout.getMapboxes layout |> Seq.map fst + + // remove all mapbox subplots from layout. Only one mapbox subplot is supported per grid cell, and leaving anything else on this layout may lead to property name clashes on combine. + allMapboxes |> Seq.iter (fun propName -> layout.Remove(propName) |> ignore) + + let geoAnchor = + StyleParam.SubPlotId.Geo(i + 1) + let mapboxAnchor = StyleParam.SubPlotId.Mapbox(i + 1) @@ -3230,13 +3341,6 @@ type Chart = |> GenericChart.mapTrace (fun t -> t :?> TraceMapbox |> TraceMapboxStyle.SetMapbox mapboxAnchor :> Trace) |> Chart.withMapbox (mapbox, (i + 1)) - | TraceID.Domain -> - let newDomain = - LayoutObjects.Domain.init (Row = rowIndex - 1, Column = colIndex - 1) - - gChart - |> GenericChart.mapTrace (fun t -> - t :?> TraceDomain |> TraceDomainStyle.SetDomain newDomain :> Trace) | TraceID.Ternary -> @@ -3247,23 +3351,40 @@ type Chart = Domain = LayoutObjects.Domain.init (Row = rowIndex - 1, Column = colIndex - 1) ) + let allTernaries = Layout.getTernaries layout |> Seq.map fst + + // remove all ternary subplots from layout. Only one ternary subplot is supported per grid cell, and leaving anything else on this layout may lead to property name clashes on combine. + allTernaries |> Seq.iter (fun propName -> layout.Remove(propName) |> ignore) + let ternaryAnchor = StyleParam.SubPlotId.Ternary(i + 1) gChart |> GenericChart.mapTrace (fun t -> t :?> TraceTernary |> TraceTernaryStyle.SetTernary ternaryAnchor :> Trace) - |> Chart.withTernary (ternary, (i + 1))) + |> Chart.withTernary (ternary, (i + 1)) + + | TraceID.Domain -> + + // no need to remove existing domains, as only one domain can exist on the original layout. Just replace it. + let newDomain = + LayoutObjects.Domain.init (Row = rowIndex - 1, Column = colIndex - 1) + + gChart + |> GenericChart.mapTrace (fun t -> + t :?> TraceDomain |> TraceDomainStyle.SetDomain newDomain :> Trace) + ) |> Chart.combine + |> Chart.withAnnotations(subPlotTitleAnnotations, Append=true) |> Chart.withLayoutGrid ( LayoutGrid.init ( Rows = nRows, Columns = nCols, Pattern = pattern, + RowOrder = rowOrder, ?SubPlots = SubPlots, ?XAxes = XAxes, ?YAxes = YAxes, - ?RowOrder = RowOrder, ?XGap = XGap, ?YGap = YGap, ?Domain = Domain, @@ -3278,7 +3399,23 @@ type Chart = /// ATTENTION: when the individual rows do not have the same amount of charts, they will be filled with dummy charts TO THE RIGHT. /// /// prevent this behaviour by using Chart.Invisible at the cells that should be empty. + /// + /// For each input chart, a corresponding subplot cell is created in the grid. The following limitations apply to the individual grid cells: + /// + /// - only one pair of 2D cartesian axes is allowed per cell. If there are multiple x or y axes on an input chart, the first one is used, and the rest is discarded (meaning, it is removed from the combined layout). + /// if you need multiple axes per grid cell, create a custom grid by manually creating axes with custom domains instead. + /// The new id of the axes corresponds to the number of the grid cell, e.g. the third grid cell will contain xaxis3 and yaxis3 + /// + /// - For other subplot layouts (Cartesian3D, Polar, Ternary, Geo, Mapbox, Smith), the same rule applies: only one subplot per grid cell, the first one is used, the rest is discarded. + /// The new id of the subplot layout corresponds to the number of the grid cell, e.g. the third grid cell will contain scene3 etc. + /// + /// - The Domain of traces that calculate their position by domain only (e.g. Pie traces) are replaced by a domain pointing to the new grid position. + /// + /// - If SubPlotTitles are provided, they are used as the titles of the individual cells in ascending order. If the number of titles is less than the number of subplots, the remaining subplots are left without a title. /// + /// A collection of titles for the individual subplots. + /// The font of the subplot titles + /// A vertical offset applied to each subplot title, moving it upwards if positive and vice versa /// 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. /// 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. /// 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. @@ -3292,6 +3429,9 @@ type Chart = [] static member Grid ( + [] ?SubPlotTitles: #seq, + [] ?SubPlotTitleFont: Font, + [] ?SubPlotTitleOffset: float, [] ?SubPlots: (StyleParam.LinearAxisId * StyleParam.LinearAxisId)[][], [] ?XAxes: StyleParam.LinearAxisId[], [] ?YAxes: StyleParam.LinearAxisId[], @@ -3340,6 +3480,9 @@ type Chart = |> Chart.Grid( nRows, nCols, + ?SubPlotTitles = SubPlotTitles, + ?SubPlotTitleFont = SubPlotTitleFont, + ?SubPlotTitleOffset = SubPlotTitleOffset, ?SubPlots = SubPlots, ?XAxes = XAxes, ?YAxes = YAxes, @@ -3357,6 +3500,9 @@ type Chart = |> Chart.Grid( nRows, nCols, + ?SubPlotTitles = SubPlotTitles, + ?SubPlotTitleFont = SubPlotTitleFont, + ?SubPlotTitleOffset = SubPlotTitleOffset, ?SubPlots = SubPlots, ?XAxes = XAxes, ?YAxes = YAxes, @@ -3370,7 +3516,23 @@ type Chart = ) /// Creates a chart stack (a subplot grid with one column) from the input charts. + /// + /// For each input chart, a corresponding subplot cell is created in the column. The following limitations apply to the individual grid cells: + /// + /// - only one pair of 2D cartesian axes is allowed per cell. If there are multiple x or y axes on an input chart, the first one is used, and the rest is discarded (meaning, it is removed from the combined layout). + /// if you need multiple axes per grid cell, create a custom grid by manually creating axes with custom domains instead. + /// The new id of the axes corresponds to the number of the grid cell, e.g. the third grid cell will contain xaxis3 and yaxis3 + /// + /// - For other subplot layouts (Cartesian3D, Polar, Ternary, Geo, Mapbox, Smith), the same rule applies: only one subplot per grid cell, the first one is used, the rest is discarded. + /// The new id of the subplot layout corresponds to the number of the grid cell, e.g. the third grid cell will contain scene3 etc. + /// + /// - The Domain of traces that calculate their position by domain only (e.g. Pie traces) are replaced by a domain pointing to the new grid position. + /// + /// - If SubPlotTitles are provided, they are used as the titles of the individual cells in ascending order. If the number of titles is less than the number of subplots, the remaining subplots are left without a title. /// + /// A collection of titles for the individual subplots. + /// The font of the subplot titles + /// A vertical offset applied to each subplot title, moving it upwards if positive and vice versa /// 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. /// 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. /// 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. @@ -3384,6 +3546,9 @@ type Chart = [] static member SingleStack ( + [] ?SubPlotTitles: #seq, + [] ?SubPlotTitleFont: Font, + [] ?SubPlotTitleOffset: float, [] ?SubPlots: (StyleParam.LinearAxisId * StyleParam.LinearAxisId)[][], [] ?XAxes: StyleParam.LinearAxisId[], [] ?YAxes: StyleParam.LinearAxisId[], @@ -3402,6 +3567,9 @@ type Chart = |> Chart.Grid( nRows = Seq.length gCharts, nCols = 1, + ?SubPlotTitles = SubPlotTitles, + ?SubPlotTitleFont = SubPlotTitleFont, + ?SubPlotTitleOffset = SubPlotTitleOffset, ?SubPlots = SubPlots, ?XAxes = XAxes, ?YAxes = YAxes, diff --git a/src/Plotly.NET/CommonAbstractions/StyleParams.fs b/src/Plotly.NET/CommonAbstractions/StyleParams.fs index 82fa791a..0afd9be9 100644 --- a/src/Plotly.NET/CommonAbstractions/StyleParams.fs +++ b/src/Plotly.NET/CommonAbstractions/StyleParams.fs @@ -1,6 +1,8 @@ namespace Plotly.NET open System +open System.Text +open System.Text.RegularExpressions // https://plot.ly/javascript/reference/ // https://plot.ly/javascript-graphing-library/reference/ @@ -284,10 +286,24 @@ module StyleParam = else sprintf "legend%i" id + static member isValidXAxisId (id: string) = Regex.IsMatch(id, "xaxis[0-9]*") + static member isValidYAxisId (id: string) = Regex.IsMatch(id, "yaxis[0-9]*") + static member isValidZAxisId (id: string) = Regex.IsMatch(id, "zaxis") + static member isValidColorAxisId (id: string) = Regex.IsMatch(id, "coloraxis[0-9]*") + static member isValidGeoId (id: string) = Regex.IsMatch(id, "geo[0-9]*") + static member isValidMapboxId (id: string) = Regex.IsMatch(id, "mapbox[0-9]*") + static member isValidPolarId (id: string) = Regex.IsMatch(id, "polar[0-9]*") + static member isValidTernaryId (id: string) = Regex.IsMatch(id, "ternary[0-9]*") + static member isValidSceneId (id: string) = Regex.IsMatch(id, "scene[0-9]*") + static member isValidSmithId (id: string) = Regex.IsMatch(id, "smith[0-9]*") + static member isValidLegendId (id: string) = Regex.IsMatch(id, "legend[0-9]*") + static member convert = SubPlotId.toString >> box override this.ToString() = this |> SubPlotId.toString member this.Convert() = this |> SubPlotId.convert + + [] type AutoTypeNumbers = | ConvertTypes diff --git a/src/Plotly.NET/Layout/Layout.fs b/src/Plotly.NET/Layout/Layout.fs index 943b66a7..2270be1f 100644 --- a/src/Plotly.NET/Layout/Layout.fs +++ b/src/Plotly.NET/Layout/Layout.fs @@ -458,7 +458,7 @@ type Layout() = FunnelGap |> DynObj.setValueOpt layout "funnelgap" FunnelGroupGap |> DynObj.setValueOpt layout "funnelgroupgap" FunnelMode |> DynObj.setValueOptBy layout "funnelmode" StyleParam.FunnelMode.convert - ExtendFunnelAreaColors |> DynObj.setValueOpt layout "extendfunnelareacolors " + ExtendFunnelAreaColors |> DynObj.setValueOpt layout "extendfunnelareacolors" FunnelAreaColorWay |> DynObj.setValueOpt layout "funnelareacolorway" ExtendSunBurstColors |> DynObj.setValueOpt layout "extendsunburstcolors" SunBurstColorWay |> DynObj.setValueOpt layout "sunburstcolorway" @@ -584,6 +584,34 @@ type Layout() = static member getLinearAxisById(id: StyleParam.SubPlotId) = (fun (layout: Layout) -> layout |> Layout.tryGetLinearAxisById id |> Option.defaultValue (LinearAxis.init ())) + /// + /// Returns a sequence of key-value pairs of the layout's dynamic members that are valid x axes (if the key matches and object can be cast to the correct type). + /// + /// The layout to get the x axes from + static member getXAxes (layout: Layout) = + layout.GetProperties(includeInstanceProperties = false) + |> Seq.choose (fun kv -> + if StyleParam.SubPlotId.isValidXAxisId kv.Key then + match layout.TryGetTypedValue(kv.Key) with + | Some axis -> Some (kv.Key, axis) + | None -> None + else None + ) + + /// + /// Returns a sequence of key-value pairs of the layout's dynamic members that are valid y axes (if the key matches and object can be cast to the correct type). + /// + /// The layout to get the y axes from + static member getYAxes (layout: Layout) = + layout.GetProperties(includeInstanceProperties = false) + |> Seq.choose (fun kv -> + if StyleParam.SubPlotId.isValidYAxisId kv.Key then + match layout.TryGetTypedValue(kv.Key) with + | Some axis -> Some (kv.Key, axis) + | None -> None + else None + ) + /// /// Sets a linear axis object on the layout as a dynamic property with the given axis id. /// @@ -633,6 +661,21 @@ type Layout() = static member getSceneById(id: StyleParam.SubPlotId) = (fun (layout: Layout) -> layout |> Layout.tryGetSceneById id |> Option.defaultValue (Scene.init ())) + + /// + /// Returns a sequence of key-value pairs of the layout's dynamic members that are valid scenes (if the key matches and object can be cast to the correct type). + /// + /// The layout to get the scenes from + static member getScenes (layout: Layout) = + layout.GetProperties(includeInstanceProperties = false) + |> Seq.choose (fun kv -> + if StyleParam.SubPlotId.isValidSceneId kv.Key then + match layout.TryGetTypedValue(kv.Key) with + | Some scene -> Some (kv.Key, scene) + | None -> None + else None + ) + /// /// Sets a scene object on the layout as a dynamic property with the given scene id. /// @@ -674,6 +717,20 @@ type Layout() = static member getGeoById(id: StyleParam.SubPlotId) = (fun (layout: Layout) -> layout |> Layout.tryGetGeoById id |> Option.defaultValue (Geo.init ())) + /// + /// Returns a sequence of key-value pairs of the layout's dynamic members that are valid geo subplots (if the key matches and object can be cast to the correct type). + /// + /// The layout to get the geos from + static member getGeos (layout: Layout) = + layout.GetProperties(includeInstanceProperties = false) + |> Seq.choose (fun kv -> + if StyleParam.SubPlotId.isValidGeoId kv.Key then + match layout.TryGetTypedValue(kv.Key) with + | Some geo -> Some (kv.Key, geo) + | None -> None + else None + ) + /// /// Sets a geo object on the layout as a dynamic property with the given geo id. /// @@ -717,6 +774,20 @@ type Layout() = static member getMapboxById(id: StyleParam.SubPlotId) = (fun (layout: Layout) -> layout |> Layout.tryGetMapboxById id |> Option.defaultValue (Mapbox.init ())) + /// + /// Returns a sequence of key-value pairs of the layout's dynamic members that are valid mapbox subplots (if the key matches and object can be cast to the correct type). + /// + /// The layout to get the mapboxes from + static member getMapboxes (layout: Layout) = + layout.GetProperties(includeInstanceProperties = false) + |> Seq.choose (fun kv -> + if StyleParam.SubPlotId.isValidMapboxId kv.Key then + match layout.TryGetTypedValue(kv.Key) with + | Some mapbox -> Some (kv.Key, mapbox) + | None -> None + else None + ) + /// /// Sets a mapbox object on the layout as a dynamic property with the given mapbox id. /// @@ -762,6 +833,20 @@ type Layout() = static member getPolarById(id: StyleParam.SubPlotId) = (fun (layout: Layout) -> layout |> Layout.tryGetPolarById id |> Option.defaultValue (Polar.init ())) + /// + /// Returns a sequence of key-value pairs of the layout's dynamic members that are valid polar subplots (if the key matches and object can be cast to the correct type). + /// + /// The layout to get the polars from + static member getPolars (layout: Layout) = + layout.GetProperties(includeInstanceProperties = false) + |> Seq.choose (fun kv -> + if StyleParam.SubPlotId.isValidPolarId kv.Key then + match layout.TryGetTypedValue(kv.Key) with + | Some polar -> Some (kv.Key, polar) + | None -> None + else None + ) + /// /// Sets a polar object on the layout as a dynamic property with the given polar id. /// @@ -807,6 +892,20 @@ type Layout() = static member getSmithById(id: StyleParam.SubPlotId) = (fun (layout: Layout) -> layout |> Layout.tryGetSmithById id |> Option.defaultValue (Smith.init ())) + /// + /// Returns a sequence of key-value pairs of the layout's dynamic members that are valid smith subplots (if the key matches and object can be cast to the correct type). + /// + /// The layout to get the smiths from + static member getSmiths (layout: Layout) = + layout.GetProperties(includeInstanceProperties = false) + |> Seq.choose (fun kv -> + if StyleParam.SubPlotId.isValidSmithId kv.Key then + match layout.TryGetTypedValue(kv.Key) with + | Some smith -> Some (kv.Key, smith) + | None -> None + else None + ) + /// /// Sets a smith object on the layout as a dynamic property with the given smith id. /// @@ -852,6 +951,20 @@ type Layout() = static member getColorAxisById(id: StyleParam.SubPlotId) = (fun (layout: Layout) -> layout |> Layout.tryGetColorAxisById id |> Option.defaultValue (ColorAxis.init ())) + /// + /// Returns a sequence of key-value pairs of the layout's dynamic members that are valid color axes (if the key matches and object can be cast to the correct type). + /// + /// The layout to get the color axes from + static member getColorAxes (layout: Layout) = + layout.GetProperties(includeInstanceProperties = false) + |> Seq.choose (fun kv -> + if StyleParam.SubPlotId.isValidColorAxisId kv.Key then + match layout.TryGetTypedValue(kv.Key) with + | Some colorAxis -> Some (kv.Key, colorAxis) + | None -> None + else None + ) + /// /// Sets a ColorAxis object on the layout as a dynamic property with the given ColorAxis id. /// @@ -897,6 +1010,20 @@ type Layout() = static member getTernaryById(id: StyleParam.SubPlotId) = (fun (layout: Layout) -> layout |> Layout.tryGetTernaryById id |> Option.defaultValue (Ternary.init ())) + /// + /// Returns a sequence of key-value pairs of the layout's dynamic members that are valid ternary subplots (if the key matches and object can be cast to the correct type). + /// + /// The layout to get the ternaries from + static member getTernaries (layout: Layout) = + layout.GetProperties(includeInstanceProperties = false) + |> Seq.choose (fun kv -> + if StyleParam.SubPlotId.isValidTernaryId kv.Key then + match layout.TryGetTypedValue(kv.Key) with + | Some ternary -> Some (kv.Key, ternary) + | None -> None + else None + ) + /// /// Sets a Ternary object on the layout as a dynamic property with the given Ternary id. /// @@ -945,6 +1072,20 @@ type Layout() = static member tryGetLegendById(id: StyleParam.SubPlotId) = (fun (layout: Layout) -> layout.TryGetTypedValue(StyleParam.SubPlotId.toString id)) + /// + /// Returns a sequence of key-value pairs of the layout's dynamic members that are valid legends (if the key matches and object can be cast to the correct type). + /// + /// The layout to get the color axes from + static member getLegends (layout: Layout) = + layout.GetProperties(includeInstanceProperties = false) + |> Seq.choose (fun kv -> + if StyleParam.SubPlotId.isValidLegendId kv.Key then + match layout.TryGetTypedValue(kv.Key) with + | Some legend -> Some (kv.Key, legend) + | None -> None + else None + ) + /// /// Combines the given Legend object with the one already present on the layout. /// diff --git a/src/Plotly.NET/Layout/ObjectAbstractions/Common/NewSelection.fs b/src/Plotly.NET/Layout/ObjectAbstractions/Common/NewSelection.fs index 8143546a..ee91cb5c 100644 --- a/src/Plotly.NET/Layout/ObjectAbstractions/Common/NewSelection.fs +++ b/src/Plotly.NET/Layout/ObjectAbstractions/Common/NewSelection.fs @@ -47,4 +47,4 @@ type NewSelection() = line |> DynObj.setValue newSelection "line" Mode |> DynObj.setValueOptBy newSelection "mode" StyleParam.NewSelectionMode.convert - NewSelection) + newSelection) diff --git a/tests/Common/FSharpTestBase/FSharpTestBase.fsproj b/tests/Common/FSharpTestBase/FSharpTestBase.fsproj index 3903efde..7617a492 100644 --- a/tests/Common/FSharpTestBase/FSharpTestBase.fsproj +++ b/tests/Common/FSharpTestBase/FSharpTestBase.fsproj @@ -12,6 +12,8 @@ + + diff --git a/tests/Common/FSharpTestBase/TestCharts/FeatureAdditions/Fix_3d_GridPosition.fs b/tests/Common/FSharpTestBase/TestCharts/FeatureAdditions/Fix_3d_GridPosition.fs new file mode 100644 index 00000000..eb16bcb4 --- /dev/null +++ b/tests/Common/FSharpTestBase/TestCharts/FeatureAdditions/Fix_3d_GridPosition.fs @@ -0,0 +1,32 @@ +module Fix_3d_GridPosition + +open Plotly.NET +open Plotly.NET.TraceObjects +open Plotly.NET.LayoutObjects +open DynamicObj + +// https://github.com/plotly/Plotly.NET/issues/413 + +module ``Remove all existing subplots from individual charts on grid creation #413`` = + + let ``2x2 grid with only 3D charts and correct scene positioning`` = + [ + Chart.Point3D(xyz = [1,3,2], UseDefaults = false) + Chart.Point3D(xyz = [1,3,2], UseDefaults = false) + Chart.Point3D(xyz = [1,3,2], UseDefaults = false) + Chart.Point3D(xyz = [1,3,2], UseDefaults = false) + ] + |> Chart.Grid(2,2, SubPlotTitles = ["1";"2";"3";"4"]) + + let ``2x2 grid chart creation ignores other scenes`` = + [ + Chart.Point3D(xyz = [1,3,2], UseDefaults = false) + |> Chart.withScene(Scene.init(), Id = 2) + Chart.Point3D(xyz = [1,3,2], UseDefaults = false) + |> Chart.withScene(Scene.init(), Id = 420) + Chart.Point3D(xyz = [1,3,2], UseDefaults = false) + |> Chart.withScene(Scene.init(), Id = 69) + Chart.Point3D(xyz = [1,3,2], UseDefaults = false) + |> Chart.withScene(Scene.init(), Id = 1337) + ] + |> Chart.Grid(2,2, SubPlotTitles = ["1";"2";"3";"4"]) \ No newline at end of file diff --git a/tests/Common/FSharpTestBase/TestCharts/FeatureAdditions/Grid_SubPlotTitles.fs b/tests/Common/FSharpTestBase/TestCharts/FeatureAdditions/Grid_SubPlotTitles.fs new file mode 100644 index 00000000..cd2fe40e --- /dev/null +++ b/tests/Common/FSharpTestBase/TestCharts/FeatureAdditions/Grid_SubPlotTitles.fs @@ -0,0 +1,101 @@ +module Grid_SubPlotTitles_TestCharts + +open Plotly.NET +open Plotly.NET.TraceObjects +open Plotly.NET.LayoutObjects +open DynamicObj + +// https://github.com/plotly/Plotly.NET/issues/446 + +module ``Add logic for positioning subplot titles in LayoutGrid #388`` = + + let ``cartesian 2x2 grid with subplot titles`` = + [for _ in 0 .. 3 -> Chart.Point([1,2], UseDefaults = false)] + |> Chart.Grid(2,2,SubPlotTitles = ["1,1";"1,2";"2,1";"2,2"]) + + let ``cartesian single stack with subplot titles`` = + [for _ in 0 .. 3 -> Chart.Point([1,2], UseDefaults = false)] + |> Chart.SingleStack(SubPlotTitles = ["1";"2";"3";"4"]) + + let ``singlestack with different subplot types and subplot titles`` = + [ + Chart.Point(xy = [1,2; 2,3], Name = "2D Cartesian", UseDefaults = false) + Chart.Point3D(xyz = [1,3,2], Name = "3D Cartesian", UseDefaults = false) + Chart.PointPolar(rTheta = [10,20], Name = "Polar", UseDefaults = false) + Chart.PointGeo(lonlat = [1,2], Name = "Geo", UseDefaults = false) + ] + |> Chart.SingleStack(SubPlotTitles = ["2D Cartesian";"3D Cartesian";"Polar";"Geo"]) + + let ``chart grid with all subplot types and subplot titles`` = + [ + Chart.Point(xy = [1,2; 2,3], Name = "2D Cartesian", UseDefaults = false) + Chart.Point3D(xyz = [1,3,2], Name = "3D Cartesian", UseDefaults = false) + Chart.PointPolar(rTheta = [10,20], Name = "Polar", UseDefaults = false) + Chart.PointGeo(lonlat = [1,2], Name = "Geo", UseDefaults = false) + Chart.PointMapbox(lonlat = [1,2], Name = "MapBox", UseDefaults = false) |> Chart.withMapbox(Mapbox.init(Style = StyleParam.MapboxStyle.OpenStreetMap)) + Chart.PointTernary(abc = [1,2,3; 2,3,4], Name = "Ternary", UseDefaults = false) + [ + Chart.Carpet( + carpetId = "contour", + A = [0.; 1.; 2.; 3.; 0.; 1.; 2.; 3.; 0.; 1.; 2.; 3.], + B = [4.; 4.; 4.; 4.; 5.; 5.; 5.; 5.; 6.; 6.; 6.; 6.], + X = [2.; 3.; 4.; 5.; 2.2; 3.1; 4.1; 5.1; 1.5; 2.5; 3.5; 4.5], + Y = [1.; 1.4; 1.6; 1.75; 2.; 2.5; 2.7; 2.75; 3.; 3.5; 3.7; 3.75], + AAxis = LinearAxis.initCarpet( + TickPrefix = "a = ", + Smoothing = 0., + MinorGridCount = 9, + AxisType = StyleParam.AxisType.Linear + ), + BAxis = LinearAxis.initCarpet( + TickPrefix = "b = ", + Smoothing = 0., + MinorGridCount = 9, + AxisType = StyleParam.AxisType.Linear + ), + UseDefaults = false, + Opacity = 0.75 + ) + Chart.ContourCarpet( + z = [1.; 1.96; 2.56; 3.0625; 4.; 5.0625; 1.; 7.5625; 9.; 12.25; 15.21; 14.0625], + carpetAnchorId = "contour", + A = [0; 1; 2; 3; 0; 1; 2; 3; 0; 1; 2; 3], + B = [4; 4; 4; 4; 5; 5; 5; 5; 6; 6; 6; 6], + UseDefaults = false, + ContourLineColor = Color.fromKeyword White, + ShowContourLabels = true, + ShowScale = false + ) + ] + |> Chart.combine + Chart.Pie(values = [10;40;50;], Name = "Domain", UseDefaults = false) + Chart.BubbleSmith( + real = [0.5; 1.; 2.; 3.], + imag = [0.5; 1.; 2.; 3.], + sizes = [10;20;30;40], + MultiText=["one";"two";"three";"four";"five";"six";"seven"], + TextPosition=StyleParam.TextPosition.TopCenter, + Name = "Smith", + UseDefaults = false + ) + [ + // you can use nested combined charts, but they have to have the same trace type (Cartesian2D in this case) + let y = [2.; 1.5; 5.; 1.5; 2.; 2.5; 2.1; 2.5; 1.5; 1.;2.; 1.5; 5.; 1.5; 3.; 2.5; 2.5; 1.5; 3.5; 1.] + Chart.BoxPlot(X = "y" ,Y = y,Name="Combined 1",Jitter=0.1,BoxPoints=StyleParam.BoxPoints.All, UseDefaults = false); + Chart.BoxPlot(X = "y'",Y = y,Name="Combined 2",Jitter=0.1,BoxPoints=StyleParam.BoxPoints.All, UseDefaults = false); + ] + |> Chart.combine + ] + |> Chart.Grid( + 4, + 3, + YGap = 0.5, + SubPlotTitles = [ + "Point"; "Point3D"; "PointPolar"; "PointGeo" + "PointMapbox";"PointTernary";"ContourCarpet";"Pie" + "BubbleSmith";"Combined BoxPlot" + ], + SubPlotTitleFont = Font.init(Size = 20), + SubPlotTitleOffset = 0.045 + ) + |> Chart.withSize(1000,1000) \ No newline at end of file diff --git a/tests/Common/FSharpTestBase/TestUtils.fs b/tests/Common/FSharpTestBase/TestUtils.fs index 40b31249..fe10a58d 100644 --- a/tests/Common/FSharpTestBase/TestUtils.fs +++ b/tests/Common/FSharpTestBase/TestUtils.fs @@ -217,7 +217,7 @@ module Objects = let jsonFieldIsSetWith fieldName expected (object:#DynamicObj) = Expect.equal - ((object :> DynamicObj)?($"{fieldName}") |> JsonConvert.SerializeObject) + ((object :> DynamicObj)?($"{fieldName}") |> fun o -> JsonConvert.SerializeObject(o, JsonSerializerSettings(ReferenceLoopHandling = ReferenceLoopHandling.Serialize))) expected ($"Field `{fieldName}` not set correctly in serialized dynamic object.") diff --git a/tests/ConsoleApps/FSharpConsole/Program.fs b/tests/ConsoleApps/FSharpConsole/Program.fs index 12e92fac..09bf2e52 100644 --- a/tests/ConsoleApps/FSharpConsole/Program.fs +++ b/tests/ConsoleApps/FSharpConsole/Program.fs @@ -10,38 +10,23 @@ open Giraffe.ViewEngine open Newtonsoft.Json [] -let main argv = - +let main args = + let x = [1.; 2.; 3.; 4.; 5.; 6.; 7.; 8.; 9.; 10.; ] + let y = [2.; 1.5; 5.; 1.5; 3.; 2.5; 2.5; 1.5; 3.5; 1.] [ - StyleParam.HoverInfo.X - StyleParam.HoverInfo.XY - StyleParam.HoverInfo.XYZ - StyleParam.HoverInfo.XYZText - StyleParam.HoverInfo.XYZTextName - StyleParam.HoverInfo.Y - StyleParam.HoverInfo.YZ - StyleParam.HoverInfo.YZText - StyleParam.HoverInfo.YZTextName - StyleParam.HoverInfo.Z - StyleParam.HoverInfo.ZText - StyleParam.HoverInfo.ZTextName - StyleParam.HoverInfo.Text - StyleParam.HoverInfo.TextName - StyleParam.HoverInfo.Name - StyleParam.HoverInfo.All - StyleParam.HoverInfo.None - StyleParam.HoverInfo.Skip + Chart.Point(x = x, y = y, UseDefaults = false) + |> Chart.withYAxisStyle("This title must") + + Chart.Line(x = x, y = y, UseDefaults = false) + |> Chart.withYAxisStyle("be set on the",ZeroLine=false) + + Chart.Spline(x = x, y = y, UseDefaults = false) + |> Chart.withYAxisStyle("respective subplots",ZeroLine=false) ] - |> List.mapi (fun i hi -> - Chart.Point3D( - xyz = [i + 1, i + 2, i + 3], - Name = $"NAME: trace with {hi.ToString()}", - Text = $"TEXT: trace with {hi.ToString()}", - UseDefaults = false - ) - |> GenericChart.mapTrace (Trace3DStyle.Scatter3D(HoverInfo = hi)) - ) - |> Chart.combine - |> Chart.withSize(1000,1000) + |> Chart.SingleStack(Pattern = StyleParam.LayoutGridPattern.Coupled) + //move xAxis to bottom and increase spacing between plots by using the withLayoutGridStyle function + |> Chart.withLayoutGridStyle(XSide=StyleParam.LayoutGridXSide.Bottom,YGap= 0.1) + |> Chart.withTitle("Hi i am the new SingleStackChart") + |> Chart.withXAxisStyle("im the shared xAxis") |> Chart.show 0 \ No newline at end of file diff --git a/tests/CoreTests/CoreTests/CoreTests.fsproj b/tests/CoreTests/CoreTests/CoreTests.fsproj index 5146d14c..3466c51c 100644 --- a/tests/CoreTests/CoreTests/CoreTests.fsproj +++ b/tests/CoreTests/CoreTests/CoreTests.fsproj @@ -29,6 +29,7 @@ + @@ -45,6 +46,8 @@ + + diff --git a/tests/CoreTests/CoreTests/FeatureAdditions/Accessible_Contours.fs b/tests/CoreTests/CoreTests/FeatureAdditions/Accessible_Contours.fs index ae45437a..96de27ad 100644 --- a/tests/CoreTests/CoreTests/FeatureAdditions/Accessible_Contours.fs +++ b/tests/CoreTests/CoreTests/FeatureAdditions/Accessible_Contours.fs @@ -41,7 +41,7 @@ module ``Contours should be accessible #426`` = |> chartGeneratedContains ``Contours should be accessible #426``.``Contours trace Grid chart with shared color axis and adapted contours ranges`` } test "Chart Grid with shared color axis and corrected contours ranges layout" { - """var layout = {"xaxis":{},"yaxis":{},"xaxis2":{},"yaxis2":{},"xaxis3":{},"yaxis3":{},"grid":{"rows":2,"columns":2,"pattern":"independent"}};""" + """var layout = {"xaxis":{},"yaxis":{},"xaxis2":{},"yaxis2":{},"xaxis3":{},"yaxis3":{},"annotations":[],"grid":{"rows":2,"columns":2,"roworder":"top to bottom","pattern":"independent"}};""" |> chartGeneratedContains ``Contours should be accessible #426``.``Contours trace Grid chart with shared color axis and adapted contours ranges`` } ] \ No newline at end of file diff --git a/tests/CoreTests/CoreTests/FeatureAdditions/Fix_3d_GridPosition.fs b/tests/CoreTests/CoreTests/FeatureAdditions/Fix_3d_GridPosition.fs new file mode 100644 index 00000000..b04be19e --- /dev/null +++ b/tests/CoreTests/CoreTests/FeatureAdditions/Fix_3d_GridPosition.fs @@ -0,0 +1,34 @@ +module CoreTests.Fix_3d_GridPosition + +open Expecto +open Plotly.NET +open Plotly.NET.LayoutObjects +open Plotly.NET.TraceObjects + +open TestUtils.HtmlCodegen +open Fix_3d_GridPosition + +// https://github.com/plotly/Plotly.NET/issues/413 + +module ``Remove all existing subplots from individual charts on grid creation #413`` = + + [] + let ``Add subplot titles`` = + testList "FeatureAddition.Fix 3D chart position in Grid" [ + test "2x2 3d charts data" { + """var data = [{"type":"scatter3d","mode":"markers","x":[1],"y":[3],"z":[2],"marker":{},"line":{},"scene":"scene"},{"type":"scatter3d","mode":"markers","x":[1],"y":[3],"z":[2],"marker":{},"line":{},"scene":"scene2"},{"type":"scatter3d","mode":"markers","x":[1],"y":[3],"z":[2],"marker":{},"line":{},"scene":"scene3"},{"type":"scatter3d","mode":"markers","x":[1],"y":[3],"z":[2],"marker":{},"line":{},"scene":"scene4"}];""" + |> chartGeneratedContains ``Remove all existing subplots from individual charts on grid creation #413``.``2x2 grid with only 3D charts and correct scene positioning`` + } + test "2x2 3d charts layout" { + """var layout = {"scene":{"camera":{"projection":{"type":"perspective"}},"domain":{"row":0,"column":0}},"scene2":{"camera":{"projection":{"type":"perspective"}},"domain":{"row":0,"column":1}},"scene3":{"camera":{"projection":{"type":"perspective"}},"domain":{"row":1,"column":0}},"scene4":{"camera":{"projection":{"type":"perspective"}},"domain":{"row":1,"column":1}},"annotations":[{"x":0.22222222222222224,"y":1.0,"showarrow":false,"text":"1","xanchor":"center","xref":"paper","yanchor":"bottom","yref":"paper"},{"x":0.7777777777777778,"y":1.0,"showarrow":false,"text":"2","xanchor":"center","xref":"paper","yanchor":"bottom","yref":"paper"},{"x":0.22222222222222224,"y":0.4117647058823529,"showarrow":false,"text":"3","xanchor":"center","xref":"paper","yanchor":"bottom","yref":"paper"},{"x":0.7777777777777778,"y":0.4117647058823529,"showarrow":false,"text":"4","xanchor":"center","xref":"paper","yanchor":"bottom","yref":"paper"}],"grid":{"rows":2,"columns":2,"roworder":"top to bottom","pattern":"independent"}};""" + |> chartGeneratedContains ``Remove all existing subplots from individual charts on grid creation #413``.``2x2 grid with only 3D charts and correct scene positioning`` + } + test "2x2 3d charts ignores additional scenes data" { + """var data = [{"type":"scatter3d","mode":"markers","x":[1],"y":[3],"z":[2],"marker":{},"line":{},"scene":"scene"},{"type":"scatter3d","mode":"markers","x":[1],"y":[3],"z":[2],"marker":{},"line":{},"scene":"scene2"},{"type":"scatter3d","mode":"markers","x":[1],"y":[3],"z":[2],"marker":{},"line":{},"scene":"scene3"},{"type":"scatter3d","mode":"markers","x":[1],"y":[3],"z":[2],"marker":{},"line":{},"scene":"scene4"}];""" + |> chartGeneratedContains ``Remove all existing subplots from individual charts on grid creation #413``.``2x2 grid chart creation ignores other scenes`` + } + test "2x2 3d charts ignores additional scenes layout" { + """var layout = {"scene2":{"camera":{"projection":{"type":"perspective"}},"domain":{"row":0,"column":1}},"scene":{"camera":{"projection":{"type":"perspective"}},"domain":{"row":0,"column":0}},"scene3":{"camera":{"projection":{"type":"perspective"}},"domain":{"row":1,"column":0}},"scene4":{"camera":{"projection":{"type":"perspective"}},"domain":{"row":1,"column":1}},"annotations":[{"x":0.22222222222222224,"y":1.0,"showarrow":false,"text":"1","xanchor":"center","xref":"paper","yanchor":"bottom","yref":"paper"},{"x":0.7777777777777778,"y":1.0,"showarrow":false,"text":"2","xanchor":"center","xref":"paper","yanchor":"bottom","yref":"paper"},{"x":0.22222222222222224,"y":0.4117647058823529,"showarrow":false,"text":"3","xanchor":"center","xref":"paper","yanchor":"bottom","yref":"paper"},{"x":0.7777777777777778,"y":0.4117647058823529,"showarrow":false,"text":"4","xanchor":"center","xref":"paper","yanchor":"bottom","yref":"paper"}],"grid":{"rows":2,"columns":2,"roworder":"top to bottom","pattern":"independent"}};""" + |> chartGeneratedContains ``Remove all existing subplots from individual charts on grid creation #413``.``2x2 grid chart creation ignores other scenes`` + } + ] \ No newline at end of file diff --git a/tests/CoreTests/CoreTests/FeatureAdditions/Grid_SubPlotTitles.fs b/tests/CoreTests/CoreTests/FeatureAdditions/Grid_SubPlotTitles.fs new file mode 100644 index 00000000..bcb68542 --- /dev/null +++ b/tests/CoreTests/CoreTests/FeatureAdditions/Grid_SubPlotTitles.fs @@ -0,0 +1,48 @@ +module CoreTests.Grid_SubPlotTitles + +open Expecto +open Plotly.NET +open Plotly.NET.LayoutObjects +open Plotly.NET.TraceObjects + +open TestUtils.HtmlCodegen +open Grid_SubPlotTitles_TestCharts + +module ``Add logic for positioning subplot titles in LayoutGrid #388`` = + + [] + let ``Add subplot titles`` = + testList "FeatureAddition.Add subplot titles in LayoutGrid" [ + test "cartesian 2x2 grid data" { + """var data = [{"type":"scatter","mode":"markers","x":[1],"y":[2],"marker":{},"line":{},"xaxis":"x","yaxis":"y"},{"type":"scatter","mode":"markers","x":[1],"y":[2],"marker":{},"line":{},"xaxis":"x2","yaxis":"y2"},{"type":"scatter","mode":"markers","x":[1],"y":[2],"marker":{},"line":{},"xaxis":"x3","yaxis":"y3"},{"type":"scatter","mode":"markers","x":[1],"y":[2],"marker":{},"line":{},"xaxis":"x4","yaxis":"y4"}];""" + |> chartGeneratedContains ``Add logic for positioning subplot titles in LayoutGrid #388``.``cartesian 2x2 grid with subplot titles`` + } + test "cartesian 2x2 grid layout" { + """var layout = {"xaxis":{},"yaxis":{},"xaxis2":{},"yaxis2":{},"xaxis3":{},"yaxis3":{},"xaxis4":{},"yaxis4":{},"annotations":[{"x":0.22222222222222224,"y":1.0,"showarrow":false,"text":"1,1","xanchor":"center","xref":"paper","yanchor":"bottom","yref":"paper"},{"x":0.7777777777777778,"y":1.0,"showarrow":false,"text":"1,2","xanchor":"center","xref":"paper","yanchor":"bottom","yref":"paper"},{"x":0.22222222222222224,"y":0.4117647058823529,"showarrow":false,"text":"2,1","xanchor":"center","xref":"paper","yanchor":"bottom","yref":"paper"},{"x":0.7777777777777778,"y":0.4117647058823529,"showarrow":false,"text":"2,2","xanchor":"center","xref":"paper","yanchor":"bottom","yref":"paper"}],"grid":{"rows":2,"columns":2,"roworder":"top to bottom","pattern":"independent"}};""" + |> chartGeneratedContains ``Add logic for positioning subplot titles in LayoutGrid #388``.``cartesian 2x2 grid with subplot titles`` + } + test "cartesian singlestack data" { + """var data = [{"type":"scatter","mode":"markers","x":[1],"y":[2],"marker":{},"line":{},"xaxis":"x","yaxis":"y"},{"type":"scatter","mode":"markers","x":[1],"y":[2],"marker":{},"line":{},"xaxis":"x2","yaxis":"y2"},{"type":"scatter","mode":"markers","x":[1],"y":[2],"marker":{},"line":{},"xaxis":"x3","yaxis":"y3"},{"type":"scatter","mode":"markers","x":[1],"y":[2],"marker":{},"line":{},"xaxis":"x4","yaxis":"y4"}];""" + |> chartGeneratedContains ``Add logic for positioning subplot titles in LayoutGrid #388``.``cartesian single stack with subplot titles`` + } + test "cartesian singlestack layout" { + """var layout = {"xaxis":{},"yaxis":{},"xaxis2":{},"yaxis2":{},"xaxis3":{},"yaxis3":{},"xaxis4":{},"yaxis4":{},"annotations":[{"x":0.5,"y":0.9999999999999999,"showarrow":false,"text":"1","xanchor":"center","xref":"paper","yanchor":"bottom","yref":"paper"},{"x":0.5,"y":0.7297297297297296,"showarrow":false,"text":"2","xanchor":"center","xref":"paper","yanchor":"bottom","yref":"paper"},{"x":0.5,"y":0.4594594594594594,"showarrow":false,"text":"3","xanchor":"center","xref":"paper","yanchor":"bottom","yref":"paper"},{"x":0.5,"y":0.18918918918918914,"showarrow":false,"text":"4","xanchor":"center","xref":"paper","yanchor":"bottom","yref":"paper"}],"grid":{"rows":4,"columns":1,"roworder":"top to bottom","pattern":"independent"}};""" + |> chartGeneratedContains ``Add logic for positioning subplot titles in LayoutGrid #388``.``cartesian single stack with subplot titles`` + } + test "singlestack with different subplot types data" { + """var data = [{"type":"scatter","name":"2D Cartesian","mode":"markers","x":[1,2],"y":[2,3],"marker":{},"line":{},"xaxis":"x","yaxis":"y"},{"type":"scatter3d","name":"3D Cartesian","mode":"markers","x":[1],"y":[3],"z":[2],"marker":{},"line":{},"scene":"scene2"},{"type":"scatterpolar","name":"Polar","mode":"markers","r":[10],"theta":[20],"marker":{},"subplot":"polar3"},{"type":"scattergeo","name":"Geo","mode":"markers","lat":[2],"lon":[1],"marker":{},"line":{},"geo":"geo4"}];""" + |> chartGeneratedContains ``Add logic for positioning subplot titles in LayoutGrid #388``.``singlestack with different subplot types and subplot titles`` + } + test "singlestack with different subplot types layout" { + """var layout = {"xaxis":{},"yaxis":{},"scene2":{"camera":{"projection":{"type":"perspective"}},"domain":{"row":1,"column":0}},"polar3":{"domain":{"row":2,"column":0}},"geo4":{"domain":{"row":3,"column":0}},"annotations":[{"x":0.5,"y":0.9999999999999999,"showarrow":false,"text":"2D Cartesian","xanchor":"center","xref":"paper","yanchor":"bottom","yref":"paper"},{"x":0.5,"y":0.7297297297297296,"showarrow":false,"text":"3D Cartesian","xanchor":"center","xref":"paper","yanchor":"bottom","yref":"paper"},{"x":0.5,"y":0.4594594594594594,"showarrow":false,"text":"Polar","xanchor":"center","xref":"paper","yanchor":"bottom","yref":"paper"},{"x":0.5,"y":0.18918918918918914,"showarrow":false,"text":"Geo","xanchor":"center","xref":"paper","yanchor":"bottom","yref":"paper"}],"grid":{"rows":4,"columns":1,"roworder":"top to bottom","pattern":"independent"}};""" + |> chartGeneratedContains ``Add logic for positioning subplot titles in LayoutGrid #388``.``singlestack with different subplot types and subplot titles`` + } + test "chart grid with all subplot types data" { + """var data = [{"type":"scatter","name":"2D Cartesian","mode":"markers","x":[1,2],"y":[2,3],"marker":{},"line":{},"xaxis":"x","yaxis":"y"},{"type":"scatter3d","name":"3D Cartesian","mode":"markers","x":[1],"y":[3],"z":[2],"marker":{},"line":{},"scene":"scene2"},{"type":"scatterpolar","name":"Polar","mode":"markers","r":[10],"theta":[20],"marker":{},"subplot":"polar3"},{"type":"scattergeo","name":"Geo","mode":"markers","lat":[2],"lon":[1],"marker":{},"line":{},"geo":"geo4"},{"type":"scattermapbox","name":"MapBox","mode":"markers","lat":[2],"lon":[1],"cluster":{},"marker":{},"line":{},"subplot":"mapbox5"},{"type":"scatterternary","name":"Ternary","mode":"markers","a":[1,2],"b":[2,3],"c":[3,4],"marker":{},"line":{},"subplot":"ternary6"},{"type":"carpet","opacity":0.75,"x":[2.0,3.0,4.0,5.0,2.2,3.1,4.1,5.1,1.5,2.5,3.5,4.5],"y":[1.0,1.4,1.6,1.75,2.0,2.5,2.7,2.75,3.0,3.5,3.7,3.75],"a":[0.0,1.0,2.0,3.0,0.0,1.0,2.0,3.0,0.0,1.0,2.0,3.0],"b":[4.0,4.0,4.0,4.0,5.0,5.0,5.0,5.0,6.0,6.0,6.0,6.0],"aaxis":{"type":"linear","tickprefix":"a = ","minorgridcount":9,"smoothing":0.0},"baxis":{"type":"linear","tickprefix":"b = ","minorgridcount":9,"smoothing":0.0},"carpet":"contour","xaxis":"x7","yaxis":"y7"},{"type":"contourcarpet","z":[1.0,1.96,2.56,3.0625,4.0,5.0625,1.0,7.5625,9.0,12.25,15.21,14.0625],"a":[0,1,2,3,0,1,2,3,0,1,2,3],"b":[4,4,4,4,5,5,5,5,6,6,6,6],"line":{"color":"rgba(255, 255, 255, 1.0)"},"showscale":false,"carpet":"contour","contours":{"showlabels":true}},{"type":"pie","name":"Domain","values":[10,40,50],"marker":{"line":{},"pattern":{}},"domain":{"row":2,"column":1}},{"type":"scattersmith","name":"Smith","mode":"markers+text","imag":[0.5,1.0,2.0,3.0],"real":[0.5,1.0,2.0,3.0],"text":["one","two","three","four","five","six","seven"],"textposition":"top center","marker":{"size":[10,20,30,40]},"line":{},"subplot":"smith9"},{"type":"box","name":"Combined 1","x":"y","y":[2.0,1.5,5.0,1.5,2.0,2.5,2.1,2.5,1.5,1.0,2.0,1.5,5.0,1.5,3.0,2.5,2.5,1.5,3.5,1.0],"marker":{},"line":{},"boxpoints":"all","jitter":0.1,"xaxis":"x10","yaxis":"y10"},{"type":"box","name":"Combined 2","x":"y'","y":[2.0,1.5,5.0,1.5,2.0,2.5,2.1,2.5,1.5,1.0,2.0,1.5,5.0,1.5,3.0,2.5,2.5,1.5,3.5,1.0],"marker":{},"line":{},"boxpoints":"all","jitter":0.1,"xaxis":"x10","yaxis":"y10"}];""" + |> chartGeneratedContains ``Add logic for positioning subplot titles in LayoutGrid #388``.``chart grid with all subplot types and subplot titles`` + } + test "chart grid with all subplot types layout" { + """var layout = {"xaxis":{},"yaxis":{},"scene2":{"camera":{"projection":{"type":"perspective"}},"domain":{"row":0,"column":1}},"polar3":{"domain":{"row":0,"column":2}},"geo4":{"domain":{"row":1,"column":0}},"mapbox5":{"style":"open-street-map","domain":{"row":1,"column":1}},"ternary6":{"domain":{"row":1,"column":2}},"xaxis7":{},"yaxis7":{},"smith9":{"domain":{"row":2,"column":2}},"xaxis10":{},"yaxis10":{},"annotations":[{"x":0.14285714285714288,"y":1.045,"font":{"size":20.0},"showarrow":false,"text":"Point","xanchor":"center","xref":"paper","yanchor":"bottom","yref":"paper"},{"x":0.5,"y":1.045,"font":{"size":20.0},"showarrow":false,"text":"Point3D","xanchor":"center","xref":"paper","yanchor":"bottom","yref":"paper"},{"x":0.8571428571428572,"y":1.045,"font":{"size":20.0},"showarrow":false,"text":"PointPolar","xanchor":"center","xref":"paper","yanchor":"bottom","yref":"paper"},{"x":0.14285714285714288,"y":0.7592857142857142,"font":{"size":20.0},"showarrow":false,"text":"PointGeo","xanchor":"center","xref":"paper","yanchor":"bottom","yref":"paper"},{"x":0.5,"y":0.7592857142857142,"font":{"size":20.0},"showarrow":false,"text":"PointMapbox","xanchor":"center","xref":"paper","yanchor":"bottom","yref":"paper"},{"x":0.8571428571428572,"y":0.7592857142857142,"font":{"size":20.0},"showarrow":false,"text":"PointTernary","xanchor":"center","xref":"paper","yanchor":"bottom","yref":"paper"},{"x":0.14285714285714288,"y":0.47357142857142853,"font":{"size":20.0},"showarrow":false,"text":"ContourCarpet","xanchor":"center","xref":"paper","yanchor":"bottom","yref":"paper"},{"x":0.5,"y":0.47357142857142853,"font":{"size":20.0},"showarrow":false,"text":"Pie","xanchor":"center","xref":"paper","yanchor":"bottom","yref":"paper"},{"x":0.8571428571428572,"y":0.47357142857142853,"font":{"size":20.0},"showarrow":false,"text":"BubbleSmith","xanchor":"center","xref":"paper","yanchor":"bottom","yref":"paper"},{"x":0.14285714285714288,"y":0.18785714285714283,"font":{"size":20.0},"showarrow":false,"text":"Combined BoxPlot","xanchor":"center","xref":"paper","yanchor":"bottom","yref":"paper"}],"grid":{"rows":4,"columns":3,"roworder":"top to bottom","pattern":"independent","ygap":0.5},"width":1000,"height":1000};""" + |> chartGeneratedContains ``Add logic for positioning subplot titles in LayoutGrid #388``.``chart grid with all subplot types and subplot titles`` + } + ] \ No newline at end of file diff --git a/tests/CoreTests/CoreTests/HTMLCodegen/ChartLayout.fs b/tests/CoreTests/CoreTests/HTMLCodegen/ChartLayout.fs index 60df601c..31757fcd 100644 --- a/tests/CoreTests/CoreTests/HTMLCodegen/ChartLayout.fs +++ b/tests/CoreTests/CoreTests/HTMLCodegen/ChartLayout.fs @@ -252,7 +252,7 @@ let ``Multicharts and subplots`` = |> chartGeneratedContains subPlotChart ); testCase "Subplot grids layout" ( fun () -> - "var layout = {\"xaxis\":{\"title\":{\"text\":\"x1\"}},\"yaxis\":{\"title\":{\"text\":\"y1\"}},\"xaxis2\":{\"title\":{\"text\":\"x2\"}},\"yaxis2\":{\"title\":{\"text\":\"y2\"}},\"xaxis3\":{\"title\":{\"text\":\"x3\"}},\"yaxis3\":{\"title\":{\"text\":\"y3\"}},\"xaxis4\":{\"title\":{\"text\":\"x4\"}},\"yaxis4\":{\"title\":{\"text\":\"y4\"}},\"grid\":{\"rows\":2,\"columns\":2,\"pattern\":\"independent\"}};" + """var layout = {"yaxis":{"title":{"text":"y1"}},"xaxis":{"title":{"text":"x1"}},"yaxis2":{"title":{"text":"y2"}},"xaxis2":{"title":{"text":"x2"}},"yaxis3":{"title":{"text":"y3"}},"xaxis3":{"title":{"text":"x3"}},"yaxis4":{"title":{"text":"y4"}},"xaxis4":{"title":{"text":"x4"}},"annotations":[],"grid":{"rows":2,"columns":2,"roworder":"top to bottom","pattern":"independent"}};""" |> chartGeneratedContains subPlotChart ); testCase "MultiTrace Subplot grid data" ( fun () -> @@ -260,7 +260,7 @@ let ``Multicharts and subplots`` = |> chartGeneratedContains multiTraceGrid ); testCase "MultiTrace Subplot grid layout" ( fun () -> - """var layout = {"xaxis":{},"yaxis":{},"scene":{"camera":{"projection":{"type":"perspective"}},"domain":{"row":0,"column":1}},"scene2":{"camera":{"projection":{"type":"perspective"}},"domain":{"row":0,"column":1}},"polar3":{"domain":{"row":0,"column":2}},"geo4":{"domain":{"row":1,"column":0}},"mapbox":{"style":"open-street-map","domain":{"row":1,"column":1}},"mapbox5":{"style":"open-street-map","domain":{"row":1,"column":1}},"ternary6":{"domain":{"row":1,"column":2}},"xaxis7":{},"yaxis7":{},"smith9":{"domain":{"row":2,"column":2}},"xaxis10":{},"yaxis10":{},"grid":{"rows":4,"columns":3,"pattern":"independent"},"width":1000,"height":1000};""" + """var layout = {"xaxis":{},"yaxis":{},"scene2":{"camera":{"projection":{"type":"perspective"}},"domain":{"row":0,"column":1}},"polar3":{"domain":{"row":0,"column":2}},"geo4":{"domain":{"row":1,"column":0}},"mapbox5":{"style":"open-street-map","domain":{"row":1,"column":1}},"ternary6":{"domain":{"row":1,"column":2}},"xaxis7":{},"yaxis7":{},"smith9":{"domain":{"row":2,"column":2}},"xaxis10":{},"yaxis10":{},"annotations":[],"grid":{"rows":4,"columns":3,"roworder":"top to bottom","pattern":"independent"},"width":1000,"height":1000};""" |> chartGeneratedContains multiTraceGrid ); testCase "Single Stack data" ( fun () -> @@ -268,7 +268,7 @@ let ``Multicharts and subplots`` = |> chartGeneratedContains singleStackChart ); testCase "Single Stack layout" ( fun () -> - "var layout = {\"yaxis\":{\"title\":{\"text\":\"This title must\"}},\"xaxis\":{\"title\":{\"text\":\"im the shared xAxis\"}},\"xaxis2\":{},\"yaxis2\":{\"title\":{\"text\":\"be set on the\"},\"zeroline\":false},\"xaxis3\":{},\"yaxis3\":{\"title\":{\"text\":\"respective subplots\"},\"zeroline\":false},\"grid\":{\"rows\":3,\"columns\":1,\"pattern\":\"coupled\",\"ygap\":0.1,\"xside\":\"bottom\"},\"title\":{\"text\":\"Hi i am the new SingleStackChart\"}};" + """var layout = {"xaxis":{"title":{"text":"im the shared xAxis"}},"yaxis":{"title":{"text":"This title must"}},"xaxis2":{},"yaxis2":{"title":{"text":"be set on the"},"zeroline":false},"xaxis3":{},"yaxis3":{"title":{"text":"respective subplots"},"zeroline":false},"annotations":[],"grid":{"rows":3,"columns":1,"roworder":"top to bottom","pattern":"coupled","ygap":0.1,"xside":"bottom"},"title":{"text":"Hi i am the new SingleStackChart"}};""" |> chartGeneratedContains singleStackChart ); @@ -277,7 +277,7 @@ let ``Multicharts and subplots`` = |> chartGeneratedContains multiTraceSingleStack ); testCase "MultiTrace Single Stack layout" ( fun () -> - """var layout = {"xaxis":{},"yaxis":{},"ternary2":{"domain":{"row":1,"column":0}},"xaxis3":{},"yaxis3":{},"scene":{"camera":{"projection":{"type":"perspective"}},"domain":{"row":3,"column":0}},"scene4":{"camera":{"projection":{"type":"perspective"}},"domain":{"row":3,"column":0}},"mapbox":{"style":"open-street-map","domain":{"row":4,"column":0}},"mapbox5":{"style":"open-street-map","domain":{"row":4,"column":0}},"xaxis6":{},"yaxis6":{},"grid":{"rows":6,"columns":1,"pattern":"independent"},"width":1000,"height":1000};""" + """var layout = {"xaxis":{},"yaxis":{},"ternary2":{"domain":{"row":1,"column":0}},"xaxis3":{},"yaxis3":{},"scene4":{"camera":{"projection":{"type":"perspective"}},"domain":{"row":3,"column":0}},"mapbox5":{"style":"open-street-map","domain":{"row":4,"column":0}},"xaxis6":{},"yaxis6":{},"annotations":[],"grid":{"rows":6,"columns":1,"roworder":"top to bottom","pattern":"independent"},"width":1000,"height":1000};""" |> chartGeneratedContains multiTraceSingleStack ); ] diff --git a/tests/CoreTests/CoreTests/LayoutObjects/Layout.fs b/tests/CoreTests/CoreTests/LayoutObjects/Layout.fs new file mode 100644 index 00000000..6d4cf0ba --- /dev/null +++ b/tests/CoreTests/CoreTests/LayoutObjects/Layout.fs @@ -0,0 +1,336 @@ +module Tests.ConfigObjects.Layout + +open Expecto +open Plotly.NET +open Plotly.NET.LayoutObjects +open Plotly.NET.ConfigObjects +open DynamicObj + +open TestUtils.Objects + +let layout = + Layout.init( + Title = Title.init(), + ShowLegend = true, + Margin = Margin.init(), + AutoSize = true, + Width = 1, + Height = 1, + Font = Font.init(), + UniformText = UniformText.init(), + Separators = ",", + PaperBGColor = Color.fromString("grey"), + PlotBGColor = Color.fromString("grey"), + AutoTypeNumbers = StyleParam.AutoTypeNumbers.Strict, + Colorscale = DefaultColorScales.init(), + Colorway = Color.fromColors [], + ModeBar = ModeBar.init(), + HoverMode = StyleParam.HoverMode.X, + ClickMode = StyleParam.ClickMode.Event, + DragMode = StyleParam.DragMode.DrawRect, + SelectDirection = StyleParam.SelectDirection.Any, + NewSelection = NewSelection.init(), + ActiveSelection = ActiveSelection.init(), + HoverDistance = 1, + SpikeDistance = 1, + Hoverlabel = Hoverlabel.init(), + Transition = Transition.init(), + DataRevision = "lol", + UIRevision = "lol", + EditRevision = "lol", + SelectRevision = "lol", + Template = Layout.init(), + Meta = "lol", + Computed = "lol", + Grid = LayoutGrid.init(), + Calendar = StyleParam.Calendar.Discworld, + NewShape = NewShape.init(), + MinReducedHeight = 1, + MinReducedWidth = 1, + ActiveShape = ActiveShape.init(), + HideSources = true, + ScatterGap = 1, + ScatterMode = StyleParam.ScatterMode.Group, + BarGap = 1, + BarGroupGap = 1, + BarMode = StyleParam.BarMode.Group, + BarNorm = StyleParam.BarNorm.Fraction, + ExtendPieColors = true, + HiddenLabels = ["lol"], + PieColorWay = Color.fromColors [], + BoxGap = 1, + BoxGroupGap = 1, + BoxMode = StyleParam.BoxMode.Group, + ViolinGap = 1, + ViolinGroupGap = 1, + ViolinMode = StyleParam.ViolinMode.Group, + WaterfallGap = 1, + WaterfallGroupGap = 1, + WaterfallMode = StyleParam.WaterfallMode.Group, + FunnelGap = 1, + FunnelGroupGap = 1, + FunnelMode = StyleParam.FunnelMode.Group, + ExtendFunnelAreaColors = true, + FunnelAreaColorWay = Color.fromColors [], + ExtendSunBurstColors = true, + SunBurstColorWay = Color.fromColors [], + ExtendTreeMapColors = true, + TreeMapColorWay = Color.fromColors [], + ExtendIcicleColors = true, + IcicleColorWay = Color.fromColors [], + Annotations = [], + Shapes = [], + Selections = [], + Images = [], + Sliders = [], + UpdateMenus = [] + ) + +[] +let ``Layout json tests`` = + testList "LayoutObjects.Layout JSON field tests" [ + testCase "title" (fun _ -> layout |> jsonFieldIsSetWith "title" "{}") + testCase "showlegend" (fun _ -> layout |> jsonFieldIsSetWith "showlegend" "true") + testCase "margin" (fun _ -> layout |> jsonFieldIsSetWith "margin" "{}") + testCase "autosize" (fun _ -> layout |> jsonFieldIsSetWith "autosize" "true") + testCase "width" (fun _ -> layout |> jsonFieldIsSetWith "width" "1") + testCase "height" (fun _ -> layout |> jsonFieldIsSetWith "height" "1") + testCase "font" (fun _ -> layout |> jsonFieldIsSetWith "font" "{}") + testCase "uniformtext" (fun _ -> layout |> jsonFieldIsSetWith "uniformtext" "{}") + testCase "separators" (fun _ -> layout |> jsonFieldIsSetWith "separators" "\",\"") + testCase "paperbgcolor" (fun _ -> layout |> jsonFieldIsSetWith "paper_bgcolor" "\"grey\"") + testCase "plotbgcolor" (fun _ -> layout |> jsonFieldIsSetWith "plot_bgcolor" "\"grey\"") + testCase "autotypenumbers" (fun _ -> layout |> jsonFieldIsSetWith "autotypenumbers" "\"strict\"") + testCase "colorscale" (fun _ -> layout |> jsonFieldIsSetWith "colorscale" "{}") + testCase "colorway" (fun _ -> layout |> jsonFieldIsSetWith "colorway" "[]") + testCase "modebar" (fun _ -> layout |> jsonFieldIsSetWith "modebar" "{}") + testCase "hovermode" (fun _ -> layout |> jsonFieldIsSetWith "hovermode" "\"x\"") + testCase "clickmode" (fun _ -> layout |> jsonFieldIsSetWith "clickmode" "\"event\"") + testCase "dragmode" (fun _ -> layout |> jsonFieldIsSetWith "dragmode" "\"drawrect\"") + testCase "selectdirection" (fun _ -> layout |> jsonFieldIsSetWith "selectdirection" "\"any\"") + testCase "newselection" (fun _ -> layout |> jsonFieldIsSetWith "newselection" "{\"line\":{}}") + testCase "activeselection" (fun _ -> layout |> jsonFieldIsSetWith "activeselection" "{}") + testCase "spikedistance" (fun _ -> layout |> jsonFieldIsSetWith "spikedistance" "1") + testCase "hoverdistance" (fun _ -> layout |> jsonFieldIsSetWith "hoverdistance" "1") + testCase "hoverlabel" (fun _ -> layout |> jsonFieldIsSetWith "hoverlabel" "{}") + testCase "transition" (fun _ -> layout |> jsonFieldIsSetWith "transition" "{}") + testCase "datarevision" (fun _ -> layout |> jsonFieldIsSetWith "datarevision" "\"lol\"") + testCase "uirevision" (fun _ -> layout |> jsonFieldIsSetWith "uirevision" "\"lol\"") + testCase "editrevision" (fun _ -> layout |> jsonFieldIsSetWith "editrevision" "\"lol\"") + testCase "selectrevision" (fun _ -> layout |> jsonFieldIsSetWith "selectrevision" "\"lol\"") + testCase "template" (fun _ -> layout |> jsonFieldIsSetWith "template" "{}") + testCase "meta" (fun _ -> layout |> jsonFieldIsSetWith "meta" "\"lol\"") + testCase "computed" (fun _ -> layout |> jsonFieldIsSetWith "computed" "\"lol\"") + testCase "grid" (fun _ -> layout |> jsonFieldIsSetWith "grid" "{}") + testCase "calendar" (fun _ -> layout |> jsonFieldIsSetWith "calendar" "\"discworld\"") + testCase "newshape" (fun _ -> layout |> jsonFieldIsSetWith "newshape" "{}") + testCase "minreducedheight" (fun _ -> layout |> jsonFieldIsSetWith "minreducedheight" "1") + testCase "minreducedwidth" (fun _ -> layout |> jsonFieldIsSetWith "minreducedwidth" "1") + testCase "activeshape" (fun _ -> layout |> jsonFieldIsSetWith "activeshape" "{}") + testCase "hidesources" (fun _ -> layout |> jsonFieldIsSetWith "hidesources" "true") + testCase "scattergap" (fun _ -> layout |> jsonFieldIsSetWith "scattergap" "1.0") + testCase "scattermode" (fun _ -> layout |> jsonFieldIsSetWith "scattermode" "\"group\"") + testCase "bargap" (fun _ -> layout |> jsonFieldIsSetWith "bargap" "1.0") + testCase "bargroupgap" (fun _ -> layout |> jsonFieldIsSetWith "bargroupgap" "1.0") + testCase "barmode" (fun _ -> layout |> jsonFieldIsSetWith "barmode" "\"group\"") + testCase "barnorm" (fun _ -> layout |> jsonFieldIsSetWith "barnorm" "\"fraction\"") + testCase "extendpiecolors" (fun _ -> layout |> jsonFieldIsSetWith "extendpiecolors" "true") + testCase "hiddenlabels" (fun _ -> layout |> jsonFieldIsSetWith "hiddenlabels" "[\"lol\"]") + testCase "piecolorway" (fun _ -> layout |> jsonFieldIsSetWith "piecolorway" "[]") + testCase "boxgap" (fun _ -> layout |> jsonFieldIsSetWith "boxgap" "1.0") + testCase "boxgroupgap" (fun _ -> layout |> jsonFieldIsSetWith "boxgroupgap" "1.0") + testCase "boxmode" (fun _ -> layout |> jsonFieldIsSetWith "boxmode" "\"group\"") + testCase "violingap" (fun _ -> layout |> jsonFieldIsSetWith "violingap" "1.0") + testCase "violingroupgap" (fun _ -> layout |> jsonFieldIsSetWith "violingroupgap" "1.0") + testCase "violinmode" (fun _ -> layout |> jsonFieldIsSetWith "violinmode" "\"group\"") + testCase "waterfallgap" (fun _ -> layout |> jsonFieldIsSetWith "waterfallgap" "1.0") + testCase "waterfallgroupgap" (fun _ -> layout |> jsonFieldIsSetWith "waterfallgroupgap" "1.0") + testCase "waterfallmode" (fun _ -> layout |> jsonFieldIsSetWith "waterfallmode" "\"group\"") + testCase "funnelgap" (fun _ -> layout |> jsonFieldIsSetWith "funnelgap" "1.0") + testCase "funnelgroupgap" (fun _ -> layout |> jsonFieldIsSetWith "funnelgroupgap" "1.0") + testCase "funnelmode" (fun _ -> layout |> jsonFieldIsSetWith "funnelmode" "\"group\"") + testCase "extendfunnelareacolors" (fun _ -> layout |> jsonFieldIsSetWith "extendfunnelareacolors" "true") + testCase "funnelareacolorway" (fun _ -> layout |> jsonFieldIsSetWith "funnelareacolorway" "[]") + testCase "extendsunburstcolors" (fun _ -> layout |> jsonFieldIsSetWith "extendsunburstcolors" "true") + testCase "sunburstcolorway" (fun _ -> layout |> jsonFieldIsSetWith "sunburstcolorway" "[]") + testCase "extendtreemapcolors" (fun _ -> layout |> jsonFieldIsSetWith "extendtreemapcolors" "true") + testCase "treemapcolorway" (fun _ -> layout |> jsonFieldIsSetWith "treemapcolorway" "[]") + testCase "extendiciclecolors" (fun _ -> layout |> jsonFieldIsSetWith "extendiciclecolors" "true") + testCase "iciclecolorway" (fun _ -> layout |> jsonFieldIsSetWith "iciclecolorway" "[]") + testCase "annotations" (fun _ -> layout |> jsonFieldIsSetWith "annotations" "[]") + testCase "shapes" (fun _ -> layout |> jsonFieldIsSetWith "shapes" "[]") + testCase "selections" (fun _ -> layout |> jsonFieldIsSetWith "selections" "[]") + testCase "images" (fun _ -> layout |> jsonFieldIsSetWith "images" "[]") + testCase "sliders" (fun _ -> layout |> jsonFieldIsSetWith "sliders" "[]") + testCase "updatemenus" (fun _ -> layout |> jsonFieldIsSetWith "updatemenus" "[]") + ] + + + +let combined = + Layout.combine + (Layout.init( + Annotations = [Annotation.init(Name = "first")], + Shapes = [Shape.init(Name = "first")], + Selections = [Selection.init(Name = "first")], + Images = [LayoutImage.init(Name = "first")], + Sliders = [Slider.init(Name = "first")], + HiddenLabels= ["first"], + UpdateMenus = [UpdateMenu.init(Name = "first")] + )) + (Layout.init( + Annotations = [Annotation.init(Name = "second")], + Shapes = [Shape.init(Name = "second")], + Selections = [Selection.init(Name = "second")], + Images = [LayoutImage.init(Name = "second")], + Sliders = [Slider.init(Name = "second")], + HiddenLabels= ["second"], + UpdateMenus = [UpdateMenu.init(Name = "second")] + )) + +let expectedCombined = + Layout.init( + Annotations = [Annotation.init(Name = "first"); Annotation.init(Name = "second")], + Shapes = [Shape.init(Name = "first"); Shape.init(Name = "second")], + Selections = [Selection.init(Name = "first"); Selection.init(Name = "second")], + Images = [LayoutImage.init(Name = "first"); LayoutImage.init(Name = "second")], + Sliders = [Slider.init(Name = "first"); Slider.init(Name = "second")], + HiddenLabels= ["first"; "second"], + UpdateMenus = [UpdateMenu.init(Name = "first"); UpdateMenu.init(Name = "second")] + ) + +[] +let ``Layout combine API tests`` = + testList "LayoutObjects.Layout API" [ + testCase "combine Annotations" (fun _ -> + Expect.sequenceEqual + (combined.TryGetTypedValue>("annotations")).Value + (expectedCombined.TryGetTypedValue>("annotations")).Value + "Layout.combine did not return the correct object" + ) + testCase "combine Shapes" (fun _ -> + Expect.sequenceEqual + (combined.TryGetTypedValue>("shapes")).Value + (expectedCombined.TryGetTypedValue>("shapes")).Value + "Layout.combine did not return the correct object" + ) + testCase "combine Selections" (fun _ -> + Expect.sequenceEqual + (combined.TryGetTypedValue>("selections")).Value + (expectedCombined.TryGetTypedValue>("selections")).Value + "Layout.combine did not return the correct object" + ) + testCase "combine Images" (fun _ -> + Expect.sequenceEqual + (combined.TryGetTypedValue>("images")).Value + (expectedCombined.TryGetTypedValue>("images")).Value + "Layout.combine did not return the correct object" + ) + testCase "combine Sliders" (fun _ -> + Expect.sequenceEqual + (combined.TryGetTypedValue>("sliders")).Value + (expectedCombined.TryGetTypedValue>("sliders")).Value + "Layout.combine did not return the correct object" + ) + testCase "combine HiddenLabels" (fun _ -> + Expect.sequenceEqual + (combined.TryGetTypedValue>("hiddenlabels")).Value + (expectedCombined.TryGetTypedValue>("hiddenlabels")).Value + "Layout.combine did not return the correct object" + ) + testCase "combine UpdateMenus" (fun _ -> + Expect.sequenceEqual + (combined.TryGetTypedValue>("updatemenus")).Value + (expectedCombined.TryGetTypedValue>("updatemenus")).Value + "Layout.combine did not return the correct object" + ) + ] + +let subplotLayout = + let l = Layout.init() + l?xaxis <- LinearAxis.init() + l?xaxis2 <- LinearAxis.init() + l?yaxis <- LinearAxis.init() + l?yaxis2 <- LinearAxis.init() + l?scene <- Scene.init() + l?scene2 <- Scene.init() + l?geo <- Geo.init() + l?geo2 <- Geo.init() + l?mapbox <- Mapbox.init() + l?mapbox2 <- Mapbox.init() + l?polar <- Polar.init() + l?polar2 <- Polar.init() + l?smith <- Smith.init() + l?smith2 <- Smith.init() + l?coloraxis <- ColorAxis.init() + l?coloraxis2 <- ColorAxis.init() + l?ternary <- Ternary.init() + l?ternary2 <- Ternary.init() + l?legend <- Legend.init() + l?legend2 <- Legend.init() + l + +[] +let ``Layout subplot API tests`` = + testList "LayoutObjects.Layout API" [ + testCase "subplots getXAxes" (fun _ -> + Expect.sequenceEqual + (Layout.getXAxes subplotLayout) + ["xaxis", LinearAxis.init(); "xaxis2", LinearAxis.init();] + "did not return correct collection of subplots" + ) + testCase "subplots getYAxes" (fun _ -> + Expect.sequenceEqual + (Layout.getYAxes subplotLayout) + ["yaxis", LinearAxis.init(); "yaxis2", LinearAxis.init();] + "did not return correct collection of subplots" + ) + testCase "subplots getScenes" (fun _ -> + Expect.sequenceEqual + (Layout.getScenes subplotLayout) + ["scene", Scene.init(); "scene2", Scene.init();] + "did not return correct collection of subplots" + ) + testCase "subplots getGeos" (fun _ -> + Expect.sequenceEqual + (Layout.getGeos subplotLayout) + ["geo", Geo.init(); "geo2", Geo.init();] + "did not return correct collection of subplots" + ) + testCase "subplots getMapboxes" (fun _ -> + Expect.sequenceEqual + (Layout.getMapboxes subplotLayout) + ["mapbox", Mapbox.init(); "mapbox2", Mapbox.init();] + "did not return correct collection of subplots" + ) + testCase "subplots getPolars" (fun _ -> + Expect.sequenceEqual + (Layout.getPolars subplotLayout) + ["polar", Polar.init(); "polar2", Polar.init();] + "did not return correct collection of subplots" + ) + testCase "subplots getSmiths" (fun _ -> + Expect.sequenceEqual + (Layout.getSmiths subplotLayout) + ["smith", Smith.init(); "smith2", Smith.init();] + "did not return correct collection of subplots" + ) + testCase "subplots getColorAxes" (fun _ -> + Expect.sequenceEqual + (Layout.getColorAxes subplotLayout) + ["coloraxis", ColorAxis.init(); "coloraxis2", ColorAxis.init();] + "did not return correct collection of subplots" + ) + testCase "subplots getTernaries" (fun _ -> + Expect.sequenceEqual + (Layout.getTernaries subplotLayout) + ["ternary", Ternary.init(); "ternary2", Ternary.init();] + "did not return correct collection of subplots" + ) + testCase "subplots getLegends" (fun _ -> + Expect.sequenceEqual + (Layout.getLegends subplotLayout) + ["legend", Legend.init(); "legend2", Legend.init();] + "did not return correct collection of subplots" + ) + ] \ No newline at end of file