Skip to content

Commit 2bf2026

Browse files
committed
add 2 and 3 set venn diagrams
1 parent ef10ded commit 2bf2026

7 files changed

Lines changed: 345 additions & 8 deletions

File tree

Lines changed: 230 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,230 @@
1+
namespace Plotly.NET
2+
3+
open Plotly.NET.LayoutObjects
4+
open Plotly.NET.TraceObjects
5+
6+
open DynamicObj
7+
open System
8+
open System.IO
9+
10+
open StyleParam
11+
open System.Runtime.InteropServices
12+
open System.Runtime.CompilerServices
13+
14+
[<AutoOpen>]
15+
module ChartComposite_SetComparison =
16+
17+
/// A single region of a Venn diagram: the labels of the sets it belongs to, together with its members.
18+
type internal VennSet<'a when 'a: comparison> = {
19+
Label: string list
20+
Set: Set<'a>
21+
}
22+
23+
/// A generic venn, mapping each region id to its corresponding set of members.
24+
type internal GenericVenn<'a when 'a: comparison> = Map<string, VennSet<'a>>
25+
26+
/// Converts a label list to a string id by concatenating the labels with "&".
27+
let internal labelToId (label: string list) = label |> String.concat "&"
28+
29+
/// Generates a generic venn from an array of labeled sets.
30+
let internal ofSetList (labels: string[]) (sets: Set<'a>[]) =
31+
let union = Set.unionMany sets
32+
33+
let toLabel (membership: bool[]) =
34+
membership
35+
|> Array.mapi (fun i isMember -> if isMember then Some labels.[i] else None)
36+
|> Array.choose id
37+
|> Array.toList
38+
39+
union
40+
|> Seq.map (fun item -> item, Array.init sets.Length (fun i -> sets.[i].Contains item))
41+
|> Seq.groupBy snd
42+
|> Seq.map (fun (membership, items) ->
43+
let label = toLabel membership
44+
let set = items |> Seq.map fst |> Set.ofSeq
45+
labelToId label, { Label = label; Set = set })
46+
|> Map.ofSeq
47+
48+
/// Converts a generic venn to an array of (labels, count) pairs.
49+
let internal toVennCountLabelArr (genericVenn: GenericVenn<'a>) =
50+
genericVenn
51+
|> Seq.map (fun kv -> kv.Value.Label |> Array.ofList, kv.Value.Set.Count)
52+
|> Array.ofSeq
53+
54+
/// Initializes the circle shape used to draw a single venn set.
55+
let internal initCircleShape x0 y0 x1 y1 color =
56+
Shape.init(
57+
Opacity = 0.3,
58+
Xref = "x",
59+
Yref = "y",
60+
FillColor = color,
61+
X0 = x0,
62+
Y0 = y0,
63+
X1 = x1,
64+
Y1 = y1,
65+
ShapeType = StyleParam.ShapeType.Circle,
66+
Line = Line.init(Color = color)
67+
)
68+
69+
[<Extension>]
70+
type Chart =
71+
72+
/// <summary>
73+
/// Creates a Venn diagram comparing two or three sets.
74+
///
75+
/// The size of each circle is fixed; the diagram annotates each region (single sets and their intersections) with the number of members it contains.
76+
/// </summary>
77+
/// <param name="set1">Sets the first set to compare.</param>
78+
/// <param name="set2">Sets the second set to compare.</param>
79+
/// <param name="Set3">Sets an optional third set to compare. If omitted, a two-set diagram is created.</param>
80+
/// <param name="Label1">Sets the label of the first set. Defaults to "Set 1".</param>
81+
/// <param name="Label2">Sets the label of the second set. Defaults to "Set 2".</param>
82+
/// <param name="Label3">Sets the label of the third set. Defaults to "Set 3". Ignored when no third set is given.</param>
83+
/// <param name="Colors">Sets the fill colors of the circle shapes, one per set.</param>
84+
/// <param name="TextFont">Sets the font used for the region count annotations.</param>
85+
/// <param name="UseDefaults">If set to false, ignore the global default settings set in `Defaults`</param>
86+
[<Extension>]
87+
static member Venn
88+
(
89+
set1: Set<'a>,
90+
set2: Set<'a>,
91+
?Set3: Set<'a>,
92+
?Label1: string,
93+
?Label2: string,
94+
?Label3: string,
95+
?Colors: Color[],
96+
?TextFont: Font,
97+
?UseDefaults: bool
98+
) =
99+
let useDefaults = defaultArg UseDefaults true
100+
101+
let labels, sets =
102+
match Set3 with
103+
| Some set3 ->
104+
[| defaultArg Label1 "Set 1"; defaultArg Label2 "Set 2"; defaultArg Label3 "Set 3" |],
105+
[| set1; set2; set3 |]
106+
| None ->
107+
[| defaultArg Label1 "Set 1"; defaultArg Label2 "Set 2" |],
108+
[| set1; set2 |]
109+
110+
let textFont =
111+
TextFont
112+
|> Option.defaultValue (
113+
Font.init(
114+
Family = StyleParam.FontFamily.Arial,
115+
Size = 18.,
116+
Color = Color.fromKeyword Black
117+
)
118+
)
119+
120+
let vennCount =
121+
ofSetList labels sets
122+
|> toVennCountLabelArr
123+
124+
let xAxis =
125+
LinearAxis.init(ShowTickLabels = false, ShowGrid = false, ZeroLine = false)
126+
127+
// anchor the y-axis to the x-axis at a 1:1 pixel-per-unit ratio so the circle shapes
128+
// render as actual circles rather than being stretched to fill the plot area
129+
let yAxis =
130+
LinearAxis.init(
131+
ShowTickLabels = false,
132+
ShowGrid = false,
133+
ZeroLine = false,
134+
ScaleAnchor = StyleParam.ScaleAnchor.X 1,
135+
ScaleRatio = 1.
136+
)
137+
138+
// single-set regions are annotated with their label and count: "<label><br><count>"
139+
let singleText =
140+
vennCount
141+
|> Array.filter (fun (label, _) -> label.Length = 1)
142+
|> Array.map (fun (label, count) -> sprintf "%s<br>%i" label.[0] count)
143+
144+
// builds the final chart from the per-arm geometry, default colors and intersection annotations
145+
let buildChart
146+
(circlePositions: {| X0: float; Y0: float; X1: float; Y1: float |}[])
147+
(textX: float[])
148+
(textY: float[])
149+
(defaultColors: Color[])
150+
(intersectionText: string[]) =
151+
152+
let colors = Colors |> Option.defaultValue defaultColors
153+
154+
let shapes =
155+
Array.zip circlePositions colors
156+
|> Array.map (fun (p, color) -> initCircleShape p.X0 p.Y0 p.X1 p.Y1 color)
157+
158+
let layout =
159+
Layout.init(
160+
Shapes = shapes,
161+
Margin = Margin.init(Left = 20, Right = 20, Bottom = 100)
162+
)
163+
|> Layout.updateLinearAxisById(StyleParam.SubPlotId.XAxis 1, xAxis)
164+
|> Layout.updateLinearAxisById(StyleParam.SubPlotId.YAxis 1, yAxis)
165+
166+
Trace2D.initScatter(
167+
Trace2DStyle.Scatter(
168+
X = textX,
169+
Y = textY,
170+
Mode = StyleParam.Mode.Text,
171+
MultiText = Array.append singleText intersectionText,
172+
TextFont = textFont
173+
)
174+
)
175+
|> GenericChart.ofTraceObject useDefaults
176+
|> Chart.withLayout layout
177+
178+
match Set3 with
179+
| None ->
180+
let intersectionText =
181+
vennCount
182+
|> Array.filter (fun (label, _) -> label.Length > 1)
183+
|> Array.map (fun (_, count) -> string count)
184+
185+
buildChart
186+
[|
187+
{| X0 = 0.; Y0 = 0.; X1 = 2.; Y1 = 2. |}
188+
{| X0 = 1.5; Y0 = 0.; X1 = 3.5; Y1 = 2. |}
189+
|]
190+
[| 1.; 2.5; 1.75 |]
191+
[| 1.; 1.; 1. |]
192+
[| Color.fromKeyword Blue; Color.fromKeyword Red |]
193+
intersectionText
194+
| Some _ ->
195+
let intersectionText =
196+
let singleLabels =
197+
vennCount
198+
|> Array.filter (fun (label, _) -> label.Length = 1)
199+
|> Array.collect fst
200+
201+
let multipleLabels =
202+
vennCount
203+
|> Array.filter (fun (label, _) -> label.Length > 1)
204+
205+
// true when a region is the pairwise intersection of exactly the two given single sets
206+
let isPairwiseOf i1 i2 (label: string[]) =
207+
label.Length = 2
208+
&& Array.contains singleLabels.[i1] label
209+
&& Array.contains singleLabels.[i2] label
210+
211+
// ordering must match the text positions below: A∩B, A∩C, B∩C, then A∩B∩C
212+
[|
213+
multipleLabels |> Array.filter (fun (label, _) -> isPairwiseOf 0 1 label)
214+
multipleLabels |> Array.filter (fun (label, _) -> isPairwiseOf 0 2 label)
215+
multipleLabels |> Array.filter (fun (label, _) -> isPairwiseOf 1 2 label)
216+
multipleLabels |> Array.filter (fun (label, _) -> label.Length = 3)
217+
|]
218+
|> Array.concat
219+
|> Array.map (fun (_, count) -> string count)
220+
221+
buildChart
222+
[|
223+
{| X0 = 0.; Y0 = 0.; X1 = 2.; Y1 = 2. |}
224+
{| X0 = 1.5; Y0 = 0.; X1 = 3.5; Y1 = 2. |}
225+
{| X0 = 0.75; Y0 = 1.3; X1 = 2.75; Y1 = 3.3 |}
226+
|]
227+
[| 1.; 2.5; 1.75; 1.75; 1.325; 2.125; 1.75 |]
228+
[| 1.; 1.; 2.25; 1.; 1.6625; 1.6625; 1.45 |]
229+
[| Color.fromKeyword Blue; Color.fromKeyword Red; Color.fromKeyword Green |]
230+
intersectionText

src/Plotly.NET/Plotly.NET.fsproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,7 @@
189189
<Compile Include="ChartAPI\ChartDomain\ChartDomain_Table.fs" />
190190
<Compile Include="ChartAPI\ChartDomain\ChartDomain_Icicle.fs" />
191191
<Compile Include="ChartAPI\ChartSmith\ChartSmith_Scatter.fs" />
192+
<Compile Include="ChartAPI\ChartComposite\ChartComposite_SetComparison.fs" />
192193
<None Include="Playground.fsx" />
193194
</ItemGroup>
194195
<ItemGroup>

tests/Common/FSharpTestBase/FSharpTestBase.fsproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
<Compile Include="TestCharts\ChartCarpetTestCharts.fs" />
3030
<Compile Include="TestCharts\ChartDomainTestCharts.fs" />
3131
<Compile Include="TestCharts\ChartSmithTestCharts.fs" />
32+
<Compile Include="TestCharts\ChartCompositeTestCharts.fs" />
3233
<Compile Include="TestCharts\UpstreamFeatures\2.25.fs" />
3334
<Compile Include="TestCharts\UpstreamFeatures\2.24.fs" />
3435
<Compile Include="TestCharts\UpstreamFeatures\2.23.fs" />
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
module ChartCompositeTestCharts
2+
3+
open Plotly.NET
4+
open Plotly.NET.LayoutObjects
5+
open Plotly.NET.TraceObjects
6+
7+
module Venn =
8+
9+
let ``Two set venn diagram`` =
10+
Chart.Venn(
11+
set1 = Set.ofList [ 1; 2; 3; 4 ],
12+
set2 = Set.ofList [ 3; 4; 5; 6 ],
13+
Label1 = "A",
14+
Label2 = "B",
15+
UseDefaults = false
16+
)
17+
18+
let ``Three set venn diagram`` =
19+
// every region (3 single, 3 pairwise, 1 triple) is populated with a distinct count
20+
Chart.Venn(
21+
set1 = Set.ofList [ 1; 2; 3; 4; 5; 6; 11 ],
22+
set2 = Set.ofList [ 1; 2; 3; 7; 8; 9; 10; 12; 13 ],
23+
Set3 = Set.ofList [ 1; 4; 5; 6; 7; 8; 9; 10; 14; 15; 16 ],
24+
Label1 = "A",
25+
Label2 = "B",
26+
Label3 = "C",
27+
UseDefaults = false
28+
)
29+
30+
let ``Styled two set venn diagram`` =
31+
Chart.Venn(
32+
set1 = Set.ofList [ 1; 2; 3; 4 ],
33+
set2 = Set.ofList [ 3; 4; 5; 6 ],
34+
Label1 = "A",
35+
Label2 = "B",
36+
Colors = [| Color.fromKeyword Aqua; Color.fromKeyword Salmon |],
37+
TextFont = Font.init (Family = StyleParam.FontFamily.Courier_New, Size = 20., Color = Color.fromKeyword Purple),
38+
UseDefaults = false
39+
)

tests/ConsoleApps/FSharpConsole/Program.fs

Lines changed: 30 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,38 @@ open Plotly.NET
33
[<EntryPoint>]
44
let main _ =
55

6-
let chartPointDensityEncodedHelpers =
7-
Chart.PointDensity(
8-
xEncoded = EncodedTypedArray.ofFloat64Array [| 0.0; 1.0; 2.0; 3.0; 4.0 |],
9-
yEncoded = EncodedTypedArray.ofFloat64Array [| 0.0; 1.0; 0.5; 2.0; 1.5 |],
10-
ContoursColoring = StyleParam.ContourColoring.Fill,
11-
Name = "encoded point density helper",
6+
// sample sets with overlapping members to exercise every venn region
7+
let setA = Set.ofList [ 1; 2; 3; 4; 5; 6; 11 ]
8+
let setB = Set.ofList [ 1; 2; 3; 7; 8; 9; 10; 12; 13 ]
9+
let setC = Set.ofList [ 1; 4; 5; 6; 7; 8; 9; 10; 14; 15; 16 ]
10+
11+
// two-set venn diagram
12+
let twoSetVenn =
13+
Chart.Venn(
14+
set1 = setA,
15+
set2 = setB,
16+
Label1 = "A",
17+
Label2 = "B",
18+
UseDefaults = true
19+
)
20+
|> Chart.withTitle "Venn: two sets"
21+
22+
// three-set venn diagram with custom colors and font
23+
let threeSetVenn =
24+
Chart.Venn(
25+
set1 = setA,
26+
set2 = setB,
27+
Set3 = setC,
28+
Label1 = "A",
29+
Label2 = "B",
30+
Label3 = "C",
31+
Colors = [| Color.fromKeyword Aqua; Color.fromKeyword Salmon; Color.fromKeyword LightGreen |],
32+
TextFont = Font.init (Family = StyleParam.FontFamily.Courier_New, Size = 18., Color = Color.fromKeyword Black),
1233
UseDefaults = true
1334
)
14-
|> Chart.withTitle "PointDensity: encoded x/y at chart helper layer"
35+
|> Chart.withTitle "Venn: three sets (styled)"
1536

16-
chartPointDensityEncodedHelpers |> Chart.show
37+
twoSetVenn |> Chart.show
38+
threeSetVenn |> Chart.show
1739

1840
0

tests/CoreTests/CoreTests/CoreTests.fsproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
<Compile Include="HTMLCodegen\ChartCarpet.fs" />
2020
<Compile Include="HTMLCodegen\ChartDomain.fs" />
2121
<Compile Include="HTMLCodegen\ChartSmith.fs" />
22+
<Compile Include="HTMLCodegen\ChartComposite.fs" />
2223
<Compile Include="HTMLCodegen\SimpleTests.fs" />
2324
<Compile Include="HTMLCodegen\ChartLayout.fs" />
2425
<Compile Include="HTMLCodegen\MulticategoryData.fs" />
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
module CoreTests.HTMLCodegen.ChartComposite
2+
3+
open Expecto
4+
open Plotly.NET
5+
open Plotly.NET.LayoutObjects
6+
open Plotly.NET.TraceObjects
7+
8+
9+
open TestUtils
10+
open TestUtils.HtmlCodegen
11+
open ChartCompositeTestCharts
12+
13+
module Venn =
14+
[<Tests>]
15+
let ``Venn chart HTML codegeneration tests`` =
16+
testList "HTMLCodegen.ChartComposite" [
17+
testList "Venn" [
18+
testCase "Two set data" ( fun () ->
19+
"""var data = [{"type":"scatter","mode":"text","x":[1.0,2.5,1.75],"y":[1.0,1.0,1.0],"text":["A<br>2","B<br>2","2"],"textfont":{"family":"Arial","size":18.0,"color":"rgba(0, 0, 0, 1.0)"}}];"""
20+
|> chartGeneratedContains Venn.``Two set venn diagram``
21+
);
22+
testCase "Two set layout" ( fun () ->
23+
"""var layout = {"margin":{"l":20,"r":20,"b":100},"shapes":[{"fillcolor":"rgba(0, 0, 255, 1.0)","line":{"color":"rgba(0, 0, 255, 1.0)"},"opacity":0.3,"type":"circle","x0":0.0,"x1":2.0,"xref":"x","y0":0.0,"y1":2.0,"yref":"y"},{"fillcolor":"rgba(255, 0, 0, 1.0)","line":{"color":"rgba(255, 0, 0, 1.0)"},"opacity":0.3,"type":"circle","x0":1.5,"x1":3.5,"xref":"x","y0":0.0,"y1":2.0,"yref":"y"}],"xaxis":{"showticklabels":false,"showgrid":false,"zeroline":false},"yaxis":{"scaleanchor":"x","scaleratio":1.0,"showticklabels":false,"showgrid":false,"zeroline":false}};"""
24+
|> chartGeneratedContains Venn.``Two set venn diagram``
25+
);
26+
testCase "Three set data" ( fun () ->
27+
"""var data = [{"type":"scatter","mode":"text","x":[1.0,2.5,1.75,1.75,1.325,2.125,1.75],"y":[1.0,1.0,2.25,1.0,1.6625,1.6625,1.45],"text":["A<br>1","B<br>2","C<br>3","2","3","4","1"],"textfont":{"family":"Arial","size":18.0,"color":"rgba(0, 0, 0, 1.0)"}}];"""
28+
|> chartGeneratedContains Venn.``Three set venn diagram``
29+
);
30+
testCase "Three set layout" ( fun () ->
31+
"""var layout = {"margin":{"l":20,"r":20,"b":100},"shapes":[{"fillcolor":"rgba(0, 0, 255, 1.0)","line":{"color":"rgba(0, 0, 255, 1.0)"},"opacity":0.3,"type":"circle","x0":0.0,"x1":2.0,"xref":"x","y0":0.0,"y1":2.0,"yref":"y"},{"fillcolor":"rgba(255, 0, 0, 1.0)","line":{"color":"rgba(255, 0, 0, 1.0)"},"opacity":0.3,"type":"circle","x0":1.5,"x1":3.5,"xref":"x","y0":0.0,"y1":2.0,"yref":"y"},{"fillcolor":"rgba(0, 128, 0, 1.0)","line":{"color":"rgba(0, 128, 0, 1.0)"},"opacity":0.3,"type":"circle","x0":0.75,"x1":2.75,"xref":"x","y0":1.3,"y1":3.3,"yref":"y"}],"xaxis":{"showticklabels":false,"showgrid":false,"zeroline":false},"yaxis":{"scaleanchor":"x","scaleratio":1.0,"showticklabels":false,"showgrid":false,"zeroline":false}};"""
32+
|> chartGeneratedContains Venn.``Three set venn diagram``
33+
);
34+
testCase "Styled two set data" ( fun () ->
35+
"""var data = [{"type":"scatter","mode":"text","x":[1.0,2.5,1.75],"y":[1.0,1.0,1.0],"text":["A<br>2","B<br>2","2"],"textfont":{"family":"Courier New","size":20.0,"color":"rgba(128, 0, 128, 1.0)"}}];"""
36+
|> chartGeneratedContains Venn.``Styled two set venn diagram``
37+
);
38+
testCase "Styled two set layout" ( fun () ->
39+
"""var layout = {"margin":{"l":20,"r":20,"b":100},"shapes":[{"fillcolor":"rgba(0, 255, 255, 1.0)","line":{"color":"rgba(0, 255, 255, 1.0)"},"opacity":0.3,"type":"circle","x0":0.0,"x1":2.0,"xref":"x","y0":0.0,"y1":2.0,"yref":"y"},{"fillcolor":"rgba(250, 128, 114, 1.0)","line":{"color":"rgba(250, 128, 114, 1.0)"},"opacity":0.3,"type":"circle","x0":1.5,"x1":3.5,"xref":"x","y0":0.0,"y1":2.0,"yref":"y"}],"xaxis":{"showticklabels":false,"showgrid":false,"zeroline":false},"yaxis":{"scaleanchor":"x","scaleratio":1.0,"showticklabels":false,"showgrid":false,"zeroline":false}};"""
40+
|> chartGeneratedContains Venn.``Styled two set venn diagram``
41+
);
42+
]
43+
]

0 commit comments

Comments
 (0)