Skip to content

Commit 3787bec

Browse files
authored
Merge pull request #453 from plotly/fix-grid
Improve chart grids
2 parents 290bfc6 + 66496cc commit 3787bec

File tree

16 files changed

+936
-61
lines changed

16 files changed

+936
-61
lines changed

src/Plotly.NET.CSharp/ChartAPI/Chart.cs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@ public static partial class Chart
2323
/// <param name ="gCharts">The charts to display on the grid.</param>
2424
/// <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>
2525
/// <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>
26+
/// <param name ="SubPlotTitles">A collection of titles for the individual subplots.</param>
27+
/// <param name ="SubPlotTitleFont">The font of the subplot titles</param>
28+
/// <param name ="SubPlotTitleOffset">A vertical offset applied to each subplot title, moving it upwards if positive and vice versa</param>
2629
/// <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>
2730
/// <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>
2831
/// <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>
@@ -37,6 +40,9 @@ public static GenericChart Grid(
3740
IEnumerable<GenericChart> gCharts,
3841
int nRows,
3942
int nCols,
43+
Optional<IEnumerable<string>> SubPlotTitles = default,
44+
Optional<Font> SubPlotTitleFont = default,
45+
Optional<double> SubPlotTitleOffset = default,
4046
Optional<Tuple<StyleParam.LinearAxisId, StyleParam.LinearAxisId>[][]> SubPlots = default,
4147
Optional<StyleParam.LinearAxisId[]> XAxes = default,
4248
Optional<StyleParam.LinearAxisId[]> YAxes = default,
@@ -48,9 +54,12 @@ public static GenericChart Grid(
4854
Optional<StyleParam.LayoutGridXSide> XSide = default,
4955
Optional<StyleParam.LayoutGridYSide> YSide = default
5056
) =>
51-
Plotly.NET.Chart.Grid<IEnumerable<GenericChart>>(
57+
Plotly.NET.Chart.Grid<IEnumerable<string>,IEnumerable<GenericChart>>(
5258
nRows: nRows,
5359
nCols: nCols,
60+
SubPlotTitles: SubPlotTitles.ToOption(),
61+
SubPlotTitleFont: SubPlotTitleFont.ToOption(),
62+
SubPlotTitleOffset: SubPlotTitleOffset.ToOption(),
5463
SubPlots: SubPlots.ToOption(),
5564
XAxes: XAxes.ToOption(),
5665
YAxes: YAxes.ToOption(),

src/Plotly.NET/ChartAPI/Chart.fs

Lines changed: 189 additions & 21 deletions
Large diffs are not rendered by default.

src/Plotly.NET/CommonAbstractions/StyleParams.fs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
namespace Plotly.NET
22

33
open System
4+
open System.Text
5+
open System.Text.RegularExpressions
46
// https://plot.ly/javascript/reference/
57
// https://plot.ly/javascript-graphing-library/reference/
68

@@ -284,10 +286,24 @@ module StyleParam =
284286
else
285287
sprintf "legend%i" id
286288

289+
static member isValidXAxisId (id: string) = Regex.IsMatch(id, "xaxis[0-9]*")
290+
static member isValidYAxisId (id: string) = Regex.IsMatch(id, "yaxis[0-9]*")
291+
static member isValidZAxisId (id: string) = Regex.IsMatch(id, "zaxis")
292+
static member isValidColorAxisId (id: string) = Regex.IsMatch(id, "coloraxis[0-9]*")
293+
static member isValidGeoId (id: string) = Regex.IsMatch(id, "geo[0-9]*")
294+
static member isValidMapboxId (id: string) = Regex.IsMatch(id, "mapbox[0-9]*")
295+
static member isValidPolarId (id: string) = Regex.IsMatch(id, "polar[0-9]*")
296+
static member isValidTernaryId (id: string) = Regex.IsMatch(id, "ternary[0-9]*")
297+
static member isValidSceneId (id: string) = Regex.IsMatch(id, "scene[0-9]*")
298+
static member isValidSmithId (id: string) = Regex.IsMatch(id, "smith[0-9]*")
299+
static member isValidLegendId (id: string) = Regex.IsMatch(id, "legend[0-9]*")
300+
287301
static member convert = SubPlotId.toString >> box
288302
override this.ToString() = this |> SubPlotId.toString
289303
member this.Convert() = this |> SubPlotId.convert
290304

305+
306+
291307
[<RequireQualifiedAccess>]
292308
type AutoTypeNumbers =
293309
| ConvertTypes

src/Plotly.NET/Layout/Layout.fs

Lines changed: 142 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -458,7 +458,7 @@ type Layout() =
458458
FunnelGap |> DynObj.setValueOpt layout "funnelgap"
459459
FunnelGroupGap |> DynObj.setValueOpt layout "funnelgroupgap"
460460
FunnelMode |> DynObj.setValueOptBy layout "funnelmode" StyleParam.FunnelMode.convert
461-
ExtendFunnelAreaColors |> DynObj.setValueOpt layout "extendfunnelareacolors "
461+
ExtendFunnelAreaColors |> DynObj.setValueOpt layout "extendfunnelareacolors"
462462
FunnelAreaColorWay |> DynObj.setValueOpt layout "funnelareacolorway"
463463
ExtendSunBurstColors |> DynObj.setValueOpt layout "extendsunburstcolors"
464464
SunBurstColorWay |> DynObj.setValueOpt layout "sunburstcolorway"
@@ -584,6 +584,34 @@ type Layout() =
584584
static member getLinearAxisById(id: StyleParam.SubPlotId) =
585585
(fun (layout: Layout) -> layout |> Layout.tryGetLinearAxisById id |> Option.defaultValue (LinearAxis.init ()))
586586

587+
/// <summary>
588+
/// 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).
589+
/// </summary>
590+
/// <param name="layout">The layout to get the x axes from</param>
591+
static member getXAxes (layout: Layout) =
592+
layout.GetProperties(includeInstanceProperties = false)
593+
|> Seq.choose (fun kv ->
594+
if StyleParam.SubPlotId.isValidXAxisId kv.Key then
595+
match layout.TryGetTypedValue<LinearAxis>(kv.Key) with
596+
| Some axis -> Some (kv.Key, axis)
597+
| None -> None
598+
else None
599+
)
600+
601+
/// <summary>
602+
/// 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).
603+
/// </summary>
604+
/// <param name="layout">The layout to get the y axes from</param>
605+
static member getYAxes (layout: Layout) =
606+
layout.GetProperties(includeInstanceProperties = false)
607+
|> Seq.choose (fun kv ->
608+
if StyleParam.SubPlotId.isValidYAxisId kv.Key then
609+
match layout.TryGetTypedValue<LinearAxis>(kv.Key) with
610+
| Some axis -> Some (kv.Key, axis)
611+
| None -> None
612+
else None
613+
)
614+
587615
/// <summary>
588616
/// Sets a linear axis object on the layout as a dynamic property with the given axis id.
589617
/// </summary>
@@ -633,6 +661,21 @@ type Layout() =
633661
static member getSceneById(id: StyleParam.SubPlotId) =
634662
(fun (layout: Layout) -> layout |> Layout.tryGetSceneById id |> Option.defaultValue (Scene.init ()))
635663

664+
665+
/// <summary>
666+
/// 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).
667+
/// </summary>
668+
/// <param name="layout">The layout to get the scenes from</param>
669+
static member getScenes (layout: Layout) =
670+
layout.GetProperties(includeInstanceProperties = false)
671+
|> Seq.choose (fun kv ->
672+
if StyleParam.SubPlotId.isValidSceneId kv.Key then
673+
match layout.TryGetTypedValue<Scene>(kv.Key) with
674+
| Some scene -> Some (kv.Key, scene)
675+
| None -> None
676+
else None
677+
)
678+
636679
/// <summary>
637680
/// Sets a scene object on the layout as a dynamic property with the given scene id.
638681
/// </summary>
@@ -674,6 +717,20 @@ type Layout() =
674717
static member getGeoById(id: StyleParam.SubPlotId) =
675718
(fun (layout: Layout) -> layout |> Layout.tryGetGeoById id |> Option.defaultValue (Geo.init ()))
676719

720+
/// <summary>
721+
/// 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).
722+
/// </summary>
723+
/// <param name="layout">The layout to get the geos from</param>
724+
static member getGeos (layout: Layout) =
725+
layout.GetProperties(includeInstanceProperties = false)
726+
|> Seq.choose (fun kv ->
727+
if StyleParam.SubPlotId.isValidGeoId kv.Key then
728+
match layout.TryGetTypedValue<Geo>(kv.Key) with
729+
| Some geo -> Some (kv.Key, geo)
730+
| None -> None
731+
else None
732+
)
733+
677734
/// <summary>
678735
/// Sets a geo object on the layout as a dynamic property with the given geo id.
679736
/// </summary>
@@ -717,6 +774,20 @@ type Layout() =
717774
static member getMapboxById(id: StyleParam.SubPlotId) =
718775
(fun (layout: Layout) -> layout |> Layout.tryGetMapboxById id |> Option.defaultValue (Mapbox.init ()))
719776

777+
/// <summary>
778+
/// 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).
779+
/// </summary>
780+
/// <param name="layout">The layout to get the mapboxes from</param>
781+
static member getMapboxes (layout: Layout) =
782+
layout.GetProperties(includeInstanceProperties = false)
783+
|> Seq.choose (fun kv ->
784+
if StyleParam.SubPlotId.isValidMapboxId kv.Key then
785+
match layout.TryGetTypedValue<Mapbox>(kv.Key) with
786+
| Some mapbox -> Some (kv.Key, mapbox)
787+
| None -> None
788+
else None
789+
)
790+
720791
/// <summary>
721792
/// Sets a mapbox object on the layout as a dynamic property with the given mapbox id.
722793
/// </summary>
@@ -762,6 +833,20 @@ type Layout() =
762833
static member getPolarById(id: StyleParam.SubPlotId) =
763834
(fun (layout: Layout) -> layout |> Layout.tryGetPolarById id |> Option.defaultValue (Polar.init ()))
764835

836+
/// <summary>
837+
/// 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).
838+
/// </summary>
839+
/// <param name="layout">The layout to get the polars from</param>
840+
static member getPolars (layout: Layout) =
841+
layout.GetProperties(includeInstanceProperties = false)
842+
|> Seq.choose (fun kv ->
843+
if StyleParam.SubPlotId.isValidPolarId kv.Key then
844+
match layout.TryGetTypedValue<Polar>(kv.Key) with
845+
| Some polar -> Some (kv.Key, polar)
846+
| None -> None
847+
else None
848+
)
849+
765850
/// <summary>
766851
/// Sets a polar object on the layout as a dynamic property with the given polar id.
767852
/// </summary>
@@ -807,6 +892,20 @@ type Layout() =
807892
static member getSmithById(id: StyleParam.SubPlotId) =
808893
(fun (layout: Layout) -> layout |> Layout.tryGetSmithById id |> Option.defaultValue (Smith.init ()))
809894

895+
/// <summary>
896+
/// 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).
897+
/// </summary>
898+
/// <param name="layout">The layout to get the smiths from</param>
899+
static member getSmiths (layout: Layout) =
900+
layout.GetProperties(includeInstanceProperties = false)
901+
|> Seq.choose (fun kv ->
902+
if StyleParam.SubPlotId.isValidSmithId kv.Key then
903+
match layout.TryGetTypedValue<Smith>(kv.Key) with
904+
| Some smith -> Some (kv.Key, smith)
905+
| None -> None
906+
else None
907+
)
908+
810909
/// <summary>
811910
/// Sets a smith object on the layout as a dynamic property with the given smith id.
812911
/// </summary>
@@ -852,6 +951,20 @@ type Layout() =
852951
static member getColorAxisById(id: StyleParam.SubPlotId) =
853952
(fun (layout: Layout) -> layout |> Layout.tryGetColorAxisById id |> Option.defaultValue (ColorAxis.init ()))
854953

954+
/// <summary>
955+
/// 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).
956+
/// </summary>
957+
/// <param name="layout">The layout to get the color axes from</param>
958+
static member getColorAxes (layout: Layout) =
959+
layout.GetProperties(includeInstanceProperties = false)
960+
|> Seq.choose (fun kv ->
961+
if StyleParam.SubPlotId.isValidColorAxisId kv.Key then
962+
match layout.TryGetTypedValue<ColorAxis>(kv.Key) with
963+
| Some colorAxis -> Some (kv.Key, colorAxis)
964+
| None -> None
965+
else None
966+
)
967+
855968
/// <summary>
856969
/// Sets a ColorAxis object on the layout as a dynamic property with the given ColorAxis id.
857970
/// </summary>
@@ -897,6 +1010,20 @@ type Layout() =
8971010
static member getTernaryById(id: StyleParam.SubPlotId) =
8981011
(fun (layout: Layout) -> layout |> Layout.tryGetTernaryById id |> Option.defaultValue (Ternary.init ()))
8991012

1013+
/// <summary>
1014+
/// 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).
1015+
/// </summary>
1016+
/// <param name="layout">The layout to get the ternaries from</param>
1017+
static member getTernaries (layout: Layout) =
1018+
layout.GetProperties(includeInstanceProperties = false)
1019+
|> Seq.choose (fun kv ->
1020+
if StyleParam.SubPlotId.isValidTernaryId kv.Key then
1021+
match layout.TryGetTypedValue<Ternary>(kv.Key) with
1022+
| Some ternary -> Some (kv.Key, ternary)
1023+
| None -> None
1024+
else None
1025+
)
1026+
9001027
/// <summary>
9011028
/// Sets a Ternary object on the layout as a dynamic property with the given Ternary id.
9021029
/// </summary>
@@ -945,6 +1072,20 @@ type Layout() =
9451072
static member tryGetLegendById(id: StyleParam.SubPlotId) =
9461073
(fun (layout: Layout) -> layout.TryGetTypedValue<Legend>(StyleParam.SubPlotId.toString id))
9471074

1075+
/// <summary>
1076+
/// 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).
1077+
/// </summary>
1078+
/// <param name="layout">The layout to get the color axes from</param>
1079+
static member getLegends (layout: Layout) =
1080+
layout.GetProperties(includeInstanceProperties = false)
1081+
|> Seq.choose (fun kv ->
1082+
if StyleParam.SubPlotId.isValidLegendId kv.Key then
1083+
match layout.TryGetTypedValue<Legend>(kv.Key) with
1084+
| Some legend -> Some (kv.Key, legend)
1085+
| None -> None
1086+
else None
1087+
)
1088+
9481089
/// <summary>
9491090
/// Combines the given Legend object with the one already present on the layout.
9501091
/// </summary>

src/Plotly.NET/Layout/ObjectAbstractions/Common/NewSelection.fs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,4 +47,4 @@ type NewSelection() =
4747
line |> DynObj.setValue newSelection "line"
4848
Mode |> DynObj.setValueOptBy newSelection "mode" StyleParam.NewSelectionMode.convert
4949

50-
NewSelection)
50+
newSelection)

tests/Common/FSharpTestBase/FSharpTestBase.fsproj

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
</ItemGroup>
1313

1414
<ItemGroup>
15+
<Compile Include="TestCharts\FeatureAdditions\Fix_3d_GridPosition.fs" />
16+
<Compile Include="TestCharts\FeatureAdditions\Grid_SubPlotTitles.fs" />
1517
<Compile Include="TestCharts\FeatureAdditions\Fix_HoverInfo.fs" />
1618
<Compile Include="TestCharts\FeatureAdditions\UpdateMenuButton_Args.fs" />
1719
<Compile Include="TestCharts\FeatureAdditions\Accessible_Contours.fs" />
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
module Fix_3d_GridPosition
2+
3+
open Plotly.NET
4+
open Plotly.NET.TraceObjects
5+
open Plotly.NET.LayoutObjects
6+
open DynamicObj
7+
8+
// https://github.com/plotly/Plotly.NET/issues/413
9+
10+
module ``Remove all existing subplots from individual charts on grid creation #413`` =
11+
12+
let ``2x2 grid with only 3D charts and correct scene positioning`` =
13+
[
14+
Chart.Point3D(xyz = [1,3,2], UseDefaults = false)
15+
Chart.Point3D(xyz = [1,3,2], UseDefaults = false)
16+
Chart.Point3D(xyz = [1,3,2], UseDefaults = false)
17+
Chart.Point3D(xyz = [1,3,2], UseDefaults = false)
18+
]
19+
|> Chart.Grid(2,2, SubPlotTitles = ["1";"2";"3";"4"])
20+
21+
let ``2x2 grid chart creation ignores other scenes`` =
22+
[
23+
Chart.Point3D(xyz = [1,3,2], UseDefaults = false)
24+
|> Chart.withScene(Scene.init(), Id = 2)
25+
Chart.Point3D(xyz = [1,3,2], UseDefaults = false)
26+
|> Chart.withScene(Scene.init(), Id = 420)
27+
Chart.Point3D(xyz = [1,3,2], UseDefaults = false)
28+
|> Chart.withScene(Scene.init(), Id = 69)
29+
Chart.Point3D(xyz = [1,3,2], UseDefaults = false)
30+
|> Chart.withScene(Scene.init(), Id = 1337)
31+
]
32+
|> Chart.Grid(2,2, SubPlotTitles = ["1";"2";"3";"4"])

0 commit comments

Comments
 (0)