Skip to content

Commit

Permalink
#406: plotly.js v2.22.0: implement basic multi-legends.
Browse files Browse the repository at this point in the history
- extend SubplotId with Legend case
- getting, setting, and updating multiple Legend objects on the Layout
- setting Legend anchor on traces
  • Loading branch information
kMutagene committed Oct 23, 2023
1 parent 2c3fb24 commit f2c1864
Show file tree
Hide file tree
Showing 14 changed files with 164 additions and 67 deletions.
9 changes: 2 additions & 7 deletions src/Plotly.NET/CSharpLayer/GenericChartExtensions.fs
Original file line number Diff line number Diff line change
Expand Up @@ -461,7 +461,6 @@ module GenericChartExtensions =
(
[<Optional; DefaultParameterValue(null)>] ?Title: Title,
[<Optional; DefaultParameterValue(null)>] ?ShowLegend: bool,
[<Optional; DefaultParameterValue(null)>] ?Legend: Legend,
[<Optional; DefaultParameterValue(null)>] ?Margin: Margin,
[<Optional; DefaultParameterValue(null)>] ?AutoSize: bool,
[<Optional; DefaultParameterValue(null)>] ?Width: int,
Expand Down Expand Up @@ -532,7 +531,6 @@ module GenericChartExtensions =
|> Chart.withLayoutStyle (
?Title = Title,
?ShowLegend = ShowLegend,
?Legend = Legend,
?Margin = Margin,
?AutoSize = AutoSize,
?Width = Width,
Expand Down Expand Up @@ -611,11 +609,8 @@ module GenericChartExtensions =

// Set the LayoutGrid options of a Chart
[<CompiledName("WithLegend")>]
member this.WithLegend(legend: Legend) =
let layout =
GenericChart.getLayout this |> Layout.setLegend legend

GenericChart.setLayout layout this
member this.WithLegend(legend: Legend, [<Optional; DefaultParameterValue(null)>] ?Id: int) =
this |> Chart.withLegend (legend, ?Id = Id)

/// Sets a map for the given chart (will only work with traces supporting geo, e.g. choropleth, scattergeo)
[<CompiledName("WithMap")>]
Expand Down
64 changes: 43 additions & 21 deletions src/Plotly.NET/ChartAPI/Chart.fs
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,14 @@ type Chart =
static member withColorAxisAnchor(id: int) =
fun (ch: GenericChart) -> ch |> GenericChart.mapTrace (Trace.setColorAxisAnchor id)

/// <summary>
/// Sets the legend id for the chart's trace(s).
/// </summary>
/// <param name="id">The new Legend id for the chart's trace(s)</param>
[<CompiledName("WithLegendAnchor")>]
static member withLegendAnchor(id: int) =
fun (ch: GenericChart) -> ch |> GenericChart.mapTrace (Trace.setLegendAnchor id)

/// <summary>
/// Sets the marker for the chart's trace(s).
/// </summary>
Expand Down Expand Up @@ -742,7 +750,6 @@ type Chart =
/// </summary>
/// <param name="Title">Sets the title of the layout.</param>
/// <param name="ShowLegend">Determines whether or not a legend is drawn. Default is `true` if there is a trace to show and any of these: a) Two or more traces would by default be shown in the legend. b) One pie trace is shown in the legend. c) One trace is explicitly given with `showlegend: true`.</param>
/// <param name="Legend">Sets the legend styles of the layout.</param>
/// <param name="Margin">Sets the margins around the layout.</param>
/// <param name="AutoSize">Determines whether or not a layout width or height that has been left undefined by the user is initialized on each relayout. Note that, regardless of this attribute, an undefined layout width or height is always initialized on the first call to plot.</param>
/// <param name="Width">Sets the plot's width (in px).</param>
Expand Down Expand Up @@ -820,7 +827,6 @@ type Chart =
(
[<Optional; DefaultParameterValue(null)>] ?Title: Title,
[<Optional; DefaultParameterValue(null)>] ?ShowLegend: bool,
[<Optional; DefaultParameterValue(null)>] ?Legend: Legend,
[<Optional; DefaultParameterValue(null)>] ?Margin: Margin,
[<Optional; DefaultParameterValue(null)>] ?AutoSize: bool,
[<Optional; DefaultParameterValue(null)>] ?Width: int,
Expand Down Expand Up @@ -900,7 +906,6 @@ type Chart =
Layout.init (
?Title = Title,
?ShowLegend = ShowLegend,
?Legend = Legend,
?Margin = Margin,
?AutoSize = AutoSize,
?Width = Width,
Expand Down Expand Up @@ -2495,32 +2500,44 @@ type Chart =
ch |> Chart.withLayoutGrid grid)

/// <summary>
/// Sets the Legend for the chart's layout.
/// Sets the given Legend with the given id on the input chart's layout.
/// </summary>
/// <param name="legend">The new Legend for the chart's layout</param>
/// <param name="Combine">Whether or not to combine the objects if there is already a Legend object set (default is false)</param>
/// <param name="legend">The Legend to set on the chart's layout</param>
/// <param name="id">The target Legend id with which the Legend should be set.</param>
/// <param name="Combine">Whether or not to combine the objects if there is already an Legend set (default is false)</param>
[<CompiledName("SetLegend")>]
static member setLegend(legend: Legend, ?Combine: bool) =
let combine = defaultArg Combine false
static member setLegend
(
legend: Legend,
id: StyleParam.SubPlotId,
[<Optional; DefaultParameterValue(null)>] ?Combine: bool
) =
let combine = defaultArg Combine false

(fun (ch: GenericChart) ->
if combine then
ch |> GenericChart.mapLayout (Layout.updateLegend legend)
else
ch |> GenericChart.mapLayout (Layout.setLegend legend))
(fun (ch: GenericChart) ->
if combine then
ch |> GenericChart.mapLayout (Layout.updateLegendById(id, legend))
else
ch |> GenericChart.mapLayout (Layout.setLegend(id,legend))
)

/// <summary>
/// Sets the Legend for the chart's layout
/// Sets the given Legend on the input chart's layout, optionally passing a target Legend id.
///
/// If there is already a Legend set, the objects are combined.
/// If there is already an Legend set at the given id, the Legend objects are combined.
/// </summary>
/// <param name="legend">The new Legend for the chart's layout</param>
/// <param name="legend">The Legend to set on the chart's layout</param>
/// <param name="Id">The target Legend id with which the Legend should be set. Default is 1.</param>
[<CompiledName("WithLegend")>]
static member withLegend(legend: Legend) =
(fun (ch: GenericChart) -> ch |> Chart.setLegend (legend, true))
static member withLegend(legend: Legend, [<Optional; DefaultParameterValue(null)>] ?Id: int) =
let id =
Id |> Option.defaultValue 1 |> StyleParam.SubPlotId.Legend

fun (ch: GenericChart) ->
ch |> Chart.setLegend (legend, id, Combine = true)

/// <summary>
/// Sets the given Legend styles on the input chart's Legend.
/// Sets the given Legend styles on the input chart's Legend, optionally passing a target Legend id.
///
/// If there is already a Legend set , the styles are applied to it. If there is no Legend present, a new Legend object with the given styles will be set.
/// </summary>
Expand All @@ -2542,10 +2559,12 @@ type Chart =
/// <param name="TraceOrder">Determines the order at which the legend items are displayed. If "normal", the items are displayed top-to-bottom in the same order as the input data. If "reversed", the items are displayed in the opposite order as "normal". If "grouped", the items are displayed in groups (when a trace `legendgroup` is provided). if "grouped+reversed", the items are displayed in the opposite order as "grouped".</param>
/// <param name="UIRevision">Controls persistence of legend-driven changes in trace and pie label visibility. Defaults to `layout.uirevision`.</param>
/// <param name="VerticalAlign">Sets the vertical alignment of the symbols with respect to their associated text.</param>
/// <param name="Visible">Determines whether or not this legend is visible.</param>
/// <param name="X">Sets the x position (in normalized coordinates) of the legend. Defaults to "1.02" for vertical legends and defaults to "0" for horizontal legends.</param>
/// <param name="XAnchor">Sets the legend's horizontal position anchor. This anchor binds the `x` position to the "left", "center" or "right" of the legend. Value "auto" anchors legends to the right for `x` values greater than or equal to 2/3, anchors legends to the left for `x` values less than or equal to 1/3 and anchors legends with respect to their center otherwise.</param>
/// <param name="Y">Sets the y position (in normalized coordinates) of the legend. Defaults to "1" for vertical legends, defaults to "-0.1" for horizontal legends on graphs w/o range sliders and defaults to "1.1" for horizontal legends on graph with one or multiple range sliders.</param>
/// <param name="YAnchor">Sets the legend's vertical position anchor This anchor binds the `y` position to the "top", "middle" or "bottom" of the legend. Value "auto" anchors legends at their bottom for `y` values less than or equal to 1/3, anchors legends to at their top for `y` values greater than or equal to 2/3 and anchors legends with respect to their middle otherwise.</param>
/// <param name="Id">The target Legend id on which the styles should be applied. Default is 1.</param>
[<CompiledName("WithLegendStyle")>]
static member withLegendStyle
(
Expand All @@ -2567,10 +2586,12 @@ type Chart =
[<Optional; DefaultParameterValue(null)>] ?TraceOrder: StyleParam.TraceOrder,
[<Optional; DefaultParameterValue(null)>] ?UIRevision: string,
[<Optional; DefaultParameterValue(null)>] ?VerticalAlign: StyleParam.VerticalAlign,
[<Optional; DefaultParameterValue(null)>] ?Visible: bool,
[<Optional; DefaultParameterValue(null)>] ?X: float,
[<Optional; DefaultParameterValue(null)>] ?XAnchor: StyleParam.XAnchorPosition,
[<Optional; DefaultParameterValue(null)>] ?Y: float,
[<Optional; DefaultParameterValue(null)>] ?YAnchor: StyleParam.YAnchorPosition
[<Optional; DefaultParameterValue(null)>] ?YAnchor: StyleParam.YAnchorPosition,
[<Optional; DefaultParameterValue(null)>] ?Id: int
) =
(fun (ch: GenericChart) ->
let legend =
Expand All @@ -2593,13 +2614,14 @@ type Chart =
?TraceOrder = TraceOrder,
?UIRevision = UIRevision,
?VerticalAlign = VerticalAlign,
?Visible = Visible,
?X = X,
?XAnchor = XAnchor,
?Y = Y,
?YAnchor = YAnchor
)

ch |> Chart.withLegend legend)
ch |> Chart.withLegend(legend, ?Id = Id))

/// <summary>
///
Expand Down
6 changes: 6 additions & 0 deletions src/Plotly.NET/CommonAbstractions/StyleParams.fs
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,7 @@ module StyleParam =
| Scene of int
| Carpet of string
| Smith of int
| Legend of int

static member toString =
function
Expand Down Expand Up @@ -267,6 +268,11 @@ module StyleParam =
else
sprintf "smith%i" id
| Carpet id -> id
| Legend id ->
if id < 2 then
"legend"
else
sprintf "legend%i" id

static member convert = SubPlotId.toString >> box
override this.ToString() = this |> SubPlotId.toString
Expand Down
2 changes: 1 addition & 1 deletion src/Plotly.NET/Globals.fs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ open Giraffe.ViewEngine

/// The plotly js version loaded from cdn in rendered html docs
[<Literal>]
let PLOTLYJS_VERSION = "2.21.0"
let PLOTLYJS_VERSION = "2.22.0"

[<Literal>]
let SCRIPT_TEMPLATE =
Expand Down
67 changes: 44 additions & 23 deletions src/Plotly.NET/Layout/Layout.fs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ type Layout() =
/// </summary>
/// <param name="Title">Sets the title of the layout.</param>
/// <param name="ShowLegend">Determines whether or not a legend is drawn. Default is `true` if there is a trace to show and any of these: a) Two or more traces would by default be shown in the legend. b) One pie trace is shown in the legend. c) One trace is explicitly given with `showlegend: true`.</param>
/// <param name="Legend">Sets the legend styles of the layout.</param>
/// <param name="Margin">Sets the margins around the layout.</param>
/// <param name="AutoSize">Determines whether or not a layout width or height that has been left undefined by the user is initialized on each relayout. Note that, regardless of this attribute, an undefined layout width or height is always initialized on the first call to plot.</param>
/// <param name="Width">Sets the plot's width (in px).</param>
Expand Down Expand Up @@ -92,7 +91,6 @@ type Layout() =
(
[<Optional; DefaultParameterValue(null)>] ?Title: Title,
[<Optional; DefaultParameterValue(null)>] ?ShowLegend: bool,
[<Optional; DefaultParameterValue(null)>] ?Legend: Legend,
[<Optional; DefaultParameterValue(null)>] ?Margin: Margin,
[<Optional; DefaultParameterValue(null)>] ?AutoSize: bool,
[<Optional; DefaultParameterValue(null)>] ?Width: int,
Expand Down Expand Up @@ -170,7 +168,6 @@ type Layout() =
|> Layout.style (
?Title = Title,
?ShowLegend = ShowLegend,
?Legend = Legend,
?Margin = Margin,
?AutoSize = AutoSize,
?Width = Width,
Expand Down Expand Up @@ -250,7 +247,6 @@ type Layout() =
/// </summary>
/// <param name="Title">Sets the title of the layout.</param>
/// <param name="ShowLegend">Determines whether or not a legend is drawn. Default is `true` if there is a trace to show and any of these: a) Two or more traces would by default be shown in the legend. b) One pie trace is shown in the legend. c) One trace is explicitly given with `showlegend: true`.</param>
/// <param name="Legend">Sets the legend styles of the layout.</param>
/// <param name="Margin">Sets the margins around the layout.</param>
/// <param name="AutoSize">Determines whether or not a layout width or height that has been left undefined by the user is initialized on each relayout. Note that, regardless of this attribute, an undefined layout width or height is always initialized on the first call to plot.</param>
/// <param name="Width">Sets the plot's width (in px).</param>
Expand Down Expand Up @@ -327,7 +323,6 @@ type Layout() =
(
[<Optional; DefaultParameterValue(null)>] ?Title: Title,
[<Optional; DefaultParameterValue(null)>] ?ShowLegend: bool,
[<Optional; DefaultParameterValue(null)>] ?Legend: Legend,
[<Optional; DefaultParameterValue(null)>] ?Margin: Margin,
[<Optional; DefaultParameterValue(null)>] ?AutoSize: bool,
[<Optional; DefaultParameterValue(null)>] ?Width: int,
Expand Down Expand Up @@ -405,7 +400,6 @@ type Layout() =

Title |> DynObj.setValueOpt layout "title"
ShowLegend |> DynObj.setValueOpt layout "showlegend"
Legend |> DynObj.setValueOpt layout "legend"
Margin |> DynObj.setValueOpt layout "margin"
AutoSize |> DynObj.setValueOpt layout "autosize"
Width |> DynObj.setValueOpt layout "width"
Expand Down Expand Up @@ -945,30 +939,57 @@ type Layout() =
layout |> Layout.setLayoutGrid combined)

/// <summary>
/// Returns the legend object of the given layout.
///
/// If there is no legend set, returns an empty Legend object.
/// Returns Some(Legend) if there is an Legend object set on the layout with the given id, and None otherwise.
/// </summary>
/// <param name="layout">The layout to get the legend from</param>
static member getLegend(layout: Layout) =
layout |> Layout.tryGetTypedMember<Legend> "legend" |> Option.defaultValue (Legend.init ())
/// <param name="id">The target Legend id</param>
static member tryGetLegendById(id: StyleParam.SubPlotId) =
(fun (layout: Layout) -> layout.TryGetTypedValue<Legend>(StyleParam.SubPlotId.toString id))

/// <summary>
/// Returns a function that sets the Legend object of the given trace.
/// Combines the given Legend object with the one already present on the layout.
/// </summary>
/// <param name="legend">The new Legend object</param>
static member setLegend(legend: Legend) =
/// <param name="id">The target Legend id</param>
/// <param name="legend">The updated Legend object.</param>
static member updateLegendById(id: StyleParam.SubPlotId, legend: Legend) =
(fun (layout: Layout) ->
layout.SetValue("legend", legend)
layout)

match id with
| StyleParam.SubPlotId.Legend _ ->

let legend' =
match Layout.tryGetLegendById id layout with
| Some l -> (DynObj.combine l legend) :?> Legend
| None -> legend

legend' |> DynObj.setValue layout (StyleParam.SubPlotId.toString id)

layout
| _ ->
failwith
$"{StyleParam.SubPlotId.toString id} is an invalid subplot id for setting a legend on layout")

/// <summary>
/// Combines the given Legend object with the one already present on the layout.
/// Returns the Legend object of the layout with the given id.
///
/// If there is no Legend set, returns an empty Legend object.
/// </summary>
/// <param name="id">The target Legend id</param>
static member getLegendById(id: StyleParam.SubPlotId) =
(fun (layout: Layout) -> layout |> Layout.tryGetLegendById id |> Option.defaultValue (Legend.init ()))

/// <summary>
/// Sets a linear Legend object on the layout as a dynamic property with the given Legend id.
/// </summary>
/// <param name="legend">The updated Legend object</param>
static member updateLegend(legend: Legend) =
/// <param name="id">The Legend id of the new Legend</param>
/// <param name="legend">The Legend to add to the layout.</param>
static member setLegend(id: StyleParam.SubPlotId, legend: Legend) =
(fun (layout: Layout) ->
let combined =
(DynObj.combine (layout |> Layout.getLegend) legend) :?> Legend

layout |> Layout.setLegend combined)
match id with
| StyleParam.SubPlotId.Legend _ ->
legend |> DynObj.setValue layout (StyleParam.SubPlotId.toString id)
layout

| _ ->
failwith
$"{StyleParam.SubPlotId.toString id} is an invalid subplot id for setting a Legend on layout")
Loading

0 comments on commit f2c1864

Please sign in to comment.