├── .purs-repl ├── src ├── D3 │ ├── Base │ │ ├── Layout │ │ │ ├── Trees.js │ │ │ ├── Trees.purs │ │ │ └── Simulation.purs │ │ ├── Transition.purs │ │ ├── Foreign.js │ │ ├── Foreign.purs │ │ ├── Element.purs │ │ ├── Attributes.purs │ │ └── Selection.purs │ ├── Base.purs │ └── Interpreter │ │ ├── Interpreter.purs │ │ ├── Types.purs │ │ ├── Attributes.purs │ │ ├── Foreign.purs │ │ ├── Selection.purs │ │ ├── Foreign.js │ │ └── Layouts │ │ └── Simulation.purs ├── examples │ ├── WrappedJS │ │ ├── Minimal.js │ │ ├── Minimal.purs │ │ ├── Force.purs │ │ ├── Tree.purs │ │ ├── oldGUP.js │ │ ├── newGUP.js │ │ ├── Force.js │ │ └── Tree.js │ └── NewSyntax │ │ ├── Force.js │ │ ├── Matrix.purs │ │ ├── Tree.js │ │ ├── GUP.purs │ │ ├── Force.purs │ │ └── Tree.purs └── Main.purs ├── .gitignore ├── test └── Main.purs ├── package.json ├── spago.dhall ├── design notes ├── nested_joins.md ├── grammar.md └── IndexedExample.purs ├── dist ├── index.html ├── flare-2.json └── miserables.json ├── LICENSE ├── README.md └── packages.dhall /.purs-repl: -------------------------------------------------------------------------------- 1 | import Prelude 2 | -------------------------------------------------------------------------------- /src/D3/Base/Layout/Trees.js: -------------------------------------------------------------------------------- 1 | exports.d3LinkRadial = angleFn => radiusFn => d3.linkRadial().angle(angleFn).radius(radiusFn) 2 | -------------------------------------------------------------------------------- /src/examples/WrappedJS/Minimal.js: -------------------------------------------------------------------------------- 1 | 2 | exports.chart = function (data) { 3 | d3.selectAll('div#minimal').append('svg') 4 | } -------------------------------------------------------------------------------- /src/examples/WrappedJS/Minimal.purs: -------------------------------------------------------------------------------- 1 | module D3.Example.Minimal (chart) where 2 | 3 | import Prelude 4 | 5 | foreign import chart :: Int -> Unit -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | .psci_modules 3 | bower_components 4 | node_modules 5 | 6 | # Generated files 7 | .psci 8 | output 9 | /.spago 10 | .DS_Store 11 | /dist/bundle.js 12 | /.psc-ide-port 13 | -------------------------------------------------------------------------------- /test/Main.purs: -------------------------------------------------------------------------------- 1 | module Test.Main where 2 | 3 | import Prelude 4 | 5 | import Effect (Effect) 6 | import Effect.Class.Console (log) 7 | 8 | main :: Effect Unit 9 | main = do 10 | log "🍝" 11 | log "You should add some tests." 12 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "devDependencies": { 3 | "http-server": "^0.12.3" 4 | }, 5 | "scripts": { 6 | "bundle": "spago bundle-app -t dist/bundle.js", 7 | "serve": "http-server dist -a localhost --port 1234 -o", 8 | "example": "npm run bundle; npm run serve" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/examples/WrappedJS/Force.purs: -------------------------------------------------------------------------------- 1 | module D3.Example.Force where 2 | 3 | import Prelude 4 | 5 | import Data.Tuple (Tuple(..)) 6 | 7 | foreign import chartFFI :: String -> Int -> Int -> String -> Unit 8 | 9 | chart :: Tuple Int Int -> String -> Unit 10 | chart (Tuple width height) fileContents = chartFFI "div#force" width height fileContents -------------------------------------------------------------------------------- /src/examples/NewSyntax/Force.js: -------------------------------------------------------------------------------- 1 | "use strict" 2 | 3 | exports.readJSONJS = filecontents => decodeFile(filecontents) 4 | 5 | const decodeFile = function (filecontents) { 6 | const json = JSON.parse(filecontents) 7 | const links = json.links.map(d => Object.create(d)) 8 | const nodes = json.nodes.map(d => Object.create(d)) 9 | return { links: links, nodes: nodes } 10 | } -------------------------------------------------------------------------------- /src/D3/Base.purs: -------------------------------------------------------------------------------- 1 | module D3.Base ( 2 | module D3.Base.Attributes 3 | , module D3.Base.Foreign 4 | , module D3.Base.Layout.Simulation 5 | , module D3.Base.Element 6 | , module D3.Base.Selection 7 | ) where 8 | 9 | import D3.Base.Attributes 10 | import D3.Base.Foreign 11 | import D3.Base.Selection 12 | import D3.Base.Element 13 | import D3.Base.Layout.Simulation 14 | -------------------------------------------------------------------------------- /src/D3/Base/Transition.purs: -------------------------------------------------------------------------------- 1 | module D3.Base.Transition where 2 | 3 | import D3.Base.Attributes (Attr) 4 | import D3.Base.Selection (Selection(..)) 5 | 6 | import Data.Maybe (Maybe(..)) 7 | 8 | -- transition :: forall model. Number -> Array Attr -> Selection model 9 | -- transition duration attributes = 10 | -- Transition { label: Nothing, duration, attributes } 11 | 12 | -------------------------------------------------------------------------------- /src/D3/Base/Foreign.js: -------------------------------------------------------------------------------- 1 | "use strict" 2 | 3 | // COLOR & SCALE functions 4 | // gross simplification here, scales can take ranges and allsorts 5 | // we just want to be able to pass d3.schemeCategory10 back in from Purescript to prove the idea tho 6 | const d3SchemeCategory10 = d3.scaleOrdinal(d3.schemeCategory10) 7 | exports.d3SchemeCategory10JS = value => d3SchemeCategory10(value) 8 | -------------------------------------------------------------------------------- /src/D3/Base/Layout/Trees.purs: -------------------------------------------------------------------------------- 1 | module D3.Layout.Trees (radialLink) where 2 | 3 | import Prelude 4 | 5 | import D3.Base.Attributes (Attr(..)) 6 | import D3.Base.Foreign (Datum) 7 | 8 | foreign import d3LinkRadial :: (Datum -> Number) -> (Datum -> Number) -> (Datum -> String) 9 | radialLink :: (Datum -> Number) -> (Datum -> Number) -> Attr 10 | radialLink angleFn radius_Fn = StringAttr "d" $ d3LinkRadial angleFn radius_Fn 11 | 12 | -------------------------------------------------------------------------------- /src/D3/Interpreter/Interpreter.purs: -------------------------------------------------------------------------------- 1 | module D3.Interpreter ( 2 | module D3.Interpreter.Types 3 | , module D3.Interpreter.Layouts.Simulation 4 | , module D3.Interpreter.Selection 5 | ) where 6 | 7 | import D3.Interpreter.Types (D3, D3State(..), initialState) 8 | import D3.Interpreter.Layouts.Simulation (addAttrFnToTick, getNativeSelection, interpretDrag, interpretForce, interpretSimulation, interpretTickMap, startSimulation, stopSimulation) 9 | import D3.Interpreter.Selection 10 | 11 | -------------------------------------------------------------------------------- /spago.dhall: -------------------------------------------------------------------------------- 1 | {- 2 | Welcome to a Spago project! 3 | You can edit this file as you like. 4 | -} 5 | { name = "my-project" 6 | , dependencies = 7 | [ "affjax" 8 | , "arrays" 9 | , "console" 10 | , "debug" 11 | , "effect" 12 | , "exists" 13 | , "foldable-traversable" 14 | , "indexed-monad" 15 | , "lists" 16 | , "nullable" 17 | , "psci-support" 18 | , "web-events" 19 | , "web-html" 20 | ] 21 | , packages = ./packages.dhall 22 | , sources = [ "src/**/*.purs", "test/**/*.purs" ] 23 | } 24 | -------------------------------------------------------------------------------- /design notes/nested_joins.md: -------------------------------------------------------------------------------- 1 | # a useful example to consider 2 | 3 | ```javascript 4 | const matrix = [ 5 | [11975, 5871, 8916, 2868], 6 | [ 1951, 10048, 2060, 6171], 7 | [ 8010, 16145, 8090, 8045], 8 | [ 1013, 990, 940, 6907] 9 | ]; 10 | 11 | d3.select("body") 12 | .append("table") 13 | .selectAll("tr") 14 | .data(matrix) 15 | .join("tr") 16 | .selectAll("td") 17 | .data(d => d) 18 | .join("td") 19 | .text(d => d); 20 | ``` 21 | 22 | ```javascript 23 | const div = d3.select("body") 24 | .selectAll("div") 25 | .data([4, 8, 15, 16, 23, 42]) 26 | .enter().append("div") 27 | .text(d => d); 28 | ``` -------------------------------------------------------------------------------- /src/examples/WrappedJS/Tree.purs: -------------------------------------------------------------------------------- 1 | module D3.Example.Tree where 2 | 3 | import Prelude 4 | 5 | import Affjax (Error) 6 | import Data.Either (Either(..)) 7 | import Data.Tuple (Tuple(..)) 8 | 9 | foreign import chartFFI :: String -> Number -> Number -> String -> Unit 10 | 11 | chart :: forall r. Tuple Number Number -> Either Error { body ∷ String | r } -> Unit 12 | chart (Tuple width height) (Right { body } ) = chartFFI "div#tree" width height body 13 | chart _ (Left error) = unit 14 | 15 | {- AS IS version of script 16 | process data using d3.hierarchy (NB non-PureScript data structure, no children field on ) 17 | 18 | -} -------------------------------------------------------------------------------- /src/D3/Interpreter/Types.purs: -------------------------------------------------------------------------------- 1 | module D3.Interpreter.Types where 2 | 3 | import Control.Monad.State (StateT) 4 | import D3.Base (NativeSelection) 5 | import Data.Map (Map, empty) 6 | import Effect (Effect) 7 | 8 | -- we model the scope of the JavaScript "script" like this (see README for rationale) 9 | data D3State model = Context model (Map String NativeSelection) 10 | 11 | initialState :: forall model. model -> D3State model 12 | initialState model = Context model empty 13 | 14 | -- TODO alternatively, modify state with new model 15 | updateState :: forall model. model -> D3State model -> D3State model 16 | updateState model (Context _ scope) = Context model scope 17 | 18 | type D3 model t = StateT(D3State model) Effect t 19 | -------------------------------------------------------------------------------- /dist/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 15 | Document 16 | 17 | 18 |
19 |
20 |
21 |
22 | 23 | 24 | -------------------------------------------------------------------------------- /src/examples/WrappedJS/oldGUP.js: -------------------------------------------------------------------------------- 1 | const svg = d3.create("svg") 2 | .attr("width", width) 3 | .attr("height", 33) 4 | .attr("viewBox", `0 -20 ${width} 33`); 5 | 6 | while (true) { 7 | const t = svg.transition() 8 | .duration(750); 9 | 10 | svg.selectAll("text") 11 | .data(randomLetters(), d => d) 12 | .join( 13 | enter => enter.append("text") 14 | .attr("fill", "green") 15 | .attr("x", (d, i) => i * 16) 16 | .attr("y", -30) 17 | .text(d => d) 18 | .call(enter => enter.transition(t) 19 | .attr("y", 0)), 20 | update => update.append("text") 21 | .attr("fill", "black") 22 | .attr("y", 0) 23 | .call(update => update.transition(t) 24 | .attr("x", (d, i) => i * 16)), 25 | exit => exit 26 | .attr("fill", "brown") 27 | .call(exit => exit.transition(t) 28 | .attr("y", 30) 29 | .remove()) 30 | ); 31 | 32 | -------------------------------------------------------------------------------- /src/D3/Base/Foreign.purs: -------------------------------------------------------------------------------- 1 | module D3.Base.Foreign where 2 | 3 | -- | these foreign types allow us to work with some very funky typing without 4 | -- | adding tonnes of syntactic noise or type complexity 5 | -- | NativeSelection is an opaque type which we keep in order to feed it back to D3 6 | -- | when we look up nameSelection Selections, Transitions, Simulations, whatever 7 | foreign import data NativeSelection :: Type 8 | 9 | -- | The Datum models the (variable / polymorphic) type of the lambdas used in Attr 10 | foreign import data Datum :: Type 11 | 12 | -- | The SubModel is that portion of the Model that we give to a particular Join 13 | foreign import data SubModel :: Type 14 | 15 | 16 | -- TODO move to separate Scales module when more fleshed out 17 | type Scale = Number -> String 18 | 19 | foreign import d3SchemeCategory10JS :: Scale -- not modelling the scale / domain distinction yet 20 | -------------------------------------------------------------------------------- /src/examples/NewSyntax/Matrix.purs: -------------------------------------------------------------------------------- 1 | module NewSyntax.Matrix where 2 | 3 | import D3.Base 4 | 5 | import Data.Tuple (Tuple(..)) 6 | import Prelude hiding (join) 7 | import Unsafe.Coerce (unsafeCoerce) 8 | 9 | type D3DOMelement = Int 10 | 11 | type Model a = Array (Array a) 12 | 13 | matrix :: Model Int 14 | matrix = [ 15 | [11975, 5871, 8916, 2868], 16 | [ 1951, 10048, 2060, 6171], 17 | [ 8010, 16145, 8090, 8045], 18 | [ 1013, 990, 940, 6907] 19 | ] 20 | {- 21 | d3.select("body") 22 | .append("table") 23 | .selectAll("tr") 24 | .data(matrix) 25 | .join("tr") 26 | .selectAll("td") 27 | .data(d => d) 28 | .join("td") 29 | .text(d => d); 30 | -} 31 | chartInit :: Tuple Number Number -> Selection (Model Int) 32 | chartInit (Tuple width height) = do 33 | let origin = { x: -width / 2.0, y: -height / 2.0 } 34 | 35 | selectInDOM "div#matrix" [] [ 36 | table [] 37 | [ join $ 38 | Enter Td $ Child $ 39 | join $ 40 | Enter Tr $ Attrs [ computeText (\d -> unsafeCoerce d) ] 41 | ] 42 | ] 43 | -------------------------------------------------------------------------------- /src/examples/WrappedJS/newGUP.js: -------------------------------------------------------------------------------- 1 | { 2 | const svg = d3.create("svg") 3 | .attr("width", width) 4 | .attr("height", 33) 5 | .attr("viewBox", `0 -20 ${width} 33`); 6 | 7 | while (true) { 8 | const t = svg.transition() 9 | .duration(750); 10 | 11 | svg.selectAll("text") 12 | .data(randomLetters(), d => d) 13 | .join( 14 | enter => enter.append("text") 15 | .attr("fill", "green") 16 | .attr("x", (d, i) => i * 16) 17 | .attr("y", -30) 18 | .text(d => d) 19 | .call(enter => enter.transition(t) 20 | .attr("y", 0)), 21 | update => update 22 | .attr("fill", "black") 23 | .attr("y", 0) 24 | .call(update => update.transition(t) 25 | .attr("x", (d, i) => i * 16)), 26 | exit => exit 27 | .attr("fill", "brown") 28 | .call(exit => exit.transition(t) 29 | .attr("y", 30) 30 | .remove()) 31 | ); 32 | 33 | yield svg.node(); 34 | await Promises.tick(2500); 35 | } 36 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Andrew Condon 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/examples/NewSyntax/Tree.js: -------------------------------------------------------------------------------- 1 | "use strict" 2 | 3 | exports.readJSONJS = filecontents => JSON.parse(filecontents) 4 | 5 | // foreign import d3HierarchyLinks :: D3Tree -> SubModel 6 | exports.d3HierarchyLinks = tree => tree.links() 7 | 8 | // foreign import d3HierarchyDescendants :: D3Tree -> SubModel 9 | exports.d3HierarchyDescendants = tree => tree.descendants() 10 | 11 | // foreign import d3Hierarchy :: forall a. Tree a -> D3Hierarchical 12 | exports.d3Hierarchy = json => d3.hierarchy(json).sort((a, b) => d3.ascending(a.data.name, b.data.name)) 13 | 14 | // foreign import d3Hierarchy :: forall a. D3Hierarchical -> D3Tree 15 | exports.d3InitTree = config => hierarchy => d3.tree().size(config.size).separation(config.separation)(hierarchy) 16 | 17 | // foreign import hasChildren :: Datum -> Boolean 18 | exports.hasChildren = function(d) { 19 | return !d.children 20 | } 21 | 22 | // this REQUIRES that the data have been passed thru the d3.herarchy (and maybe d3.tree?) function 23 | // NB because this is called directly from deep in the bowels of D3 (tree first walk to be precise) it isn't a curried function 24 | exports.radialSeparationJS = (a,b) => (a.parent == b.parent ? 1 : 2) / a.depth -------------------------------------------------------------------------------- /src/D3/Interpreter/Attributes.purs: -------------------------------------------------------------------------------- 1 | module D3.Interpreter.Attributes where 2 | 3 | import D3.Interpreter.Foreign 4 | 5 | import D3.Base (Attr(..), NativeSelection) 6 | import Prelude (Unit, pure, ($)) 7 | import D3.Interpreter.Types (D3) 8 | 9 | -- interprets and calls D3 directly, as opposed to storing attr associations on JS side, 10 | -- as is needed for, for example tick function 11 | applyAttr :: forall model. NativeSelection -> Attr -> D3 model Unit 12 | applyAttr selection = case _ of 13 | (StaticString attr value) -> pure $ runSimpleAttrJS selection attr (stringToNativeJS value) 14 | (StaticNumber attr value) -> pure $ runSimpleAttrJS selection attr (numberToNativeJS value) 15 | (StaticArrayNumber attr value) -> pure $ runSimpleAttrJS selection attr (arrayNumberToNativeJS value) 16 | 17 | (StringAttr attr fnD) -> pure $ runDatumAttrJS selection attr fnD 18 | (NumberAttr attr fnD) -> pure $ runDatumAttrJS selection attr fnD 19 | (ArrayNumberAttr attr fnD) -> pure $ runDatumAttrJS selection attr fnD 20 | 21 | (StringAttrI attr fnDI) -> pure $ runDatumIndexAttrJS selection attr fnDI 22 | (NumberAttrI attr fnDI) -> pure $ runDatumIndexAttrJS selection attr fnDI 23 | (ArrayNumberAttrI attr fnDI) -> pure $ runDatumIndexAttrJS selection attr fnDI 24 | 25 | (TextAttr fnD) -> pure $ runDatumTextJS selection fnD 26 | -------------------------------------------------------------------------------- /src/D3/Base/Layout/Simulation.purs: -------------------------------------------------------------------------------- 1 | module D3.Base.Layout.Simulation where 2 | 3 | import D3.Base.Attributes (Attr) 4 | import D3.Base.Selection (Label) 5 | import Prelude (Unit) 6 | 7 | import Data.Map (Map) 8 | 9 | -- | a record to initialize / configure simulations 10 | type SimulationConfig = { 11 | alpha :: Number 12 | , alphaTarget :: Number 13 | , alphaMin :: Number 14 | , alphaDecay :: Number 15 | , velocityDecay :: Number 16 | } 17 | 18 | defaultConfigSimulation :: SimulationConfig 19 | defaultConfigSimulation = { 20 | alpha : 1.0 21 | , alphaTarget : 0.0 22 | , alphaMin : 0.0001 23 | , alphaDecay : 0.0228 24 | , velocityDecay: 0.4 25 | } 26 | 27 | -- this is the row that gets added ot your Model's nodes when initialized by D3 28 | type SimulationNodeRow :: forall k. k -> Type 29 | type SimulationNodeRow r = { x :: Number, y :: Number, group :: Number, vx :: Number, vy :: Number, index :: Number } 30 | 31 | -- | Force Layout core types 32 | type ID = Int -- TODO this needs to be polymorphic eventually 33 | type Link = forall r. { id :: ID, source :: ID, target :: ID | r } 34 | type Node = forall r i. { id :: i | r } 35 | type IdFn = Link -> ID 36 | data Force = Force Label ForceType 37 | data ForceType = 38 | ForceMany 39 | | ForceCenter Number Number 40 | -- | ForceLink (Array Link) IdFn 41 | | ForceCollide Number 42 | | ForceX Number 43 | | ForceY Number 44 | | ForceRadial Number Number 45 | | Custom 46 | 47 | type SimulationRecord = { 48 | label :: String 49 | , config :: SimulationConfig 50 | , nodes :: Array Node 51 | , links :: Array Link 52 | , forces :: Array Force 53 | , tick :: Unit -> Unit -- could be Effect Unit 54 | , drag :: Simulation -> Unit -- could be Effect Unit 55 | } 56 | 57 | data Simulation = Simulation SimulationRecord 58 | type TickMap :: forall k. k -> Type 59 | type TickMap model = Map String (Array Attr) 60 | data DragBehavior = DefaultDrag String String -- only one implementation rn and implemented on JS side 61 | -------------------------------------------------------------------------------- /src/D3/Base/Element.purs: -------------------------------------------------------------------------------- 1 | module D3.Base.Element where 2 | 3 | import D3.Base.Attributes (Attr) 4 | import D3.Base.Selection (Element(..), Selection, append, append_) 5 | 6 | -- Group and Div are containers so the underscore versions elide attrs, not children 7 | -- for a deeper consistency 8 | group :: forall model. Array Attr -> Array (Selection model) 9 | -> Selection model 10 | group = append Group 11 | 12 | group_ :: forall model. Array (Selection model) 13 | -> Selection model 14 | group_ = append Group [] 15 | 16 | div :: forall model. Array Attr -> Array (Selection model) 17 | -> Selection model 18 | div = append Div 19 | 20 | div_ :: forall model. Array (Selection model) 21 | -> Selection model 22 | div_ = append Div [] 23 | 24 | 25 | 26 | svg :: forall model. Array Attr -> Array (Selection model) 27 | -> Selection model 28 | svg = append Svg 29 | 30 | svg_ :: forall model. Array Attr 31 | -> Selection model 32 | svg_ = append_ Svg 33 | 34 | line :: forall model. Array Attr -> Array (Selection model) 35 | -> Selection model 36 | line = append Line 37 | 38 | line_ :: forall model. Array Attr 39 | -> Selection model 40 | line_ = append_ Line 41 | 42 | circle :: forall model. Array Attr -> Array (Selection model) 43 | -> Selection model 44 | circle = append Circle 45 | 46 | circle_ :: forall model. Array Attr 47 | -> Selection model 48 | circle_ = append_ Circle 49 | 50 | path :: forall model. Array Attr -> Array (Selection model) 51 | -> Selection model 52 | path = append Path 53 | 54 | path_ :: forall model. Array Attr 55 | -> Selection model 56 | path_ = append_ Path 57 | 58 | text :: forall model. Array Attr -> Array (Selection model) 59 | -> Selection model 60 | text = append Text 61 | 62 | text_ :: forall model. Array Attr 63 | -> Selection model 64 | text_ = append_ Text 65 | 66 | 67 | 68 | table :: forall model. Array Attr -> Array (Selection model) 69 | -> Selection model 70 | table = append Table 71 | 72 | table_ :: forall model. Array Attr 73 | -> Selection model 74 | table_ = append_ Table 75 | -------------------------------------------------------------------------------- /src/examples/WrappedJS/Force.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | exports.chartFFI = element => width => height => data => { 4 | const json = JSON.parse(data) 5 | const links = json.links.map(d => Object.create(d)); 6 | const nodes = json.nodes.map(d => Object.create(d)); 7 | 8 | const simulation = d3.forceSimulation(nodes) 9 | .force("link", d3.forceLink(links).id(d => d.id)) 10 | .force("charge", d3.forceManyBody()) 11 | .force("center", d3.forceCenter(width / 2, height / 2)); 12 | 13 | const svg = d3.selectAll(element).append("svg") 14 | .attr("viewBox", [0, 0, width, height]); 15 | 16 | const link = svg.append("g") 17 | .attr("stroke", "#999") 18 | .attr("stroke-opacity", 0.6) 19 | .selectAll("line") 20 | .data(links) 21 | .join("line") 22 | .attr("stroke-width", d => Math.sqrt(d.value)); 23 | 24 | const node = svg.append("g") 25 | .attr("stroke", "#fff") 26 | .attr("stroke-width", 1.5) 27 | .selectAll("circle") 28 | .data(nodes) 29 | .join("circle") 30 | .attr("r", 5) // altho static, can't promote 31 | .attr("fill", d => scale(d.group)) 32 | 33 | 34 | node.call(drag(simulation)); 35 | 36 | simulation.on("tick", () => { 37 | link 38 | .attr("x1", d => d.source.x) 39 | .attr("y1", d => d.source.y) 40 | .attr("x2", d => d.target.x) 41 | .attr("y2", d => d.target.y); 42 | 43 | node 44 | .attr("cx", d => d.x) 45 | .attr("cy", d => d.y); 46 | }); 47 | 48 | 49 | return svg.node(); 50 | } 51 | 52 | var scale = d3.scaleOrdinal(d3.schemeCategory10); 53 | 54 | var drag = function (simulation) { 55 | 56 | function dragstarted(event, d) { 57 | if (!event.active) simulation.alphaTarget(0.3).restart(); 58 | d.fx = d.x; 59 | d.fy = d.y; 60 | } 61 | 62 | function dragged(event,d) { 63 | d.fx = event.x; 64 | d.fy = event.y; 65 | } 66 | 67 | function dragended(event,d) { 68 | if (!event.active) simulation.alphaTarget(0); 69 | d.fx = null; 70 | d.fy = null; 71 | } 72 | 73 | return d3.drag() 74 | .on("start", dragstarted) 75 | .on("drag", dragged) 76 | .on("end", dragended); 77 | } -------------------------------------------------------------------------------- /src/examples/WrappedJS/Tree.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | 4 | exports.chartFFI = element => width => height => data => { 5 | var treeH = d3.hierarchy(JSON.parse(data)) 6 | .sort((a, b) => d3.ascending(a.data.name, b.data.name)) 7 | 8 | var radius = width / 2 9 | 10 | var tree = d3.tree() 11 | .size([2 * Math.PI, radius]) 12 | .separation((a, b) => (a.parent == b.parent ? 1 : 2) / a.depth) 13 | 14 | function autoBox() { 15 | document.body.appendChild(this); 16 | const {x, y, width, height} = this.getBBox(); 17 | document.body.removeChild(this); 18 | return [x, y, width, height]; 19 | } 20 | 21 | const root = tree(treeH); 22 | 23 | const svg = 24 | d3.selectAll(element) 25 | .append("svg") 26 | .attr("viewBox", [-width/2, -height/2, width, height]) 27 | 28 | svg.append("g") 29 | .selectAll("path") 30 | .data(root.links()) 31 | .join("path") 32 | .attr("fill", "none") 33 | .attr("stroke", "#555") 34 | .attr("stroke-opacity", 0.4) 35 | .attr("stroke-width", 1.5) 36 | .attr("d", d3.linkRadial() 37 | .angle(d => d.x) 38 | .radius(d => d.y)); 39 | 40 | svg.append("g") 41 | .selectAll("circle") 42 | .data(root.descendants()) 43 | .join("circle") 44 | .attr("transform", d => ` 45 | rotate(${d.x * 180 / Math.PI - 90}) 46 | translate(${d.y},0) 47 | `) 48 | .attr("fill", d => d.children ? "#555" : "#999") 49 | .attr("r", 2.5); 50 | 51 | svg.append("g") 52 | .attr("font-family", "sans-serif") 53 | .attr("font-size", 10) 54 | .attr("stroke-linejoin", "round") 55 | .attr("stroke-width", 3) 56 | .selectAll("text") 57 | .data(root.descendants()) 58 | .join("text") 59 | .attr("transform", d => ` 60 | rotate(${d.x * 180 / Math.PI - 90}) 61 | translate(${d.y},0) 62 | rotate(${d.x >= Math.PI ? 180 : 0}) 63 | `) 64 | .attr("dy", "0.31em") 65 | .attr("x", d => d.x < Math.PI === !d.children ? 6 : -6) 66 | .attr("text-anchor", d => d.x < Math.PI === !d.children ? "start" : "end") 67 | .text(d => d.data.name) 68 | .clone(true).lower() 69 | .attr("stroke", "white"); 70 | } 71 | -------------------------------------------------------------------------------- /design notes/grammar.md: -------------------------------------------------------------------------------- 1 | 2 | # analysis of d3 grammar 3 | 4 | ## top of mind ideas 5 | * maintain full expressivity of D3 6 | * no attempt to model imperative, chained model of D3 7 | * use StateT to hold the stateful elements like named selections, transitions, scales, interpolators etc 8 | * no performance / space optimizations required in Purs code, for example, entirely static attributes can be promoted from data-bound elements to parent as is done in D3 practice but this would be done by interpreter 9 | 10 | ## abstract grammar of force example 11 | ``` 12 | make simulation w. [force] [config] 13 | add tick with reference to named selections 14 | 15 | make selection "div#chart" 16 | append 'svg' [props] [ 17 | append 'g' "links" [props] -- and export selection 18 | insert Line links 19 | ( Join (Enter lines [props]) -- static props can be promoted to 'g' 20 | (Update []) 21 | (Exit []) ) 22 | , append 'g' "nodes" [props] -- and export selection 23 | insert Circle nodes 24 | ( Join (Enter circles [props]) 25 | (Update []) 26 | (Exit []) ) 27 | call custom drag simulation -- causes simulation to wake up too 28 | ] 29 | ``` 30 | 31 | ## abstract grammar of tree example 32 | ``` 33 | make hierarchyData from data 34 | make treeFn [config] 35 | feed hierarchyData into treeFn 36 | make selection "div#app" 37 | append 'svg' props [ 38 | append 'g' for links 39 | selectAll `path` 40 | data bind root.links 41 | join 42 | enter 'path' [props] -- one prop, 'd' is d3.linkRadial().angle(d=>d.x).radius(d=>d.y) 43 | , append 'g' for circles 44 | selectAll `circle` 45 | data bind root.descendants() 46 | join 47 | enter 'circle' props -- one prop, 'transform' is printf-ish thing 48 | , append 'g' for text 49 | selectAll `text` 50 | data bind root.descendants() 51 | join 52 | enter 'text' [props] -- one prop, 'transform' is printf-ish thing 53 | clone(true).lower() [props] -- clones whole text selection, lowers it, white outline 54 | ] 55 | ``` 56 | 57 | ## abstract grammar of lsm.js 58 | 59 | ``` 60 | 61 | ``` 62 | 63 | ## abstract grammar of (new) GUP 64 | 65 | ``` 66 | make selection "div#chart" 67 | append 'svg' w. [props] [ -- width, height, viewbox 68 | append 69 | ``` -------------------------------------------------------------------------------- /design notes/IndexedExample.purs: -------------------------------------------------------------------------------- 1 | module Data.Viz.Indexed where 2 | 3 | import Data.List as L 4 | 5 | -- || FFI for D3 6 | foreign import data D3Element :: Type 7 | -- the underlying D3 selection that is passed between calls 8 | foreign import data D3Selection :: Type 9 | -- a Selection that's passed back in some callbacks 10 | foreign import data Peers :: Type 11 | -- the `this` pointer in a callback, DOM element receiving an event 12 | foreign import data DomElement :: Type 13 | 14 | 15 | data Model a = Model a 16 | 17 | data D3Model a b = D3Model b 18 | 19 | data Empty 20 | data HasModel 21 | data HasSelection 22 | data HasDataBind 23 | data InTransition 24 | 25 | newtype IxDataViz i o a = IxDataViz (Model a) 26 | 27 | -- the function that will get your result out of the IndexedMonad 28 | runIxDataViz :: forall prev next spec. IxDataViz prev next spec -> spec 29 | runIxDataViz (IxDataViz model) = model 30 | 31 | instance ixMonadDataViz :: IxMonad IxDataViz where 32 | ipure = IxDataViz 33 | ibind (IxDataViz model) f = IxDataViz <<< runIxDataViz $ f model 34 | 35 | initDataViz :: forall a. Model a -> IxDataViz Empty HasModel (Model a) 36 | initDataViz model = IxDataViz model 37 | 38 | data EventType = MouseEnter | MouseLeave | MouseDown | MouseUp | Click 39 | | ContextMenu | DragStart | Drag | DragEnd 40 | data Property a = StaticString String String | StaticNumber String Number 41 | | DynamicString String (a -> String) | DynamicNumber String (a -> Number) 42 | | EventHandler EventType (Event -> Unit) 43 | type Selector = String -- could add more validation here 44 | type Properties = Array Property 45 | data Select = Select Selector Properties 46 | type Name = String 47 | type Duration = Int 48 | data Transition = Transition Name Duration Properties 49 | 50 | makeSelectionWithModel :: forall model. Select -> IxDataViz HasModel OpenSelection 51 | 52 | addPropertiesToSelection :: forall model. Properties -> IxDataViz OpenSelection OpenSelection 53 | 54 | recoverSelectionFromTransition :: forall model. 55 | 56 | simpleModel :: L.List Int 57 | simpleModel = L.fromFoldable [1,2,3,4] 58 | 59 | sampleViz :: forall model. model -> IxDataViz Empty HasDataBind model 60 | sampleViz model = initDataViz model 61 | :>>= makeSelectionWithModel (Select "div#chart" []) 62 | :>>= addPropertiesToSelection [] 63 | :>>= bindModelToDOM 64 | :>>= addTransition (Transition "foo" 2000 []) 65 | :>>= addOnions 66 | :>>= noLettuce 67 | :>>= addTomato 68 | :>>= addTopBun 69 | -------------------------------------------------------------------------------- /src/examples/NewSyntax/GUP.purs: -------------------------------------------------------------------------------- 1 | module NewSyntax.GUP where 2 | 3 | import D3.Base 4 | import Data.Tuple (Tuple(..)) 5 | import Prelude (negate, ($), (*), (/)) 6 | import Unsafe.Coerce (unsafeCoerce) 7 | 8 | -- with such simple data and simple data viz, the data in the DOM will be exactly same as Model data 9 | -- this means that a simple coerce suffices to turn Datum -> D3DOMelement and also that no Projection 10 | -- function is required, hence the use of joinElement_ 11 | type D3DOMelement = Char 12 | 13 | type Model a = Array a 14 | 15 | chartInit :: Tuple Number Number -> Selection (Model Char) 16 | chartInit (Tuple width height) = do 17 | let origin = { x: -width / 2.0, y: -height / 2.0 } 18 | t = [ Duration 750 ] 19 | xPosition d i = i * 16.0 20 | 21 | selectInDOM "div#GUP" [] [-- this is an InitialSelect 22 | svg 23 | [ viewBox origin.x origin.y width height ] 24 | [ join $ 25 | EnterUpdateExit Text 26 | -- Enter 27 | (Attrs [ fill "green" 28 | , y (-30.0) Px 29 | , computeXusingIndex Px xPosition 30 | , computeText (\d -> unsafeCoerce d ) 31 | -- , transition t -- TODO maybe this can become proper selection child now? 32 | , y 0.0 Px ]) 33 | -- Update 34 | (Attrs [ fill "black", y 0.0 Px 35 | -- , transition t -- TODO make this a proper child later 36 | , computeXusingIndex Px xPosition ]) 37 | -- exit 38 | (Attrs [ fill "brown", y 30.0 Px 39 | -- , transition t -- TODO make this a proper child later 40 | , y 30.0 Px ]) 41 | ] 42 | ] 43 | 44 | 45 | -- const t = svg.transition() 46 | -- .duration(750); 47 | 48 | -- svg.selectAll("text") 49 | -- .data(randomLetters(), d => d) 50 | -- .join( 51 | -- enter => enter.append("text") 52 | -- .attr("fill", "green") 53 | -- .attr("x", (d, i) => i * 16) 54 | -- .attr("y", -30) 55 | -- .text(d => d) 56 | -- .call(enter => enter.transition(t) 57 | -- .attr("y", 0)), 58 | -- update => update 59 | -- .attr("fill", "black") 60 | -- .attr("y", 0) 61 | -- .call(update => update.transition(t) 62 | -- .attr("x", (d, i) => i * 16)), 63 | -- exit => exit 64 | -- .attr("fill", "brown") 65 | -- .call(exit => exit.transition(t) 66 | -- .attr("y", 30) 67 | -- .remove()) 68 | -- ); 69 | 70 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # purescript-d3v6 2 | 3 | ## Approach taken / project goals 4 | 5 | The goal has been to add some type-safety and composability to the construction of visualizations with D3. 6 | 7 | The aim is to do this with ZERO compromise to D3js' graphical expressivity, ie want to be able to reproduce any D3js viz. 8 | 9 | A secondary, but important goal is to improve readability. 10 | 11 | *Non-goals* include replicating parts of D3js that would - IMHO - be better done in PureScript, data-processing and cleaning and so forth. 12 | 13 | Performance neither a goal or a non-goal, this library is only interpreting a kind of AST for the D3 chart, D3 is doing all the work under the hood including, importantly all the DOM updates and animations etc. The Purescript layer shouldn't be adding any overhead at all, practically speaking. 14 | 15 | # status as of this commit 16 | 17 | Abstract syntax tree of chart (for simple force layout only) now exists and also an interpreter (generic, not specific to the force layout) to do the effectful business of putting the chart into the DOM. The mainline is reading and decoding the exact same JSON file as in the standard D3 example and interpreting this chart using that model data. 18 | 19 | 20 | ## example of current syntax model 21 | 22 | ```purescript 23 | chart :: Tuple Number Number -> Selection Model 24 | chart (Tuple width height) = 25 | initialSelect "div#force" "forceLayout" [] $ [ 26 | appendNamed "svg" Svg [ viewBox 0.0 0.0 width height ] [ 27 | append Group [ strokeColor "#999", strokeOpacity 0.6 ] 28 | [ join Line modelLinks 29 | (appendNamed "link" Line [ strokeWidth_D (\d -> sqrt (d3Link d).value)] []) 30 | noUpdate noExit ] 31 | 32 | , append Group [ strokeColor "#fff", strokeOpacity 1.5 ] 33 | [ join Circle modelNodes 34 | (appendNamed "node" Circle [ radius 5.0, fill_D colorByGroup] []) 35 | noUpdate noExit ] 36 | ] 37 | ] 38 | ``` 39 | 40 | ## Compare to this JS for example 41 | 42 | ```JavaScript 43 | const svg = d3.selectAll(element).append("svg") 44 | .attr("viewBox", [0, 0, width, height]); 45 | 46 | const link = svg.append("g") 47 | .attr("stroke", "#999") 48 | .attr("stroke-opacity", 0.6) 49 | .selectAll("line") 50 | .data(links) 51 | .join("line") 52 | .attr("stroke-width", d => Math.sqrt(d.value)); 53 | 54 | const node = svg.append("g") 55 | .attr("stroke", "#fff") 56 | .attr("stroke-width", 1.5) 57 | .selectAll("circle") 58 | .data(nodes) 59 | .join("circle") 60 | .attr("r", 5) // altho static, can't promote 61 | .attr("fill", d => scale(d.group)) 62 | ``` 63 | 64 | # Quickstart to run an example 65 | 66 | Note that this is for a different example than the above, and assumes you already have `purescript` and `spago` installed. 67 | ``` 68 | npm i 69 | npm run example 70 | ``` 71 | 72 | -------------------------------------------------------------------------------- /src/D3/Interpreter/Foreign.purs: -------------------------------------------------------------------------------- 1 | module D3.Interpreter.Foreign where 2 | 3 | import D3.Base 4 | import Prelude 5 | 6 | import Data.Nullable (Nullable) 7 | import Unsafe.Coerce (unsafeCoerce) 8 | 9 | -- | right upfront we're just going to hand wave over the types given to D3 10 | -- | design decision here to prefer (aliased) `unsafeCoerce` to complex wrappers 11 | -- | D3 WILL modify any data you give it, it will also require types that CANNOT be 12 | -- | expressed in PureScript AND also transparently shared with JavaScript 13 | -- | (ie Variants, Exists, Nullable etc) 14 | -- | IMPORTANT - coercions should not be visible outside this module 15 | foreign import data NativeJS :: Type 16 | 17 | -- | foreign types associated with D3 Selection 18 | -- all this FFI stuff can be made Effect Unit later 19 | foreign import runSimpleAttrJS :: NativeSelection -> String -> NativeJS -> Unit 20 | foreign import runDatumAttrJS :: forall f. NativeSelection -> String -> f -> Unit 21 | foreign import runDatumTextJS :: forall f. NativeSelection -> f -> Unit 22 | foreign import runDatumIndexAttrJS :: forall f. NativeSelection -> String -> f -> Unit 23 | foreign import d3SelectAllJS :: String -> NativeSelection 24 | foreign import d3AppendElementJS :: NativeSelection -> String -> NativeSelection 25 | -- foreign import d3EnterElementJS :: NativeSelection -> String -> NativeSelection 26 | foreign import d3JoinJS :: forall d. 27 | NativeSelection 28 | -> String 29 | -> d 30 | -> { enter :: NativeSelection -> NativeSelection, 31 | update :: NativeSelection -> NativeSelection, 32 | exit :: NativeSelection -> NativeSelection } 33 | -> NativeSelection 34 | 35 | foreign import d3EnterWithIndexJS :: forall d. NativeSelection -> d -> (d -> NativeJS) -> NativeSelection 36 | foreign import nullSelectionJS :: NativeSelection 37 | 38 | -- | foreign types associated with Force Layout Simulation 39 | foreign import initSimulationJS :: SimulationConfig -> NativeSelection 40 | -- TODO tick functions should be nameSelection too, so this should be param'd with a tick NativeSelection too! 41 | foreign import addAttrFnToTickJS :: NativeSelection -> Attr -> Unit 42 | foreign import attachTickFnToSimulationJS :: NativeSelection -> Unit 43 | foreign import attachDefaultDragBehaviorJS :: NativeSelection -> NativeSelection -> Unit 44 | foreign import putNodesInSimulationJS :: NativeSelection -> Array NativeJS -> Array NativeJS 45 | foreign import putLinksInSimulationJS :: NativeSelection -> Array NativeJS -> Array NativeJS 46 | foreign import startSimulationJS :: NativeSelection -> Unit 47 | foreign import stopSimulationJS :: NativeSelection -> Unit 48 | foreign import setAlphaTargetJS :: NativeSelection -> Number -> Unit 49 | foreign import forceManyJS :: NativeSelection -> String -> Unit 50 | foreign import forceCenterJS :: NativeSelection -> String -> Number -> Number -> Unit 51 | foreign import forceCollideJS :: NativeSelection -> String -> Number -> Unit 52 | foreign import forceXJS :: NativeSelection -> String -> Number -> Unit 53 | foreign import forceYJS :: NativeSelection -> String -> Number -> Unit 54 | foreign import forceRadialJS :: NativeSelection -> String -> Number -> Number -> Unit 55 | 56 | stringToNativeJS :: String -> NativeJS 57 | stringToNativeJS = unsafeCoerce 58 | 59 | numberToNativeJS :: Number -> NativeJS 60 | numberToNativeJS = unsafeCoerce 61 | 62 | arrayNumberToNativeJS :: Array Number -> NativeJS 63 | arrayNumberToNativeJS = unsafeCoerce 64 | -------------------------------------------------------------------------------- /src/Main.purs: -------------------------------------------------------------------------------- 1 | module Main where 2 | 3 | import Prelude 4 | 5 | import Affjax (get) as AJAX 6 | import Affjax (printError) 7 | import Affjax.ResponseFormat as ResponseFormat 8 | import Control.Monad.State (StateT, runStateT) 9 | import D3.Base (Selection) 10 | import D3.Interpreter (D3State(..), initialState, interpretDrag, runInitial, interpretSimulation, interpretTickMap, startSimulation) 11 | import D3.Interpreter.Types (updateState) 12 | import Data.Bifunctor (rmap) 13 | import Data.Either (Either(..)) 14 | import Data.Int (toNumber) 15 | import Data.String.CodeUnits (toCharArray) 16 | import Data.Tuple (Tuple(..)) 17 | import Effect (Effect) 18 | import Effect.Aff (Milliseconds(..), delay, launchAff_) 19 | import Effect.Class (liftEffect) 20 | import Effect.Class.Console (log) 21 | import NewSyntax.Force as Force 22 | import NewSyntax.GUP as GUP 23 | import NewSyntax.Tree as Tree 24 | import Web.HTML (window) 25 | import Web.HTML.Window (innerHeight, innerWidth) 26 | 27 | getWindowWidthHeight :: Effect (Tuple Number Number) 28 | getWindowWidthHeight = do 29 | win <- window 30 | width <- innerWidth win 31 | height <- innerHeight win 32 | pure $ Tuple (toNumber width) (toNumber height) 33 | 34 | forceInterpreter :: Selection Force.Model -> StateT (D3State Force.Model) Effect Unit 35 | forceInterpreter forceChart = do 36 | simulation <- interpretSimulation Force.simulation Force.getNodes Force.getLinks Force.makeModel 37 | _ <- runInitial forceChart 38 | interpretTickMap simulation Force.myTickMap 39 | interpretDrag Force.myDrag 40 | startSimulation simulation 41 | pure unit -- should be we returning the updated Selection? and the Simulation? 42 | 43 | -- TODO take the file reading stuff out so that we can see the essentials 44 | main :: Effect Unit 45 | main = launchAff_ do -- Aff 46 | widthHeight <- liftEffect getWindowWidthHeight 47 | 48 | let numberOfExamples = 3.0 49 | widthHeight' = rmap (\h -> h/numberOfExamples) widthHeight 50 | 51 | -- first, a force layout example 52 | log "Force layout example" 53 | forceJSON <- AJAX.get ResponseFormat.string "http://localhost:1234/miserables.json" 54 | let fileData = Force.readModelFromFileContents forceJSON 55 | let forceChart = Force.chart widthHeight' 56 | let forceModel = Force.makeModel fileData.links fileData.nodes 57 | 58 | _ <- liftEffect $ 59 | runStateT (forceInterpreter forceChart) (initialState forceModel) 60 | 61 | -- then a radial tree 62 | log "Radial tree example" 63 | treeJSON <- AJAX.get ResponseFormat.string "http://localhost:1234/flare-2.json" 64 | let treeChart = Tree.chart widthHeight' 65 | case Tree.readModelFromFileContents widthHeight' treeJSON of 66 | (Left error) -> liftEffect $ log $ printError error 67 | 68 | (Right treeModel) -> liftEffect $ 69 | runStateT (runInitial treeChart) (initialState treeModel) *> pure unit 70 | 71 | -- then the General Update Pattern example 72 | log "General Update Pattern example" 73 | let lettersChart = GUP.chartInit widthHeight' 74 | let letters1 = toCharArray "abcdefghijklmnopqrstuvwxyz" 75 | let letters2 = toCharArray "acefghiklnpqrtuwyz" 76 | 77 | (Tuple _ s) <- liftEffect $ 78 | runStateT (runInitial lettersChart) (initialState letters1) 79 | 80 | _ <- delay $ Milliseconds 2000.0 81 | 82 | -- _ <- liftEffect $ 83 | -- runStateT (runInitial lettersChart) (updateState letters2 s) 84 | -- TODO now we need to use the monadic context inside StateT to (repeatedly) add the GUP.chartUpdate 85 | -- and we really want the NativeSelection to be passed in via scope, right? 86 | -- _ <- liftEffect $ runStateT (interpretSelection GUP.chartUpdate ) (Context letters2 lettersScope) 87 | -- _ <- liftEffect $ runStateT (interpretSelection chartUpdate ) (Context letters2 lettersScope) 88 | -- _ <- liftEffect $ runStateT (interpretSelection chartUpdate ) (Context letters2 lettersScope) 89 | pure unit -------------------------------------------------------------------------------- /src/D3/Interpreter/Selection.purs: -------------------------------------------------------------------------------- 1 | module D3.Interpreter.Selection where 2 | 3 | import Control.Monad.State (get, modify_) 4 | import D3.Base (Element(..), NativeSelection, Selection(..)) 5 | import D3.Interpreter.Attributes (applyAttr) 6 | import D3.Interpreter.Foreign (d3AppendElementJS, d3JoinJS, d3SelectAllJS, nullSelectionJS) 7 | import D3.Interpreter.Types (D3, D3State(..)) 8 | import Data.Array (reverse) 9 | import Data.Foldable (traverse_) 10 | import Data.Map (insert) 11 | import Data.Maybe (Maybe(..)) 12 | import Debug.Trace (spy) 13 | import Effect.Console (log) 14 | import Prelude (Unit, bind, discard, pure, show, unit, ($)) 15 | import Unsafe.Coerce (unsafeCoerce) 16 | 17 | -- TODO fugly, fix later 18 | runInitial :: forall model. Selection model -> D3 model NativeSelection 19 | runInitial = case _ of 20 | -- selectInDOMion is the only one that we can start with (alt. use IxMonad) 21 | s@(InitialSelect r) -> run nullSelectionJS s 22 | -- TODO handle extend selection cases needed for update / re-entrancy 23 | -- TODO raise error, or structure differently 24 | _ -> pure nullSelectionJS 25 | 26 | run :: forall model. NativeSelection -> Selection model -> D3 model NativeSelection 27 | run activeSelection selection = do 28 | case selection of 29 | (InitialSelect r) -> do 30 | let root = spy "selectInDOM" $ 31 | d3SelectAllJS r.selector 32 | traverse_ (run root) r.children 33 | pure root 34 | 35 | (Append r) -> do 36 | let appendSelection = spy "Append" $ 37 | d3AppendElementJS activeSelection (show r.element) 38 | traverse_ (applyAttr appendSelection) r.attributes 39 | traverse_ (run appendSelection) (reverse r.children) -- TODO why reverse children here? 40 | pure appendSelection 41 | 42 | (Join { element, projection, selections }) -> do 43 | (Context model scope) <- get 44 | let 45 | enterFunction :: forall model. Selection model -> NativeSelection -> D3 model NativeSelection 46 | enterFunction selection nativeSelection = run nativeSelection (Append { attributes: [], children: [], element: Text }) 47 | where 48 | _ = log "callback from join enter" 49 | 50 | updateFunction :: forall model. Maybe (Selection model) -> NativeSelection -> D3 model NativeSelection 51 | updateFunction selection nativeSelection = run nativeSelection (Update { attributes: [], children: [] }) 52 | where 53 | _ = log "callback from join update" 54 | 55 | exitFunction :: forall model. Maybe (Selection model) -> NativeSelection -> D3 model NativeSelection 56 | exitFunction selection nativeSelection = run nativeSelection (Exit { attributes: [], children: [] }) 57 | where 58 | _ = log "callback from join exit" 59 | dataForJoin = 60 | case projection of 61 | Nothing -> unsafeCoerce model 62 | (Just fn) -> fn model 63 | 64 | joinSelection = spy "Join: " $ 65 | d3JoinJS activeSelection (show element) (spy "submodel: " dataForJoin) 66 | { enter: enterFunction selections.enter 67 | , update: updateFunction selections.update 68 | , exit: exitFunction selections.exit } 69 | 70 | pure joinSelection 71 | 72 | 73 | -- TODO lookup the selection given by name and run interpreter on it with 74 | (RunTimeSelection _ _) -> do 75 | pure activeSelection 76 | 77 | -- (Transition _) -> do 78 | -- pure activeSelection 79 | (Update _) -> do 80 | pure activeSelection 81 | (Exit _) -> do 82 | pure activeSelection 83 | 84 | NullSelection -> do 85 | pure activeSelection 86 | 87 | 88 | 89 | updateScope :: forall model. NativeSelection -> Maybe String -> D3 model Unit 90 | updateScope selection Nothing = pure unit 91 | updateScope selection (Just label) = 92 | modify_ (\(Context model scope) -> Context model (insert label selection scope)) 93 | 94 | -------------------------------------------------------------------------------- /packages.dhall: -------------------------------------------------------------------------------- 1 | {- 2 | Welcome to your new Dhall package-set! 3 | 4 | Below are instructions for how to edit this file for most use 5 | cases, so that you don't need to know Dhall to use it. 6 | 7 | ## Warning: Don't Move This Top-Level Comment! 8 | 9 | Due to how `dhall format` currently works, this comment's 10 | instructions cannot appear near corresponding sections below 11 | because `dhall format` will delete the comment. However, 12 | it will not delete a top-level comment like this one. 13 | 14 | ## Use Cases 15 | 16 | Most will want to do one or both of these options: 17 | 1. Override/Patch a package's dependency 18 | 2. Add a package not already in the default package set 19 | 20 | This file will continue to work whether you use one or both options. 21 | Instructions for each option are explained below. 22 | 23 | ### Overriding/Patching a package 24 | 25 | Purpose: 26 | - Change a package's dependency to a newer/older release than the 27 | default package set's release 28 | - Use your own modified version of some dependency that may 29 | include new API, changed API, removed API by 30 | using your custom git repo of the library rather than 31 | the package set's repo 32 | 33 | Syntax: 34 | Replace the overrides' "{=}" (an empty record) with the following idea 35 | The "//" or "⫽" means "merge these two records and 36 | when they have the same value, use the one on the right:" 37 | ------------------------------- 38 | let overrides = 39 | { packageName = 40 | upstream.packageName // { updateEntity1 = "new value", updateEntity2 = "new value" } 41 | , packageName = 42 | upstream.packageName // { version = "v4.0.0" } 43 | , packageName = 44 | upstream.packageName // { repo = "https://www.example.com/path/to/new/repo.git" } 45 | } 46 | ------------------------------- 47 | 48 | Example: 49 | ------------------------------- 50 | let overrides = 51 | { halogen = 52 | upstream.halogen // { version = "master" } 53 | , halogen-vdom = 54 | upstream.halogen-vdom // { version = "v4.0.0" } 55 | } 56 | ------------------------------- 57 | 58 | ### Additions 59 | 60 | Purpose: 61 | - Add packages that aren't already included in the default package set 62 | 63 | Syntax: 64 | Replace the additions' "{=}" (an empty record) with the following idea: 65 | ------------------------------- 66 | let additions = 67 | { package-name = 68 | { dependencies = 69 | [ "dependency1" 70 | , "dependency2" 71 | ] 72 | , repo = 73 | "https://example.com/path/to/git/repo.git" 74 | , version = 75 | "tag ('v4.0.0') or branch ('master')" 76 | } 77 | , package-name = 78 | { dependencies = 79 | [ "dependency1" 80 | , "dependency2" 81 | ] 82 | , repo = 83 | "https://example.com/path/to/git/repo.git" 84 | , version = 85 | "tag ('v4.0.0') or branch ('master')" 86 | } 87 | , etc. 88 | } 89 | ------------------------------- 90 | 91 | Example: 92 | ------------------------------- 93 | let additions = 94 | { benchotron = 95 | { dependencies = 96 | [ "arrays" 97 | , "exists" 98 | , "profunctor" 99 | , "strings" 100 | , "quickcheck" 101 | , "lcg" 102 | , "transformers" 103 | , "foldable-traversable" 104 | , "exceptions" 105 | , "node-fs" 106 | , "node-buffer" 107 | , "node-readline" 108 | , "datetime" 109 | , "now" 110 | ] 111 | , repo = 112 | "https://github.com/hdgarrood/purescript-benchotron.git" 113 | , version = 114 | "v7.0.0" 115 | } 116 | } 117 | ------------------------------- 118 | -} 119 | let upstream = 120 | https://github.com/purescript/package-sets/releases/download/psc-0.14.0-20210311/packages.dhall sha256:3da8be2b7b4a0e7de6186591167b363023695accffb98a8639e9e7d06e2070d6 121 | 122 | let overrides = {=} 123 | 124 | let additions = {=} 125 | 126 | in upstream // overrides // additions 127 | -------------------------------------------------------------------------------- /src/examples/NewSyntax/Force.purs: -------------------------------------------------------------------------------- 1 | module NewSyntax.Force ( 2 | chart, simulation 3 | , Model, GraphLink, GraphNode 4 | , getLinks, getNodes, makeModel 5 | , myTickMap, myDrag 6 | , readModelFromFileContents) where 7 | 8 | import D3.Base 9 | 10 | import Affjax (Error) 11 | import Data.Either (Either(..)) 12 | import Data.Map (fromFoldable) 13 | import Data.Tuple (Tuple(..)) 14 | import Math (sqrt) 15 | import Prelude (const, identity, unit, ($), (/)) 16 | import Unsafe.Coerce (unsafeCoerce) 17 | 18 | 19 | -- this is the model used by this particular "chart" (ie force layout simulation) 20 | type Model = { links :: Array GraphLink, nodes :: Array GraphNode } 21 | 22 | type GraphNode = SimulationNodeRow { group :: Number } 23 | type GraphLink = { id :: ID, source :: ID, target :: ID, value :: Number } 24 | 25 | -- | express the additions that D3 makes in terms of rows for clarity and DRY 26 | -- after the GraphLink type has been bound in D3 it is changed to the following 27 | type D3GraphLink = { id :: ID, source :: GraphNode, target :: GraphNode, value :: Number } 28 | 29 | -- Projection functions to get subModels out of the Model for sub-selections 30 | modelLinks :: Projection Model 31 | modelLinks model = unsafeCoerce model.links 32 | 33 | modelNodes :: Projection Model 34 | modelNodes model = unsafeCoerce model.nodes 35 | 36 | 37 | -- | definition of the force layout, resulting Selection is run by Interpreter 38 | chart :: Tuple Number Number -> Selection Model 39 | chart (Tuple width height) = 40 | selectInDOM "div#force" [] [ 41 | svg [ viewBox 0.0 0.0 width height ] [ 42 | group 43 | [ strokeColor "#999" 44 | , strokeOpacity 0.6 ] 45 | [ joinUsing modelLinks $ 46 | Enter Line $ Attrs [ computeStrokeWidth Px $ (\d -> sqrt (d3Link d).value) ] 47 | ] 48 | 49 | , group 50 | [ strokeColor "#fff" 51 | , strokeOpacity 1.5 ] 52 | [ joinUsing modelNodes $ -- we need to name this selection for Drag tho 53 | Enter Circle $ Attrs [ radius 5.0 Px, computeFill colorByGroup] 54 | ] 55 | ] 56 | ] 57 | 58 | 59 | -- | definition of the particular Simulation that we are going to run 60 | simulation :: Simulation 61 | simulation = 62 | Simulation { 63 | label: "simulation" -- TODO stringy label 64 | , config: defaultConfigSimulation 65 | , forces: [ Force "charge" ForceMany, centerForce 800.0 900.0 ] 66 | , nodes: [] 67 | , links: [] 68 | , tick: identity 69 | , drag: const unit 70 | } 71 | 72 | centerForce :: Number -> Number -> Force 73 | centerForce width height = Force "center" $ ForceCenter (width / 2.0) (height / 2.0) 74 | 75 | -- | function to build the tick function, quite tricky 76 | myTickMap :: TickMap Model 77 | myTickMap = fromFoldable 78 | [ Tuple "link" [ NumberAttr "x1" (\d -> (unsafeCoerce d).source.x) 79 | , NumberAttr "y1" (\d -> (unsafeCoerce d).source.y) 80 | , NumberAttr "x2" (\d -> (unsafeCoerce d).target.x) 81 | , NumberAttr "y2" (\d -> (unsafeCoerce d).target.y) ] 82 | , Tuple "node" [ NumberAttr "cx" (\d -> (unsafeCoerce d).x) 83 | , NumberAttr "cy" (\d -> (unsafeCoerce d).y) ] 84 | ] 85 | 86 | -- | utility functions and boilerplate 87 | 88 | myDrag :: DragBehavior 89 | myDrag = DefaultDrag "node" "simulation" 90 | 91 | makeModel :: Array GraphLink -> Array GraphNode -> Model 92 | makeModel links nodes = { links, nodes } 93 | 94 | getLinks :: Model -> Array GraphLink 95 | getLinks m = m.links 96 | 97 | getNodes :: Model -> Array GraphNode 98 | getNodes m = m.nodes 99 | 100 | -- TODO the decode on the Purescript side unless files are ginormous, this is just for prototyping 101 | foreign import readJSONJS :: String -> Model -- TODO no error handling at all here RN 102 | 103 | readModelFromFileContents :: forall r. Either Error { body ∷ String | r } -> Model 104 | readModelFromFileContents (Right { body } ) = readJSONJS body 105 | readModelFromFileContents (Left err) = { links: [], nodes: [] } 106 | 107 | 108 | -- we give the chart our Model type but behind the scenes it is mutated by D3 and additionally 109 | -- which projection of the "Model" is active in each Join varies so we can't have both strong 110 | -- static type representations AND lightweight syntax with JS compatible lambdas 111 | d3Link :: Datum -> D3GraphLink 112 | d3Link = unsafeCoerce 113 | d3Node :: Datum -> GraphNode 114 | d3Node = unsafeCoerce 115 | 116 | colorByGroup :: Datum -> String 117 | colorByGroup d = d3SchemeCategory10JS (unsafeCoerce d :: GraphNode).group 118 | 119 | -- drag = 120 | -- d3Drag "node" simulation { 121 | -- start: dragstarted 122 | -- , drag: dragged 123 | -- , end: dragended 124 | -- } 125 | 126 | -- attachTickMap :: (Unit -> Unit) -> Simulation -> Unit 127 | -- attachTickMap tick simulation = 128 | 129 | -------------------------------------------------------------------------------- /src/D3/Interpreter/Foreign.js: -------------------------------------------------------------------------------- 1 | "use strict" 2 | 3 | // ATTR functions 4 | 5 | // :: NativeSelection -> String -> NativeJS -> Unit 6 | exports.runSimpleAttrJS = selection => attr => value => selection.attr(attr, value) 7 | // :: forall f. NativeSelection -> String -> f -> Unit 8 | exports.runDatumAttrJS = selection => attr => f => selection.attr(attr, f) 9 | 10 | // NB the (d,i) => f(d)(i) in runDatumIndexAttrJS - the attribute-setter 11 | // function is not curried, since it’s been written on the PureScript side 12 | 13 | // :: forall f. NativeSelection -> String -> f -> Unit 14 | exports.runDatumIndexAttrJS = selection => attr => f => selection.attr(attr, (d,i) => f(d)(i)) 15 | // foreign import runDatumTextJS :: forall f. NativeSelection -> f -> Unit 16 | exports.runDatumTextJS = selection => f => selection.text(f) 17 | 18 | // SELECTION functions 19 | // :: String -> NativeSelection 20 | exports.d3SelectAllJS = selector => d3.selectAll(selector) 21 | // :: NativeSelection -> String -> NativeSelection 22 | exports.d3AppendElementJS = selection => element => selection.append(element) 23 | // :: forall d. d -> NativeSelection 24 | exports.d3JoinJS = selection => element => data => fns => { 25 | console.log("d3JoinJS"); 26 | return selection.selectAll(element).data(data).join(fns.enter, fns.update, fns.exit) 27 | } 28 | 29 | exports.d3EnterWithIndexJS = selection => data => idFunction => selection.data(data, idFunction) 30 | // :: NativeSelection 31 | exports.nullSelectionJS = null // NB only used on init, maybe still a bad idea 32 | 33 | 34 | // SIMULATION functions 35 | exports.initSimulationJS = config => { 36 | return d3.forceSimulation() 37 | .alpha(config.alpha) // default is 1 38 | .alphaTarget(config.alphaTarget) // default is 0 39 | .alphaMin(config.alphaMin) // default is 0.0001 40 | .alphaDecay(config.alphaDecay) // default is 0.0228 41 | .velocityDecay(config.velocityDecay) // default is 0.4 42 | } 43 | // :: Simulation -> Array NativeNode -> Array NativeNode 44 | exports.putNodesInSimulationJS = simulation => nodes => { 45 | simulation.nodes(nodes) 46 | return nodes 47 | } 48 | // :: Simulation -> Array NativeLink -> Array NativeLink 49 | exports.putLinksInSimulationJS = simulation => links => { 50 | simulation.force("links", d3.forceLink(links).id(d => d.id)) 51 | return links 52 | } 53 | // :: NativeSelection -> Number -> Unit 54 | exports.setAlphaTargetJS = simulation => target => simulation.alphaTarget(target) 55 | // :: NativeSelection -> Unit 56 | exports.startSimulationJS = simulation => simulation.restart() 57 | // :: NativeSelection -> Unit 58 | exports.stopSimulationJS = simulation => simulation.stop() 59 | 60 | var tickAttrArray = [] // TODO probably want API to reset this too, but defer til adding named tick functions 61 | exports.addAttrFnToTickJS = selection => pursAttr => { 62 | tickAttrArray.push({ selection: selection, attr: pursAttr.value0, fn: pursAttr.value1 }) 63 | } 64 | // assumes we've already put all the things we want to happen into the tickAttrArray 65 | exports.attachTickFnToSimulationJS = simulation => simulation.on("tick", () => { 66 | tickAttrArray.forEach(element => (element.selection).attr(element.attr, element.fn)) 67 | }) 68 | // default drag function, useful in probably most simulations 69 | exports.attachDefaultDragBehaviorJS = selection => simulation => { 70 | 71 | var drag = function(simulation) { 72 | function dragstarted(event, d) { 73 | if (!event.active) simulation.alphaTarget(0.3).restart(); 74 | d.fx = d.x; 75 | d.fy = d.y; 76 | } 77 | 78 | function dragged(event,d) { 79 | d.fx = event.x; 80 | d.fy = event.y; 81 | } 82 | 83 | function dragended(event,d) { 84 | if (!event.active) simulation.alphaTarget(0); 85 | d.fx = null; 86 | d.fy = null; 87 | } 88 | 89 | return d3.drag() 90 | .on("start", dragstarted) 91 | .on("drag", dragged) 92 | .on("end", dragended); 93 | } 94 | 95 | selection.call(drag(simulation)) 96 | } 97 | 98 | // FORCE functions 99 | // :: Simulation -> Unit 100 | exports.forceManyJS = simulation => label => simulation.force(label, d3.forceManyBody()) 101 | // :: Simulation -> Number -> Number -> Unit 102 | exports.forceCenterJS = simulation => label => cx => cy => simulation.force(label, d3.forceCenter(cx,cy)) 103 | // :: Simulation -> Unit 104 | // exports.forceLinks = simulation => label => simulation.x() 105 | // :: Simulation -> Number -> Unit 106 | exports.forceCollideJS = simulation => label => radius => simulation.force(label, d3.forceCollide(radius)) 107 | // :: Simulation -> Number -> Unit 108 | exports.forceXJS = simulation => label => cx => simulation.force(label, d3.forceX(cx)) 109 | // :: Simulation -> Number -> Unit 110 | exports.forceYJS = simulation => label => cy => simulation.force(label, d3.forceY(cy)) 111 | // :: Simulation -> Number -> Number -> Unit 112 | exports.forceRadialJS = simulation => label => cx => cy => simulation.force(label, d3.forceRadial(cx, cy)) 113 | 114 | -------------------------------------------------------------------------------- /src/D3/Interpreter/Layouts/Simulation.purs: -------------------------------------------------------------------------------- 1 | module D3.Interpreter.Layouts.Simulation where 2 | 3 | import D3.Interpreter.Foreign (NativeJS, addAttrFnToTickJS, attachDefaultDragBehaviorJS, attachTickFnToSimulationJS, forceCenterJS, forceCollideJS, forceManyJS, forceRadialJS, forceXJS, forceYJS, initSimulationJS, putLinksInSimulationJS, putNodesInSimulationJS, startSimulationJS, stopSimulationJS) 4 | import Prelude (Unit, bind, discard, flip, pure, unit, ($), (<$>)) 5 | 6 | import Control.Monad.State (get, put) 7 | import D3.Base (Attr, DragBehavior(..), Force(..), NativeSelection, Simulation(..), TickMap) 8 | import D3.Base.Layout.Simulation (ForceType(..)) 9 | import D3.Interpreter.Types (D3, D3State(..)) 10 | import Data.Array (concatMap, foldl, fromFoldable, (:)) 11 | import Data.Bifunctor (lmap) 12 | import Data.Map (Map, insert, lookup, toUnfoldable) 13 | import Data.Maybe (Maybe(..)) 14 | import Data.Traversable (traverse_) 15 | import Data.Tuple (Tuple(..)) 16 | import Unsafe.Coerce (unsafeCoerce) 17 | 18 | -- | FORCE LAYOUT (SIMULATION) interpreter 19 | startSimulation :: forall model. NativeSelection -> D3 model Unit 20 | startSimulation simulation = pure $ startSimulationJS simulation 21 | 22 | stopSimulation :: forall model. NativeSelection -> D3 model Unit 23 | stopSimulation simulation = pure $ stopSimulationJS simulation 24 | 25 | -- | get a reference to a simulation that we can then use elsewhere 26 | -- | ie having interpreted a Selection such that the DOM is set up to run a simulation 27 | -- | NB this is actually modifying the model in the Context 28 | interpretSimulation :: forall model node link. Simulation -> 29 | (model -> Array node) -> -- project nodes from model 30 | (model -> Array link) -> -- project links from model 31 | (Array link -> Array node -> model) -> -- repackage nodes & links as model 32 | D3 model NativeSelection 33 | interpretSimulation (Simulation r) getNodes getLinks repackage = 34 | do 35 | (Context model scope) <- get 36 | let sim = initSimulationJS r.config 37 | nodes = nativeNodes $ getNodes model 38 | links = nativeLinks $ getLinks model 39 | -- updateScope sim (Just r.label) 40 | traverse_ (interpretForce sim) r.forces 41 | let initializedNodes = unnativeNodes $ putNodesInSimulationJS sim nodes 42 | initializedLinks = unnativeLinks $ putLinksInSimulationJS sim links 43 | initializedModel = repackage initializedLinks initializedNodes 44 | put (Context initializedModel (insert r.label sim scope)) 45 | pure sim 46 | where 47 | nativeNodes = unsafeCoerce :: Array node -> Array NativeJS 48 | nativeLinks = unsafeCoerce :: Array link -> Array NativeJS 49 | unnativeNodes = unsafeCoerce :: Array NativeJS -> Array node 50 | unnativeLinks = unsafeCoerce :: Array NativeJS -> Array link 51 | 52 | interpretForce :: forall model. NativeSelection -> Force -> D3 model Unit 53 | interpretForce simulation = do 54 | case _ of 55 | (Force label ForceMany) -> pure $ forceManyJS simulation label 56 | (Force label (ForceCenter cx cy)) -> pure $ forceCenterJS simulation label cx cy 57 | -- (Force (ForceLink links idFn)) -> pure $ forceLinks 58 | (Force label (ForceCollide radius_)) -> pure $ forceCollideJS simulation label radius_ 59 | (Force label (ForceX x)) -> pure $ forceXJS simulation label x 60 | (Force label (ForceY y)) -> pure $ forceYJS simulation label y 61 | (Force label (ForceRadial cx cy)) -> pure $ forceRadialJS simulation label cx cy 62 | (Force label Custom) -> pure $ unit -- do this later as needed 63 | 64 | -- getNativeSelections :: Map String NativeSelection -> TickMap model -> Array (Tuple NativeSelection (Array (Tuple attr fn))) 65 | -- getNativeSelections scope [] 66 | 67 | 68 | getNativeSelection :: forall x. (Map String NativeSelection) -> Map String x -> Array (Tuple NativeSelection x) 69 | getNativeSelection scopeMap tickMap = fromFoldable nativeTuples 70 | where 71 | tickTuples :: Array (Tuple String x) 72 | tickTuples = toUnfoldable tickMap 73 | 74 | maybeNativeTuple :: Tuple String x -> Tuple (Maybe NativeSelection) x 75 | maybeNativeTuple = lmap (flip lookup scopeMap) 76 | 77 | maybeNativeTuples :: Array (Tuple (Maybe NativeSelection) x) 78 | maybeNativeTuples = maybeNativeTuple <$> tickTuples 79 | 80 | nativeTuples :: Array (Tuple NativeSelection x) 81 | nativeTuples = foldl foldFn [] maybeNativeTuples 82 | 83 | foldFn list (Tuple (Just a) b) = (Tuple a b) : list 84 | foldFn list (Tuple Nothing _) = list 85 | 86 | addAttrFnToTick :: forall model. Tuple NativeSelection Attr -> D3 model Unit 87 | addAttrFnToTick (Tuple selection attr) = pure $ addAttrFnToTickJS selection attr 88 | 89 | interpretTickMap :: forall model. NativeSelection -> TickMap model -> D3 model Unit 90 | interpretTickMap simulation tickMap = do 91 | (Context model scope) <- get 92 | -- [(label,attrs)] -> [(nativeselection, attr)] so as enable build up of list on JS side 93 | let 94 | attrs :: Array (Tuple NativeSelection Attr) 95 | attrs = concatMap (\(Tuple x ys) -> (Tuple x) <$> ys) (getNativeSelection scope tickMap) 96 | -- TODO pending better solution will pass Attr (purescript type) over FFI and decode there 97 | traverse_ addAttrFnToTick attrs 98 | pure $ attachTickFnToSimulationJS simulation 99 | 100 | interpretDrag :: forall model. DragBehavior -> D3 model Unit 101 | interpretDrag (DefaultDrag selectionName simulationName) = do 102 | (Context model scope) <- get 103 | let selection = lookup selectionName scope 104 | let simulation = lookup simulationName scope 105 | pure $ case selection, simulation of 106 | (Just sel), (Just sim) -> attachDefaultDragBehaviorJS sel sim 107 | _, _ -> unit 108 | 109 | 110 | -------------------------------------------------------------------------------- /src/examples/NewSyntax/Tree.purs: -------------------------------------------------------------------------------- 1 | module NewSyntax.Tree ( 2 | Model, TreeConfig -- TODO move to Base 3 | , Tree(..), TreeJson -- NB no constructor 4 | , chart 5 | , readModelFromFileContents -- read the tree structure 6 | , makeModel -- post process tree structure such that it can be used to render using chart 7 | ) where 8 | 9 | import D3.Base 10 | 11 | import Affjax (Error) 12 | import D3.Base.Attributes (LineJoin(..), strokeLineJoin, toString, transform) 13 | import D3.Layout.Trees (radialLink) 14 | import Data.Either (Either(..)) 15 | import Data.Tuple (Tuple(..)) 16 | import Debug.Trace (spy) 17 | import Math (pi) 18 | import Prelude (negate, show, ($), (*), (-), (/), (<), (<>), (==), (>=)) 19 | import Unsafe.Coerce (unsafeCoerce) 20 | 21 | type ModelData = { name :: String } 22 | -- the Model given to D3 23 | type MyModel = Model ModelData 24 | -- nodes of tree AFTER D3 has processed them, contains original ModelData as "data" field 25 | type MyTreeNode = D3TreeNode ModelData 26 | 27 | makeModel :: Number -> TreeJson -> Model String 28 | makeModel width json = Model { json, d3Tree, config } 29 | where 30 | config = radialTreeConfig width 31 | hierarchicalData = d3Hierarchy json 32 | d3Tree = d3InitTree config hierarchicalData 33 | 34 | -- Projection functions to get subModels out of the Model for sub-selections 35 | linkData :: Projection (Model String) 36 | linkData (Model m) = d3HierarchyLinks m.d3Tree 37 | 38 | descendentsData :: Projection (Model String) 39 | descendentsData (Model m) = d3HierarchyDescendants m.d3Tree 40 | 41 | -- we give the chart our Model type but behind the scenes it is mutated by D3 and additionally 42 | -- which projection of the "Model" is active in each Join varies so we can't have both strong 43 | -- static type representations AND lightweight syntax with JS compatible lambdas 44 | d3TreeNode :: Datum -> MyTreeNode 45 | d3TreeNode = unsafeCoerce 46 | 47 | chart :: Tuple Number Number -> Selection (Model String) 48 | chart (Tuple width height) = 49 | let 50 | origin = { 51 | x: -width / 2.0 52 | , y: -height / 2.0 53 | } 54 | -- three little transform functions to build up the transforms on nodes and labels 55 | rotate x = show $ (x * 180.0 / pi - 90.0) 56 | rotateCommon d = "rotate(" <> rotate (d3TreeNode d).x <> ")" 57 | rotateText2 d = "rotate(" <> if (d3TreeNode d).x >= pi 58 | then "180" <> ")" 59 | else "0" <> ")" 60 | -- same translation for both text and node 61 | translate d = "translate(" <> show (d3TreeNode d).y <> ",0)" 62 | 63 | labelOffset :: Datum -> Number 64 | labelOffset d = 65 | if ((d3TreeNode d).x < pi) == hasChildren d 66 | then 6.0 67 | else (-6.0) 68 | 69 | textOffset :: Datum -> String 70 | textOffset d = 71 | if ((d3TreeNode d).x < pi) == hasChildren d 72 | then "start" 73 | else "end" 74 | 75 | in 76 | selectInDOM "div#tree" [] $ [ 77 | svg [ viewBox origin.x origin.y width height ] [ 78 | group_ 79 | [ joinUsing linkData $ 80 | Enter Path $ 81 | Attrs [ strokeWidth 1.5 Px 82 | , strokeColor "#555" 83 | , strokeOpacity 0.4 84 | , fill "none" 85 | , radialLink (\d -> (d3TreeNode d).x) (\d -> (d3TreeNode d).y) 86 | ] 87 | ] 88 | 89 | , group_ 90 | [ joinUsing descendentsData $ 91 | Enter Circle $ 92 | Attrs [ radius 2.5 Px 93 | , computeFill (\d -> if hasChildren d then "#555" else "#999") 94 | , transform [ rotateCommon, translate ] 95 | ] 96 | ] 97 | 98 | , group [ fontFamily "sans-serif" 99 | , fontSize 10.0 Pt 100 | , strokeLineJoin Round 101 | , strokeWidth 3.0 Px ] 102 | [ joinUsing descendentsData $ 103 | Enter Text $ 104 | Attrs [ transform [ rotateCommon, translate, rotateText2] 105 | , dy 0.31 Em 106 | , computeX Px labelOffset -- TODO this expression is clumsy 107 | , computeTextAnchor textOffset 108 | , computeText (\d -> (d3TreeNode d).data.name) 109 | -- TODO add clone step later, perhaps branch of Selection? 110 | ] 111 | ] 112 | ] 113 | ] 114 | 115 | -- | TODO All this stuff belongs eventually in the d3 base 116 | data Tree a = Node a (Array (Tree a)) 117 | type TreeConfig :: forall k. k -> Type 118 | type TreeConfig a = { 119 | size :: Array Number 120 | , separation :: Datum -> Datum -> Int 121 | } 122 | 123 | radialTreeConfig :: forall a. Number -> TreeConfig a 124 | radialTreeConfig width = 125 | { size: [2.0 * pi, width / 2.0] 126 | , separation: radialSeparationJS 127 | } 128 | 129 | data Model :: forall k. k -> Type 130 | data Model a = Model { 131 | json :: TreeJson 132 | , d3Tree :: D3Tree 133 | , config :: TreeConfig a 134 | } 135 | 136 | type D3TreeNode a = { 137 | "data" :: a -- guaranteed coercible to the `a` of the `Model a` 138 | , x :: Number 139 | , y :: Number 140 | , value :: String 141 | , depth :: Number 142 | , height :: Number 143 | -- these next too are guaranteed coercible to the same type, ie D3TreeNode 144 | -- BUT ONLY IF the D3Tree is a successful conversion using d3Hierarchy 145 | -- TODO code out exceptions 146 | , parent :: RecursiveD3TreeNode -- this won't be present in the root node 147 | , children :: Array RecursiveD3TreeNode -- this won't be present in leaf nodes 148 | } 149 | 150 | -- do the decode on the Purescript side unless files are ginormous, this is just for prototyping 151 | -- this is an opaque type behind which hides the data type of the Purescript tree that was converted 152 | foreign import data RecursiveD3TreeNode :: Type 153 | -- this is the Purescript Tree after processing in JS to remove empty child fields from leaves etc 154 | -- need to ensure that this structure is encapsulated in libraries (ie by moving this code) 155 | foreign import data D3Tree :: Type 156 | foreign import data D3Hierarchical :: Type 157 | foreign import data TreeJson :: Type 158 | foreign import radialSeparationJS :: Datum -> Datum -> Int 159 | foreign import readJSONJS :: String -> TreeJson -- TODO no error handling at all here RN 160 | foreign import d3Hierarchy :: TreeJson -> D3Hierarchical 161 | foreign import d3InitTree :: forall a. TreeConfig a -> D3Hierarchical -> D3Tree 162 | foreign import hasChildren :: Datum -> Boolean 163 | 164 | foreign import d3HierarchyLinks :: D3Tree -> SubModel 165 | foreign import d3HierarchyDescendants :: D3Tree -> SubModel 166 | 167 | 168 | 169 | -- TODO read file elsewhere and pass it in 170 | readModelFromFileContents :: forall r. Tuple Number Number -> Either Error { body ∷ String | r } -> Either Error (Model String) 171 | readModelFromFileContents (Tuple width _) (Right { body } ) = Right $ makeModel width (readJSONJS body) 172 | readModelFromFileContents _ (Left error) = Left error 173 | 174 | -------------------------------------------------------------------------------- /src/D3/Base/Attributes.purs: -------------------------------------------------------------------------------- 1 | module D3.Base.Attributes where 2 | 3 | import D3.Base.Foreign (Datum) 4 | import Data.Array (intercalate) 5 | import Prelude (class Show, flap, show, ($), (<>)) 6 | 7 | -- internal definitions of Attrs, this is what the interpreter will work with 8 | -- these definitions correspond exactly to what D3 allows polymorphically for attribute setters 9 | -- the idea is to NOT make this available outside this module eventually, preferring to use 10 | -- stronger typing and more expressive functions that wrap them, as below 11 | -- HOWEVER it might be that there are just too many to be practical, time will tell 12 | data Attr = 13 | -- first the direct, static attributes 14 | StaticString String String 15 | | StaticNumber String Number 16 | | StaticArrayNumber String (Array Number) 17 | -- then the ones that are function of Datum only 18 | | StringAttr String (Datum -> String) 19 | | NumberAttr String (Datum -> Number) 20 | | ArrayNumberAttr String (Datum -> Array Number) 21 | -- lastly attribute functions that take Datum and the index 22 | | StringAttrI String (Datum -> Number -> String) 23 | | NumberAttrI String (Datum -> Number -> Number) 24 | | ArrayNumberAttrI String (Datum -> Number -> Array Number) 25 | -- Text in D3 is not an attribute in D3 but syntactically and semantically it really is 26 | -- so in our DSL we will just make it one and hide that fact 27 | | TextAttr (Datum -> String) 28 | -- Transition is also not an attribute in D3 but in this DSL it is 29 | -- TODO there may be real difficulties with this with the attributes that are inherited from Selection 30 | -- that's not clear right now 31 | -- | Transition TransitionAttributes 32 | 33 | -- many things that are attached to Transition in D3 will simply revert to Selection 34 | data TransitionAttributes = Duration Int | Filter | Remove -- nowhere close to exhaustive 35 | 36 | data NumberUnit = Em | Px | Rem | Percent | Pt | NoUnit 37 | 38 | -- prettier definitions for attributes 39 | -- TODO we're not doing anything to check / constrain the semantics of the units here, but it would be good to do so (type-level machinery?) 40 | -- TODO do we need more units like "1fr" etc for grids etc? 41 | 42 | toString :: Number -> NumberUnit -> String 43 | toString n = 44 | case _ of 45 | Em -> show n <> "em" 46 | Px -> show n <> "px" 47 | Rem -> show n <> "rem" 48 | Percent -> show n <> "%" 49 | Pt -> show n <> "pt" 50 | NoUnit -> show n 51 | 52 | staticNumberAttrWithUnits :: String -> Number -> NumberUnit -> Attr 53 | staticNumberAttrWithUnits label n u = StaticString label (toString n u) 54 | 55 | datumToStringWithUnit :: NumberUnit -> (Datum -> Number) -> (Datum -> String) 56 | datumToStringWithUnit u f = (\d -> toString (f d) u ) 57 | 58 | datumAndIndexToStringWithUnit :: NumberUnit -> (Datum -> Number -> Number) -> (Datum -> Number -> String) 59 | datumAndIndexToStringWithUnit u f = (\d i -> toString (f d i) u ) 60 | 61 | -- | static versions of attribute setters, where all selected elements will be given the same value for this attribute 62 | strokeColor :: String -> Attr 63 | strokeColor = StaticString "stroke" 64 | 65 | strokeOpacity :: Number -> Attr 66 | strokeOpacity = StaticNumber "stroke-opacity" 67 | 68 | fill :: String -> Attr 69 | fill = StaticString "fill" 70 | 71 | viewBox :: Number -> Number -> Number -> Number -> Attr 72 | viewBox xo yo width height = StaticArrayNumber "viewBox" [ xo, yo, width, height ] 73 | 74 | fontFamily :: String -> Attr 75 | fontFamily = StaticString "font-family" 76 | 77 | textAnchor :: String -> Attr 78 | textAnchor = StaticString "text-anchor" 79 | 80 | strokeWidth :: Number -> NumberUnit -> Attr 81 | strokeWidth n u = staticNumberAttrWithUnits "stroke-width" n u 82 | 83 | radius :: Number -> NumberUnit -> Attr 84 | radius n u = staticNumberAttrWithUnits "r" n u 85 | 86 | fontSize :: Number -> NumberUnit -> Attr 87 | fontSize n u = staticNumberAttrWithUnits "font-size" n u 88 | 89 | width :: Number -> NumberUnit -> Attr 90 | width n u = staticNumberAttrWithUnits "width" n u 91 | 92 | height :: Number -> NumberUnit -> Attr 93 | height n u = staticNumberAttrWithUnits "height" n u 94 | 95 | x :: Number -> NumberUnit -> Attr 96 | x n u = staticNumberAttrWithUnits "x" n u 97 | 98 | y :: Number -> NumberUnit -> Attr 99 | y n u = staticNumberAttrWithUnits "y" n u 100 | 101 | dx :: Number -> NumberUnit -> Attr 102 | dx n u = staticNumberAttrWithUnits "dx" n u 103 | 104 | dy :: Number -> NumberUnit -> Attr 105 | dy n u = staticNumberAttrWithUnits "dy" n u 106 | 107 | -- | computed versions of attribute setters, which need a function because the attribute is determined by the datum 108 | computeStrokeColor :: (Datum -> String) -> Attr 109 | computeStrokeColor = StringAttr "stroke" 110 | 111 | computeFill :: (Datum -> String) -> Attr 112 | computeFill = StringAttr "fill" 113 | 114 | computeText :: (Datum -> String) -> Attr 115 | computeText = TextAttr 116 | 117 | computeTextAnchor :: (Datum -> String) -> Attr 118 | computeTextAnchor = StringAttr "text-anchor" 119 | 120 | computeStrokeWidth :: NumberUnit -> (Datum -> Number) -> Attr 121 | computeStrokeWidth u f = StringAttr "stroke-width" (datumToStringWithUnit u f) 122 | 123 | computeStrokeOpacity :: (Datum -> Number) -> Attr 124 | computeStrokeOpacity = NumberAttr "stroke-opacity" 125 | 126 | computeRadius :: NumberUnit -> (Datum -> Number) -> Attr 127 | computeRadius u f = StringAttr "r" (datumToStringWithUnit u f) 128 | 129 | computeX :: NumberUnit -> (Datum -> Number) -> Attr 130 | computeX u f = StringAttr "x" (datumToStringWithUnit u f) 131 | 132 | computeY :: NumberUnit -> (Datum -> Number) -> Attr 133 | computeY u f = StringAttr "y" (datumToStringWithUnit u f) 134 | 135 | computeDX :: NumberUnit -> (Datum -> Number) -> Attr 136 | computeDX u f = StringAttr "dx" (datumToStringWithUnit u f) 137 | 138 | computeDY :: NumberUnit -> (Datum -> Number) -> Attr 139 | computeDY u f = StringAttr "dy" (datumToStringWithUnit u f) 140 | 141 | -- TODO refactor all these functions that differ only in attr "string" when finished 142 | computeXusingIndex :: NumberUnit -> (Datum -> Number -> Number) -> Attr 143 | computeXusingIndex u f = StringAttrI "x" (datumAndIndexToStringWithUnit u f) 144 | 145 | computeYusingIndex :: NumberUnit -> (Datum -> Number -> Number) -> Attr 146 | computeYusingIndex u f = StringAttrI "y" (datumAndIndexToStringWithUnit u f) 147 | 148 | data LineJoin = Arcs | Bevel | Miter | MiterClip | Round 149 | instance showLineJoin :: Show LineJoin where 150 | show Arcs = "arcs" 151 | show Bevel = "bevel" 152 | show Miter = "miter" 153 | show MiterClip = "miter-clip" 154 | show Round = "round" 155 | 156 | strokeLineJoin :: LineJoin -> Attr 157 | strokeLineJoin linejoin = StaticString "stroke-linejoin" $ show linejoin 158 | 159 | type TransformFn = Datum -> String 160 | 161 | transform :: Array TransformFn -> Attr 162 | transform fs = StringAttr "transform" (\d -> showTransform d) 163 | where 164 | showTransform d = intercalate " " $ flap fs d 165 | 166 | -- | Show instance etc 167 | 168 | bracket :: Array String -> String 169 | bracket ss = "(" <> 170 | (intercalate "," ss) 171 | <> ")" 172 | 173 | prefix :: String -> String 174 | prefix s = "\n.attr" <> s 175 | 176 | enQuote :: String -> String 177 | enQuote string = "\"" <> string <> "\"" 178 | 179 | instance showAttribute :: Show Attr where 180 | show (StaticString a v) = prefix $ bracket $ [ enQuote a, enQuote v] 181 | show (StaticNumber a v) = prefix $ bracket $ [ enQuote a, enQuote $ show v ] 182 | show (StaticArrayNumber a v) = prefix $ bracket $ [ enQuote a, enQuote $ show v ] 183 | show (TextAttr fn) = prefix $ bracket $ [ enQuote "Text", "<\\d -> result>" ] 184 | show (StringAttr a fn) = prefix $ bracket $ [ enQuote a, "<\\d -> result>" ] 185 | show (NumberAttr a fn) = prefix $ bracket $ [ enQuote a, "<\\d -> result>" ] 186 | show (ArrayNumberAttr a fn) = prefix $ bracket $ [ enQuote a, "<\\d -> result>" ] 187 | show (StringAttrI a fn) = prefix $ bracket $ [ enQuote a, "<\\d i -> result>" ] 188 | show (NumberAttrI a fn) = prefix $ bracket $ [ enQuote a, "<\\d i -> result>" ] 189 | show (ArrayNumberAttrI a fn) = prefix $ bracket $ [ enQuote a, "<\\d i -> result>" ] 190 | -- show (Transition attrs) = "no proper show instance for Transition yet" 191 | -------------------------------------------------------------------------------- /src/D3/Base/Selection.purs: -------------------------------------------------------------------------------- 1 | module D3.Base.Selection where 2 | 3 | import D3.Base.Attributes (Attr) 4 | import D3.Base.Foreign (SubModel) 5 | import Prelude (class Show, ($), (<>)) 6 | 7 | import Data.Maybe (Maybe(..)) 8 | import Data.Array (singleton) 9 | 10 | type Label = String 11 | type Selector = String 12 | data Element = Svg | Group | Div | Line | Circle | Path | Text | Table | Td | Tr 13 | 14 | type Projection model = model -> SubModel 15 | 16 | -- | Types to represent Selection and Insertion 17 | -- | you can append a list of many (different) elements 18 | -- | or an entire selection of only one type of element bound to some data 19 | -- | Selection will i guess be an indexed monad? in order to move back and forth 20 | -- | between Selection/Transition 21 | data Selection model = 22 | InitialSelect { 23 | selector :: String 24 | , attributes :: Array Attr 25 | , children :: Array (Selection model) 26 | } 27 | 28 | | Append { 29 | element :: Element 30 | , children :: Array (Selection model) 31 | , attributes :: Array Attr 32 | } 33 | 34 | | Join { -- enter with option update and/or exit enter will append the element, exit will terminate with remove 35 | element :: Element 36 | , selections :: { enter :: Selection model 37 | , update :: Maybe (Selection model) -- would have to be an Update 38 | , exit :: Maybe (Selection model) } -- would have to be an Exit 39 | , projection :: Maybe (Projection model) 40 | } 41 | 42 | | Update { 43 | children :: Array (Selection model) 44 | , attributes :: Array Attr 45 | } 46 | 47 | | Exit { 48 | children :: Array (Selection model) 49 | , attributes :: Array Attr 50 | } 51 | -- TODO can Transition, Call, Clone and other things be part of Selection? 52 | 53 | -- placeholder for a selection that isn't determined til runtime 54 | -- and which must be looked up 55 | | RunTimeSelection String (Selection model) 56 | | NullSelection -- used for optional Join functions 57 | 58 | selectInDOM :: forall model. Selector -> Array Attr -> Array (Selection model) -> Selection model 59 | selectInDOM selector attributes children = 60 | InitialSelect { selector, attributes, children } 61 | 62 | append :: forall model. Element -> Array Attr -> Array (Selection model) -> Selection model 63 | append element attributes children = 64 | Append { element, attributes, children } 65 | 66 | append_ :: forall model. Element -> Array Attr -> Selection model 67 | append_ element attributes = 68 | Append { element, attributes, children: [] } 69 | 70 | modifySelection :: forall model. String -> Selection model -> Selection model 71 | modifySelection name b = RunTimeSelection name b 72 | 73 | 74 | data JoinParameters model = 75 | AttrsAndChildren (Array Attr) (Array (Selection model)) 76 | | AttrsAndChild (Array Attr) (Selection model) 77 | | Attrs (Array Attr) 78 | | Children (Array (Selection model)) 79 | | Child (Selection model) 80 | 81 | data OnJoin model = 82 | Enter Element (JoinParameters model) 83 | | EnterUpdate Element (JoinParameters model) (JoinParameters model) 84 | | EnterExit Element (JoinParameters model) (JoinParameters model) 85 | | EnterUpdateExit Element (JoinParameters model) (JoinParameters model) (JoinParameters model) 86 | 87 | -- underlying function for all the enter variations 88 | join_ :: forall model. Maybe (Projection model) -> OnJoin model -> Selection model 89 | join_ projection = 90 | case _ of 91 | (Enter element p) -> 92 | Join { element 93 | , projection 94 | , selections: { enter: makeEnterSelection element p 95 | , update: Nothing 96 | , exit: Nothing } 97 | } 98 | 99 | (EnterUpdate element pe pu) -> 100 | Join { element 101 | , projection 102 | , selections: { enter: makeEnterSelection element pe 103 | , update: Just (makeUpdateSelection pu) 104 | , exit: Nothing } 105 | } 106 | 107 | (EnterExit element pe px) -> 108 | Join { element 109 | , projection 110 | , selections: { enter: makeEnterSelection element pe 111 | , update: Nothing 112 | , exit: Just (makeExitSelection px) } 113 | } 114 | 115 | (EnterUpdateExit element pe pu px) -> 116 | Join { element 117 | , projection 118 | , selections: { enter: makeEnterSelection element pe 119 | , update: Just (makeUpdateSelection pu) 120 | , exit: Just (makeExitSelection px) } 121 | } 122 | 123 | 124 | join :: forall model. OnJoin model -> Selection model 125 | join onJoin = join_ Nothing onJoin 126 | 127 | joinUsing :: forall model. Projection model -> OnJoin model -> Selection model 128 | joinUsing projection onJoin = join_ (Just projection) onJoin 129 | 130 | makeEnterSelection :: forall model. Element -> JoinParameters model -> Selection model 131 | makeEnterSelection element = 132 | case _ of 133 | (AttrsAndChildren attributes children) -> Append { element, attributes, children } 134 | (AttrsAndChild attributes child) -> Append { element, attributes, children: [child] } 135 | (Children children) -> Append { element, attributes: [], children } 136 | (Child child) -> Append { element, attributes: [], children: [child] } 137 | (Attrs attributes) -> Append { element, attributes, children: [] } 138 | 139 | makeUpdateSelection :: forall model. JoinParameters model -> Selection model 140 | makeUpdateSelection = 141 | case _ of 142 | (AttrsAndChildren attributes children) -> Update { attributes, children } 143 | (AttrsAndChild attributes child) -> Update { attributes, children: [child] } 144 | (Children children) -> Update { attributes: [], children } 145 | (Child child) -> Update { attributes: [], children: [child] } 146 | (Attrs attributes) -> Update { attributes, children: [] } 147 | 148 | makeExitSelection :: forall model. JoinParameters model -> Selection model 149 | makeExitSelection = 150 | case _ of 151 | (AttrsAndChildren attributes children) -> Exit { attributes, children } 152 | (AttrsAndChild attributes child) -> Exit { attributes, children: [child] } 153 | (Children children) -> Exit { attributes: [], children } 154 | (Child child) -> Exit { attributes: [], children: [child] } 155 | (Attrs attributes) -> Exit { attributes, children: [] } 156 | 157 | 158 | 159 | 160 | -- TODO rewrite the show instance once Selection ADT settles down 161 | -- | Show instance etc 162 | 163 | -- instance showSelection :: Show (Selection a) where 164 | -- show (InitialSelect r) = "d3.selectAll(\"" <> r.selector <> "\")" <> " name: " <> show r.label <> " " <> show r.attributes <> show r.children 165 | -- show (Append r) = 166 | -- let prefix = case r.label of 167 | -- (Just name) -> "const " <> name <> " = " 168 | -- Nothing -> "\n" 169 | -- in prefix <> ".append(\"" <> show r.element <> "\")" <> show r.attributes <> show r.children 170 | -- show (Join r) = "Join" <> 171 | -- show r.enterUpdateExit.enter <> 172 | -- show r.enterUpdateExit.update <> 173 | -- show r.enterUpdateExit.exit 174 | -- show (Transition r) = 175 | -- let prefix = case r.label of 176 | -- (Just name) -> "const " <> name <> " = " 177 | -- Nothing -> "\n" 178 | -- in prefix <> ".transition(\"" <> show r.duration <> "\")" <> show r.attributes 179 | -- show NullSelection = "" 180 | -- show (RunTimeSelection name selection) = 181 | 182 | instance showElement :: Show Element where 183 | show Svg = "svg" 184 | show Group = "g" 185 | show Div = "div" 186 | show Line = "line" 187 | show Circle = "circle" 188 | show Path = "path" 189 | show Text = "text" 190 | show Table = "table" 191 | show Td = "td" 192 | show Tr = "tr" 193 | -------------------------------------------------------------------------------- /dist/flare-2.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "flare", 3 | "children": [ 4 | { 5 | "name": "analytics", 6 | "children": [ 7 | { 8 | "name": "cluster", 9 | "children": [ 10 | {"name": "AgglomerativeCluster", "value": 3938}, 11 | {"name": "CommunityStructure", "value": 3812}, 12 | {"name": "HierarchicalCluster", "value": 6714}, 13 | {"name": "MergeEdge", "value": 743} 14 | ] 15 | }, 16 | { 17 | "name": "graph", 18 | "children": [ 19 | {"name": "BetweennessCentrality", "value": 3534}, 20 | {"name": "LinkDistance", "value": 5731}, 21 | {"name": "MaxFlowMinCut", "value": 7840}, 22 | {"name": "ShortestPaths", "value": 5914}, 23 | {"name": "SpanningTree", "value": 3416} 24 | ] 25 | }, 26 | { 27 | "name": "optimization", 28 | "children": [ 29 | {"name": "AspectRatioBanker", "value": 7074} 30 | ] 31 | } 32 | ] 33 | }, 34 | { 35 | "name": "animate", 36 | "children": [ 37 | {"name": "Easing", "value": 17010}, 38 | {"name": "FunctionSequence", "value": 5842}, 39 | { 40 | "name": "interpolate", 41 | "children": [ 42 | {"name": "ArrayInterpolator", "value": 1983}, 43 | {"name": "ColorInterpolator", "value": 2047}, 44 | {"name": "DateInterpolator", "value": 1375}, 45 | {"name": "Interpolator", "value": 8746}, 46 | {"name": "MatrixInterpolator", "value": 2202}, 47 | {"name": "NumberInterpolator", "value": 1382}, 48 | {"name": "ObjectInterpolator", "value": 1629}, 49 | {"name": "PointInterpolator", "value": 1675}, 50 | {"name": "RectangleInterpolator", "value": 2042} 51 | ] 52 | }, 53 | {"name": "ISchedulable", "value": 1041}, 54 | {"name": "Parallel", "value": 5176}, 55 | {"name": "Pause", "value": 449}, 56 | {"name": "Scheduler", "value": 5593}, 57 | {"name": "Sequence", "value": 5534}, 58 | {"name": "Transition", "value": 9201}, 59 | {"name": "Transitioner", "value": 19975}, 60 | {"name": "TransitionEvent", "value": 1116}, 61 | {"name": "Tween", "value": 6006} 62 | ] 63 | }, 64 | { 65 | "name": "data", 66 | "children": [ 67 | { 68 | "name": "converters", 69 | "children": [ 70 | {"name": "Converters", "value": 721}, 71 | {"name": "DelimitedTextConverter", "value": 4294}, 72 | {"name": "GraphMLConverter", "value": 9800}, 73 | {"name": "IDataConverter", "value": 1314}, 74 | {"name": "JSONConverter", "value": 2220} 75 | ] 76 | }, 77 | {"name": "DataField", "value": 1759}, 78 | {"name": "DataSchema", "value": 2165}, 79 | {"name": "DataSet", "value": 586}, 80 | {"name": "DataSource", "value": 3331}, 81 | {"name": "DataTable", "value": 772}, 82 | {"name": "DataUtil", "value": 3322} 83 | ] 84 | }, 85 | { 86 | "name": "display", 87 | "children": [ 88 | {"name": "DirtySprite", "value": 8833}, 89 | {"name": "LineSprite", "value": 1732}, 90 | {"name": "RectSprite", "value": 3623}, 91 | {"name": "TextSprite", "value": 10066} 92 | ] 93 | }, 94 | { 95 | "name": "flex", 96 | "children": [ 97 | {"name": "FlareVis", "value": 4116} 98 | ] 99 | }, 100 | { 101 | "name": "physics", 102 | "children": [ 103 | {"name": "DragForce", "value": 1082}, 104 | {"name": "GravityForce", "value": 1336}, 105 | {"name": "IForce", "value": 319}, 106 | {"name": "NBodyForce", "value": 10498}, 107 | {"name": "Particle", "value": 2822}, 108 | {"name": "Simulation", "value": 9983}, 109 | {"name": "Spring", "value": 2213}, 110 | {"name": "SpringForce", "value": 1681} 111 | ] 112 | }, 113 | { 114 | "name": "query", 115 | "children": [ 116 | {"name": "AggregateExpression", "value": 1616}, 117 | {"name": "And", "value": 1027}, 118 | {"name": "Arithmetic", "value": 3891}, 119 | {"name": "Average", "value": 891}, 120 | {"name": "BinaryExpression", "value": 2893}, 121 | {"name": "Comparison", "value": 5103}, 122 | {"name": "CompositeExpression", "value": 3677}, 123 | {"name": "Count", "value": 781}, 124 | {"name": "DateUtil", "value": 4141}, 125 | {"name": "Distinct", "value": 933}, 126 | {"name": "Expression", "value": 5130}, 127 | {"name": "ExpressionIterator", "value": 3617}, 128 | {"name": "Fn", "value": 3240}, 129 | {"name": "If", "value": 2732}, 130 | {"name": "IsA", "value": 2039}, 131 | {"name": "Literal", "value": 1214}, 132 | {"name": "Match", "value": 3748}, 133 | {"name": "Maximum", "value": 843}, 134 | { 135 | "name": "methods", 136 | "children": [ 137 | {"name": "add", "value": 593}, 138 | {"name": "and", "value": 330}, 139 | {"name": "average", "value": 287}, 140 | {"name": "count", "value": 277}, 141 | {"name": "distinct", "value": 292}, 142 | {"name": "div", "value": 595}, 143 | {"name": "eq", "value": 594}, 144 | {"name": "fn", "value": 460}, 145 | {"name": "gt", "value": 603}, 146 | {"name": "gte", "value": 625}, 147 | {"name": "iff", "value": 748}, 148 | {"name": "isa", "value": 461}, 149 | {"name": "lt", "value": 597}, 150 | {"name": "lte", "value": 619}, 151 | {"name": "max", "value": 283}, 152 | {"name": "min", "value": 283}, 153 | {"name": "mod", "value": 591}, 154 | {"name": "mul", "value": 603}, 155 | {"name": "neq", "value": 599}, 156 | {"name": "not", "value": 386}, 157 | {"name": "or", "value": 323}, 158 | {"name": "orderby", "value": 307}, 159 | {"name": "range", "value": 772}, 160 | {"name": "select", "value": 296}, 161 | {"name": "stddev", "value": 363}, 162 | {"name": "sub", "value": 600}, 163 | {"name": "sum", "value": 280}, 164 | {"name": "update", "value": 307}, 165 | {"name": "variance", "value": 335}, 166 | {"name": "where", "value": 299}, 167 | {"name": "xor", "value": 354}, 168 | {"name": "_", "value": 264} 169 | ] 170 | }, 171 | {"name": "Minimum", "value": 843}, 172 | {"name": "Not", "value": 1554}, 173 | {"name": "Or", "value": 970}, 174 | {"name": "Query", "value": 13896}, 175 | {"name": "Range", "value": 1594}, 176 | {"name": "StringUtil", "value": 4130}, 177 | {"name": "Sum", "value": 791}, 178 | {"name": "Variable", "value": 1124}, 179 | {"name": "Variance", "value": 1876}, 180 | {"name": "Xor", "value": 1101} 181 | ] 182 | }, 183 | { 184 | "name": "scale", 185 | "children": [ 186 | {"name": "IScaleMap", "value": 2105}, 187 | {"name": "LinearScale", "value": 1316}, 188 | {"name": "LogScale", "value": 3151}, 189 | {"name": "OrdinalScale", "value": 3770}, 190 | {"name": "QuantileScale", "value": 2435}, 191 | {"name": "QuantitativeScale", "value": 4839}, 192 | {"name": "RootScale", "value": 1756}, 193 | {"name": "Scale", "value": 4268}, 194 | {"name": "ScaleType", "value": 1821}, 195 | {"name": "TimeScale", "value": 5833} 196 | ] 197 | }, 198 | { 199 | "name": "util", 200 | "children": [ 201 | {"name": "Arrays", "value": 8258}, 202 | {"name": "Colors", "value": 10001}, 203 | {"name": "Dates", "value": 8217}, 204 | {"name": "Displays", "value": 12555}, 205 | {"name": "Filter", "value": 2324}, 206 | {"name": "Geometry", "value": 10993}, 207 | { 208 | "name": "heap", 209 | "children": [ 210 | {"name": "FibonacciHeap", "value": 9354}, 211 | {"name": "HeapNode", "value": 1233} 212 | ] 213 | }, 214 | {"name": "IEvaluable", "value": 335}, 215 | {"name": "IPredicate", "value": 383}, 216 | {"name": "IValueProxy", "value": 874}, 217 | { 218 | "name": "math", 219 | "children": [ 220 | {"name": "DenseMatrix", "value": 3165}, 221 | {"name": "IMatrix", "value": 2815}, 222 | {"name": "SparseMatrix", "value": 3366} 223 | ] 224 | }, 225 | {"name": "Maths", "value": 17705}, 226 | {"name": "Orientation", "value": 1486}, 227 | { 228 | "name": "palette", 229 | "children": [ 230 | {"name": "ColorPalette", "value": 6367}, 231 | {"name": "Palette", "value": 1229}, 232 | {"name": "ShapePalette", "value": 2059}, 233 | {"name": "SizePalette", "value": 2291} 234 | ] 235 | }, 236 | {"name": "Property", "value": 5559}, 237 | {"name": "Shapes", "value": 19118}, 238 | {"name": "Sort", "value": 6887}, 239 | {"name": "Stats", "value": 6557}, 240 | {"name": "Strings", "value": 22026} 241 | ] 242 | }, 243 | { 244 | "name": "vis", 245 | "children": [ 246 | { 247 | "name": "axis", 248 | "children": [ 249 | {"name": "Axes", "value": 1302}, 250 | {"name": "Axis", "value": 24593}, 251 | {"name": "AxisGridLine", "value": 652}, 252 | {"name": "AxisLabel", "value": 636}, 253 | {"name": "CartesianAxes", "value": 6703} 254 | ] 255 | }, 256 | { 257 | "name": "controls", 258 | "children": [ 259 | {"name": "AnchorControl", "value": 2138}, 260 | {"name": "ClickControl", "value": 3824}, 261 | {"name": "Control", "value": 1353}, 262 | {"name": "ControlList", "value": 4665}, 263 | {"name": "DragControl", "value": 2649}, 264 | {"name": "ExpandControl", "value": 2832}, 265 | {"name": "HoverControl", "value": 4896}, 266 | {"name": "IControl", "value": 763}, 267 | {"name": "PanZoomControl", "value": 5222}, 268 | {"name": "SelectionControl", "value": 7862}, 269 | {"name": "TooltipControl", "value": 8435} 270 | ] 271 | }, 272 | { 273 | "name": "data", 274 | "children": [ 275 | {"name": "Data", "value": 20544}, 276 | {"name": "DataList", "value": 19788}, 277 | {"name": "DataSprite", "value": 10349}, 278 | {"name": "EdgeSprite", "value": 3301}, 279 | {"name": "NodeSprite", "value": 19382}, 280 | { 281 | "name": "render", 282 | "children": [ 283 | {"name": "ArrowType", "value": 698}, 284 | {"name": "EdgeRenderer", "value": 5569}, 285 | {"name": "IRenderer", "value": 353}, 286 | {"name": "ShapeRenderer", "value": 2247} 287 | ] 288 | }, 289 | {"name": "ScaleBinding", "value": 11275}, 290 | {"name": "Tree", "value": 7147}, 291 | {"name": "TreeBuilder", "value": 9930} 292 | ] 293 | }, 294 | { 295 | "name": "events", 296 | "children": [ 297 | {"name": "DataEvent", "value": 2313}, 298 | {"name": "SelectionEvent", "value": 1880}, 299 | {"name": "TooltipEvent", "value": 1701}, 300 | {"name": "VisualizationEvent", "value": 1117} 301 | ] 302 | }, 303 | { 304 | "name": "legend", 305 | "children": [ 306 | {"name": "Legend", "value": 20859}, 307 | {"name": "LegendItem", "value": 4614}, 308 | {"name": "LegendRange", "value": 10530} 309 | ] 310 | }, 311 | { 312 | "name": "operator", 313 | "children": [ 314 | { 315 | "name": "distortion", 316 | "children": [ 317 | {"name": "BifocalDistortion", "value": 4461}, 318 | {"name": "Distortion", "value": 6314}, 319 | {"name": "FisheyeDistortion", "value": 3444} 320 | ] 321 | }, 322 | { 323 | "name": "encoder", 324 | "children": [ 325 | {"name": "ColorEncoder", "value": 3179}, 326 | {"name": "Encoder", "value": 4060}, 327 | {"name": "PropertyEncoder", "value": 4138}, 328 | {"name": "ShapeEncoder", "value": 1690}, 329 | {"name": "SizeEncoder", "value": 1830} 330 | ] 331 | }, 332 | { 333 | "name": "filter", 334 | "children": [ 335 | {"name": "FisheyeTreeFilter", "value": 5219}, 336 | {"name": "GraphDistanceFilter", "value": 3165}, 337 | {"name": "VisibilityFilter", "value": 3509} 338 | ] 339 | }, 340 | {"name": "IOperator", "value": 1286}, 341 | { 342 | "name": "label", 343 | "children": [ 344 | {"name": "Labeler", "value": 9956}, 345 | {"name": "RadialLabeler", "value": 3899}, 346 | {"name": "StackedAreaLabeler", "value": 3202} 347 | ] 348 | }, 349 | { 350 | "name": "layout", 351 | "children": [ 352 | {"name": "AxisLayout", "value": 6725}, 353 | {"name": "BundledEdgeRouter", "value": 3727}, 354 | {"name": "CircleLayout", "value": 9317}, 355 | {"name": "CirclePackingLayout", "value": 12003}, 356 | {"name": "DendrogramLayout", "value": 4853}, 357 | {"name": "ForceDirectedLayout", "value": 8411}, 358 | {"name": "IcicleTreeLayout", "value": 4864}, 359 | {"name": "IndentedTreeLayout", "value": 3174}, 360 | {"name": "Layout", "value": 7881}, 361 | {"name": "NodeLinkTreeLayout", "value": 12870}, 362 | {"name": "PieLayout", "value": 2728}, 363 | {"name": "RadialTreeLayout", "value": 12348}, 364 | {"name": "RandomLayout", "value": 870}, 365 | {"name": "StackedAreaLayout", "value": 9121}, 366 | {"name": "TreeMapLayout", "value": 9191} 367 | ] 368 | }, 369 | {"name": "Operator", "value": 2490}, 370 | {"name": "OperatorList", "value": 5248}, 371 | {"name": "OperatorSequence", "value": 4190}, 372 | {"name": "OperatorSwitch", "value": 2581}, 373 | {"name": "SortOperator", "value": 2023} 374 | ] 375 | }, 376 | {"name": "Visualization", "value": 16540} 377 | ] 378 | } 379 | ] 380 | } 381 | -------------------------------------------------------------------------------- /dist/miserables.json: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": [ 3 | {"id": "Myriel", "group": 1}, 4 | {"id": "Napoleon", "group": 1}, 5 | {"id": "Mlle.Baptistine", "group": 1}, 6 | {"id": "Mme.Magloire", "group": 1}, 7 | {"id": "CountessdeLo", "group": 1}, 8 | {"id": "Geborand", "group": 1}, 9 | {"id": "Champtercier", "group": 1}, 10 | {"id": "Cravatte", "group": 1}, 11 | {"id": "Count", "group": 1}, 12 | {"id": "OldMan", "group": 1}, 13 | {"id": "Labarre", "group": 2}, 14 | {"id": "Valjean", "group": 2}, 15 | {"id": "Marguerite", "group": 3}, 16 | {"id": "Mme.deR", "group": 2}, 17 | {"id": "Isabeau", "group": 2}, 18 | {"id": "Gervais", "group": 2}, 19 | {"id": "Tholomyes", "group": 3}, 20 | {"id": "Listolier", "group": 3}, 21 | {"id": "Fameuil", "group": 3}, 22 | {"id": "Blacheville", "group": 3}, 23 | {"id": "Favourite", "group": 3}, 24 | {"id": "Dahlia", "group": 3}, 25 | {"id": "Zephine", "group": 3}, 26 | {"id": "Fantine", "group": 3}, 27 | {"id": "Mme.Thenardier", "group": 4}, 28 | {"id": "Thenardier", "group": 4}, 29 | {"id": "Cosette", "group": 5}, 30 | {"id": "Javert", "group": 4}, 31 | {"id": "Fauchelevent", "group": 0}, 32 | {"id": "Bamatabois", "group": 2}, 33 | {"id": "Perpetue", "group": 3}, 34 | {"id": "Simplice", "group": 2}, 35 | {"id": "Scaufflaire", "group": 2}, 36 | {"id": "Woman1", "group": 2}, 37 | {"id": "Judge", "group": 2}, 38 | {"id": "Champmathieu", "group": 2}, 39 | {"id": "Brevet", "group": 2}, 40 | {"id": "Chenildieu", "group": 2}, 41 | {"id": "Cochepaille", "group": 2}, 42 | {"id": "Pontmercy", "group": 4}, 43 | {"id": "Boulatruelle", "group": 6}, 44 | {"id": "Eponine", "group": 4}, 45 | {"id": "Anzelma", "group": 4}, 46 | {"id": "Woman2", "group": 5}, 47 | {"id": "MotherInnocent", "group": 0}, 48 | {"id": "Gribier", "group": 0}, 49 | {"id": "Jondrette", "group": 7}, 50 | {"id": "Mme.Burgon", "group": 7}, 51 | {"id": "Gavroche", "group": 8}, 52 | {"id": "Gillenormand", "group": 5}, 53 | {"id": "Magnon", "group": 5}, 54 | {"id": "Mlle.Gillenormand", "group": 5}, 55 | {"id": "Mme.Pontmercy", "group": 5}, 56 | {"id": "Mlle.Vaubois", "group": 5}, 57 | {"id": "Lt.Gillenormand", "group": 5}, 58 | {"id": "Marius", "group": 8}, 59 | {"id": "BaronessT", "group": 5}, 60 | {"id": "Mabeuf", "group": 8}, 61 | {"id": "Enjolras", "group": 8}, 62 | {"id": "Combeferre", "group": 8}, 63 | {"id": "Prouvaire", "group": 8}, 64 | {"id": "Feuilly", "group": 8}, 65 | {"id": "Courfeyrac", "group": 8}, 66 | {"id": "Bahorel", "group": 8}, 67 | {"id": "Bossuet", "group": 8}, 68 | {"id": "Joly", "group": 8}, 69 | {"id": "Grantaire", "group": 8}, 70 | {"id": "MotherPlutarch", "group": 9}, 71 | {"id": "Gueulemer", "group": 4}, 72 | {"id": "Babet", "group": 4}, 73 | {"id": "Claquesous", "group": 4}, 74 | {"id": "Montparnasse", "group": 4}, 75 | {"id": "Toussaint", "group": 5}, 76 | {"id": "Child1", "group": 10}, 77 | {"id": "Child2", "group": 10}, 78 | {"id": "Brujon", "group": 4}, 79 | {"id": "Mme.Hucheloup", "group": 8} 80 | ], 81 | "links": [ 82 | {"source": "Napoleon", "target": "Myriel", "value": 1}, 83 | {"source": "Mlle.Baptistine", "target": "Myriel", "value": 8}, 84 | {"source": "Mme.Magloire", "target": "Myriel", "value": 10}, 85 | {"source": "Mme.Magloire", "target": "Mlle.Baptistine", "value": 6}, 86 | {"source": "CountessdeLo", "target": "Myriel", "value": 1}, 87 | {"source": "Geborand", "target": "Myriel", "value": 1}, 88 | {"source": "Champtercier", "target": "Myriel", "value": 1}, 89 | {"source": "Cravatte", "target": "Myriel", "value": 1}, 90 | {"source": "Count", "target": "Myriel", "value": 2}, 91 | {"source": "OldMan", "target": "Myriel", "value": 1}, 92 | {"source": "Valjean", "target": "Labarre", "value": 1}, 93 | {"source": "Valjean", "target": "Mme.Magloire", "value": 3}, 94 | {"source": "Valjean", "target": "Mlle.Baptistine", "value": 3}, 95 | {"source": "Valjean", "target": "Myriel", "value": 5}, 96 | {"source": "Marguerite", "target": "Valjean", "value": 1}, 97 | {"source": "Mme.deR", "target": "Valjean", "value": 1}, 98 | {"source": "Isabeau", "target": "Valjean", "value": 1}, 99 | {"source": "Gervais", "target": "Valjean", "value": 1}, 100 | {"source": "Listolier", "target": "Tholomyes", "value": 4}, 101 | {"source": "Fameuil", "target": "Tholomyes", "value": 4}, 102 | {"source": "Fameuil", "target": "Listolier", "value": 4}, 103 | {"source": "Blacheville", "target": "Tholomyes", "value": 4}, 104 | {"source": "Blacheville", "target": "Listolier", "value": 4}, 105 | {"source": "Blacheville", "target": "Fameuil", "value": 4}, 106 | {"source": "Favourite", "target": "Tholomyes", "value": 3}, 107 | {"source": "Favourite", "target": "Listolier", "value": 3}, 108 | {"source": "Favourite", "target": "Fameuil", "value": 3}, 109 | {"source": "Favourite", "target": "Blacheville", "value": 4}, 110 | {"source": "Dahlia", "target": "Tholomyes", "value": 3}, 111 | {"source": "Dahlia", "target": "Listolier", "value": 3}, 112 | {"source": "Dahlia", "target": "Fameuil", "value": 3}, 113 | {"source": "Dahlia", "target": "Blacheville", "value": 3}, 114 | {"source": "Dahlia", "target": "Favourite", "value": 5}, 115 | {"source": "Zephine", "target": "Tholomyes", "value": 3}, 116 | {"source": "Zephine", "target": "Listolier", "value": 3}, 117 | {"source": "Zephine", "target": "Fameuil", "value": 3}, 118 | {"source": "Zephine", "target": "Blacheville", "value": 3}, 119 | {"source": "Zephine", "target": "Favourite", "value": 4}, 120 | {"source": "Zephine", "target": "Dahlia", "value": 4}, 121 | {"source": "Fantine", "target": "Tholomyes", "value": 3}, 122 | {"source": "Fantine", "target": "Listolier", "value": 3}, 123 | {"source": "Fantine", "target": "Fameuil", "value": 3}, 124 | {"source": "Fantine", "target": "Blacheville", "value": 3}, 125 | {"source": "Fantine", "target": "Favourite", "value": 4}, 126 | {"source": "Fantine", "target": "Dahlia", "value": 4}, 127 | {"source": "Fantine", "target": "Zephine", "value": 4}, 128 | {"source": "Fantine", "target": "Marguerite", "value": 2}, 129 | {"source": "Fantine", "target": "Valjean", "value": 9}, 130 | {"source": "Mme.Thenardier", "target": "Fantine", "value": 2}, 131 | {"source": "Mme.Thenardier", "target": "Valjean", "value": 7}, 132 | {"source": "Thenardier", "target": "Mme.Thenardier", "value": 13}, 133 | {"source": "Thenardier", "target": "Fantine", "value": 1}, 134 | {"source": "Thenardier", "target": "Valjean", "value": 12}, 135 | {"source": "Cosette", "target": "Mme.Thenardier", "value": 4}, 136 | {"source": "Cosette", "target": "Valjean", "value": 31}, 137 | {"source": "Cosette", "target": "Tholomyes", "value": 1}, 138 | {"source": "Cosette", "target": "Thenardier", "value": 1}, 139 | {"source": "Javert", "target": "Valjean", "value": 17}, 140 | {"source": "Javert", "target": "Fantine", "value": 5}, 141 | {"source": "Javert", "target": "Thenardier", "value": 5}, 142 | {"source": "Javert", "target": "Mme.Thenardier", "value": 1}, 143 | {"source": "Javert", "target": "Cosette", "value": 1}, 144 | {"source": "Fauchelevent", "target": "Valjean", "value": 8}, 145 | {"source": "Fauchelevent", "target": "Javert", "value": 1}, 146 | {"source": "Bamatabois", "target": "Fantine", "value": 1}, 147 | {"source": "Bamatabois", "target": "Javert", "value": 1}, 148 | {"source": "Bamatabois", "target": "Valjean", "value": 2}, 149 | {"source": "Perpetue", "target": "Fantine", "value": 1}, 150 | {"source": "Simplice", "target": "Perpetue", "value": 2}, 151 | {"source": "Simplice", "target": "Valjean", "value": 3}, 152 | {"source": "Simplice", "target": "Fantine", "value": 2}, 153 | {"source": "Simplice", "target": "Javert", "value": 1}, 154 | {"source": "Scaufflaire", "target": "Valjean", "value": 1}, 155 | {"source": "Woman1", "target": "Valjean", "value": 2}, 156 | {"source": "Woman1", "target": "Javert", "value": 1}, 157 | {"source": "Judge", "target": "Valjean", "value": 3}, 158 | {"source": "Judge", "target": "Bamatabois", "value": 2}, 159 | {"source": "Champmathieu", "target": "Valjean", "value": 3}, 160 | {"source": "Champmathieu", "target": "Judge", "value": 3}, 161 | {"source": "Champmathieu", "target": "Bamatabois", "value": 2}, 162 | {"source": "Brevet", "target": "Judge", "value": 2}, 163 | {"source": "Brevet", "target": "Champmathieu", "value": 2}, 164 | {"source": "Brevet", "target": "Valjean", "value": 2}, 165 | {"source": "Brevet", "target": "Bamatabois", "value": 1}, 166 | {"source": "Chenildieu", "target": "Judge", "value": 2}, 167 | {"source": "Chenildieu", "target": "Champmathieu", "value": 2}, 168 | {"source": "Chenildieu", "target": "Brevet", "value": 2}, 169 | {"source": "Chenildieu", "target": "Valjean", "value": 2}, 170 | {"source": "Chenildieu", "target": "Bamatabois", "value": 1}, 171 | {"source": "Cochepaille", "target": "Judge", "value": 2}, 172 | {"source": "Cochepaille", "target": "Champmathieu", "value": 2}, 173 | {"source": "Cochepaille", "target": "Brevet", "value": 2}, 174 | {"source": "Cochepaille", "target": "Chenildieu", "value": 2}, 175 | {"source": "Cochepaille", "target": "Valjean", "value": 2}, 176 | {"source": "Cochepaille", "target": "Bamatabois", "value": 1}, 177 | {"source": "Pontmercy", "target": "Thenardier", "value": 1}, 178 | {"source": "Boulatruelle", "target": "Thenardier", "value": 1}, 179 | {"source": "Eponine", "target": "Mme.Thenardier", "value": 2}, 180 | {"source": "Eponine", "target": "Thenardier", "value": 3}, 181 | {"source": "Anzelma", "target": "Eponine", "value": 2}, 182 | {"source": "Anzelma", "target": "Thenardier", "value": 2}, 183 | {"source": "Anzelma", "target": "Mme.Thenardier", "value": 1}, 184 | {"source": "Woman2", "target": "Valjean", "value": 3}, 185 | {"source": "Woman2", "target": "Cosette", "value": 1}, 186 | {"source": "Woman2", "target": "Javert", "value": 1}, 187 | {"source": "MotherInnocent", "target": "Fauchelevent", "value": 3}, 188 | {"source": "MotherInnocent", "target": "Valjean", "value": 1}, 189 | {"source": "Gribier", "target": "Fauchelevent", "value": 2}, 190 | {"source": "Mme.Burgon", "target": "Jondrette", "value": 1}, 191 | {"source": "Gavroche", "target": "Mme.Burgon", "value": 2}, 192 | {"source": "Gavroche", "target": "Thenardier", "value": 1}, 193 | {"source": "Gavroche", "target": "Javert", "value": 1}, 194 | {"source": "Gavroche", "target": "Valjean", "value": 1}, 195 | {"source": "Gillenormand", "target": "Cosette", "value": 3}, 196 | {"source": "Gillenormand", "target": "Valjean", "value": 2}, 197 | {"source": "Magnon", "target": "Gillenormand", "value": 1}, 198 | {"source": "Magnon", "target": "Mme.Thenardier", "value": 1}, 199 | {"source": "Mlle.Gillenormand", "target": "Gillenormand", "value": 9}, 200 | {"source": "Mlle.Gillenormand", "target": "Cosette", "value": 2}, 201 | {"source": "Mlle.Gillenormand", "target": "Valjean", "value": 2}, 202 | {"source": "Mme.Pontmercy", "target": "Mlle.Gillenormand", "value": 1}, 203 | {"source": "Mme.Pontmercy", "target": "Pontmercy", "value": 1}, 204 | {"source": "Mlle.Vaubois", "target": "Mlle.Gillenormand", "value": 1}, 205 | {"source": "Lt.Gillenormand", "target": "Mlle.Gillenormand", "value": 2}, 206 | {"source": "Lt.Gillenormand", "target": "Gillenormand", "value": 1}, 207 | {"source": "Lt.Gillenormand", "target": "Cosette", "value": 1}, 208 | {"source": "Marius", "target": "Mlle.Gillenormand", "value": 6}, 209 | {"source": "Marius", "target": "Gillenormand", "value": 12}, 210 | {"source": "Marius", "target": "Pontmercy", "value": 1}, 211 | {"source": "Marius", "target": "Lt.Gillenormand", "value": 1}, 212 | {"source": "Marius", "target": "Cosette", "value": 21}, 213 | {"source": "Marius", "target": "Valjean", "value": 19}, 214 | {"source": "Marius", "target": "Tholomyes", "value": 1}, 215 | {"source": "Marius", "target": "Thenardier", "value": 2}, 216 | {"source": "Marius", "target": "Eponine", "value": 5}, 217 | {"source": "Marius", "target": "Gavroche", "value": 4}, 218 | {"source": "BaronessT", "target": "Gillenormand", "value": 1}, 219 | {"source": "BaronessT", "target": "Marius", "value": 1}, 220 | {"source": "Mabeuf", "target": "Marius", "value": 1}, 221 | {"source": "Mabeuf", "target": "Eponine", "value": 1}, 222 | {"source": "Mabeuf", "target": "Gavroche", "value": 1}, 223 | {"source": "Enjolras", "target": "Marius", "value": 7}, 224 | {"source": "Enjolras", "target": "Gavroche", "value": 7}, 225 | {"source": "Enjolras", "target": "Javert", "value": 6}, 226 | {"source": "Enjolras", "target": "Mabeuf", "value": 1}, 227 | {"source": "Enjolras", "target": "Valjean", "value": 4}, 228 | {"source": "Combeferre", "target": "Enjolras", "value": 15}, 229 | {"source": "Combeferre", "target": "Marius", "value": 5}, 230 | {"source": "Combeferre", "target": "Gavroche", "value": 6}, 231 | {"source": "Combeferre", "target": "Mabeuf", "value": 2}, 232 | {"source": "Prouvaire", "target": "Gavroche", "value": 1}, 233 | {"source": "Prouvaire", "target": "Enjolras", "value": 4}, 234 | {"source": "Prouvaire", "target": "Combeferre", "value": 2}, 235 | {"source": "Feuilly", "target": "Gavroche", "value": 2}, 236 | {"source": "Feuilly", "target": "Enjolras", "value": 6}, 237 | {"source": "Feuilly", "target": "Prouvaire", "value": 2}, 238 | {"source": "Feuilly", "target": "Combeferre", "value": 5}, 239 | {"source": "Feuilly", "target": "Mabeuf", "value": 1}, 240 | {"source": "Feuilly", "target": "Marius", "value": 1}, 241 | {"source": "Courfeyrac", "target": "Marius", "value": 9}, 242 | {"source": "Courfeyrac", "target": "Enjolras", "value": 17}, 243 | {"source": "Courfeyrac", "target": "Combeferre", "value": 13}, 244 | {"source": "Courfeyrac", "target": "Gavroche", "value": 7}, 245 | {"source": "Courfeyrac", "target": "Mabeuf", "value": 2}, 246 | {"source": "Courfeyrac", "target": "Eponine", "value": 1}, 247 | {"source": "Courfeyrac", "target": "Feuilly", "value": 6}, 248 | {"source": "Courfeyrac", "target": "Prouvaire", "value": 3}, 249 | {"source": "Bahorel", "target": "Combeferre", "value": 5}, 250 | {"source": "Bahorel", "target": "Gavroche", "value": 5}, 251 | {"source": "Bahorel", "target": "Courfeyrac", "value": 6}, 252 | {"source": "Bahorel", "target": "Mabeuf", "value": 2}, 253 | {"source": "Bahorel", "target": "Enjolras", "value": 4}, 254 | {"source": "Bahorel", "target": "Feuilly", "value": 3}, 255 | {"source": "Bahorel", "target": "Prouvaire", "value": 2}, 256 | {"source": "Bahorel", "target": "Marius", "value": 1}, 257 | {"source": "Bossuet", "target": "Marius", "value": 5}, 258 | {"source": "Bossuet", "target": "Courfeyrac", "value": 12}, 259 | {"source": "Bossuet", "target": "Gavroche", "value": 5}, 260 | {"source": "Bossuet", "target": "Bahorel", "value": 4}, 261 | {"source": "Bossuet", "target": "Enjolras", "value": 10}, 262 | {"source": "Bossuet", "target": "Feuilly", "value": 6}, 263 | {"source": "Bossuet", "target": "Prouvaire", "value": 2}, 264 | {"source": "Bossuet", "target": "Combeferre", "value": 9}, 265 | {"source": "Bossuet", "target": "Mabeuf", "value": 1}, 266 | {"source": "Bossuet", "target": "Valjean", "value": 1}, 267 | {"source": "Joly", "target": "Bahorel", "value": 5}, 268 | {"source": "Joly", "target": "Bossuet", "value": 7}, 269 | {"source": "Joly", "target": "Gavroche", "value": 3}, 270 | {"source": "Joly", "target": "Courfeyrac", "value": 5}, 271 | {"source": "Joly", "target": "Enjolras", "value": 5}, 272 | {"source": "Joly", "target": "Feuilly", "value": 5}, 273 | {"source": "Joly", "target": "Prouvaire", "value": 2}, 274 | {"source": "Joly", "target": "Combeferre", "value": 5}, 275 | {"source": "Joly", "target": "Mabeuf", "value": 1}, 276 | {"source": "Joly", "target": "Marius", "value": 2}, 277 | {"source": "Grantaire", "target": "Bossuet", "value": 3}, 278 | {"source": "Grantaire", "target": "Enjolras", "value": 3}, 279 | {"source": "Grantaire", "target": "Combeferre", "value": 1}, 280 | {"source": "Grantaire", "target": "Courfeyrac", "value": 2}, 281 | {"source": "Grantaire", "target": "Joly", "value": 2}, 282 | {"source": "Grantaire", "target": "Gavroche", "value": 1}, 283 | {"source": "Grantaire", "target": "Bahorel", "value": 1}, 284 | {"source": "Grantaire", "target": "Feuilly", "value": 1}, 285 | {"source": "Grantaire", "target": "Prouvaire", "value": 1}, 286 | {"source": "MotherPlutarch", "target": "Mabeuf", "value": 3}, 287 | {"source": "Gueulemer", "target": "Thenardier", "value": 5}, 288 | {"source": "Gueulemer", "target": "Valjean", "value": 1}, 289 | {"source": "Gueulemer", "target": "Mme.Thenardier", "value": 1}, 290 | {"source": "Gueulemer", "target": "Javert", "value": 1}, 291 | {"source": "Gueulemer", "target": "Gavroche", "value": 1}, 292 | {"source": "Gueulemer", "target": "Eponine", "value": 1}, 293 | {"source": "Babet", "target": "Thenardier", "value": 6}, 294 | {"source": "Babet", "target": "Gueulemer", "value": 6}, 295 | {"source": "Babet", "target": "Valjean", "value": 1}, 296 | {"source": "Babet", "target": "Mme.Thenardier", "value": 1}, 297 | {"source": "Babet", "target": "Javert", "value": 2}, 298 | {"source": "Babet", "target": "Gavroche", "value": 1}, 299 | {"source": "Babet", "target": "Eponine", "value": 1}, 300 | {"source": "Claquesous", "target": "Thenardier", "value": 4}, 301 | {"source": "Claquesous", "target": "Babet", "value": 4}, 302 | {"source": "Claquesous", "target": "Gueulemer", "value": 4}, 303 | {"source": "Claquesous", "target": "Valjean", "value": 1}, 304 | {"source": "Claquesous", "target": "Mme.Thenardier", "value": 1}, 305 | {"source": "Claquesous", "target": "Javert", "value": 1}, 306 | {"source": "Claquesous", "target": "Eponine", "value": 1}, 307 | {"source": "Claquesous", "target": "Enjolras", "value": 1}, 308 | {"source": "Montparnasse", "target": "Javert", "value": 1}, 309 | {"source": "Montparnasse", "target": "Babet", "value": 2}, 310 | {"source": "Montparnasse", "target": "Gueulemer", "value": 2}, 311 | {"source": "Montparnasse", "target": "Claquesous", "value": 2}, 312 | {"source": "Montparnasse", "target": "Valjean", "value": 1}, 313 | {"source": "Montparnasse", "target": "Gavroche", "value": 1}, 314 | {"source": "Montparnasse", "target": "Eponine", "value": 1}, 315 | {"source": "Montparnasse", "target": "Thenardier", "value": 1}, 316 | {"source": "Toussaint", "target": "Cosette", "value": 2}, 317 | {"source": "Toussaint", "target": "Javert", "value": 1}, 318 | {"source": "Toussaint", "target": "Valjean", "value": 1}, 319 | {"source": "Child1", "target": "Gavroche", "value": 2}, 320 | {"source": "Child2", "target": "Gavroche", "value": 2}, 321 | {"source": "Child2", "target": "Child1", "value": 3}, 322 | {"source": "Brujon", "target": "Babet", "value": 3}, 323 | {"source": "Brujon", "target": "Gueulemer", "value": 3}, 324 | {"source": "Brujon", "target": "Thenardier", "value": 3}, 325 | {"source": "Brujon", "target": "Gavroche", "value": 1}, 326 | {"source": "Brujon", "target": "Eponine", "value": 1}, 327 | {"source": "Brujon", "target": "Claquesous", "value": 1}, 328 | {"source": "Brujon", "target": "Montparnasse", "value": 1}, 329 | {"source": "Mme.Hucheloup", "target": "Bossuet", "value": 1}, 330 | {"source": "Mme.Hucheloup", "target": "Joly", "value": 1}, 331 | {"source": "Mme.Hucheloup", "target": "Grantaire", "value": 1}, 332 | {"source": "Mme.Hucheloup", "target": "Bahorel", "value": 1}, 333 | {"source": "Mme.Hucheloup", "target": "Courfeyrac", "value": 1}, 334 | {"source": "Mme.Hucheloup", "target": "Gavroche", "value": 1}, 335 | {"source": "Mme.Hucheloup", "target": "Enjolras", "value": 1} 336 | ] 337 | } 338 | --------------------------------------------------------------------------------