Skip to content

Commit

Permalink
Merge pull request #453 from plotly/fix-grid
Browse files Browse the repository at this point in the history
Improve chart grids
  • Loading branch information
kMutagene committed May 27, 2024
2 parents 290bfc6 + 66496cc commit 3787bec
Show file tree
Hide file tree
Showing 16 changed files with 936 additions and 61 deletions.
11 changes: 10 additions & 1 deletion src/Plotly.NET.CSharp/ChartAPI/Chart.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ public static partial class Chart
/// <param name ="gCharts">The charts to display on the grid.</param>
/// <param name ="nRows">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.</param>
/// <param name ="nCols">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.</param>
/// <param name ="SubPlotTitles">A collection of titles for the individual subplots.</param>
/// <param name ="SubPlotTitleFont">The font of the subplot titles</param>
/// <param name ="SubPlotTitleOffset">A vertical offset applied to each subplot title, moving it upwards if positive and vice versa</param>
/// <param name ="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.</param>
/// <param name ="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.</param>
/// <param name ="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.</param>
Expand All @@ -37,6 +40,9 @@ public static GenericChart Grid(
IEnumerable<GenericChart> gCharts,
int nRows,
int nCols,
Optional<IEnumerable<string>> SubPlotTitles = default,
Optional<Font> SubPlotTitleFont = default,
Optional<double> SubPlotTitleOffset = default,
Optional<Tuple<StyleParam.LinearAxisId, StyleParam.LinearAxisId>[][]> SubPlots = default,
Optional<StyleParam.LinearAxisId[]> XAxes = default,
Optional<StyleParam.LinearAxisId[]> YAxes = default,
Expand All @@ -48,9 +54,12 @@ public static GenericChart Grid(
Optional<StyleParam.LayoutGridXSide> XSide = default,
Optional<StyleParam.LayoutGridYSide> YSide = default
) =>
Plotly.NET.Chart.Grid<IEnumerable<GenericChart>>(
Plotly.NET.Chart.Grid<IEnumerable<string>,IEnumerable<GenericChart>>(
nRows: nRows,
nCols: nCols,
SubPlotTitles: SubPlotTitles.ToOption(),
SubPlotTitleFont: SubPlotTitleFont.ToOption(),
SubPlotTitleOffset: SubPlotTitleOffset.ToOption(),
SubPlots: SubPlots.ToOption(),
XAxes: XAxes.ToOption(),
YAxes: YAxes.ToOption(),
Expand Down
210 changes: 189 additions & 21 deletions src/Plotly.NET/ChartAPI/Chart.fs

Large diffs are not rendered by default.

16 changes: 16 additions & 0 deletions src/Plotly.NET/CommonAbstractions/StyleParams.fs
Original file line number Diff line number Diff line change
@@ -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/

Expand Down Expand Up @@ -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



[<RequireQualifiedAccess>]
type AutoTypeNumbers =
| ConvertTypes
Expand Down
143 changes: 142 additions & 1 deletion src/Plotly.NET/Layout/Layout.fs
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -584,6 +584,34 @@ type Layout() =
static member getLinearAxisById(id: StyleParam.SubPlotId) =
(fun (layout: Layout) -> layout |> Layout.tryGetLinearAxisById id |> Option.defaultValue (LinearAxis.init ()))

/// <summary>
/// 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).
/// </summary>
/// <param name="layout">The layout to get the x axes from</param>
static member getXAxes (layout: Layout) =
layout.GetProperties(includeInstanceProperties = false)
|> Seq.choose (fun kv ->
if StyleParam.SubPlotId.isValidXAxisId kv.Key then
match layout.TryGetTypedValue<LinearAxis>(kv.Key) with
| Some axis -> Some (kv.Key, axis)
| None -> None
else None
)

/// <summary>
/// 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).
/// </summary>
/// <param name="layout">The layout to get the y axes from</param>
static member getYAxes (layout: Layout) =
layout.GetProperties(includeInstanceProperties = false)
|> Seq.choose (fun kv ->
if StyleParam.SubPlotId.isValidYAxisId kv.Key then
match layout.TryGetTypedValue<LinearAxis>(kv.Key) with
| Some axis -> Some (kv.Key, axis)
| None -> None
else None
)

/// <summary>
/// Sets a linear axis object on the layout as a dynamic property with the given axis id.
/// </summary>
Expand Down Expand Up @@ -633,6 +661,21 @@ type Layout() =
static member getSceneById(id: StyleParam.SubPlotId) =
(fun (layout: Layout) -> layout |> Layout.tryGetSceneById id |> Option.defaultValue (Scene.init ()))


/// <summary>
/// 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).
/// </summary>
/// <param name="layout">The layout to get the scenes from</param>
static member getScenes (layout: Layout) =
layout.GetProperties(includeInstanceProperties = false)
|> Seq.choose (fun kv ->
if StyleParam.SubPlotId.isValidSceneId kv.Key then
match layout.TryGetTypedValue<Scene>(kv.Key) with
| Some scene -> Some (kv.Key, scene)
| None -> None
else None
)

/// <summary>
/// Sets a scene object on the layout as a dynamic property with the given scene id.
/// </summary>
Expand Down Expand Up @@ -674,6 +717,20 @@ type Layout() =
static member getGeoById(id: StyleParam.SubPlotId) =
(fun (layout: Layout) -> layout |> Layout.tryGetGeoById id |> Option.defaultValue (Geo.init ()))

/// <summary>
/// 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).
/// </summary>
/// <param name="layout">The layout to get the geos from</param>
static member getGeos (layout: Layout) =
layout.GetProperties(includeInstanceProperties = false)
|> Seq.choose (fun kv ->
if StyleParam.SubPlotId.isValidGeoId kv.Key then
match layout.TryGetTypedValue<Geo>(kv.Key) with
| Some geo -> Some (kv.Key, geo)
| None -> None
else None
)

/// <summary>
/// Sets a geo object on the layout as a dynamic property with the given geo id.
/// </summary>
Expand Down Expand Up @@ -717,6 +774,20 @@ type Layout() =
static member getMapboxById(id: StyleParam.SubPlotId) =
(fun (layout: Layout) -> layout |> Layout.tryGetMapboxById id |> Option.defaultValue (Mapbox.init ()))

/// <summary>
/// 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).
/// </summary>
/// <param name="layout">The layout to get the mapboxes from</param>
static member getMapboxes (layout: Layout) =
layout.GetProperties(includeInstanceProperties = false)
|> Seq.choose (fun kv ->
if StyleParam.SubPlotId.isValidMapboxId kv.Key then
match layout.TryGetTypedValue<Mapbox>(kv.Key) with
| Some mapbox -> Some (kv.Key, mapbox)
| None -> None
else None
)

/// <summary>
/// Sets a mapbox object on the layout as a dynamic property with the given mapbox id.
/// </summary>
Expand Down Expand Up @@ -762,6 +833,20 @@ type Layout() =
static member getPolarById(id: StyleParam.SubPlotId) =
(fun (layout: Layout) -> layout |> Layout.tryGetPolarById id |> Option.defaultValue (Polar.init ()))

/// <summary>
/// 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).
/// </summary>
/// <param name="layout">The layout to get the polars from</param>
static member getPolars (layout: Layout) =
layout.GetProperties(includeInstanceProperties = false)
|> Seq.choose (fun kv ->
if StyleParam.SubPlotId.isValidPolarId kv.Key then
match layout.TryGetTypedValue<Polar>(kv.Key) with
| Some polar -> Some (kv.Key, polar)
| None -> None
else None
)

/// <summary>
/// Sets a polar object on the layout as a dynamic property with the given polar id.
/// </summary>
Expand Down Expand Up @@ -807,6 +892,20 @@ type Layout() =
static member getSmithById(id: StyleParam.SubPlotId) =
(fun (layout: Layout) -> layout |> Layout.tryGetSmithById id |> Option.defaultValue (Smith.init ()))

/// <summary>
/// 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).
/// </summary>
/// <param name="layout">The layout to get the smiths from</param>
static member getSmiths (layout: Layout) =
layout.GetProperties(includeInstanceProperties = false)
|> Seq.choose (fun kv ->
if StyleParam.SubPlotId.isValidSmithId kv.Key then
match layout.TryGetTypedValue<Smith>(kv.Key) with
| Some smith -> Some (kv.Key, smith)
| None -> None
else None
)

/// <summary>
/// Sets a smith object on the layout as a dynamic property with the given smith id.
/// </summary>
Expand Down Expand Up @@ -852,6 +951,20 @@ type Layout() =
static member getColorAxisById(id: StyleParam.SubPlotId) =
(fun (layout: Layout) -> layout |> Layout.tryGetColorAxisById id |> Option.defaultValue (ColorAxis.init ()))

/// <summary>
/// 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).
/// </summary>
/// <param name="layout">The layout to get the color axes from</param>
static member getColorAxes (layout: Layout) =
layout.GetProperties(includeInstanceProperties = false)
|> Seq.choose (fun kv ->
if StyleParam.SubPlotId.isValidColorAxisId kv.Key then
match layout.TryGetTypedValue<ColorAxis>(kv.Key) with
| Some colorAxis -> Some (kv.Key, colorAxis)
| None -> None
else None
)

/// <summary>
/// Sets a ColorAxis object on the layout as a dynamic property with the given ColorAxis id.
/// </summary>
Expand Down Expand Up @@ -897,6 +1010,20 @@ type Layout() =
static member getTernaryById(id: StyleParam.SubPlotId) =
(fun (layout: Layout) -> layout |> Layout.tryGetTernaryById id |> Option.defaultValue (Ternary.init ()))

/// <summary>
/// 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).
/// </summary>
/// <param name="layout">The layout to get the ternaries from</param>
static member getTernaries (layout: Layout) =
layout.GetProperties(includeInstanceProperties = false)
|> Seq.choose (fun kv ->
if StyleParam.SubPlotId.isValidTernaryId kv.Key then
match layout.TryGetTypedValue<Ternary>(kv.Key) with
| Some ternary -> Some (kv.Key, ternary)
| None -> None
else None
)

/// <summary>
/// Sets a Ternary object on the layout as a dynamic property with the given Ternary id.
/// </summary>
Expand Down Expand Up @@ -945,6 +1072,20 @@ type Layout() =
static member tryGetLegendById(id: StyleParam.SubPlotId) =
(fun (layout: Layout) -> layout.TryGetTypedValue<Legend>(StyleParam.SubPlotId.toString id))

/// <summary>
/// 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).
/// </summary>
/// <param name="layout">The layout to get the color axes from</param>
static member getLegends (layout: Layout) =
layout.GetProperties(includeInstanceProperties = false)
|> Seq.choose (fun kv ->
if StyleParam.SubPlotId.isValidLegendId kv.Key then
match layout.TryGetTypedValue<Legend>(kv.Key) with
| Some legend -> Some (kv.Key, legend)
| None -> None
else None
)

/// <summary>
/// Combines the given Legend object with the one already present on the layout.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,4 +47,4 @@ type NewSelection() =
line |> DynObj.setValue newSelection "line"
Mode |> DynObj.setValueOptBy newSelection "mode" StyleParam.NewSelectionMode.convert

NewSelection)
newSelection)
2 changes: 2 additions & 0 deletions tests/Common/FSharpTestBase/FSharpTestBase.fsproj
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
</ItemGroup>

<ItemGroup>
<Compile Include="TestCharts\FeatureAdditions\Fix_3d_GridPosition.fs" />
<Compile Include="TestCharts\FeatureAdditions\Grid_SubPlotTitles.fs" />
<Compile Include="TestCharts\FeatureAdditions\Fix_HoverInfo.fs" />
<Compile Include="TestCharts\FeatureAdditions\UpdateMenuButton_Args.fs" />
<Compile Include="TestCharts\FeatureAdditions\Accessible_Contours.fs" />
Expand Down
Original file line number Diff line number Diff line change
@@ -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"])
Loading

0 comments on commit 3787bec

Please sign in to comment.