Skip to content

Commit ab7a9e8

Browse files
committed
#388: Add logic for positioning subplot titles in LayoutGrid
1 parent 9ae74f5 commit ab7a9e8

File tree

9 files changed

+274
-45
lines changed

9 files changed

+274
-45
lines changed

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

+10-1
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

+93-1
Original file line numberDiff line numberDiff line change
@@ -3084,6 +3084,9 @@ type Chart =
30843084
/// </summary>
30853085
/// <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>
30863086
/// <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>
3087+
/// <param name ="SubPlotTitles">A collection of titles for the individual subplots.</param>
3088+
/// <param name ="SubPlotTitleFont">The font of the subplot titles</param>
3089+
/// <param name ="SubPlotTitleOffset">A vertical offset applied to each subplot title, moving it upwards if positive and vice versa</param>
30873090
/// <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>
30883091
/// <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>
30893092
/// <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>
@@ -3099,6 +3102,9 @@ type Chart =
30993102
(
31003103
nRows: int,
31013104
nCols: int,
3105+
[<Optional; DefaultParameterValue(null)>] ?SubPlotTitles: #seq<string>,
3106+
[<Optional; DefaultParameterValue(null)>] ?SubPlotTitleFont: Font,
3107+
[<Optional; DefaultParameterValue(null)>] ?SubPlotTitleOffset: float,
31023108
[<Optional; DefaultParameterValue(null)>] ?SubPlots: (StyleParam.LinearAxisId * StyleParam.LinearAxisId)[][],
31033109
[<Optional; DefaultParameterValue(null)>] ?XAxes: StyleParam.LinearAxisId[],
31043110
[<Optional; DefaultParameterValue(null)>] ?YAxes: StyleParam.LinearAxisId[],
@@ -3112,12 +3118,76 @@ type Chart =
31123118
) =
31133119
fun (gCharts: #seq<GenericChart>) ->
31143120

3121+
// 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
3122+
let getGridCellDimensions (gridDimensionStart: float) (gridDimensionEnd: float) (gap: float) (length: int) (reversed: bool) =
3123+
// 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
3124+
3125+
let step = (gridDimensionEnd - gridDimensionStart) / (float length - gap)
3126+
let cellDomain = step * (1. - gap)
3127+
3128+
Array.init length (fun i ->
3129+
let cellStart = gridDimensionStart + (step * float i)
3130+
(cellStart, cellStart + cellDomain)
3131+
)
3132+
|> fun p -> if reversed then p else Array.rev p
3133+
3134+
// calculates the positions of the subplot titles
3135+
// titles are placed in the middle of the top edge of each cell in a layout grid as annotations with paper copordinates.
3136+
let calculateSubplotTitlePositions (gridDimensionStart: float) (gridDimensionEnd: float) (xgap: float) (ygap: float) (nrows: int) (ncols: int) (reversed:bool) =
3137+
3138+
let subPlotTitleOffset = defaultArg SubPlotTitleOffset 0.
3139+
3140+
let xDomains = getGridCellDimensions gridDimensionStart gridDimensionEnd xgap ncols true
3141+
let yDomains = getGridCellDimensions gridDimensionStart gridDimensionEnd ygap nrows reversed
3142+
3143+
Array.init nrows (fun r ->
3144+
Array.init ncols (fun c ->
3145+
let xStart = fst xDomains.[c]
3146+
let xEnd = snd xDomains.[c]
3147+
let yEnd = snd yDomains.[r]
3148+
(r,c), ((xStart + xEnd) / 2., yEnd + subPlotTitleOffset)
3149+
)
3150+
)
3151+
|> Array.concat
3152+
31153153
let pattern =
31163154
defaultArg Pattern StyleParam.LayoutGridPattern.Independent
31173155

3156+
let rowOrder = defaultArg RowOrder StyleParam.LayoutGridRowOrder.TopToBottom
3157+
3158+
let xGap = defaultArg XGap (if pattern = StyleParam.LayoutGridPattern.Coupled then 0.1 else 0.2)
3159+
let yGap = defaultArg YGap (if pattern = StyleParam.LayoutGridPattern.Coupled then 0.1 else 0.3)
3160+
3161+
31183162
let hasSharedAxes =
31193163
pattern = StyleParam.LayoutGridPattern.Coupled
31203164

3165+
let subPlotTitleAnnotations =
3166+
match SubPlotTitles with
3167+
| Some titles ->
3168+
3169+
let reversed = rowOrder = StyleParam.LayoutGridRowOrder.BottomToTop
3170+
3171+
let positions =
3172+
calculateSubplotTitlePositions 0. 1. xGap yGap nRows nCols reversed
3173+
3174+
titles
3175+
|> Seq.zip positions[0 .. (Seq.length titles) - 1]
3176+
|> Seq.map (fun (((rowIndex, colIndex), (x, y)), title) ->
3177+
Annotation.init(
3178+
X = x,
3179+
XRef = "paper",
3180+
XAnchor = StyleParam.XAnchorPosition.Center,
3181+
Y = y,
3182+
YRef = "paper",
3183+
YAnchor = StyleParam.YAnchorPosition.Bottom,
3184+
Text = title,
3185+
ShowArrow = false,
3186+
?Font = SubPlotTitleFont
3187+
)
3188+
)
3189+
| None -> [||]
3190+
31213191
// rows x cols coordinate grid
31223192
let gridCoordinates =
31233193
Array.init nRows (fun rowIndex -> Array.init nCols (fun colIndex -> rowIndex + 1, colIndex + 1))
@@ -3255,15 +3325,16 @@ type Chart =
32553325
t :?> TraceTernary |> TraceTernaryStyle.SetTernary ternaryAnchor :> Trace)
32563326
|> Chart.withTernary (ternary, (i + 1)))
32573327
|> Chart.combine
3328+
|> Chart.withAnnotations(subPlotTitleAnnotations, Append=true)
32583329
|> Chart.withLayoutGrid (
32593330
LayoutGrid.init (
32603331
Rows = nRows,
32613332
Columns = nCols,
32623333
Pattern = pattern,
3334+
RowOrder = rowOrder,
32633335
?SubPlots = SubPlots,
32643336
?XAxes = XAxes,
32653337
?YAxes = YAxes,
3266-
?RowOrder = RowOrder,
32673338
?XGap = XGap,
32683339
?YGap = YGap,
32693340
?Domain = Domain,
@@ -3279,6 +3350,9 @@ type Chart =
32793350
///
32803351
/// prevent this behaviour by using Chart.Invisible at the cells that should be empty.
32813352
/// </summary>
3353+
/// <param name ="SubPlotTitles">A collection of titles for the individual subplots.</param>
3354+
/// <param name ="SubPlotTitleFont">The font of the subplot titles</param>
3355+
/// <param name ="SubPlotTitleOffset">A vertical offset applied to each subplot title, moving it upwards if positive and vice versa</param>
32823356
/// <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>
32833357
/// <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>
32843358
/// <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>
@@ -3292,6 +3366,9 @@ type Chart =
32923366
[<CompiledName("Grid")>]
32933367
static member Grid
32943368
(
3369+
[<Optional; DefaultParameterValue(null)>] ?SubPlotTitles: #seq<string>,
3370+
[<Optional; DefaultParameterValue(null)>] ?SubPlotTitleFont: Font,
3371+
[<Optional; DefaultParameterValue(null)>] ?SubPlotTitleOffset: float,
32953372
[<Optional; DefaultParameterValue(null)>] ?SubPlots: (StyleParam.LinearAxisId * StyleParam.LinearAxisId)[][],
32963373
[<Optional; DefaultParameterValue(null)>] ?XAxes: StyleParam.LinearAxisId[],
32973374
[<Optional; DefaultParameterValue(null)>] ?YAxes: StyleParam.LinearAxisId[],
@@ -3340,6 +3417,9 @@ type Chart =
33403417
|> Chart.Grid(
33413418
nRows,
33423419
nCols,
3420+
?SubPlotTitles = SubPlotTitles,
3421+
?SubPlotTitleFont = SubPlotTitleFont,
3422+
?SubPlotTitleOffset = SubPlotTitleOffset,
33433423
?SubPlots = SubPlots,
33443424
?XAxes = XAxes,
33453425
?YAxes = YAxes,
@@ -3357,6 +3437,9 @@ type Chart =
33573437
|> Chart.Grid(
33583438
nRows,
33593439
nCols,
3440+
?SubPlotTitles = SubPlotTitles,
3441+
?SubPlotTitleFont = SubPlotTitleFont,
3442+
?SubPlotTitleOffset = SubPlotTitleOffset,
33603443
?SubPlots = SubPlots,
33613444
?XAxes = XAxes,
33623445
?YAxes = YAxes,
@@ -3371,6 +3454,9 @@ type Chart =
33713454

33723455
/// Creates a chart stack (a subplot grid with one column) from the input charts.
33733456
/// </summary>
3457+
/// <param name ="SubPlotTitles">A collection of titles for the individual subplots.</param>
3458+
/// <param name ="SubPlotTitleFont">The font of the subplot titles</param>
3459+
/// <param name ="SubPlotTitleOffset">A vertical offset applied to each subplot title, moving it upwards if positive and vice versa</param>
33743460
/// <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>
33753461
/// <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>
33763462
/// <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>
@@ -3384,6 +3470,9 @@ type Chart =
33843470
[<CompiledName("SingleStack")>]
33853471
static member SingleStack
33863472
(
3473+
[<Optional; DefaultParameterValue(null)>] ?SubPlotTitles: #seq<string>,
3474+
[<Optional; DefaultParameterValue(null)>] ?SubPlotTitleFont: Font,
3475+
[<Optional; DefaultParameterValue(null)>] ?SubPlotTitleOffset: float,
33873476
[<Optional; DefaultParameterValue(null)>] ?SubPlots: (StyleParam.LinearAxisId * StyleParam.LinearAxisId)[][],
33883477
[<Optional; DefaultParameterValue(null)>] ?XAxes: StyleParam.LinearAxisId[],
33893478
[<Optional; DefaultParameterValue(null)>] ?YAxes: StyleParam.LinearAxisId[],
@@ -3402,6 +3491,9 @@ type Chart =
34023491
|> Chart.Grid(
34033492
nRows = Seq.length gCharts,
34043493
nCols = 1,
3494+
?SubPlotTitles = SubPlotTitles,
3495+
?SubPlotTitleFont = SubPlotTitleFont,
3496+
?SubPlotTitleOffset = SubPlotTitleOffset,
34053497
?SubPlots = SubPlots,
34063498
?XAxes = XAxes,
34073499
?YAxes = YAxes,

tests/Common/FSharpTestBase/FSharpTestBase.fsproj

+1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
</ItemGroup>
1313

1414
<ItemGroup>
15+
<Compile Include="TestCharts\FeatureAdditions\Grid_SubPlotTitles.fs" />
1516
<Compile Include="TestCharts\FeatureAdditions\Fix_HoverInfo.fs" />
1617
<Compile Include="TestCharts\FeatureAdditions\UpdateMenuButton_Args.fs" />
1718
<Compile Include="TestCharts\FeatureAdditions\Accessible_Contours.fs" />

0 commit comments

Comments
 (0)