Skip to content

Improve chart grids #453

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
May 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading