Skip to content

Commit 37006fd

Browse files
committed
Add encoded Chart.Scatter root support
Summary: - add XEncoded and YEncoded to the foundational Chart.Scatter constructor - forward encoded scatter inputs through Trace2DStyle.Scatter without changing the higher convenience overloads yet - add chart-level encoded serialization and precedence tests for the top-level Scatter API - replace the FSharpConsole playground with a focused H1-A sample for Chart.Scatter using encoded x/y Verification: - .\\build.cmd runTestsCore - 804 tests passed Notes: - plans/EncodedArraySupport.md was updated locally before commit and intentionally left uncommitted
1 parent 8a9fcb6 commit 37006fd

File tree

3 files changed

+57
-273
lines changed

3 files changed

+57
-273
lines changed

src/Plotly.NET/ChartAPI/Chart2D.fs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,10 @@ module Chart2D =
3434
/// Scatter charts are the basis of Point, Line, and Bubble Charts, and can be customized as such. We also provide abstractions for those: Chart.Line, Chart.Point, Chart.Bubble
3535
/// </summary>
3636
/// <param name="X">Sets the x coordinates of the plotted data.</param>
37+
/// <param name="XEncoded">Sets the x coordinates of the plotted data as an encoded typed array.</param>
3738
/// <param name="MultiX">Sets the x coordinates of the plotted data. Use two inner arrays here to plot multicategorial data</param>
3839
/// <param name="Y">Sets the y coordinates of the plotted data.</param>
40+
/// <param name="YEncoded">Sets the y coordinates of the plotted data as an encoded typed array.</param>
3941
/// <param name="MultiY">Sets the x coordinates of the plotted data. Use two inner arrays here to plot multicategorial data</param>
4042
/// <param name="Mode">Determines the drawing mode for this scatter trace.</param>
4143
/// <param name="Name">Sets the trace name. The trace name appear as the legend item and on hover</param>
@@ -71,8 +73,10 @@ module Chart2D =
7173
static member Scatter
7274
(
7375
?X: seq<#IConvertible>,
76+
?XEncoded: EncodedTypedArray,
7477
?MultiX: seq<seq<#IConvertible>>,
7578
?Y: seq<#IConvertible>,
79+
?YEncoded: EncodedTypedArray,
7680
?MultiY: seq<seq<#IConvertible>>,
7781
?Mode: StyleParam.Mode,
7882
?Name: string,
@@ -134,8 +138,10 @@ module Chart2D =
134138
let style =
135139
Trace2DStyle.Scatter(
136140
?X = X,
141+
?XEncoded = XEncoded,
137142
?MultiX = MultiX,
138143
?Y = Y,
144+
?YEncoded = YEncoded,
139145
?MultiY = MultiY,
140146
?Mode = Mode,
141147
Marker = marker,
@@ -6549,4 +6555,4 @@ module Chart2D =
65496555
?MultiReferenceTextPosition = MultiReferenceTextPosition,
65506556
?TextFont = TextFont,
65516557
?UseDefaults = UseDefaults
6552-
)
6558+
)
Lines changed: 12 additions & 272 deletions
Original file line numberDiff line numberDiff line change
@@ -1,283 +1,23 @@
11
open System
22
open Plotly.NET
3-
open Plotly.NET.TraceObjects
43

54
[<EntryPoint>]
6-
let main args =
5+
let main _ =
76

8-
// Low-level demo: construct a scatter trace whose x/y come from base64-encoded
9-
// typed arrays (plotly.js >= 2.28.0 "data_array" form).
10-
let n = 500
11-
let xs = [| for i in 0 .. n - 1 -> float i * 2.0 * Math.PI / float (n - 1) |]
7+
let pointCount = 500
8+
let xs = [| for i in 0 .. pointCount - 1 -> float i * 2.0 * Math.PI / float (pointCount - 1) |]
129
let ys = xs |> Array.map sin
1310

14-
let simpleEncodedScatter =
15-
Trace2D.initScatter (
16-
Trace2DStyle.Scatter(
17-
Name = "sin (encoded)",
18-
Mode = StyleParam.Mode.Lines_Markers,
19-
XEncoded = EncodedTypedArray.ofFloat64Array xs,
20-
YEncoded = EncodedTypedArray.ofFloat64Array ys
21-
)
11+
let chartScatterEncodedPoC =
12+
Chart.Scatter(
13+
XEncoded = EncodedTypedArray.ofFloat64Array xs,
14+
YEncoded = EncodedTypedArray.ofFloat64Array ys,
15+
Mode = StyleParam.Mode.Lines_Markers,
16+
Name = "sin(x) via Chart.Scatter",
17+
UseDefaults = true
2218
)
23-
|> GenericChart.ofTraceObject true
24-
|> Chart.withTitle "Encoded scatter x/y"
19+
|> Chart.withTitle "H1 PoC: Chart.Scatter with encoded x/y"
2520

26-
let fullyEncodedScatterWithErrorBars =
27-
Trace2D.initScatter (
28-
Trace2DStyle.Scatter(
29-
Name = "encoded scatter + error bars",
30-
Mode = StyleParam.Mode.Lines_Markers,
31-
XEncoded = EncodedTypedArray.ofFloat64Array [| 1.0; 2.0; 3.0 |],
32-
YEncoded = EncodedTypedArray.ofFloat64Array [| 4.0; 5.0; 6.0 |],
33-
IdsEncoded = EncodedTypedArray.ofInt32Array [| 101; 102; 103 |],
34-
CustomDataEncoded = EncodedTypedArray.ofFloat64Array [| 10.0; 20.0; 30.0 |],
35-
SelectedPointsEncoded = EncodedTypedArray.ofInt32Array [| 0; 2 |],
36-
MultiTextEncoded = EncodedTypedArray.ofFloat64Array [| 7.0; 8.0; 9.0 |],
37-
XError =
38-
Error.init(
39-
Type = StyleParam.ErrorType.Data,
40-
ArrayEncoded = EncodedTypedArray.ofFloat64Array [| 0.1; 0.2; 0.3 |]
41-
),
42-
YError =
43-
Error.init(
44-
Type = StyleParam.ErrorType.Data,
45-
ArrayEncoded = EncodedTypedArray.ofFloat64Array [| 0.4; 0.5; 0.6 |],
46-
ArrayminusEncoded = EncodedTypedArray.ofFloat64Array [| 0.3; 0.2; 0.1 |]
47-
)
48-
)
49-
)
50-
|> GenericChart.ofTraceObject true
51-
|> Chart.withTitle "Fully encoded scatter with error bars"
52-
53-
let fullyEncodedBar =
54-
Trace2D.initBar (
55-
Trace2DStyle.Bar(
56-
Name = "encoded bar",
57-
XEncoded = EncodedTypedArray.ofFloat64Array [| 1.0; 2.0; 3.0 |],
58-
YEncoded = EncodedTypedArray.ofFloat64Array [| 4.0; 5.0; 6.0 |],
59-
IdsEncoded = EncodedTypedArray.ofInt32Array [| 101; 102; 103 |],
60-
CustomDataEncoded = EncodedTypedArray.ofFloat64Array [| 10.0; 20.0; 30.0 |],
61-
SelectedPointsEncoded = EncodedTypedArray.ofInt32Array [| 0; 2 |],
62-
MultiTextEncoded = EncodedTypedArray.ofFloat64Array [| 7.0; 8.0; 9.0 |],
63-
MultiWidthEncoded = EncodedTypedArray.ofFloat64Array [| 0.3; 0.4; 0.5 |],
64-
MultiOffsetEncoded = EncodedTypedArray.ofFloat64Array [| -0.1; 0.0; 0.1 |]
65-
)
66-
)
67-
|> GenericChart.ofTraceObject true
68-
|> Chart.withTitle "Fully encoded bar"
69-
70-
let fullyEncodedBoxPlot =
71-
Trace2D.initBoxPlot (
72-
Trace2DStyle.BoxPlot(
73-
Name = "encoded boxplot",
74-
XEncoded = EncodedTypedArray.ofFloat64Array [| 1.0; 2.0 |],
75-
IdsEncoded = EncodedTypedArray.ofInt32Array [| 21; 22 |],
76-
CustomDataEncoded = EncodedTypedArray.ofFloat64Array [| 31.0; 32.0 |],
77-
SelectedPointsEncoded = EncodedTypedArray.ofInt32Array [| 1 |],
78-
MultiTextEncoded = EncodedTypedArray.ofFloat64Array [| 41.0; 42.0 |],
79-
Q1Encoded = EncodedTypedArray.ofFloat64Array [| 1.0; 2.0 |],
80-
MedianEncoded = EncodedTypedArray.ofFloat64Array [| 1.5; 2.5 |],
81-
Q3Encoded = EncodedTypedArray.ofFloat64Array [| 2.0; 3.0 |],
82-
LowerFenceEncoded = EncodedTypedArray.ofFloat64Array [| 0.5; 1.5 |],
83-
UpperFenceEncoded = EncodedTypedArray.ofFloat64Array [| 2.5; 3.5 |],
84-
NotchSpanEncoded = EncodedTypedArray.ofFloat64Array [| 0.2; 0.3 |],
85-
MeanEncoded = EncodedTypedArray.ofFloat64Array [| 1.6; 2.6 |],
86-
SDEncoded = EncodedTypedArray.ofFloat64Array [| 0.4; 0.5 |]
87-
)
88-
)
89-
|> GenericChart.ofTraceObject true
90-
|> Chart.withTitle "Fully encoded boxplot"
91-
92-
let fullyEncodedCandlestick =
93-
Trace2D.initCandlestick (
94-
Trace2DStyle.Candlestick(
95-
Name = "encoded candlestick",
96-
XEncoded = EncodedTypedArray.ofFloat64Array [| 1.0; 2.0; 3.0 |],
97-
IdsEncoded = EncodedTypedArray.ofInt32Array [| 111; 112; 113 |],
98-
CustomDataEncoded = EncodedTypedArray.ofFloat64Array [| 121.0; 122.0; 123.0 |],
99-
SelectedPointsEncoded = EncodedTypedArray.ofInt32Array [| 0; 2 |],
100-
MultiTextEncoded = EncodedTypedArray.ofFloat64Array [| 131.0; 132.0; 133.0 |],
101-
OpenEncoded = EncodedTypedArray.ofFloat64Array [| 20.0; 21.0; 22.0 |],
102-
HighEncoded = EncodedTypedArray.ofFloat64Array [| 25.0; 26.0; 27.0 |],
103-
LowEncoded = EncodedTypedArray.ofFloat64Array [| 18.0; 19.0; 20.0 |],
104-
CloseEncoded = EncodedTypedArray.ofFloat64Array [| 22.0; 23.0; 24.0 |]
105-
)
106-
)
107-
|> GenericChart.ofTraceObject true
108-
|> Chart.withTitle "Fully encoded candlestick"
109-
110-
let fullyEncodedHeatmap =
111-
Trace2D.initHeatmap (
112-
Trace2DStyle.Heatmap(
113-
Name = "encoded heatmap",
114-
XEncoded = EncodedTypedArray.ofFloat64Array [| 1.0; 2.0; 3.0 |],
115-
YEncoded = EncodedTypedArray.ofFloat64Array [| 10.0; 20.0 |],
116-
ZEncoded = EncodedTypedArray.ofFloat64Array([| 1.0; 2.0; 3.0; 4.0; 5.0; 6.0 |], shape = [ 2; 3 ]),
117-
IdsEncoded = EncodedTypedArray.ofInt32Array [| 201; 202 |],
118-
MultiTextEncoded = EncodedTypedArray.ofFloat64Array [| 11.0; 12.0; 13.0; 14.0; 15.0; 16.0 |],
119-
CustomDataEncoded = EncodedTypedArray.ofFloat64Array [| 21.0; 22.0 |]
120-
)
121-
)
122-
|> GenericChart.ofTraceObject true
123-
|> Chart.withTitle "Fully encoded heatmap"
124-
125-
let fullyEncodedCone =
126-
Trace3D.initCone (
127-
Trace3DStyle.Cone(
128-
Name = "encoded cone",
129-
IdsEncoded = EncodedTypedArray.ofInt32Array [| 301; 302 |],
130-
XEncoded = EncodedTypedArray.ofFloat64Array [| 1.0; 2.0 |],
131-
YEncoded = EncodedTypedArray.ofFloat64Array [| 3.0; 4.0 |],
132-
ZEncoded = EncodedTypedArray.ofFloat64Array [| 5.0; 6.0 |],
133-
UEncoded = EncodedTypedArray.ofFloat64Array [| 0.2; 0.4 |],
134-
VEncoded = EncodedTypedArray.ofFloat64Array [| 0.3; 0.5 |],
135-
WEncoded = EncodedTypedArray.ofFloat64Array [| 0.6; 0.8 |],
136-
MultiTextEncoded = EncodedTypedArray.ofFloat64Array [| 31.0; 32.0 |],
137-
CustomDataEncoded = EncodedTypedArray.ofFloat64Array [| 41.0; 42.0 |]
138-
)
139-
)
140-
|> GenericChart.ofTraceObject true
141-
|> Chart.withTitle "Fully encoded cone"
142-
143-
let fullyEncodedScatterPolar =
144-
TracePolar.initScatterPolar (
145-
TracePolarStyle.ScatterPolar(
146-
Name = "encoded scatterpolar",
147-
Mode = StyleParam.Mode.Lines_Markers,
148-
IdsEncoded = EncodedTypedArray.ofInt32Array [| 401; 402; 403 |],
149-
REncoded = EncodedTypedArray.ofFloat64Array [| 1.0; 2.0; 1.5 |],
150-
ThetaEncoded = EncodedTypedArray.ofFloat64Array [| 0.0; 120.0; 240.0 |],
151-
MultiTextEncoded = EncodedTypedArray.ofFloat64Array [| 11.0; 12.0; 13.0 |],
152-
CustomDataEncoded = EncodedTypedArray.ofFloat64Array [| 21.0; 22.0; 23.0 |],
153-
SelectedPointsEncoded = EncodedTypedArray.ofInt32Array [| 1 |]
154-
)
155-
)
156-
|> GenericChart.ofTraceObject true
157-
|> Chart.withTitle "Fully encoded scatterpolar"
158-
159-
let fullyEncodedScatterGeo =
160-
TraceGeo.initScatterGeo (
161-
TraceGeoStyle.ScatterGeo(
162-
Name = "encoded scattergeo",
163-
Mode = StyleParam.Mode.Markers_Text,
164-
IdsEncoded = EncodedTypedArray.ofInt32Array [| 501; 502; 503 |],
165-
LatEncoded = EncodedTypedArray.ofFloat64Array [| 52.52; 48.85; 41.90 |],
166-
LonEncoded = EncodedTypedArray.ofFloat64Array [| 13.40; 2.35; 12.49 |],
167-
MultiTextEncoded = EncodedTypedArray.ofFloat64Array [| 31.0; 32.0; 33.0 |],
168-
CustomDataEncoded = EncodedTypedArray.ofFloat64Array [| 41.0; 42.0; 43.0 |],
169-
SelectedPointsEncoded = EncodedTypedArray.ofInt32Array [| 0; 2 |]
170-
)
171-
)
172-
|> GenericChart.ofTraceObject true
173-
|> Chart.withTitle "Fully encoded scattergeo"
174-
175-
let fullyEncodedScatterMapbox =
176-
TraceMapbox.initScatterMapbox (
177-
TraceMapboxStyle.ScatterMapbox(
178-
Name = "encoded scattermapbox",
179-
Mode = StyleParam.Mode.Markers_Text,
180-
IdsEncoded = EncodedTypedArray.ofInt32Array [| 601; 602; 603 |],
181-
LatEncoded = EncodedTypedArray.ofFloat64Array [| 37.77; 34.05; 47.61 |],
182-
LonEncoded = EncodedTypedArray.ofFloat64Array [| -122.42; -118.24; -122.33 |],
183-
MultiTextEncoded = EncodedTypedArray.ofFloat64Array [| 51.0; 52.0; 53.0 |],
184-
CustomDataEncoded = EncodedTypedArray.ofFloat64Array [| 61.0; 62.0; 63.0 |],
185-
SelectedPointsEncoded = EncodedTypedArray.ofInt32Array [| 1 |]
186-
)
187-
)
188-
|> GenericChart.ofTraceObject true
189-
|> Chart.withTitle "Fully encoded scattermapbox"
190-
191-
let fullyEncodedScatterTernary =
192-
TraceTernary.initScatterTernary (
193-
TraceTernaryStyle.ScatterTernary(
194-
Name = "encoded scatterternary",
195-
Mode = StyleParam.Mode.Markers_Text,
196-
IdsEncoded = EncodedTypedArray.ofInt32Array [| 701; 702; 703 |],
197-
AEncoded = EncodedTypedArray.ofFloat64Array [| 0.2; 0.3; 0.4 |],
198-
BEncoded = EncodedTypedArray.ofFloat64Array [| 0.5; 0.3; 0.2 |],
199-
CEncoded = EncodedTypedArray.ofFloat64Array [| 0.3; 0.4; 0.4 |],
200-
MultiTextEncoded = EncodedTypedArray.ofFloat64Array [| 71.0; 72.0; 73.0 |],
201-
CustomDataEncoded = EncodedTypedArray.ofFloat64Array [| 81.0; 82.0; 83.0 |],
202-
SelectedPointsEncoded = EncodedTypedArray.ofInt32Array [| 2 |]
203-
)
204-
)
205-
|> GenericChart.ofTraceObject true
206-
|> Chart.withTitle "Fully encoded scatterternary"
207-
208-
let fullyEncodedScatterSmith =
209-
TraceSmith.initScatterSmith (
210-
TraceSmithStyle.ScatterSmith(
211-
Name = "encoded scattersmith",
212-
Mode = StyleParam.Mode.Markers_Text,
213-
IdsEncoded = EncodedTypedArray.ofInt32Array [| 801; 802; 803 |],
214-
RealEncoded = EncodedTypedArray.ofFloat64Array [| 0.5; 1.0; 1.5 |],
215-
ImagEncoded = EncodedTypedArray.ofFloat64Array [| -0.2; 0.0; 0.2 |],
216-
MultiTextEncoded = EncodedTypedArray.ofFloat64Array [| 91.0; 92.0; 93.0 |],
217-
CustomDataEncoded = EncodedTypedArray.ofFloat64Array [| 101.0; 102.0; 103.0 |],
218-
SelectedPointsEncoded = EncodedTypedArray.ofInt32Array [| 0; 1 |]
219-
)
220-
)
221-
|> GenericChart.ofTraceObject true
222-
|> Chart.withTitle "Fully encoded scattersmith"
223-
224-
let fullyEncodedCarpet =
225-
TraceCarpet.initCarpet (
226-
TraceCarpetStyle.Carpet(
227-
Name = "encoded carpet",
228-
IdsEncoded = EncodedTypedArray.ofInt32Array [| 901; 902; 903 |],
229-
XEncoded = EncodedTypedArray.ofFloat64Array [| 10.0; 20.0; 30.0 |],
230-
YEncoded = EncodedTypedArray.ofFloat64Array [| 40.0; 50.0; 60.0 |],
231-
AEncoded = EncodedTypedArray.ofFloat64Array [| 0.0; 1.0; 2.0 |],
232-
BEncoded = EncodedTypedArray.ofFloat64Array [| 0.0; 1.0; 2.0 |],
233-
CustomDataEncoded = EncodedTypedArray.ofFloat64Array [| 7.0; 8.0; 9.0 |]
234-
)
235-
)
236-
|> GenericChart.ofTraceObject true
237-
|> Chart.withTitle "Fully encoded carpet"
238-
239-
let fullyEncodedPie =
240-
TraceDomain.initPie (
241-
TraceDomainStyle.Pie(
242-
Name = "encoded pie",
243-
IdsEncoded = EncodedTypedArray.ofInt32Array [| 911; 912; 913 |],
244-
ValuesEncoded = EncodedTypedArray.ofFloat64Array [| 10.0; 20.0; 30.0 |],
245-
LabelsEncoded = EncodedTypedArray.ofInt32Array [| 1; 2; 3 |],
246-
MultiTextEncoded = EncodedTypedArray.ofFloat64Array [| 61.0; 62.0; 63.0 |],
247-
MetaEncoded = EncodedTypedArray.ofFloat64Array [| 71.0; 72.0; 73.0 |],
248-
CustomDataEncoded = EncodedTypedArray.ofFloat64Array [| 81.0; 82.0; 83.0 |]
249-
)
250-
)
251-
|> GenericChart.ofTraceObject true
252-
|> Chart.withTitle "Fully encoded pie"
253-
254-
let fullyEncodedSankey =
255-
TraceDomain.initSankey (
256-
TraceDomainStyle.Sankey(
257-
Name = "encoded sankey",
258-
IdsEncoded = EncodedTypedArray.ofInt32Array [| 921; 922 |],
259-
MetaEncoded = EncodedTypedArray.ofFloat64Array [| 141.0; 142.0 |],
260-
CustomDataEncoded = EncodedTypedArray.ofFloat64Array [| 151.0; 152.0 |],
261-
SelectedPointsEncoded = EncodedTypedArray.ofInt32Array [| 1 |]
262-
)
263-
)
264-
|> GenericChart.ofTraceObject true
265-
|> Chart.withTitle "Fully encoded sankey"
266-
267-
simpleEncodedScatter |> Chart.show
268-
fullyEncodedScatterWithErrorBars |> Chart.show
269-
fullyEncodedBar |> Chart.show
270-
fullyEncodedBoxPlot |> Chart.show
271-
fullyEncodedCandlestick |> Chart.show
272-
fullyEncodedHeatmap |> Chart.show
273-
fullyEncodedCone |> Chart.show
274-
fullyEncodedScatterPolar |> Chart.show
275-
fullyEncodedScatterGeo |> Chart.show
276-
fullyEncodedScatterMapbox |> Chart.show
277-
fullyEncodedScatterTernary |> Chart.show
278-
fullyEncodedScatterSmith |> Chart.show
279-
fullyEncodedCarpet |> Chart.show
280-
fullyEncodedPie |> Chart.show
281-
fullyEncodedSankey |> Chart.show
21+
chartScatterEncodedPoC |> Chart.show
28222

28323
0

tests/CoreTests/CoreTests/CommonAbstractions/EncodedTypedArray.fs

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -437,6 +437,44 @@ let ``Scatter trace XEncoded/YEncoded`` =
437437
)
438438
]
439439

440+
[<Tests>]
441+
let ``Chart.Scatter XEncoded/YEncoded`` =
442+
testList "CommonAbstractions.EncodedTypedArray Chart.Scatter integration" [
443+
444+
testCase "Chart.Scatter serializes XEncoded/YEncoded under x/y as encoded objects" (fun () ->
445+
let chart =
446+
Chart.Scatter(
447+
XEncoded = EncodedTypedArray.ofFloat64Array [| 1.0; 2.0; 3.0 |],
448+
YEncoded = EncodedTypedArray.ofFloat64Array [| 4.0; 5.0; 6.0 |],
449+
Mode = StyleParam.Mode.Lines_Markers,
450+
UseDefaults = false
451+
)
452+
453+
let json = chart |> GenericChart.toFigureJson
454+
Expect.stringContains json "\"x\":{\"bdata\":" "chart scatter x must be an encoded object"
455+
Expect.stringContains json "\"y\":{\"bdata\":" "chart scatter y must be an encoded object"
456+
Expect.stringContains json "\"dtype\":\"f8\"" "chart scatter encoded dtype must be present"
457+
)
458+
459+
testCase "Chart.Scatter encoded arrays override the plain x/y path when both are provided" (fun () ->
460+
let chart =
461+
Chart.Scatter(
462+
X = [ 10.0; 20.0 ],
463+
XEncoded = EncodedTypedArray.ofFloat64Array [| 1.0; 2.0 |],
464+
Y = [ 30.0; 40.0 ],
465+
YEncoded = EncodedTypedArray.ofFloat64Array [| 3.0; 4.0 |],
466+
Mode = StyleParam.Mode.Lines,
467+
UseDefaults = false
468+
)
469+
470+
let json = chart |> GenericChart.toFigureJson
471+
Expect.stringContains json "\"x\":{\"bdata\":" "chart scatter XEncoded must win over X"
472+
Expect.stringContains json "\"y\":{\"bdata\":" "chart scatter YEncoded must win over Y"
473+
Expect.isFalse (json.Contains "\"x\":[10.0,20.0]") "plain chart scatter x array must not be present"
474+
Expect.isFalse (json.Contains "\"y\":[30.0,40.0]") "plain chart scatter y array must not be present"
475+
)
476+
]
477+
440478
[<Tests>]
441479
let ``Scatter trace remaining encoded fields`` =
442480
testList "CommonAbstractions.EncodedTypedArray Scatter additional integration" [

0 commit comments

Comments
 (0)