Chechy Levas
Chechy Levas

Reputation: 2312

How do I interop with this Javascript code, from Fable F#?

I want to create a binding of the Plotly.js library to Fable. I am looking at this js code

import React from 'react';
import Plot from 'react-plotly.js';
class App extends React.Component {
  render() {
    return (
      <Plot
        data={[
          {
            x: [1, 2, 3],
            y: [2, 6, 3],
            type: 'scatter',
            mode: 'lines+points',
            marker: {color: 'red'},
          },
          {type: 'bar', x: [1, 2, 3], y: [2, 5, 3]},
        ]}
        layout={ {width: 320, height: 240, title: 'A Fancy Plot'} }
      />
    );
  }
}

and my (faulty) attempt of creating a simple test binding looks like this

open Fable.Core
open Fable.Core.JsInterop
open Browser.Types
open Fable.React

// module Props =
type Chart =
    |X of int list
    |Y of int List
    |Type of string

type IProp =
    | Data of obj list

let inline plot (props: IProp) : ReactElement =
    ofImport "Plot" "react-plotly.js" props []

let myTrace = createObj [
    "x" ==> [1,2,3]
    "y" ==> [2,6,3]
    "type" ==> "scatter"
    "mode" ==> "lines"
]

let myData = Data [myTrace]

let testPlot = plot myData

But obviously it does not work. How do I get it to work? Also, what does {[...]} mean? I am new to Javascript, and as far as I know {...} denotes an object which must contain name value pairs, and [...] denotes an array. So {[...]} seems to denote an object with a single nameless member that is an array, but as far as I know, there are no objects with nameless members.

Upvotes: 1

Views: 1581

Answers (2)

Cody Johnson
Cody Johnson

Reputation: 74

I'm a bit late to the party here, but wanted to give you a different option if you're still looking to use plotly.js with Fable.

I've been working on bindings for plotly.js for the past month or so, and it's in a pretty usable state as of now. That being said, I wouldn't say it's production ready.

This is what the example you want to convert would look like written with Feliz.Plotly:

open Feliz
open Feliz.Plotly

let chart () =
    Plotly.plot [
        plot.traces [
            traces.scatter [
                scatter.x [ 1; 2; 3 ]
                scatter.y [ 2; 6; 3 ]
                scatter.mode [
                    scatter.mode.lines
                    scatter.mode.markers
                ]
                scatter.marker [
                    marker.color color.red
                ]
            ]
            traces.bar [
                bar.x [ 1; 2; 3 ]
                bar.y [ 2; 5; 3 ]
            ]
        ]
        plot.layout [
            layout.width 320
            layout.height 240
            layout.title [
                title.text "A Fancy Plot"
            ]
        ]
    ]

You can find more information out here.

Upvotes: 1

Maxime Mangel
Maxime Mangel

Reputation: 2006

I have been able to reproduce the example you linked. Please note that I don't Plotly and that I went the empiric way and so things can probably be improved :)

I have created the code as I would probably have done it if I had to use it in my production app. So there is a bit more code than in your question because I don't use createObj.

If you don't like the typed DSL you can always simplify it, remove it and use createObj or anonymous record like I did for the marker property :)

You need to install both react-plotly.js plotly.js in your project.

open Fable.Core.JsInterop
open Fable.Core
open Fable.React

// Define props using DUs this helps create a typed version of the React props
// You can then transform a list of props into an object using `keyValueList`

[<RequireQualifiedAccess>]
type LayoutProps =
    | Title of string
    | Width of int
    | Height of int

// GraphType is marked as a `StringEnum` this means
// the value will be replace at compile time with
// their string representation so:
// `Scatter` becomes `"scatter"`
// You can customise the output by using `[<CompiledName("MyCustomName")>] 

[<RequireQualifiedAccess; StringEnum>]
type GraphType =
    | Scatter
    | Bar

[<RequireQualifiedAccess; StringEnum>]
type GraphMode =
    | Lines
    | Points
    | Markers
    | Text
    | None

[<RequireQualifiedAccess>]
type DataProps =
    | X of obj array
    | Y of obj array
    | Type of GraphType
    | Marker of obj

    // This is an helpers to generate the `flagList` waited by Plotly, if you don't like it you can just remove 
    // member and replace it with `| Mode of string` and so you have to pass the string by yourself
    static member Mode (modes : GraphMode seq) : DataProps =
        let flags =
            modes
            |> Seq.map unbox<string> // This is safe to do that because GraphMode is a StringEnum
            |> String.concat "+"

        unbox ("mode", flags)


[<RequireQualifiedAccess>]
type PlotProps =
    | Nothing // Should have real props here is there exist more than Data and Layout

    // Here notes that we are asking for an `Array` or Data
    // Array being the type expected by the JavaScript library
    // `DataProps seq` is our way to represents props
    static member Data (dataList : (DataProps seq) array) : PlotProps =
        let datas =
            dataList
            |> Array.map (fun v ->
                keyValueList CaseRules.LowerFirst v // Transform the list of props into a JavaScript object
            )

        unbox ("data", datas)

    static member Layout (props : LayoutProps seq) : PlotProps =
        unbox ("layout", keyValueList CaseRules.LowerFirst props)

// All the example I saw from react-plotly was using this factory function to transform the plotly library into a React component
// Even, the example you shown if you look at the Babel tab in the live example
let createPlotlyComponent (plotly : obj) = import "default" "react-plotly.js/factory"

// Immport the plotly.js library
let plotlyLib : obj = import "default" "plotly.js"

// Apply the factory on the plotly library
let Plot : obj = createPlotlyComponent plotlyLib

// Helper function to instantiate the react components
// This is really low level, in general we use `ofImport` like you did but if I use `ofImport` then I got a React error
let inline renderPlot (plot : obj) (props : PlotProps list) =
    ReactBindings.React.createElement(plot, (keyValueList CaseRules.LowerFirst props), [])

let root =
    // Here we can render the plot using our Typed DSL
    renderPlot
        Plot
        [
            PlotProps.Data
                [|
                    [
                        DataProps.X [| 1; 2; 3 |]
                        DataProps.Y [| 2; 6; 3 |]
                        DataProps.Type GraphType.Scatter
                        DataProps.Mode
                            [
                                GraphMode.Lines
                                GraphMode.Points
                            ]
                        DataProps.Marker {| color = "red" |}
                    ]
                    [
                        DataProps.Type GraphType.Bar
                        DataProps.X [| 1; 2; 3 |]
                        DataProps.Y [| 2; 5; 3 |]
                    ]
                |]
            PlotProps.Layout
                [
                    LayoutProps.Width 640
                    LayoutProps.Height 480
                    LayoutProps.Title "A Fancy Plot"
                ]
        ]

Upvotes: 5

Related Questions