├── .gitignore ├── .npmignore ├── LICENSE.md ├── Nuget.Config ├── README.md ├── compost.gif ├── docs ├── api.html ├── bundle.js ├── demos.html ├── index.html ├── lib │ ├── docs.js │ └── style.css ├── paper.pdf ├── releases │ ├── compost-0.0.2.js │ ├── compost-0.0.3.js │ ├── compost-0.0.4.js │ ├── compost-0.0.5.js │ └── compost-latest.js ├── tutorial.html └── usage.html ├── package-lock.json ├── package.json ├── public └── index.html └── src ├── compost ├── compost.fs ├── compost.fsproj ├── core.fs └── html.fs ├── project ├── data.js ├── demos.js └── standalone.js ├── webpack.dev.js ├── webpack.latest.js └── webpack.pub.js /.gitignore: -------------------------------------------------------------------------------- 1 | .ionide 2 | node_modules/ 3 | src/compost/obj/ 4 | src/compost/bin/ 5 | docs/_site 6 | dist/ 7 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .ionide 2 | docs 3 | public 4 | src 5 | compost.gif 6 | Nuget.config 7 | webpack.config.js 8 | node_modules -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016, Tomas Petricek and contributors 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. -------------------------------------------------------------------------------- /Nuget.Config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Compost.js: Composable data visualization library 2 | 3 | Compost is a data visualization library that lets you compose rich interactive data visualizations 4 | from a small number of basic primitives. The library is based on the functional programming idea of 5 | composable domain-specific languages. Compost is simple (implemented in just 700 lines of code) and 6 | easy to understand. Compost is a plain JavaScript library. You use it by writing JavaScript code 7 | that generates a chart using some 15 basic Compost primitives. 8 | 9 | For more information, see the [Compost web page and documentation](https://compostjs.github.io/compost). 10 | 11 | ## Getting started with Compost 12 | 13 | Compost is written using [the F# language](https://fsharp.org) and compiled to JavaScript 14 | using [the Fable compiler](https://fable.io). To build Compost, you will need to install 15 | [.NET Core](https://dotnet.microsoft.com/download). You may also want to get the 16 | [Ionide plugin](http://ionide.io/) for Visual Studio Code. 17 | 18 | ### Developing Compost 19 | To work on Compost, you can use the WebPack dev server. The following will serve the 20 | `public/index.html` file and compile the `src/project/demos.js` source code at 21 | http://localhost:8080 22 | 23 | ``` 24 | npm install 25 | npm start 26 | ``` 27 | 28 | ### Building Compost 29 | There are two ways to build Compost. Running `npm run build` will use `fable-splitter` 30 | to generate nice JavaScript files for a NPM package in the `dist` folder, which is then 31 | packaged and published on NPM. Running `npm run standalone` builds a standalone 32 | JavaScript file that is added to the `releases` folder of the `docs` with the current 33 | version number in the filename (and also updates the `latest` file). 34 | This should all happen automatically when using `npm run release`. 35 | 36 | ## What is the story behind the name?? 37 | 38 | ![Compost](https://github.com/compostjs/compost/raw/master/compost.gif) 39 | -------------------------------------------------------------------------------- /compost.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/compostjs/compost/77afecb737a1cde524664d313215259f2b569b62/compost.gif -------------------------------------------------------------------------------- /docs/api.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Compost API reference - Compost.js 6 | 7 | 8 | 9 | 10 | 11 | 13 | 14 | 18 | 19 | 20 | 21 | 36 |
37 |
38 |

Compost API reference

39 | 40 | 49 | 50 |

Coordinates: Continuous and categorical coordinates

51 |

When specifying coordinates in Compost, you do so using a value from your domain 52 | rather than using a value in pixels. A value can be either categorical (such as 53 | a political party or a country) or a continuous (such as an exchange rate or a year).

54 |

When specifying a location using a continuous value, you specify just a number. 55 | When using a categorical value, Compost associates a whole area to each category and 56 | so you need to give a category together with a number specifying a location within 57 | this area. The following are valid ways of specifying a Coord:

58 | 59 |
// A continuous value such as exchange rate
 60 | let v1 = 1.52
 61 | 
 62 | // A continuous value such as a number of MPs
 63 | let v2 = 152
 64 | 
 65 | // Leftmost corner of an area associated with categorical value
 66 | let v3 = ["Labour", 0]
 67 | 
 68 | // Middle of an area associated with categorical value
 69 | let v4 = ["Labour", 0.5]
 70 | 
71 |

In other words, Coord can be either a number or an array of two 72 | elements containing a string (the name of the category) and a number (between 0 and 1).

73 |

When you want to specify a location on a chart, you need an x and y coordinate. 74 | A Point in the following documentation refers to an array of two 75 | Coord elements, one for x and one for y coordinate. The following gives 76 | some examples of valid points:

77 | 78 |
// A pair of continuous values
 79 | let p1 = [ 3.14, Math.sin(3.14) ]
 80 | 
 81 | // A point with categorical X and continuous Y
 82 | let p2 = [ ["Labour", 0.5], 152 ]
 83 | 
 84 | // A list specifying a several points on a line
 85 | let l1 = [ [0, Math.sin(0)], [1.57, Math.sin(1.57)],
 86 |   [3.14, Math.sin(3.14)], [4.71, Math.sin(4.71)]  ]
 87 | 
 88 | // A list specifying four corners of a rectangle
 89 | let s1 = [ [["Labour", 0], 0], [["Labour", 0], 152],
 90 |   [["Labour", 1], 152], [["Labour", 1], 0] ]
 91 | 
92 | 93 |

Scales: Categorical and continuous

94 | 95 | 96 | 97 |
s.continuousfloat * float -> Scale

Creates a continuous scale that can contain value in the specified range

s.categoricalstring[] -> Scale

Creates a categorical scale that can contain categorical values specified in the given array of strings

98 | 99 |

Compost primitives: Basic shapes

100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 |
c.textCoord * Coord * string * ?string * ?float -> Shape

Draws a text specified as the third parameter at a given x and y coordinates specified by the first two parameters. The last two optional parameters specify alignment (baseline, hanging, middle, start, end, center) and rotation in radians.

c.bubbleCoord * Coord * float * float -> Shape

Creates a bubble (point) at a specified x and y coordinates. The last two parameters specify the width and height of the bubble in pixels.

c.shapePoint[] -> Shape

Creates a filled shape. The shape is specified as an array of points (see the section on coordinates).

c.linePoint[] -> Shape

Creates a line drawn using the current stroke color. The line is specified as an array of points (see the section on coordinates)

c.columnstring * float -> Shape

Creates a filled rectangle for use in a column chart. This is a shorthand for c.shape. It creates a rectangle that fills the whole area for a given categorical value and has a specified height.

c.barfloat * string -> Shape

Creates a filled rectangle for use in a bar chart. This is a shorthand for c.shape. It creates a rectangle that fills the whole area for a given categorical value and has a specified width.

108 | 109 |

Compost primitives: Specifying visual properties

110 | 111 | 112 | 113 | 114 |
c.fillColorstring * Shape -> Shape

Sets the fill color to be used for all shapes drawn using c.shape in the given shape.

c.strokeColorstring * Shape -> Shape

Sets the line color to be used for all lines drawn using c.line in the given shape.

c.fontstring * string * Shape -> Shape

Sets the font and text color to be used for all text occurring in the given shape.

115 | 116 |

Compost primitives: Transforming scales

117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 |
c.nestCoord * Coord * Coord * Coord * Shape -> Shape

Creates a shape that occupies an explicitly specified space using the four coordinates as left and right X value and top and bottom Y values. Inside this explicitly specified space, the nested shape is drawn, using its own scales.

c.nestXCoord * Coord * Shape -> Shape

Same as above, but this primitive only overrides the X scale of the nested shape while the Y scale is left unchanged and can be shared with other shapes.

c.nestYCoord * Coord * Shape -> Shape

Same as above, but this primitive only overrides the Y scale of the nested shape while the X scale is left unchanged and can be shared with other shapes.

c.scaleScale * Scale * Shape -> Shape

Override the automatically inferred scale with an explicitly specified one. You can use this to define a custom minimal and maximal value. To create scales use s.continuous or s.categorical.

c.scaleXScale * Shape -> Shape

Override the automatically inferred X scale (as above).

c.scaleYScale * Shape -> Shape

Override the automatically inferred Y scale (as above).

c.paddingfloat * float * float * float * Shape -> Shape

Adds a padding around the given shape. The padding is specified as top, right, bottom, left. This will subtract the padding from the available space and draw the nested shape into the smaller space.

126 | 127 |

Compost primitives: Combining shapes and axes

128 | 129 | 130 | 131 |
c.overlayShape[] -> Shape

Compose a given array of shapes by drawing them all in the same chart area. This calculates the scale of all nested shapes and those are then automatically aligned based on their coordinates.

c.axesstring * Shape -> Shape

Draw axes around a given shape. The string parameter can be any string containing the words left, right, bottom and/or top, for example using space as a separator.

132 | 133 |

Compost primitives: Rendering charts and interactivity

134 | 135 | 136 | 142 | 143 | 144 | 151 |
c.renderstring * Shape -> unit

Render a given chart on a HTML element specified by a given ID. When called, Compost will get the width and height of the element and render a chart using this as the size.

c.onHandlers * Shape -> Shape

137 | Specify event handlers for events that occur in the specified shape. The first parameter is a JavaScript object with fields 138 | representing individual handlers. The supported handlers are mousedown, mouseup, mousemove, click, 139 | touchstart, touchmove. Those are called with x and y coordinates of the event. You can also specify touchend 140 | and mouseleave handlers, but those do not get coordinates of the event. 141 | For example of how to specify handlers, see the You draw it demo.

c.svgfloat * float * Shape -> Html

Render the given shape and build an object representing the chart as an HTML <svg> element. The first two arguments specify the desired width and height of the SVG element in pixels. This operation is useful if you want to create an interactive chart using c.interactive and want to add some custom HTML elements using c.html.

c.htmlstring * obj * (Html | string)[] -> Html

Creates a HTML element that can be returned as a result of the rendering function in c.interactive. The API is inspired by HyperAcript. The first element is a tag name, followed by an object that specifies element properties (value of type string) and event handlers (value of type function). The third parameter is an array of children, which can be either text or other HTML elements.

c.interactivestring * State * (State -> Event -> State) * ((Event -> unit) -> State -> (Html | Shape)) -> unit

145 | Create an interactive chart using a given HTML element ID. For an example of how this works, 146 | see the You draw it demo. This is based on the Elm architecture 147 | (also known Model-View-Update). The last three parameters specify the initial state, 148 | an update function (given a state and an event, produce a new state) and a view function 149 | (given a function to trigger an event and a current state, produce a shape). 150 |

152 | 153 | 154 |

Learn more about Compost

155 | 169 | 170 |
171 |
172 | 173 | 174 | 175 | 181 | 182 | 183 | -------------------------------------------------------------------------------- /docs/demos.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Sample data visualizations built using Compost - Compost.js 6 | 7 | 8 | 9 | 10 | 11 | 13 | 14 | 18 | 19 | 20 | 21 | 36 |
37 |
38 |

Sample Compost data visualizations

39 |

This page contains various examples of using Compost. The code is executed on the 40 | fly, so each of the code blocks is fully standalone. This also makes some examples 41 | longer, because we cannot reuse helper functions between files. All sample data 42 | is defined in the docs.js file on GitHub. 43 |

56 | 57 |

UK elections: Basic bar chart

58 |

The following code sample shows how to construct a basic bar chart. 59 |

60 |
let bars =
 61 |   c.axes("left bottom", c.scaleY(s.continuous(0, 410), c.overlay(
 62 |     elections.map(e =>
 63 |       c.padding(0, 10, 0, 10,
 64 |         c.fillColor(e.color, c.column(e.party, e.y17))
 65 |       ))
 66 |     )))
 67 | 
 68 | c.render("out1a", bars)
 69 | 
70 |
71 | 72 |

UK elections: Double bar chart

73 |

The following chart shows two bars for each political party. The x coordinates 74 | of a bar is given as ["Green", 0] and ["Green", 0.5] 75 | for the first bar and ["Green", 0.5] and ["Green", 1] 76 | for the second bar. 77 |

78 |
// Makes a color given in "#rrggbb" format darker or lighter
 79 | // (by multiplying each component by the specified number k)
 80 | function adjust(color, k) {
 81 |   let r = parseInt(color.substr(1, 2), 16)
 82 |   let g = parseInt(color.substr(3, 2), 16)
 83 |   let b = parseInt(color.substr(5, 2), 16)
 84 |   let f = n => n*k > 255 ? 255 : n*k;
 85 |   return "#" + ((f(r) << 16) + (f(g) << 8) + (f(b) << 0)).toString(16);
 86 | }
 87 | 
 88 | // Creates a bar of height 'y' that is witin a categorical value 'x'
 89 | // starting at the offset 'f' and ending at the offset 't'.
 90 | function partColumn(f, t, x, y) {
 91 |   return c.shape([ [ [x,f], y ], [ [x,t], y ], [ [x,t], 0 ], [ [x,f], 0 ] ])
 92 | }
 93 | 
 94 | let bars =
 95 |   c.axes("left bottom", c.scaleY(s.continuous(0, 410), c.overlay(
 96 |     elections.map(e =>
 97 |       c.padding(0, 10, 0, 10, c.overlay([
 98 |         c.fillColor(adjust(e.color, 0.8), partColumn(0, 0.5, e.party, e.y17)),
 99 |         c.fillColor(adjust(e.color, 1.2), partColumn(0.5, 1, e.party, e.y19))
100 |       ]))
101 |     )
102 |   )))
103 | 
104 | c.render("out1b", bars)
105 | 
106 |
107 | 108 |

UK elections: Double bar chart with a title

109 |

The following chart shows two bars for each political party. The x coordinates 110 | of a bar is given as ["Green", 0] and ["Green", 0.5] 111 | for the first bar and ["Green", 0.5] and ["Green", 1] 112 | for the second bar. We also add a title using the c.nest primitive 113 | to align the title and the chart itself. 114 |

115 |
// Makes a color given in "#rrggbb" format darker or lighter
116 | // (by multiplying each component by the specified number k)
117 | function adjust(color, k) {
118 |   let r = parseInt(color.substr(1, 2), 16)
119 |   let g = parseInt(color.substr(3, 2), 16)
120 |   let b = parseInt(color.substr(5, 2), 16)
121 |   let f = n => n*k > 255 ? 255 : n*k;
122 |   return "#" + ((f(r) << 16) + (f(g) << 8) + (f(b) << 0)).toString(16);
123 | }
124 | 
125 | // A derived Compost operation that adds a title to any given chart.
126 | // This works by creating text element and using 'nest' to allocate top
127 | // 15% of space for the title and the remaining 85% of space for the title.
128 | function title(text, chart) {
129 |   let title = c.scale(s.continuous(0, 100), s.continuous(0, 100),
130 |     c.font("11pt arial", "black", c.text(50, 80, text)))
131 |   return c.overlay([
132 |     c.nest(0, 100, 85, 100, title),
133 |     c.nest(0, 100, 0, 85, chart)
134 |   ])
135 | }
136 | 
137 | // Creates a bar of height 'y' that is witin a categorical value 'x'
138 | // starting at the offset 'f' and ending at the offset 't'.
139 | function partColumn(f, t, x, y) {
140 |   return c.shape([ [ [x,f], y ], [ [x,t], y ], [ [x,t], 0 ], [ [x,f], 0 ] ])
141 | }
142 | 
143 | let bars =
144 |   c.axes("left bottom", c.scaleY(s.continuous(0, 410), c.overlay(
145 |     elections.map(e =>
146 |       c.padding(0, 10, 0, 10, c.overlay([
147 |         c.fillColor(adjust(e.color, 0.8), partColumn(0, 0.5, e.party, e.y17)),
148 |         c.fillColor(adjust(e.color, 1.2), partColumn(0.5, 1, e.party, e.y19))
149 |       ]))
150 |     )
151 |   )))
152 | 
153 | c.render("out1c", title("United Kingdom general elections (2017 vs 2019)", bars))
154 | 
155 |
156 | 157 |

Exchange rates: Line chart with explicit scale

158 |

This demo creates a basic line chart showing GBP/USD exchange rate around 23 June 2016. 159 | This is done using c.line which takes an array of x and y coordinates. 160 | We then change the line color, explicitly set the y scale (to make it look nicer) 161 | and add axes.

162 | 163 |
let rates = c.axes("left right bottom",
164 |     c.scaleY(s.continuous(1.25, 1.52), c.strokeColor("#202020",
165 |       c.line(gbpusd.map((v, i) => [i, v])))
166 |   ))
167 | c.render("out2a", rates)
168 | 
169 |
170 | 171 |

Exchange rates: Line chart with highlighted background

172 |

This is the demo from the Compost home page. We create a line 173 | chart, but with two areas highlighted in different colors. This is achieved by overlaying 174 | two rectangular shapes and a line.

175 | 176 |
let lo = 1.25, hi = 1.52
177 | let rates = c.axes("left right bottom", c.overlay([
178 |     c.fillColor("#1F77B460",  c.shape(
179 |       [ [0,lo], [16,lo], [16,hi], [0,hi] ])),
180 |     c.fillColor("#D6272860",  c.shape(
181 |       [ [gbpusd.length-1,lo], [16,lo], [16,hi], [gbpusd.length-1,hi] ])),
182 |     c.strokeColor("#202020",
183 |       c.line(gbpusd.map((v, i) => [i, v])))
184 |   ]))
185 | c.render("out2b", rates)
186 | 
187 |
188 | 189 |

Exchange rates: Highlighting area under a line

190 |

What if we wanted to highlight just the area under a line, rather than the whole 191 | background of the chart? The c.shape primitive can fill any polygon, 192 | so we are not limited to specifying a rectangle as above. In the following, we generate 193 | the background shapes so that the bottom is flat, but the top is aligned with the 194 | data points on the line.

195 | 196 |
let lo = 1.25, hi = 1.52
197 | let rates = c.axes("left right bottom", c.overlay([
198 |     c.fillColor("#1F77B460",  c.shape(
199 |       gbpusd.slice(0, 17).map((v, i) => [i, v])
200 |         .concat([[16, lo], [0, lo]]) )),
201 |     c.fillColor("#D6272860",  c.shape(
202 |       gbpusd.slice(16).map((v, i) => [i+16, v])
203 |         .concat([[gbpusd.length-1, lo], [16, lo]]) )),
204 |     c.strokeColor("#202020",
205 |       c.line(gbpusd.map((v, i) => [i, v])))
206 |   ]))
207 | c.render("out2c", rates)
208 | 
209 |
210 | 211 |

Exchange rates: Two line charts with shared axis

212 |

In this demo, we compare two different exchange rates. To do this, we create two 213 | line charts and then place one above the other. The charts will share the X axis, but 214 | they will have separate Y axes. This is done using the c.nestY primitive, 215 | which nests a scale of a shape inside a newly defined locations. Here, the bottom chart 216 | takes are from 0 to 50 and the top chart takes an area from 50 to 100.

217 | 218 |
function body(lo, hi, data) {
219 |   return c.axes("left right bottom", c.overlay([
220 |     c.fillColor("#1F77B460",  c.shape(
221 |       [ [0,lo], [16,lo], [16,hi], [0,hi] ])),
222 |     c.fillColor("#D6272860",  c.shape(
223 |       [ [data.length-1,lo], [16,lo], [16,hi], [data.length-1,hi] ])),
224 |     c.strokeColor("#202020",
225 |       c.line(data.map((v, i) => [i, v])))
226 |   ]))
227 | }
228 | let rates = c.overlay([
229 |   c.nestY(0, 50, body(1.25, 1.52, gbpusd)),
230 |   c.nestY(50, 100, body(1.15, 1.32, gbpeur)),
231 | ])
232 | 
233 | c.render("out2d", rates)
234 | 
235 |
236 | 237 |

Iris dataset: Creating basic scatterplot

238 |

This example shows how to create a scatterplot comparing two features of the 239 | data from the Iris dataset. This is done by overlaying bubbles. In addition, 240 | we use the Iris species to set the colors of the dots.

241 | 242 |
let irisColors = { Setosa:"blue",
243 |     Virginica:"green", Versicolor:"red" }
244 | let x = "sepal_width", y = "petal_width"
245 | 
246 | let scatter =
247 |   c.axes("left bottom", c.overlay(
248 |     iris.map(i => c.strokeColor(
249 |       irisColors[i.species], c.bubble(i[x], i[y], 1, 1)))
250 |   ))
251 | c.render("out3a", scatter)
252 | 
253 |
254 | 255 |

Iris dataset: Creating basic histogram

256 |

Compost does not have a built-in histogram chart, but we can create this ourselves 257 | by writing a helper function that takes an array of data, splits it into bins of equal 258 | size and then counts the number of data points in each bin. The results can then be 259 | easily rendered by overlaying rectangular shapes.

260 | 261 |
// Calculate bins of a histogram. The function splits the data into 10
262 | // equally sized bins, counts the values in each bin and returns an array
263 | // of three-element arrays with start of the bin, end of the bin and count
264 | function bins(data) {
265 |   let lo = Math.min(...data), hi = Math.max(...data);
266 |   let bins = {}
267 |   for(var i=0; i<data.length; i++) {
268 |     let k = Math.round((data[i]-lo)/(hi-lo)*9);
269 |     if (bins[k]==undefined) bins[k]=1; else bins[k]++;
270 |   }
271 |   let keys = Object.keys(bins).map(k => k*1).sort()
272 |   return keys.map(k =>
273 |     [ lo + (hi - lo) * (k / 10),
274 |       lo + (hi - lo) * ((k + 1) / 10), bins[k]]);
275 | }
276 | 
277 | let hist =
278 |   c.axes("left bottom", c.overlay(
279 |     bins(iris.map(i => i["petal_width"])).map(b =>
280 |       c.fillColor("#808080", c.shape(
281 |         [ [b[0], b[2]], [b[1], b[2]], [b[1], 0], [b[0], 0] ])) )
282 |     ))
283 | c.render("out3b", hist)
284 | 
285 |
286 | 287 |

Iris dataset: Histogram and scatter in a pairplot

288 |

This example combines the previous two into a single chart. It is a reimplementation 289 | of the seaborn library pairplot 290 | chart. We build a matrix of chart showing a pairwise scatter-plot for all features and 291 | a histogram of feature distributions at the diagonal.

292 | 293 |
// Calculate bins of a histogram. The function splits the data into 10
294 | // equally sized bins, counts the values in each bin and returns an array
295 | // of three-element arrays with start of the bin, end of the bin and count
296 | function bins(data) {
297 |   let lo = Math.min(...data), hi = Math.max(...data);
298 |   let bins = {}
299 |   for(var i=0; i<data.length; i++) {
300 |     let k = Math.round((data[i]-lo)/(hi-lo)*9);
301 |     if (bins[k]==undefined) bins[k]=1; else bins[k]++;
302 |   }
303 |   let keys = Object.keys(bins).map(k => k*1).sort()
304 |   return keys.map(k =>
305 |     [ lo + (hi - lo) * (k / 10),
306 |       lo + (hi - lo) * ((k + 1) / 10), bins[k]]);
307 | }
308 | 
309 | let irisColors = {Setosa:"blue", Virginica:"green", Versicolor:"red" }
310 | let cats = ["sepal_width", "petal_length", "petal_width"]
311 | 
312 | let pairplot =
313 |   c.overlay(cats.map(x => cats.reverse().map(y =>
314 |     c.nest([x, 0], [x, 1], [y, 0], [y, 1],
315 |       c.axes("left bottom", c.overlay(
316 |         x == y
317 |         ? bins(iris.map(i => i[x])).map(b =>
318 |             c.fillColor("#808080", c.shape(
319 |               [ [b[0], b[2]], [b[1], b[2]], [b[1], 0], [b[0], 0] ])) )
320 |         : iris.map(i => c.strokeColor(irisColors[i.species],
321 |             c.bubble(i[x], i[y], 1, 1)))
322 |       ))))).flat())
323 | 
324 | c.render("out3c", pairplot)
325 | 
326 |
327 | 328 |

You draw it: Interactive bar chart

329 |

This demo shows how to create interactive charts using Compost. It is inspired by the 330 | You draw it 331 | visualizations by New York Times. You can click on the chart to adjust the size of the bars. 332 | The demo is based on the Elm architecture and the main nice thing is that events are 333 | reported in domain units. 334 |

335 | 336 |
let partyColors = {}
337 | for(var i = 0; i < elections.length; i++)
338 |   partyColors[elections[i].party] = elections[i].color;
339 | 
340 | function update(state, evt) {
341 |   switch (evt.kind) {
342 |     case 'set':
343 |       if (!state.enabled) return state;
344 |       let newValues = state.values.map(kv =>
345 |         kv[0] == evt.party ? [kv[0], evt.newValue] : kv)
346 |       return { ...state, values: newValues }
347 |     case 'enable':
348 |       return { ...state, enabled: evt.enabled }
349 |   }
350 | }
351 | 
352 | function render(trigger, state) {
353 |   return c.axes("left bottom", c.scaleY(s.continuous(0, 400),
354 |       c.on({
355 |         mousedown: () => trigger({ kind:'enable', enabled:true }),
356 |         mouseup: () => trigger({ kind:'enable', enabled:false }),
357 |         mousemove: (x, y) => trigger({ kind:'set', party:x[0], newValue:y })
358 |       }, c.overlay(state.values.map(kv =>
359 |         c.fillColor(partyColors[kv[0]],
360 |           c.padding(0, 10, 0, 10, c.column(kv[0], kv[1]))) ))
361 |     )))
362 | }
363 | 
364 | let init = { enabled:false, values: elections.map(e => [e.party, e.y19]) }
365 | c.interactive("out4", init, update, render)
366 | 
367 |
368 | 369 |

Learn more about Compost

370 | 384 | 385 |
386 |
387 | 388 | 389 | 390 | 396 | 397 | 398 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Compost.js: Composable data visualization library 6 | 12 | 13 | 14 | 15 | 16 | 17 | 19 | 20 | 24 | 25 | 26 | 27 | 42 |
43 |
44 |

Compost.js: Composable data visualization library

45 |

Compost is a data visualization library that lets you compose rich interactive 46 | data visualizations from a small number of basic primitives. The library is 47 | based on the functional programming idea of composable domain-specific languages. 48 | Compost is simple (implemented in just 700 lines of code) and easy to understand. 49 | Compost is a plain JavaScript library. You use it by writing JavaScript code that 50 | generates a chart using some 15 basic Compost primitives. 51 |

52 | 53 | 58 | 59 |

Compost in 10 lines of code

60 |

The following example is a line chart showing the GBP/USD exchange rate 61 | around 23 June 2016. 62 | An interesting feature is that the chart highlights a part before the date using one 63 | color and the part after the date using another color.

64 | 65 |

In Compost, we create the chart by composing three chart elements using 66 | c.overlay. The three chart elements are two rectangles constructed 67 | using c.shape and one line constructed using c.line. 68 | Results of those functions are chart elements that can be further transformed 69 | using functions such as c.fillColor and c.strokeColor 70 | that change the color of the element.

71 |
// Exchange rate range for the background
 72 | let lo = 1.25, hi = 1.52;
 73 | 
 74 | // Overlay three shapes and add axes on three sides
 75 | let xchg = c.axes("left right bottom", c.overlay([
 76 |   // Fill area behind first 16 values in blue
 77 |   c.fillColor("#1F77B460",  c.shape(
 78 |     [ [0,lo], [16,lo], [16,hi], [0,hi] ])),
 79 |   // Fill area behind the remaining values in red
 80 |   c.fillColor("#D6272860",  c.shape(
 81 |     [ [gbpusd.length-1,lo], [16,lo], [16,hi], [gbpusd.length-1,hi] ])),
 82 |   // Draw a black line using 'gbpusd' array
 83 |   c.strokeColor("#202020", c.line(gbpusd.map((v, i) => [i, v])))
 84 | ]));
 85 | 
 86 | // Render chart on <div id="demo" />
 87 | c.render("demo", xchg)
 88 | 
89 |
90 |

The example illustrates two key ideas of Compost:

91 | 102 | 103 |

Learn more about Compost

104 | 120 |
121 |
122 | 123 | 124 | 125 | 131 | 132 | 133 | 134 | -------------------------------------------------------------------------------- /docs/lib/docs.js: -------------------------------------------------------------------------------- 1 | var elections = 2 | [ { party:"Conservative", color:"#1F77B4", y17:317, y19:365}, 3 | { party:"Labour", color:"#D62728", y17:262, y19:202}, 4 | { party:"LibDem", color:"#FF7F0E", y17:12, y19:11}, 5 | { party:"SNP", color:"#BCBD22", y17:35, y19:48}, 6 | { party:"Green", color:"#2CA02C", y17:1, y19:1}, 7 | { party:"DUP", color:"#8C564B", y17:10, y19:8} ] 8 | 9 | var gbpusd = 10 | [ 1.3206, 1.3267, 1.312, 1.3114, 1.3116, 1.3122, 1.3085, 1.3211, 1.3175, 11 | 1.3136, 1.3286, 1.3231, 1.3323, 1.3215, 1.3186, 1.2987, 1.296, 1.2932, 12 | 1.2885, 1.3048, 1.3287, 1.327, 1.3429, 1.3523, 1.3322, 1.3152, 1.3621, 13 | 1.4798, 1.4687, 1.467, 1.4694, 1.4293, 1.4064, 1.4196, 1.4114, 1.4282, 14 | 1.4334, 1.4465, 1.4552, 1.456, 1.4464, 1.4517, 1.4447, 1.4414 ].reverse() 15 | 16 | var gbpeur = 17 | [ 1.1823, 1.1867, 1.1838, 1.1936, 1.1944, 1.1961, 1.1917, 1.2017, 1.1969, 18 | 1.193, 1.2006, 1.1952, 1.1998, 1.1903, 1.1909, 1.1759, 1.1743, 1.168, 19 | 1.1639, 1.175, 1.1929, 1.192, 1.2081, 1.2177, 1.2054, 1.1986, 1.2254, 20 | 1.3039, 1.3018, 1.3018, 1.296, 1.2709, 1.2617, 1.2634, 1.2589, 1.2639, 21 | 1.2687, 1.2771, 1.2773, 1.2823, 1.2726, 1.2814, 1.2947, 1.2898 ].reverse() 22 | 23 | var iris = 24 | [ [5.1, 3.5, 1.4, 0.2, "Setosa"], [4.9, 3, 1.4, 0.2, "Setosa"], [4.7, 3.2, 1.3, 0.2, "Setosa"], 25 | [4.6, 3.1, 1.5, 0.2, "Setosa"], [5, 3.6, 1.4, 0.2, "Setosa"], [5.4, 3.9, 1.7, 0.4, "Setosa"], 26 | [4.6, 3.4, 1.4, 0.3, "Setosa"], [5, 3.4, 1.5, 0.2, "Setosa"], [4.4, 2.9, 1.4, 0.2, "Setosa"], 27 | [4.9, 3.1, 1.5, 0.1, "Setosa"], [5.4, 3.7, 1.5, 0.2, "Setosa"], [4.8, 3.4, 1.6, 0.2, "Setosa"], 28 | [4.8, 3, 1.4, 0.1, "Setosa"], [4.3, 3, 1.1, 0.1, "Setosa"], [5.8, 4, 1.2, 0.2, "Setosa"], 29 | [5.7, 4.4, 1.5, 0.4, "Setosa"], [5.4, 3.9, 1.3, 0.4, "Setosa"], [5.1, 3.5, 1.4, 0.3, "Setosa"], 30 | [5.7, 3.8, 1.7, 0.3, "Setosa"], [5.1, 3.8, 1.5, 0.3, "Setosa"], [5.4, 3.4, 1.7, 0.2, "Setosa"], 31 | [5.1, 3.7, 1.5, 0.4, "Setosa"], [4.6, 3.6, 1, 0.2, "Setosa"], [5.1, 3.3, 1.7, 0.5, "Setosa"], 32 | [4.8, 3.4, 1.9, 0.2, "Setosa"], [5, 3, 1.6, 0.2, "Setosa"], [5, 3.4, 1.6, 0.4, "Setosa"], 33 | [5.2, 3.5, 1.5, 0.2, "Setosa"], [5.2, 3.4, 1.4, 0.2, "Setosa"], [4.7, 3.2, 1.6, 0.2, "Setosa"], 34 | [4.8, 3.1, 1.6, 0.2, "Setosa"], [5.4, 3.4, 1.5, 0.4, "Setosa"], [5.2, 4.1, 1.5, 0.1, "Setosa"], 35 | [5.5, 4.2, 1.4, 0.2, "Setosa"], [4.9, 3.1, 1.5, 0.2, "Setosa"], [5, 3.2, 1.2, 0.2, "Setosa"], 36 | [5.5, 3.5, 1.3, 0.2, "Setosa"], [4.9, 3.6, 1.4, 0.1, "Setosa"], [4.4, 3, 1.3, 0.2, "Setosa"], 37 | [5.1, 3.4, 1.5, 0.2, "Setosa"], [5, 3.5, 1.3, 0.3, "Setosa"], [4.5, 2.3, 1.3, 0.3, "Setosa"], 38 | [4.4, 3.2, 1.3, 0.2, "Setosa"], [5, 3.5, 1.6, 0.6, "Setosa"], [5.1, 3.8, 1.9, 0.4, "Setosa"], 39 | [4.8, 3, 1.4, 0.3, "Setosa"], [5.1, 3.8, 1.6, 0.2, "Setosa"], [4.6, 3.2, 1.4, 0.2, "Setosa"], 40 | [5.3, 3.7, 1.5, 0.2, "Setosa"], [5, 3.3, 1.4, 0.2, "Setosa"], [7, 3.2, 4.7, 1.4, "Versicolor"], 41 | [6.4, 3.2, 4.5, 1.5, "Versicolor"], [6.9, 3.1, 4.9, 1.5, "Versicolor"], [5.5, 2.3, 4, 1.3, "Versicolor"], 42 | [6.5, 2.8, 4.6, 1.5, "Versicolor"], [5.7, 2.8, 4.5, 1.3, "Versicolor"], [6.3, 3.3, 4.7, 1.6, "Versicolor"], 43 | [4.9, 2.4, 3.3, 1, "Versicolor"], [6.6, 2.9, 4.6, 1.3, "Versicolor"], [5.2, 2.7, 3.9, 1.4, "Versicolor"], 44 | [5, 2, 3.5, 1, "Versicolor"], [5.9, 3, 4.2, 1.5, "Versicolor"], [6, 2.2, 4, 1, "Versicolor"], 45 | [6.1, 2.9, 4.7, 1.4, "Versicolor"], [5.6, 2.9, 3.6, 1.3, "Versicolor"], [6.7, 3.1, 4.4, 1.4, "Versicolor"], 46 | [5.6, 3, 4.5, 1.5, "Versicolor"], [5.8, 2.7, 4.1, 1, "Versicolor"], [6.2, 2.2, 4.5, 1.5, "Versicolor"], 47 | [5.6, 2.5, 3.9, 1.1, "Versicolor"], [5.9, 3.2, 4.8, 1.8, "Versicolor"], [6.1, 2.8, 4, 1.3, "Versicolor"], 48 | [6.3, 2.5, 4.9, 1.5, "Versicolor"], [6.1, 2.8, 4.7, 1.2, "Versicolor"], [6.4, 2.9, 4.3, 1.3, "Versicolor"], 49 | [6.6, 3, 4.4, 1.4, "Versicolor"], [6.8, 2.8, 4.8, 1.4, "Versicolor"], [6.7, 3, 5, 1.7, "Versicolor"], 50 | [6, 2.9, 4.5, 1.5, "Versicolor"], [5.7, 2.6, 3.5, 1, "Versicolor"], [5.5, 2.4, 3.8, 1.1, "Versicolor"], 51 | [5.5, 2.4, 3.7, 1, "Versicolor"], [5.8, 2.7, 3.9, 1.2, "Versicolor"], [6, 2.7, 5.1, 1.6, "Versicolor"], 52 | [5.4, 3, 4.5, 1.5, "Versicolor"], [6, 3.4, 4.5, 1.6, "Versicolor"], [6.7, 3.1, 4.7, 1.5, "Versicolor"], 53 | [6.3, 2.3, 4.4, 1.3, "Versicolor"], [5.6, 3, 4.1, 1.3, "Versicolor"], [5.5, 2.5, 4, 1.3, "Versicolor"], 54 | [5.5, 2.6, 4.4, 1.2, "Versicolor"], [6.1, 3, 4.6, 1.4, "Versicolor"], [5.8, 2.6, 4, 1.2, "Versicolor"], 55 | [5, 2.3, 3.3, 1, "Versicolor"], [5.6, 2.7, 4.2, 1.3, "Versicolor"], [5.7, 3, 4.2, 1.2, "Versicolor"], 56 | [5.7, 2.9, 4.2, 1.3, "Versicolor"], [6.2, 2.9, 4.3, 1.3, "Versicolor"], [5.1, 2.5, 3, 1.1, "Versicolor"], 57 | [5.7, 2.8, 4.1, 1.3, "Versicolor"], [6.3, 3.3, 6, 2.5, "Virginica"], [5.8, 2.7, 5.1, 1.9, "Virginica"], 58 | [7.1, 3, 5.9, 2.1, "Virginica"], [6.3, 2.9, 5.6, 1.8, "Virginica"], [6.5, 3, 5.8, 2.2, "Virginica"], 59 | [7.6, 3, 6.6, 2.1, "Virginica"], [4.9, 2.5, 4.5, 1.7, "Virginica"], [7.3, 2.9, 6.3, 1.8, "Virginica"], 60 | [6.7, 2.5, 5.8, 1.8, "Virginica"], [7.2, 3.6, 6.1, 2.5, "Virginica"], [6.5, 3.2, 5.1, 2, "Virginica"], 61 | [6.4, 2.7, 5.3, 1.9, "Virginica"], [6.8, 3, 5.5, 2.1, "Virginica"], [5.7, 2.5, 5, 2, "Virginica"], 62 | [5.8, 2.8, 5.1, 2.4, "Virginica"], [6.4, 3.2, 5.3, 2.3, "Virginica"], [6.5, 3, 5.5, 1.8, "Virginica"], 63 | [7.7, 3.8, 6.7, 2.2, "Virginica"], [7.7, 2.6, 6.9, 2.3, "Virginica"], [6, 2.2, 5, 1.5, "Virginica"], 64 | [6.9, 3.2, 5.7, 2.3, "Virginica"], [5.6, 2.8, 4.9, 2, "Virginica"], [7.7, 2.8, 6.7, 2, "Virginica"], 65 | [6.3, 2.7, 4.9, 1.8, "Virginica"], [6.7, 3.3, 5.7, 2.1, "Virginica"], [7.2, 3.2, 6, 1.8, "Virginica"], 66 | [6.2, 2.8, 4.8, 1.8, "Virginica"], [6.1, 3, 4.9, 1.8, "Virginica"], [6.4, 2.8, 5.6, 2.1, "Virginica"], 67 | [7.2, 3, 5.8, 1.6, "Virginica"], [7.4, 2.8, 6.1, 1.9, "Virginica"], [7.9, 3.8, 6.4, 2, "Virginica"], 68 | [6.4, 2.8, 5.6, 2.2, "Virginica"], [6.3, 2.8, 5.1, 1.5, "Virginica"], [6.1, 2.6, 5.6, 1.4, "Virginica"], 69 | [7.7, 3, 6.1, 2.3, "Virginica"], [6.3, 3.4, 5.6, 2.4, "Virginica"], [6.4, 3.1, 5.5, 1.8, "Virginica"], 70 | [6, 3, 4.8, 1.8, "Virginica"], [6.9, 3.1, 5.4, 2.1, "Virginica"], [6.7, 3.1, 5.6, 2.4, "Virginica"], 71 | [6.9, 3.1, 5.1, 2.3, "Virginica"], [5.8, 2.7, 5.1, 1.9, "Virginica"], [6.8, 3.2, 5.9, 2.3, "Virginica"], 72 | [6.7, 3.3, 5.7, 2.5, "Virginica"], [6.7, 3, 5.2, 2.3, "Virginica"], [6.3, 2.5, 5, 1.9, "Virginica"], 73 | [6.5, 3, 5.2, 2, "Virginica"], [6.2, 3.4, 5.4, 2.3, "Virginica"], [5.9, 3, 5.1, 1.8, "Virginica"] 74 | ].map(a => ({ sepal_length: a[0], sepal_width: a[1], petal_length: a[2], petal_width: a[3], species: a[4] })) 75 | 76 | var pres = document.getElementsByClassName("compost"); 77 | for(var i = 0; i < pres.length; i++) { 78 | eval("(function(c,s,elections,gbpusd,gbpeur,iris) { " + pres[i].innerText + "})") 79 | (c, s, elections, gbpusd, gbpeur, iris); 80 | } 81 | -------------------------------------------------------------------------------- /docs/lib/style.css: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css2?family=Kreon:wght@300;400;500;600;700&family=PT+Sans:ital,wght@0,400;0,700;1,400&family=Roboto+Mono&display=swap'); 2 | 3 | section, div, html, body { margin:0px; padding:0px; } 4 | body { 5 | padding-bottom:150px; 6 | } 7 | h1, h2, h3, h4 { 8 | font-family:Kreon; 9 | font-weight:400; 10 | } 11 | h1 { 12 | font-size:26pt; 13 | margin:0px 0px 25px 0px; 14 | } 15 | h2 { 16 | font-size:20pt; 17 | margin:35px 0px 10px 0px; 18 | } 19 | code.hljs { 20 | background:transparent; 21 | } 22 | pre, code { 23 | font-family:'Roboto Mono', monospace; 24 | font-size:10pt; 25 | } 26 | pre { 27 | margin-left:20px; 28 | margin-right:20px; 29 | margin-bottom:20px; 30 | margin-top:0px; 31 | } 32 | pre.compost { 33 | margin-left:50px; 34 | margin-right:50px; 35 | margin-bottom:10px; 36 | } 37 | p { 38 | font-size:13pt; 39 | max-width:800px; 40 | margin:0px 0px 15px 0px; 41 | } 42 | li { 43 | font-size:13pt; 44 | padding:0px; 45 | margin:0px 0px 10px 30px; 46 | } 47 | ul { 48 | max-width:800px; 49 | padding:0px; 50 | margin:0px 0px 25px 0px; 51 | } 52 | ul.nosep li { 53 | margin-bottom:5px; 54 | } 55 | body { 56 | font-family:'PT Sans', sans-serif; 57 | } 58 | .container { 59 | max-width:960px; 60 | margin:0px auto 0px auto; 61 | } 62 | section { 63 | padding:50px 30px 0px 30px; 64 | } 65 | nav { 66 | background:white; 67 | font-size:11pt; 68 | } 69 | nav .container { 70 | border-bottom:solid 1px #e0e0e0; 71 | padding:15px 0px 15px 0px; 72 | } 73 | a, a:visited { 74 | color:#e06030; 75 | text-decoration:none; 76 | } 77 | a:hover { 78 | color:#ff8050; 79 | text-decoration:none; 80 | } 81 | nav a { 82 | margin:0px 0px 0px 20px; 83 | } 84 | nav, nav a, nav a:visited { 85 | font-family:Kreon; 86 | color:#404040; 87 | text-decoration:none; 88 | text-transform:uppercase; 89 | } 90 | nav a:hover { 91 | color:#ff8050; 92 | } 93 | #navsm { 94 | display:none; 95 | } 96 | @media only screen and (max-width: 500px) { 97 | #navsm a { 98 | font-size:12pt; 99 | } 100 | #navsm { 101 | margin-bottom:-5px; 102 | } 103 | #navlg { 104 | margin-top:15px; 105 | margin-bottom:-10px; 106 | border-top:solid 1px #e0e0e0; 107 | } 108 | #navsm { 109 | display:block; 110 | } 111 | #navlg.hidden { 112 | display:none; 113 | } 114 | #navlg a { 115 | display:block; 116 | margin:10px 0px 10px 25px; 117 | } 118 | } 119 | 120 | .buttons { 121 | max-width:800px; 122 | padding-right:50px; 123 | text-align:center; 124 | padding-bottom:0px; 125 | } 126 | .buttons a, .buttons a:visited { 127 | background:#ff8050; 128 | color:white; 129 | padding:10px 20px 10px 20px; 130 | text-align:center; 131 | display:inline-block; 132 | margin:0px 10px 15px 10px; 133 | border-radius:10px; 134 | font-family:Kreon; 135 | font-size:14pt; 136 | } 137 | .buttons a:hover { 138 | background:#e06030; 139 | } 140 | table.docs { 141 | margin:20px 0px 10px 0px; 142 | } 143 | table.docs th { 144 | vertical-align:top; 145 | text-align:left; 146 | padding-right:10px; 147 | min-width:120px; 148 | } 149 | table.docs p { 150 | margin:5px 0px 15px 0px; 151 | } 152 | table.docs code.f { 153 | color:#e06030; 154 | } 155 | .compost-out { 156 | margin:0px 0px 30px 50px; 157 | } 158 | @media only screen and (max-width: 600px) { 159 | .compost-out { 160 | margin:0px 0px 30px 0px; 161 | } 162 | pre { 163 | margin-left:0px; 164 | margin-right:0px; 165 | } 166 | pre.compost { 167 | margin-left:0px; 168 | margin-right:0px; 169 | } 170 | table.docs tr > *{ 171 | display: block; 172 | } 173 | 174 | } 175 | 176 | @media only screen and (max-width: 420px) { 177 | nav a { 178 | margin:0px 10px 0px 10px; 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /docs/paper.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/compostjs/compost/77afecb737a1cde524664d313215259f2b569b62/docs/paper.pdf -------------------------------------------------------------------------------- /docs/tutorial.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Getting started with Compost - Compost.js 6 | 7 | 8 | 9 | 10 | 11 | 13 | 14 | 18 | 19 | 20 | 21 | 36 |
37 |
38 |

Getting started with Compost

39 |

40 | In this tutorial, we're going to progressively create a chart using Compost. We 41 | will start with the most basic one and gradually add a number of interesting features. 42 | This covers most of the core primitives that Compost.js provides for constructing charts. 43 | There are two things to keep in mind when reading the tutorial. 44 |

45 |

46 | First, the primitives are quite basic on their own. This is precisely the 47 | point of the library. You have only a small number of basic primitives, but you can 48 | easily compose them to build interesting charts. 49 |

50 |

51 | Second, the primitives are just JavaScript functions that you call from your own 52 | JavaScript code. Most of the code snippets will look more like declarative specifications 53 | than like code, but that's just because we are building only basic charts with hard-coded 54 | data. In a real case, you would generate elements for example using the 55 | data.map(...) function or using a for loop. Crucially, this 56 | also means that you can wrap a part of the logic into a reusable function and define 57 | your own high-level operations. 58 |

59 |

60 | The final chart that we'll construct in the tutorial shows 61 | Boris 62 | Johnson's approval rating as of June 2020: 63 |

64 | 101 |
102 | 103 |

Creating and aligning columns

104 |

Just for fun, let's start with a snippet that defines a single column using 105 | c.column and then renders it using the c.render function. 106 | Our column has a categorical X value "Positive" and a continuous 107 | Y value 39. The result is not very impressive:

108 |
let demo = c.column("Positive", 39);
109 | c.render("out1a", demo)
110 |
111 | 112 |

What has happened here? Compost first infers the scales of the chart, which represent 113 | the range of values that may appear on its axes. For the X axis, the scale is categorical 114 | containing only one value "Positive". The scale for the Y axis is continuous 115 | and contains values form 0 (the bottom of the column) to 39 (the top of the column). 116 | Compost then projects the single column onto the available space and the single column 117 | takes the entire available space. It is also drawn using a boring default gray color.

118 |

For the next step, we're going to combine multiple columns. To do this, we use 119 | c.overlay, which takes a list of chart elements (shapes) and automatically 120 | aligns them according to the inferred scales:

121 |
let demo =
122 |   c.overlay([
123 |     c.column("Positive", 39),
124 |     c.column("Negative", 43),
125 |     c.column("Neutral", 17)
126 |   ])
127 | 
128 | c.render("out1b", demo)
129 |
130 | 131 |

This is a bit better! We can see that the X axis has been divided into three 132 | equally-sized parts. Compost inferred that the X scale now contains three different 133 | (categorical) values. When rendering a column, it then maps the values onto the 134 | inferred scale and so each column takes only a part of the space. 135 | 136 |

Adding colors and axes

137 |

Compost does not (currently) automatically assign colors to different columns. 138 | They are just shapes that are drawn using a specified color. To change the 139 | specified color for a part of a chart, we can use c.fillColor, which 140 | changes color for a shape given as the second parameter. For lines, there is 141 | c.strokeColor. 142 |

143 |
let demo =
144 |   c.overlay([
145 |     c.fillColor("#2CA02C", c.column("Positive", 39)),
146 |     c.fillColor("#D62728", c.column("Negative", 43)),
147 |     c.fillColor("#1F77B4", c.column("Neutral", 17))
148 |   ])
149 | 
150 | c.render("out2a", demo)
151 | 
152 |
153 | 154 |

Now we can see that the chart, indeed, consists of three separate columns! 155 | But how can we find out which one is which? To make this clear, we need to add 156 | axes. This is done using c.axes, which is a primitive that adds axes 157 | around a specified (part) of a chart. The first parameter is a string that can contain 158 | any combination of left/right/top/bottom, depending on what axes we want: 159 |

160 |
let demo =
161 |   c.axes("left bottom", c.overlay([
162 |     c.fillColor("#2CA02C", c.column("Positive", 39)),
163 |     c.fillColor("#D62728", c.column("Negative", 43)),
164 |     c.fillColor("#1F77B4", c.column("Neutral", 17))
165 |   ]))
166 | 
167 | c.render("out2b", demo)
168 | 
169 |
170 |

An important thing to note is that we did not need to tell c.axes what 171 | the ranges on the axes are. This is inferred automatically. As discussed before, 172 | Compost infers the scales for both axes (so that it can automatically align shapes) 173 | and the c.axes primitive just accesses this information.

174 | 175 |

Specifying coordinates

176 |

So far, our only primitive shape was a column created using c.column. 177 | This is cheating slightly, because c.column is not actually a primitive; 178 | it is just a very convenient helper. Under the cover, it uses the c.shape 179 | primitive, which takes a list of locations and fills a polygon specified by those 180 | locations.

181 |

A location consists of an X and Y coordinate and we specify them as 182 | two-element JavaScript arrays. If you have coordinates X1, Y1, X2, Y2, X3 and Y3 183 | and want to fill a triangle specified by those three points, you would specify a list 184 | of locations as: 185 |

186 |
[ [x1, y1], [x2, y2], [x3, y3] ]
187 |

How do we specify a single X or Y coordinate? There are two options, depending on 188 | whether the scale that we are using is continuous or categorical. For continous scales, 189 | the coordinate is just a numerical value. If the Y axis has a continous scale representing 190 | the approval ratings, then Y1 would be just 0 or 39. 191 |

192 |

For categorical scales, specifying a location is trickier. A categorical value such as 193 | "Positive" refers to a range rather than a single location, so if we want 194 | to specify a location, we need to add a numerical value between 0 and 1 that specifies an 195 | offset within the range. For example, ["Positive", 0] is on the very left of 196 | the range and ["Positive", 1] is on the very right.

197 |

To use c.shape to recreate the same column chart as in the previous example, 198 | we need to give it four locations (representing the four corners of the rectangle), each 199 | with a categorical X value and a continuous Y value:

200 | 201 |
let demo =
202 |   c.axes("left bottom", c.overlay([
203 |     c.fillColor("#2CA02C", c.shape([
204 |       [["Positive", 0], 0], [["Positive", 1], 0],
205 |       [["Positive", 1], 39], [["Positive", 0], 39] ])),
206 |     c.fillColor("#D62728", c.shape([
207 |       [["Negative", 0], 0], [["Negative", 1], 0],
208 |       [["Negative", 1], 43], [["Negative", 0], 43] ]))
209 |   ]))
210 | 
211 | c.render("out3a", demo)
212 | 
213 |
214 | 215 |

One nice consequence of how Compost works is that there is no need to distinguish between 216 | a column chart and a bar chart. Compost actually provides c.column and 217 | c.bar for convenience, but if we want to turn the previous column chart 218 | into a bar chart, we just switch the X and Y coordinates in the two lists: 219 |

220 |
let demo =
221 |   c.axes("left bottom", c.overlay([
222 |     c.fillColor("#2CA02C", c.shape([
223 |       [0, ["Positive", 0]], [0, ["Positive", 1]],
224 |       [39, ["Positive", 1]], [39, ["Positive", 0]] ])),
225 |     c.fillColor("#D62728", c.shape([
226 |       [0, ["Negative", 0]], [0, ["Negative", 1]],
227 |       [43, ["Negative", 1]], [43, ["Negative", 0]] ]))
228 |   ]))
229 | 
230 | c.render("out3b", demo)
231 | 
232 |
233 |

This produces a bar chart as we wanted. It is also worth noting that the axes 234 | "just work", because they display values according to the inferred scales. Those 235 | are inferred from the X or Y values that appear in the individual shapes.

236 | 237 |

Combining different shapes

238 |

Aside from filled polygons, Compost also provides primitives for drawing lines 239 | and adding text. Lines are obviously useful if you want to create a line chart, 240 | but there is nothing preventing you from combining multiple different shapes in 241 | a single chart.

242 |

You might not be used to think about charts in this way, but there is a number 243 | of nice use cases. For example, what if we wanted to add an error bar to our bar 244 | chart? This is just a line in the middle of the bar.

245 |

A line is specified in the same way as a shape, i.e. as a list of locations that 246 | consist of X and Y coordinates. Let's say that we want to display a bar chart with 247 | values 39+/-7 and 43+/-5 (those are made up numbers, not actual standard deviation 248 | of the approval ratings):

249 |
let demo =
250 |   c.axes("left bottom", c.overlay([
251 |     c.fillColor("#2CA02C", c.shape([
252 |       [["Positive", 0], 0], [["Positive", 1], 0],
253 |       [["Positive", 1], 39], [["Positive", 0], 39] ])),
254 |     c.strokeColor("black", c.line([
255 |       [["Positive", 0.5], 32], [["Positive", 0.5], 46] ])),
256 |     c.fillColor("#D62728", c.shape([
257 |       [["Negative", 0], 0], [["Negative", 1], 0],
258 |       [["Negative", 1], 43], [["Negative", 0], 43] ])),
259 |     c.strokeColor("black", c.line([
260 |       [["Negative", 0.5], 38], [["Negative", 0.5], 48] ])),
261 |   ]))
262 | 
263 | c.render("out4a", demo)
264 | 
265 |
266 |

This works fine, but it is not exactly great. The lines would look nicer if 267 | they also had smaller horizontal lines at the bottom and at the top. The code is also 268 | becoming quite long and would get much longer with more than two columns.

269 |

Wouldn't it be nicer if you just had a built-in primitive for error bars? 270 | The nice thing about Compost is that this is pretty much exactly what we can do 271 | on our own! Just like there is c.column helper, we can define our own 272 | helper errorLine for creating error bars. This is just a JavaScript 273 | function that takes the X and Y values together with a standard deviation for the bar:

274 |
function errorLine(x, y, sdv) {
275 |   return c.strokeColor("black", c.overlay([
276 |     c.line([[[x, 0.5], y-sdv], [[x, 0.5], y+sdv]]),
277 |     c.line([[[x, 0.45], y-sdv], [[x, 0.55], y-sdv]]),
278 |     c.line([[[x, 0.45], y+sdv], [[x, 0.55], y+sdv]])
279 |   ]));
280 | }
281 | 
282 | let demo =
283 |   c.axes("left bottom", c.overlay([
284 |     c.fillColor("#2CA02C", c.column("Positive", 39)),
285 |     errorLine("Positive", 39, 7),
286 |     c.fillColor("#D62728", c.column("Negative", 43)),
287 |     errorLine("Negative", 43, 5),
288 |     c.fillColor("#1F77B4", c.column("Neutral", 17)),
289 |     errorLine("Neutral", 17, 2)
290 |   ]))
291 | 
292 | c.render("out4b", demo)
293 | 
294 |
295 |

Once we define the errorLine function, the rest of the code to create the 296 | chart is very succinct and clear. Of course, we had to define the helper function, but 297 | this is something that you only need to do once.

298 | 299 |

Overriding inferred scales

300 |

In all the previous examples, the scales were inferred automatically. In some cases, 301 | it is useful to be able to override the inferred scale and specify your own. For example, 302 | let's say that we want the range displayed on the Y axis to be from 0 to 100. We can 303 | do this using the c.scaleY operation. The first argument of this operation 304 | is a scale, which can be created using the s.continuous function:

305 |
let demo =
306 |   c.axes("left bottom", c.scaleY(s.continuous(0, 100), c.overlay([
307 |     c.fillColor("#2CA02C", c.column("Positive", 39)),
308 |     c.fillColor("#D62728", c.column("Negative", 43)),
309 |     c.fillColor("#1F77B4", c.column("Neutral", 17))
310 |   ])))
311 | 
312 | c.render("out5", demo)
313 | 
314 |
315 |

Here, we create a continuous scale using s.continuous, which takes the 316 | smallest and the largest values as arguments. You can also explicitly create categorical 317 | scales using s.categorical. In this case, you need to provide an array of 318 | strings representing the individual categories. This may be useful if you want to add 319 | a category for which you don't have data, or if you want to explicitly specify their order.

320 | 321 |

Combining different charts and more

322 |

So far, we've seen how to combine multiple shapes using c.overlay. This 323 | is, in fact, the only way to combine multiple shapes. However, there is much more we can 324 | do using this primitive if we combine it with a "nesting" operation that lets us 325 | create a new virtual space with its own scales within a larger chart.

326 | 327 |

To demonstrate nesting, we're going to add a legend to the right side of our bar 328 | chart. There is no built-in primitive for legends in Compost, but we can create one 329 | just by overlaying three bars (shapes) of the same size and three labels created using 330 | c.text:

331 |
let demo =
332 |   c.overlay([
333 |     c.fillColor("#2CA02C", c.padding(10,0,10,0, c.bar(10, "Positive"))),
334 |     c.fillColor("#D62728", c.padding(10,0,10,0, c.bar(10, "Negative"))),
335 |     c.fillColor("#1F77B4", c.padding(10,0,10,0, c.bar(10, "Neutral"))),
336 |     c.font("11pt arial", "black", c.overlay([
337 |       c.text(12, ["Positive",0.5], "Positive", "start"),
338 |       c.text(12, ["Negative",0.5], "Negative", "start"),
339 |       c.text(12, ["Neutral",0.5], "Neutral", "start")
340 |     ]))
341 |   ])
342 | 
343 | c.render("out6a", demo)
344 | 
345 |
346 |

There are three new primitives in this snippet. First, we use c.padding 347 | to add a space around the bar. This takes a padding that should be added from the 348 | top, right, bottom and the left side (the order is the same as in CSS). Second, 349 | we use c.text to add a label. This takes the X and Y coordinates, 350 | followed by the label and a location with respect to the x, Y location. You can use 351 | baseline, hanging or middle for vertical alignment and start, end or center for 352 | horizontal alignment. Finally, we also specify font and text color using c.font. 353 | 354 |

If you imagine the above chart as being much smaller, then it looks like a chart 355 | legend. There is one last thing that we need to do, which is to put everything we've 356 | done together. The following snippet (re)defines the errorLine helper, 357 | defines a legend shape from the previous code sample and creates a chart 358 | chart. It then combines legend with chart into 359 | a single chart that is rendered. We discuss how this is done below:

360 |
function errorLine(x, y, sdv) {
361 |   return c.strokeColor("black", c.overlay([
362 |     c.line([[[x, 0.5], y-sdv], [[x, 0.5], y+sdv]]),
363 |     c.line([[[x, 0.45], y-sdv], [[x, 0.55], y-sdv]]),
364 |     c.line([[[x, 0.45], y+sdv], [[x, 0.55], y+sdv]])
365 |   ]));
366 | }
367 | 
368 | let legend =
369 |   c.overlay([
370 |     c.fillColor("#2CA02C", c.padding(7,0,7,0, c.bar(10, "Positive"))),
371 |     c.fillColor("#D62728", c.padding(7,0,7,0, c.bar(10, "Negative"))),
372 |     c.fillColor("#1F77B4", c.padding(7,0,7,0, c.bar(10, "Neutral"))),
373 |     c.font("11pt arial", "black", c.overlay([
374 |       c.text(12, ["Positive",0.5], "Positive", "start"),
375 |       c.text(12, ["Negative",0.5], "Negative", "start"),
376 |       c.text(12, ["Neutral",0.5], "Neutral", "start")
377 |     ]))
378 |   ])
379 | 
380 | let chart =
381 |   c.axes("left bottom", c.scaleY(s.continuous(0, 55), c.overlay([
382 |     c.fillColor("#2CA02C", c.column("Positive", 39)),
383 |     errorLine("Positive", 39, 7),
384 |     c.fillColor("#D62728", c.column("Negative", 43)),
385 |     errorLine("Negative", 43, 5),
386 |     c.fillColor("#1F77B4", c.column("Neutral", 17)),
387 |     errorLine("Neutral", 17, 2)
388 |   ])))
389 | 
390 | let demo = c.overlay([
391 |   c.nest(0, 85, 0, 100, chart),
392 |   c.nest(90, 100, 50, 100, legend)
393 | ])
394 | c.render("out6b", demo)
395 | 
396 |
397 |

In order to put the legend on the right side of the chart, we use the 398 | c.nest operation and then overlay the results. The nest operation 399 | takes two locations (X1, X2, Y1, Y2) and draws the shape it gets as the last argument 400 | inside a rectangle specified by those two points. The scales inside the shape can be 401 | completely different from the scales on the outside.

402 |

In our example, both shapes that we 403 | combine are constructed using c.nest and the scale on the outside is 404 | thus defined only by the locations given to c.nest. 405 | The X and Y values for the rectangle containing the main chart are 0 to 85 (for X) 406 | and 0 to 100 (for Y). For the legend, those are 90 to 100 (for X) and 0 to 50 (for Y). 407 | This means that we're dedicating 85% of the horizontal space for the main chart, 408 | followed by 5% gap and 10% for the legend. Vertically, the legend only occupies the top 409 | half of the space. Note that our choice of numbers from 0 to 100 is arbitrary. This is 410 | just an (automatically inferred) continuous scale, so the code would work equally well 411 | with values between 0 and 1.

412 | 413 |

Conclusions

414 |

To summarize, the tutorial shows the two key features of Compost:

415 | 431 | 432 |

Learn more about Compost

433 | 447 | 448 |
449 |
450 | 451 | 452 | 453 | 459 | 460 | 461 | -------------------------------------------------------------------------------- /docs/usage.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | How to use Compost in your projects - Compost.js 6 | 7 | 8 | 9 | 10 | 11 | 13 | 14 | 18 | 19 | 20 | 21 | 36 |
37 |
38 |

How to use Compost in your projects

39 | 40 |

Using standalone JavaScript file

41 |

If you want to experiment with Compost, the easiest option is to add a reference 42 | to the latest version of Compost using a standalone JavaScript file from GitHub. 43 | This defines global variables c (for creating Compost charts) 44 | and s (for defining scales) that you can use in your scripts. 45 | The following complete HTML file creates a basic line chart:

46 |
<!DOCTYPE html>
 47 | <html>
 48 | <head>
 49 |   <script src="https://compostjs.github.io/compost/releases/compost-latest.js">
 50 |   </script>
 51 | </head>
 52 | <body>
 53 |   <div id="demo" style="width:600px;height:300px"></div>
 54 |   <script type="text/javascript">
 55 |     let d = c.axes("left bottom",
 56 |       c.line([[1,1],[2,4],[3,9],[4,16],[5,25],[6,36] ]))
 57 |     c.render("demo", d)
 58 |   </script>
 59 | </body>
 60 | </html>
 61 | 
62 |

For production use, you should download the file 63 | and add it to your project rather than referencing it directly from GitHub. You can 64 | get a specific version by replacing latest with, for example, 0.0.2. 65 | 66 |

Using Compost from a Node.js project

67 |

If you want to reference Compost.js properly, you can get it from the NPM repository 68 | by installing the compostjs package. 69 | Just run the following command:

70 |
npm install compostjs
71 | 72 |

Then you can use Compost in your project by importing the two exported 73 | global variables compost (for access to Compost primitives) 74 | and scale (for creating scales) as follows:

75 | 76 |
import { scale as s, compost as c } from "compostjs"
 77 | 
 78 | let d = c.axes("left bottom",
 79 |   c.line([[1,1],[2,4],[3,9],[4,16],[5,25],[6,36] ]))
 80 | c.render("demo", d)
 81 | 
82 | 83 |

To see how this works, you can also look at the complete Node.js sample project 84 | compost-node-demos 85 | on GitHub. To clone the repository and run the demos, you can run the following 86 | commands. Once you do this, you'll see the running demos at 87 | http://localhost:8080. 88 | 89 |

git clone https://github.com/compostjs/compost-node-demos.git
 90 | cd compost-node-demos
 91 | npm install
 92 | npm start
93 | 94 |

Learn more about Compost

95 | 109 | 110 |
111 |
112 | 113 | 114 | 115 | 121 | 122 | 123 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "compostjs", 3 | "version": "0.0.5", 4 | "description": "Composable data visualization library for JavaScript", 5 | "author": "Tomas Petricek", 6 | "license": "MIT", 7 | "scripts": { 8 | "standalone": "webpack --config src/webpack.pub.js && webpack --config src/webpack.latest.js", 9 | "build": "fable-splitter src/compost/compost.fsproj -o dist", 10 | "start": "webpack-dev-server --config src/webpack.dev.js", 11 | "prepublish": "npm run standalone && git add . && git commit -m \"Update standalone release file in docs\" && npm run build", 12 | "release": "np --yolo --no-release-draft" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "git+https://github.com/compostjs/compost.git" 17 | }, 18 | "keywords": [ 19 | "visualization", 20 | "data science", 21 | "charting" 22 | ], 23 | "bugs": { 24 | "url": "https://github.com/compostjs/compost/issues" 25 | }, 26 | "homepage": "https://compostjs.github.io", 27 | "main": "./dist/compost.js", 28 | "dependencies": { 29 | "virtual-dom": "^2.1.1" 30 | }, 31 | "devDependencies": { 32 | "@babel/core": "^7.8.4", 33 | "fable-compiler": "^2.4.15", 34 | "fable-loader": "^2.1.8", 35 | "fable-splitter": "^2.2.0", 36 | "np": "^6.2.4", 37 | "webpack": "^4.41.6", 38 | "webpack-cli": "^3.3.11", 39 | "webpack-dev-server": "^3.10.3" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 29 | 30 | 31 |

DEMO #1

32 |
33 | 34 |

DEMO #2

35 |
36 | 37 |

DEMO #3

38 |
39 | 40 |

DEMO #4

41 |
42 | 43 |

DEMO #5

44 |
45 | 46 |






47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /src/compost/compost.fs: -------------------------------------------------------------------------------- 1 | module main 2 | 3 | open Compost 4 | open Compost.Html 5 | open Browser 6 | 7 | module Helpers = 8 | let formatValue v = 9 | match v with 10 | | CAR(CA c, r) -> box [| box c; box r |] 11 | | COV(CO v) -> box v 12 | 13 | let parseValue v = 14 | if Common.isNumber(v) then COV(CO(unbox v)) 15 | elif Common.isArray(v) then 16 | let a = unbox v 17 | if a.Length <> 2 then failwithf "Cannot parse value: %A. Expected a number or an array with two elements." a 18 | if not (Common.isNumber(a.[1])) then failwithf "Cannot parse value: %A. The second element should be a number." a 19 | CAR(CA (unbox a.[0]), unbox a.[1]) 20 | else failwithf "Cannot parse value: %A. Expected a number or an array with two elements." v 21 | 22 | open Helpers 23 | 24 | type Scale = Scale<1> 25 | type Shape = Shape<1, 1> 26 | 27 | type JsScale = 28 | abstract continuous : float * float -> Scale 29 | abstract categorical : string[] -> Scale 30 | 31 | type JsCompost = 32 | abstract nestX : obj * obj * Shape -> Shape 33 | abstract nestY : obj * obj * Shape -> Shape 34 | abstract nest : obj * obj * obj * obj * Shape -> Shape 35 | abstract scaleX : Scale * Shape -> Shape 36 | abstract scaleY : Scale * Shape -> Shape 37 | abstract scale : Scale * Scale * Shape -> Shape 38 | abstract overlay : Shape[] -> Shape 39 | abstract fillColor : string * Shape -> Shape 40 | abstract strokeColor : string * Shape -> Shape 41 | abstract font : string * string * Shape -> Shape 42 | abstract padding : float * float * float * float * Shape -> Shape 43 | abstract text : obj * obj * string * string * float -> Shape 44 | abstract column : string * float -> Shape 45 | abstract bar : float * string -> Shape 46 | abstract bubble : obj * obj * float * float -> Shape 47 | abstract shape : obj[][] -> Shape 48 | abstract line : obj[][] -> Shape 49 | abstract on : obj * Shape -> Shape 50 | abstract axes : string * Shape -> Shape 51 | abstract render : string * Shape -> unit 52 | abstract svg : float * float * Shape -> DomNode 53 | abstract html : string * obj * DomNode[] -> DomNode 54 | abstract interactive<'e, 's> : string * 's * ('s -> 'e -> 's) * (('e -> unit) -> 's -> Shape) -> unit 55 | 56 | let scale = 57 | { new JsScale with 58 | member x.continuous(lo, hi) = Continuous(CO lo, CO hi) 59 | member x.categorical(cats) = Categorical [| for c in cats -> CA c |] } 60 | 61 | let compost = 62 | { new JsCompost with 63 | member x.scaleX(sc, sh) = Shape.InnerScale(Some(sc), None, sh) 64 | member x.scaleY(sc, sh) = Shape.InnerScale(None, Some(sc), sh) 65 | member x.scale(sx, sy, sh) = Shape.InnerScale(Some(sx), Some(sy), sh) 66 | member x.nestX(lx, hx, s) = Shape.NestX(parseValue lx, parseValue hx, s) 67 | member x.nestY(ly, hy, s) = Shape.NestY(parseValue ly, parseValue hy, s) 68 | member x.nest(lx, hx, ly, hy, s) = Shape.NestY(parseValue ly, parseValue hy, Shape.NestX(parseValue lx, parseValue hx, s)) 69 | member x.overlay(sh) = Shape.Layered(List.ofArray sh) 70 | member x.padding(t, r, b, l, s) = Shape.Padding((t, r, b, l), s) 71 | member x.fillColor(c, s) = Derived.FillColor(c, s) 72 | member x.strokeColor(c, s) = Derived.StrokeColor(c, s) 73 | member x.font(f, c, s) = Derived.Font(f, c, s) 74 | member x.column(xp, yp) = Derived.Column(CA xp, CO yp) 75 | member x.bar(xp, yp) = Derived.Bar(CO xp, CA yp) 76 | member x.bubble(xp, yp, w, h) = Shape.Bubble(parseValue xp, parseValue yp, w, h) 77 | member x.text(xp, yp, t, s, r) = 78 | let r = if box r = null then 0.0 else r 79 | let s = if box s = null then "" else s 80 | let va = if s.Contains("baseline") then Baseline elif s.Contains("hanging") then Hanging else Middle 81 | let ha = if s.Contains("start") then Start elif s.Contains("end") then End else Center 82 | Shape.Text(parseValue xp, parseValue yp, va, ha, r, t) 83 | member x.shape(a) = Shape.Shape [ for p in a -> parseValue p.[0], parseValue p.[1] ] 84 | member x.line(a) = Shape.Line [ for p in a -> parseValue p.[0], parseValue p.[1] ] 85 | member x.axes(a, s) = Shape.Axes(a.Contains("top"), a.Contains("right"), a.Contains("bottom"), a.Contains("left"), s) 86 | member x.on(o, s) = 87 | Shape.Interactive 88 | ([ for k in Common.keys(o) -> 89 | let f = Common.apply (Common.getProperty o k) 90 | match k with 91 | | "mousedown" -> MouseDown(fun me (x, y) -> f [| box (formatValue x); box (formatValue y); box me |]) 92 | | "mouseup" -> MouseUp(fun me (x, y) -> f [| box (formatValue x); box (formatValue y); box me |]) 93 | | "mousemove" -> MouseMove(fun me (x, y) -> f [| box (formatValue x); box (formatValue y); box me |]) 94 | | "touchstart" -> TouchStart(fun me (x, y) -> f [| box (formatValue x); box (formatValue y); box me |]) 95 | | "touchmove" -> TouchMove(fun me (x, y) -> f [| box (formatValue x); box (formatValue y); box me |]) 96 | | "click" -> Click(fun me (x, y) -> f [| box (formatValue x); box (formatValue y); box me |]) 97 | | "mouseleave" -> MouseLeave(fun me -> f [| me |]) 98 | | "touchend" -> TouchEnd(fun me -> f [| me |]) 99 | | s -> failwithf "Unsupported event type '%s' passed to the 'on' primitive." s 100 | ], s) 101 | member x.svg(w, h, shape) = 102 | Compost.createSvg false false (w, h) shape 103 | member x.html(tag, attrs, children) = 104 | let attrs = 105 | [| for a in Common.keys(attrs) -> 106 | let p = Common.getProperty attrs a 107 | if (Common.typeOf p = "function") then 108 | a, DomAttribute.Event(fun e h -> 109 | Common.apply p [| box e; box h |]) 110 | else 111 | a, DomAttribute.Attribute(unbox p) |] 112 | let children = children |> Array.map (fun c -> 113 | if Common.typeOf c = "string" then DomNode.Text(unbox c) 114 | else c ) 115 | DomNode.Element(null, tag, attrs, children) 116 | member x.interactive(id, init, update, render) = 117 | let render t s = 118 | let el = document.getElementById(id) 119 | let res = render t s 120 | if Common.getProperty res "constructor" = Common.getProperty (DomNode.Text "") "constructor" then 121 | unbox res 122 | else 123 | Compost.createSvg false false (el.clientWidth, el.clientHeight) res 124 | Html.createVirtualDomApp id init render update 125 | member x.render(id, viz) = 126 | let el = document.getElementById(id) 127 | let svg = Compost.createSvg false false (el.clientWidth, el.clientHeight) viz 128 | svg |> Html.renderTo el } -------------------------------------------------------------------------------- /src/compost/compost.fsproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | netstandard2.0 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/compost/core.fs: -------------------------------------------------------------------------------- 1 | namespace Compost 2 | 3 | open Compost.Html 4 | open Browser.Types 5 | 6 | // ------------------------------------------------------------------------------------------------ 7 | // Domain that users see 8 | // ------------------------------------------------------------------------------------------------ 9 | 10 | type Color = 11 | | RGB of int * int * int 12 | | HTML of string 13 | 14 | type AlphaColor = float * Color 15 | type Width = Pixels of int 16 | type GradientStop = float * AlphaColor 17 | 18 | type FillStyle = 19 | | Solid of AlphaColor 20 | | LinearGradient of seq 21 | 22 | type Number = 23 | | Integer of int 24 | | Percentage of float 25 | 26 | type HorizontalAlign = Start | Center | End 27 | type VerticalAlign = Baseline | Middle | Hanging 28 | 29 | type continuous<[] 'u> = CO of float<'u> 30 | type categorical<[] 'u> = CA of string 31 | 32 | type Value<[] 'u> = 33 | | CAR of categorical<'u> * float 34 | | COV of continuous<'u> 35 | 36 | type Scale<[] 'v> = 37 | | Continuous of continuous<'v> * continuous<'v> 38 | | Categorical of categorical<'v>[] 39 | 40 | type Style = 41 | { StrokeColor : AlphaColor 42 | StrokeWidth : Width 43 | StrokeDashArray : seq 44 | Fill : FillStyle 45 | Animation : option Style)> 46 | Font : string 47 | Cursor : string 48 | FormatAxisXLabel : Scale<1> -> Value<1> -> string 49 | FormatAxisYLabel : Scale<1> -> Value<1> -> string } 50 | 51 | type EventHandler<[] 'vx, [] 'vy> = 52 | | MouseMove of (MouseEvent -> (Value<'vx> * Value<'vy>) -> unit) 53 | | MouseUp of (MouseEvent -> (Value<'vx> * Value<'vy>) -> unit) 54 | | MouseDown of (MouseEvent -> (Value<'vx> * Value<'vy>) -> unit) 55 | | Click of (MouseEvent -> (Value<'vx> * Value<'vy>) -> unit) 56 | | TouchStart of (TouchEvent -> (Value<'vx> * Value<'vy>) -> unit) 57 | | TouchMove of (TouchEvent -> (Value<'vx> * Value<'vy>) -> unit) 58 | | TouchEnd of (TouchEvent -> unit) 59 | | MouseLeave of (MouseEvent -> unit) 60 | 61 | type Orientation = 62 | | Vertical 63 | | Horizontal 64 | 65 | type Shape<[] 'vx, [] 'vy> = 66 | | Style of (Style -> Style) * Shape<'vx, 'vy> 67 | | Text of Value<'vx> * Value<'vy> * VerticalAlign * HorizontalAlign * float * string 68 | | AutoScale of bool * bool * Shape<'vx, 'vy> 69 | | InnerScale of option> * option> * Shape<'vx, 'vy> 70 | | NestX of Value<'vx> * Value<'vx> * Shape<'vx, 'vy> 71 | | NestY of Value<'vy> * Value<'vy> * Shape<'vx, 'vy> 72 | | Line of seq * Value<'vy>> 73 | | Bubble of Value<'vx> * Value<'vy> * float * float 74 | | Shape of seq * Value<'vy>> 75 | //| Stack of Orientation * seq> 76 | | Layered of seq> 77 | | Axes of bool * bool * bool * bool * Shape<'vx, 'vy> 78 | | Interactive of seq> * Shape<'vx, 'vy> 79 | | Padding of (float * float * float * float) * Shape<'vx, 'vy> 80 | | Offset of (float * float) * Shape<'vx, 'vy> 81 | 82 | // ------------------------------------------------------------------------------------------------ 83 | // SVG stuff 84 | // ------------------------------------------------------------------------------------------------ 85 | 86 | module Svg = 87 | 88 | type StringBuilder() = 89 | let mutable strs = [] 90 | member x.Append(s) = strs <- s::strs 91 | override x.ToString() = String.concat "" (List.rev strs) 92 | 93 | type PathSegment = 94 | | MoveTo of (float * float) 95 | | LineTo of (float * float) 96 | 97 | type SvgStyle = string 98 | 99 | type Svg = 100 | | Path of PathSegment[] * SvgStyle 101 | | Ellipse of (float * float) * (float * float) * SvgStyle 102 | | Rect of (float * float) * (float * float) * SvgStyle 103 | | Text of (float * float) * string * float * SvgStyle 104 | | Combine of Svg[] 105 | | Empty 106 | 107 | let rec mapSvg f = function 108 | | Combine svgs -> Combine(Array.map (mapSvg f) svgs) 109 | | svg -> f svg 110 | 111 | let formatPath path = 112 | let sb = StringBuilder() 113 | for ps in path do 114 | match ps with 115 | | MoveTo(x, y) -> sb.Append("M" + string x + " " + string y + " ") 116 | | LineTo(x, y) -> sb.Append("L" + string x + " " + string y + " ") 117 | sb.ToString() 118 | 119 | type RenderingContext = 120 | { Definitions : ResizeArray } 121 | 122 | let rec renderSvg ctx svg = seq { 123 | match svg with 124 | | Empty -> () 125 | | Text((x,y), t, rotation, style) -> 126 | let attrs = 127 | [ yield "style" => style 128 | if rotation = 0.0 then 129 | yield "x" => string x 130 | yield "y" => string y 131 | else 132 | yield "x" => "0" 133 | yield "y" => "0" 134 | yield "transform" => sprintf "translate(%f,%f) rotate(%f)" x y rotation ] 135 | yield s?text attrs [ text t ] 136 | 137 | | Combine ss -> 138 | for s in ss do yield! renderSvg ctx s 139 | 140 | | Ellipse((cx, cy),(rx, ry), style) -> 141 | let attrs = 142 | [ "cx" => string cx; "cy" => string cy; 143 | "rx" => string rx; "ry" => string ry; "style" => style ] 144 | yield s?ellipse attrs [] 145 | 146 | | Rect((x1, y1),(x2, y2), style) -> 147 | let l, t = min x1 x2, min y1 y2 148 | let w, h = abs (x1 - x2), abs (y1 - y2) 149 | let attrs = 150 | [ "x" => string l; "y" => string t; "width" => string w; 151 | "height" => string h; "style" => style ] 152 | yield s?rect attrs [] 153 | 154 | | Path(p, style) -> 155 | let attrs = [ "d" => formatPath p; "style" => style ] 156 | yield s?path attrs [] } 157 | 158 | let formatColor = function 159 | | RGB(r,g,b) -> sprintf "rgb(%d, %d, %d)" r g b 160 | | HTML(clr) -> clr 161 | 162 | let formatNumber = function 163 | | Integer n -> string n 164 | | Percentage p -> string p + "%" 165 | 166 | let rec formatStyle (defs:ResizeArray<_>) style = 167 | let style, anim = 168 | match style.Animation with 169 | | Some (ms, ease, anim) -> 170 | let id = "anim_" + System.Guid.NewGuid().ToString().Replace("-", "") 171 | let fromstyle = formatStyle defs { style with Animation = None } 172 | let tostyle = formatStyle defs { anim style with Animation = None } 173 | h?style [] [ text (sprintf "@keyframes %s { from { %s } to { %s } }" id fromstyle tostyle) ] |> defs.Add 174 | anim style, sprintf "animation: %s %dms %s; " id ms ease 175 | | None -> style, "" 176 | 177 | anim + 178 | ( String.concat "" [ for c in style.Cursor.Split(',') -> "cursor:" + c + ";" ] ) + 179 | ( "font:" + style.Font + ";" ) + 180 | ( let (so, clr) = style.StrokeColor 181 | let (Pixels sw) = style.StrokeWidth 182 | sprintf "stroke-opacity:%f; stroke-width:%dpx; stroke:%s; " so sw (formatColor clr) ) + 183 | ( if Seq.isEmpty style.StrokeDashArray then "" 184 | else "stroke-dasharray:" + String.concat "," (Seq.map formatNumber style.StrokeDashArray) + ";" ) + 185 | ( match style.Fill with 186 | | LinearGradient(points) -> 187 | let id = "gradient_" + System.Guid.NewGuid().ToString().Replace("-", "") 188 | s?linearGradient ["id"=>id] 189 | [ for pt, (o, clr) in points -> 190 | s?stop ["offset"=> string pt + "%"; "stop-color" => formatColor clr; "stop-opacity" => string o ] [] ] 191 | |> defs.Add 192 | sprintf "fill:url(#%s)" id 193 | | Solid(fo, clr) -> 194 | sprintf "fill-opacity:%f; fill:%s; " fo (formatColor clr) ) 195 | 196 | // ------------------------------------------------------------------------------------------------ 197 | // Calculating scales 198 | // ------------------------------------------------------------------------------------------------ 199 | 200 | module Scales = 201 | 202 | type ScaledShape<[] 'vx, [] 'vy> = 203 | | ScaledStyle of (Style -> Style) * ScaledShape<'vx, 'vy> 204 | | ScaledText of Value<'vx> * Value<'vy> * VerticalAlign * HorizontalAlign * float * string 205 | | ScaledLine of (Value<'vx> * Value<'vy>)[] 206 | | ScaledBubble of Value<'vx> * Value<'vy> * float * float 207 | | ScaledShape of (Value<'vx> * Value<'vy>)[] 208 | | ScaledLayered of ScaledShape<'vx, 'vy>[] 209 | | ScaledInteractive of seq> * Scale<'vx> * Scale<'vy> * ScaledShape<'vx, 'vy> 210 | | ScaledPadding of (float * float * float * float) * Scale<'vx> * Scale<'vy> * ScaledShape<'vx, 'vy> 211 | | ScaledOffset of (float * float) * ScaledShape<'vx, 'vy> 212 | 213 | | ScaledNestX of Value<'vx> * Value<'vx> * Scale<'vx> * ScaledShape<'vx, 'vy> 214 | | ScaledNestY of Value<'vy> * Value<'vy> * Scale<'vy> * ScaledShape<'vx, 'vy> 215 | 216 | let getExtremes = function 217 | | Continuous(l, h) -> COV l, COV h 218 | | Categorical(vals) -> CAR(vals.[0], 0.0), CAR(vals.[vals.Length-1], 1.0) 219 | 220 | /// Given a range, return a new aligned range together with the magnitude 221 | let calculateMagnitudeAndRange (lo:float, hi:float) = 222 | let magnitude = 10. ** round (log10 (hi - lo)) 223 | let magnitude = magnitude / 2. 224 | magnitude, (floor (lo / magnitude) * magnitude, ceil (hi / magnitude) * magnitude) 225 | 226 | /// Get number of decimal points to show for the given range 227 | let decimalPoints range = 228 | let magnitude, _ = calculateMagnitudeAndRange range 229 | max 0. (ceil (-(log10 magnitude))) 230 | 231 | /// Extend the given range to a nicely adjusted size 232 | let adjustRange range = snd (calculateMagnitudeAndRange range) 233 | let adjustRangeUnits (l:float<'u>,h:float<'u>) : float<'u> * float<'u> = 234 | let l, h = adjustRange (unbox l, unbox h) in unbox l, unbox h 235 | 236 | let toArray s = Array.ofSeq s // REVIEW: Hack to avoid Float64Array (which behaves oddly in Safari) see https://github.com/zloirock/core-js/issues/285 237 | 238 | /// Generate points for a grid. Count specifies how many points to generate 239 | /// (this is minimm - the result will be up to 5x more). 240 | let generateSteps count k (lo, hi) = 241 | let magnitude, (nlo, nhi) = calculateMagnitudeAndRange (lo, hi) 242 | let dividers = [0.2; 0.5; 1.; 2.; 5.; 10.; 20.; 40.; 50.; 60.; 80.; 100.] 243 | let magnitudes = dividers |> Seq.map (fun d -> magnitude / d) 244 | let step = magnitudes |> Seq.filter (fun m -> (hi - lo) / m >= count) |> Seq.tryHead 245 | let step = defaultArg step (magnitude / 100.) 246 | seq { for v in nlo .. step * k .. nhi do 247 | if v >= lo && v <= hi then yield v } |> toArray 248 | 249 | let generateAxisSteps s = 250 | match s with 251 | | Continuous(CO l, CO h) -> 252 | generateSteps 6. 1. (float l, float h) |> Array.map (fun f -> COV(CO (unbox f))) 253 | | Categorical vs -> [| for CA s in vs -> CAR(CA s, 0.5) |] 254 | 255 | let generateAxisLabels fmt (s:Scale<'v>) : (Value<'v> * string)[] = 256 | let sunit = unbox> s 257 | match s with 258 | | Continuous(CO l, CO h) -> 259 | generateSteps 6. 2. (float l, float h) 260 | |> Array.map (fun f -> COV(CO (unbox f)), fmt sunit (COV(CO(unbox> f)))) 261 | | Categorical vs -> [| for v & CA s in vs -> CAR(CA s, 0.5), fmt sunit (CAR(CA s, 0.5)) |] 262 | 263 | let unionScales s1 s2 = 264 | match s1, s2 with 265 | | Continuous(l1, h1), Continuous(l2, h2) -> Continuous(min l1 l2, max h1 h2) 266 | | Categorical(v1), Categorical(v2) -> Categorical(Array.distinct (Array.append v1 v2)) 267 | | _ -> 268 | failwith "Cannot union continuous with categorical" 269 | 270 | let calculateShapeScale vals = 271 | let scales = 272 | vals |> Array.fold (fun state value -> 273 | match state, value with 274 | | Choice1Of3(), COV(CO v) -> Choice2Of3([v]) 275 | | Choice2Of3(vs), COV(CO v) -> Choice2Of3(v::vs) 276 | | Choice1Of3(), CAR(CA x, _) -> Choice3Of3([x]) 277 | | Choice3Of3(xs), CAR(CA x, _) -> Choice3Of3(x::xs) 278 | | _ -> failwith "Values with mismatching scales") (Choice1Of3()) 279 | match scales with 280 | | Choice1Of3() -> failwith "No values for calculating a scale" 281 | | Choice2Of3(vs) -> Continuous (CO (List.min vs), CO (List.max vs)) 282 | | Choice3Of3(xs) -> Categorical (Array.distinct [| for x in List.rev xs -> CA x |]) 283 | 284 | let calculateShapeScales points = 285 | let xs = points |> Array.map fst 286 | let ys = points |> Array.map snd 287 | calculateShapeScale xs, calculateShapeScale ys 288 | 289 | // Always returns objects with the same inner and outer scales 290 | // but outer scales can be replaced later by replaceScales 291 | let rec calculateScales<[] 'ux, [] 'uy> style (shape:Shape<'ux, 'uy>) = 292 | let calculateScalesStyle = calculateScales 293 | let calculateScales = calculateScales style 294 | match shape with 295 | | Style(f, shape) -> 296 | let scales, shape = calculateScalesStyle (f style) shape 297 | scales, ScaledStyle(f, shape) 298 | 299 | | NestX(nx1, nx2, shape) -> 300 | let (isx, isy), shape = calculateScales shape 301 | (calculateShapeScale [| nx1; nx2 |], isy), ScaledNestX(nx1, nx2, isx, shape) 302 | 303 | | NestY(ny1, ny2, shape) -> 304 | let (isx, isy), shape = calculateScales shape 305 | (isx, calculateShapeScale [| ny1; ny2 |]), ScaledNestY(ny1, ny2, isy, shape) 306 | 307 | | InnerScale(sx, sy, shape) -> 308 | let (isx, isy), shape = calculateScales shape 309 | let sx = match sx with Some sx -> sx | _ -> isx // TODO: check that 'sx' is compatible with 'isx' 310 | let sy = match sy with Some sy -> sy | _ -> isy // TODO: check that 'sy' is compatible with 'isy' 311 | (sx, sy), shape 312 | 313 | | AutoScale(ax, ay, shape) -> 314 | let (isx, isy), shape = calculateScales shape 315 | let autoScale = function 316 | | Continuous(CO l, CO h) -> let l, h = adjustRangeUnits (l, h) in Continuous(CO l, CO h) 317 | | scale -> scale 318 | let scales = 319 | ( if ax then autoScale isx else isx ), 320 | ( if ay then autoScale isy else isy ) 321 | scales, shape 322 | 323 | | Offset(offs, shape) -> 324 | let scales, shape = calculateScales shape 325 | scales, ScaledOffset(offs, shape) 326 | 327 | | Padding(pads, shape) -> 328 | let (sx, sy), shape = calculateScales shape 329 | (sx, sy), ScaledPadding(pads, sx, sy, shape) 330 | 331 | | Bubble(x, y, rx, ry) -> 332 | let makeSingletonScale = function COV(v) -> Continuous(v, v) | CAR(v, _) -> Categorical [| v |] 333 | let scales = makeSingletonScale x, makeSingletonScale y 334 | scales, ScaledBubble(x, y, rx, ry) 335 | 336 | | Shape.Text(x, y, va, ha, r, t) -> 337 | let makeSingletonScale = function COV(v) -> Continuous(v, v) | CAR(v, _) -> Categorical [| v |] 338 | let scales = makeSingletonScale x, makeSingletonScale y 339 | scales, ScaledText(x, y, va, ha, r, t) 340 | 341 | | Line line -> 342 | let line = Seq.toArray line 343 | let scales = calculateShapeScales line 344 | scales, ScaledLine(line) 345 | 346 | | Shape points -> 347 | let points = Seq.toArray points 348 | let scales = calculateShapeScales points 349 | scales, ScaledShape(points) 350 | 351 | | Axes(showTop, showRight, showBottom, showLeft, shape) -> 352 | 353 | let (origScales & (sx, sy)), _ = calculateScales shape 354 | let (lx, hx), (ly, hy) = getExtremes sx, getExtremes sy 355 | 356 | let LineStyle clr alpha width shape = 357 | Style((fun s -> { s with Fill = Solid(1.0, HTML "transparent"); StrokeWidth = Pixels width; StrokeColor=alpha, HTML clr }), shape) 358 | let FontStyle style shape = 359 | Style((fun s -> { s with Font = style; Fill = Solid(1.0, HTML "black"); StrokeColor = 0.0, HTML "transparent" }), shape) 360 | 361 | let shape = 362 | Layered [ 363 | yield InnerScale(Some sx, Some sy, Layered [ 364 | for x in generateAxisSteps sx do 365 | yield Line [x,ly; x,hy] |> LineStyle "#e4e4e4" 1.0 1 366 | for y in generateAxisSteps sy do 367 | yield Line [lx,y; hx,y] |> LineStyle "#e4e4e4" 1.0 1 ]) 368 | if showTop then 369 | yield Line [lx,hy; hx,hy] |> LineStyle "black" 1.0 2 370 | for x, l in generateAxisLabels style.FormatAxisXLabel sx do 371 | yield Offset((0., -10.), Text(x, hy, VerticalAlign.Baseline, HorizontalAlign.Center, 0.0, l)) |> FontStyle "9pt sans-serif" 372 | if showRight then 373 | yield Line [hx,hy; hx,ly] |> LineStyle "black" 1.0 2 374 | for y, l in generateAxisLabels style.FormatAxisYLabel sy do 375 | yield Offset((10., 0.), Text(hx, y, VerticalAlign.Middle, HorizontalAlign.Start, 0.0, l)) |> FontStyle "9pt sans-serif" 376 | if showBottom then 377 | yield Line [lx,ly; hx,ly] |> LineStyle "black" 1.0 2 378 | for x, l in generateAxisLabels style.FormatAxisXLabel sx do 379 | yield Offset((0., 10.), Text(x, ly, VerticalAlign.Hanging, HorizontalAlign.Center, 0.0, l)) |> FontStyle "9pt sans-serif" 380 | if showLeft then 381 | yield Line [lx,hy; lx,ly] |> LineStyle "black" 1.0 2 382 | for y, l in generateAxisLabels style.FormatAxisYLabel sy do 383 | yield Offset((-10., 0.), Text(lx, y, VerticalAlign.Middle, HorizontalAlign.End, 0.0, l)) |> FontStyle "9pt sans-serif" 384 | yield shape ] 385 | 386 | let padding = 387 | (if showTop then 30. else 0.), (if showRight then 50. else 0.), 388 | (if showBottom then 30. else 0.), (if showLeft then 50. else 0.) 389 | calculateScales (Padding(padding, shape)) 390 | 391 | | Layered shapes -> 392 | let shapes = shapes |> Array.ofSeq 393 | let scaled = shapes |> Array.map calculateScales 394 | let sxs = scaled |> Array.map (fun ((sx, _), _) -> sx) 395 | let sys = scaled |> Array.map (fun ((_, sy), _) -> sy) 396 | let scales = (Array.reduce unionScales sxs, Array.reduce unionScales sys) 397 | scales, ScaledLayered (Array.map snd scaled) 398 | 399 | | Interactive(f, shape) -> 400 | let scales, shape = calculateScales shape 401 | scales, ScaledInteractive(f, fst scales, snd scales, shape) 402 | 403 | 404 | // ------------------------------------------------------------------------------------------------ 405 | // Projections 406 | // ------------------------------------------------------------------------------------------------ 407 | 408 | module Projections = 409 | let projectOne reversed (tlv:float<_>, thv:float<_>) scale coord = 410 | match scale, coord with 411 | | Categorical(vals), (CAR(CA v,f)) -> 412 | let size = (thv - tlv) / float vals.Length 413 | let i = vals |> Array.findIndex (fun (CA vv) -> v = vv) 414 | let i = float i + f 415 | if reversed then thv - (i * size) else tlv + (i * size) 416 | | Continuous(CO slv, CO shv), (COV (CO v)) -> 417 | if reversed then thv - (v - slv) / (shv - slv) * (thv - tlv) 418 | else tlv + (v - slv) / (shv - slv) * (thv - tlv) 419 | | Categorical _, COV _ -> failwithf "Cannot project continuous value (%A) on a categorical scale (%A)." coord scale 420 | | Continuous _, CAR _ -> failwithf "Cannot project categorical value (%A) on a continuous scale (%A)." coord scale 421 | 422 | let projectOneX a = projectOne false a 423 | let projectOneY a = projectOne true a 424 | 425 | let projectInvOne reversed (l:float, h:float) s (v:float) = 426 | match s with 427 | | Continuous(CO slv, CO shv) -> 428 | if reversed then COV(CO (shv - (v - l) / (h - l) * (shv - slv))) 429 | else COV(CO (slv + (v - l) / (h - l) * (shv - slv))) 430 | 431 | | Categorical(cats) -> 432 | let size = (h - l) / float cats.Length 433 | let i = if reversed then floor ((h - v) / size) else floor ((v - l) / size) 434 | let f = if reversed then ((h - v) / size) - i else ((v - l) / size) - i 435 | let i = if size < 0. then (float cats.Length) + i else i // Negative when thv < tlv 436 | if int i < 0 || int i >= cats.Length then CAR(CA "", f) 437 | else CAR(cats.[int i], f) 438 | 439 | let projectInv (x1, y1, x2, y2) (sx, sy) (x, y) = 440 | projectInvOne false (x1, x2) sx x, 441 | projectInvOne true (y1, y2) sy y 442 | 443 | // ------------------------------------------------------------------------------------------------ 444 | // Drawing 445 | // ------------------------------------------------------------------------------------------------ 446 | 447 | 448 | module Drawing = 449 | open Svg 450 | open Scales 451 | open Projections 452 | 453 | type DrawingContext = 454 | { Style : Style 455 | Definitions : ResizeArray } 456 | 457 | let rec hideFill style = 458 | { style with Fill = Solid(0.0, RGB(0, 0, 0)); Animation = match style.Animation with Some(n,e,f) -> Some(n,e,f >> hideFill) | _ -> None } 459 | let rec hideStroke style = 460 | { style with StrokeColor = (0.0, snd style.StrokeColor); Animation = match style.Animation with Some(n,e,f) -> Some(n,e,f >> hideStroke) | _ -> None } 461 | 462 | let rec drawShape ctx ((x1, y1, x2, y2) as area) ((sx, sy) as scales) (shape:ScaledShape<'ux, 'uy>) = 463 | 464 | let project (vx, vy) = 465 | projectOneX (x1, x2) sx vx, projectOneY (y1, y2) sy vy 466 | 467 | match shape with 468 | | ScaledNestX(p1, p2, isx, shape) -> 469 | let x1' = projectOneX (x1, x2) sx p1 470 | let x2' = projectOneX (x1, x2) sx p2 471 | //let x1', x2' = if x2 < x1 then max x1' x2', min x1' x2' else min x1' x2', max x1' x2' 472 | drawShape ctx (x1', y1, x2', y2) (isx, sy) shape 473 | 474 | | ScaledNestY(p1, p2, isy, shape) -> 475 | let y1' = projectOneY (y1, y2) sy p1 476 | let y2' = projectOneY (y1, y2) sy p2 477 | //let y1', y2' = if y2 < y1 then min y1' y2', max y1' y2' else max y1' y2', min y1' y2' 478 | drawShape ctx (x1, y1', x2, y2') (sx, isy) shape 479 | 480 | | ScaledOffset((dx, dy), shape) -> 481 | drawShape ctx (x1 + dx, y1 + dy, x2 + dx, y2 + dy) scales shape 482 | 483 | | ScaledLayered shapes -> 484 | Combine(Array.map (drawShape ctx area scales) shapes) 485 | 486 | | ScaledStyle(f, shape) -> 487 | drawShape { ctx with Style = f ctx.Style } area scales shape 488 | 489 | | ScaledShape(points) -> 490 | let path = 491 | [| yield MoveTo(project (points.[0])) 492 | for pt in Seq.skip 1 points do yield LineTo(project pt) 493 | yield LineTo(project (points.[0])) |] 494 | Path(path, formatStyle ctx.Definitions (hideStroke ctx.Style)) 495 | 496 | | ScaledPadding((t, r, b, l), isx, isy, shape) -> 497 | let calculateNestedRange rev (v1, v2) ins outs = 498 | match ins with 499 | | Continuous(CO l, CO h) -> 500 | let pos = 501 | [ projectOne rev (v1, v2) outs (COV (CO l)) 502 | projectOne rev (v1, v2) outs (COV (CO h)) ] 503 | Seq.min pos, Seq.max pos 504 | | Categorical(vals) -> 505 | let pos = vals |> Seq.collect (fun v -> 506 | [ projectOne rev (v1, v2) outs (CAR(v, 0.0)) 507 | projectOne rev (v1, v2) outs (CAR(v, 1.0)) ]) 508 | Seq.min pos, Seq.max pos 509 | //|> fun rs -> printfn "calculateNestedRange %A %A %A = %A" (v1, v2) ins outs rs; rs 510 | 511 | let x1', x2' = calculateNestedRange false (x1, x2) isx sx 512 | let y1', y2' = calculateNestedRange true (y1, y2) isy sy 513 | //printfn "PADDING: %A\nAXES: %A\nAREA: %A\nNEW AREA: %A\nAFTER PAD: %A\n\n" (t, r, b, l) (isx, isy) area (x1', y1', x2', y2') (x1' + l, y1' + t, x2' - r, y2' - b) 514 | drawShape ctx (x1'+l, y1'+t, x2'-r, y2'-b) (isx, isy) shape 515 | 516 | | ScaledLine line -> 517 | let path = 518 | [ yield MoveTo(project (Seq.head line)) 519 | for pt in Seq.skip 1 line do yield LineTo (project pt) ] 520 | |> Array.ofList 521 | Path(path, formatStyle ctx.Definitions (hideFill ctx.Style)) 522 | 523 | | ScaledText(x, y, va, ha, r, t) -> 524 | let va = match va with Baseline -> "baseline" | Hanging -> "hanging" | Middle -> "middle" 525 | let ha = match ha with Start -> "start" | Center -> "middle" | End -> "end" 526 | let xy = project (x, y) 527 | Text(xy, t, r, sprintf "alignment-baseline:%s; text-anchor:%s;" va ha + formatStyle ctx.Definitions ctx.Style) 528 | 529 | | ScaledBubble(x, y, rx, ry) -> 530 | Ellipse(project (x, y), (rx, ry), formatStyle ctx.Definitions ctx.Style) 531 | 532 | | ScaledInteractive(f, _, _, shape) -> 533 | drawShape ctx area scales shape 534 | 535 | 536 | // ------------------------------------------------------------------------------------------------ 537 | // Event handling 538 | // ------------------------------------------------------------------------------------------------ 539 | 540 | module Events = 541 | open Scales 542 | open Projections 543 | 544 | type MouseEventKind = Click | Move | Up | Down 545 | type TouchEventKind = Move | Start 546 | 547 | type InteractiveEvent<[] 'vx, [] 'vy> = 548 | | MouseEvent of MouseEventKind * (Value<'vx> * Value<'vy>) 549 | | TouchEvent of TouchEventKind * (Value<'vx> * Value<'vy>) 550 | | TouchEnd 551 | | MouseLeave 552 | 553 | let projectEvent area scales event = 554 | match event with 555 | | MouseEvent(kind, (COV (CO x), COV (CO y))) -> MouseEvent(kind, projectInv area scales (x, y)) 556 | | TouchEvent(kind, (COV (CO x), COV (CO y))) -> TouchEvent(kind, projectInv area scales (x, y)) 557 | | MouseEvent _ 558 | | TouchEvent _ -> failwith "TODO: projectEvent - not continuous" 559 | | TouchEnd -> TouchEnd 560 | | MouseLeave -> MouseLeave 561 | 562 | let inScale s v = 563 | match s, v with 564 | | Continuous(CO l, CO h), COV(CO v) -> v >= min l h && v <= max l h 565 | | Categorical(cats), CAR(v, _) -> cats |> Seq.exists ((=) v) 566 | | Continuous _, CAR _ -> failwith "inScale: Cannot test if categorical value is in continuous scale" 567 | | Categorical _, COV _ -> failwith "inScale: Cannot test if continuous value is in categorical scale" 568 | 569 | let inScales (sx, sy) event = 570 | match event with 571 | | MouseLeave -> true 572 | | TouchEnd -> true 573 | | MouseEvent(_, (x, y)) 574 | | TouchEvent(_, (x, y)) -> inScale sx x && inScale sy y 575 | 576 | let rec triggerEvent<[] 'ux, [] 'uy> 577 | ((x1, y1, x2, y2) as area) ((sx, sy) as scales) (shape:ScaledShape<'ux, 'uy>) 578 | (jse:Event) (event:InteractiveEvent<1,1>) = 579 | match shape with 580 | | ScaledLine _ 581 | | ScaledText _ 582 | | ScaledBubble _ 583 | | ScaledShape _ -> () 584 | | ScaledStyle(_, shape) -> triggerEvent area scales shape jse event 585 | | ScaledOffset((dx, dy), shape) -> 586 | triggerEvent (x1 + dx, y1 + dy, x2 + dx, y2 + dy) scales shape jse event 587 | | ScaledNestX(p1, p2, isx, shape) -> 588 | let x1' = projectOneX (x1, x2) sx p1 589 | let x2' = projectOneX (x1, x2) sx p2 590 | //let x1', x2' = if x2 < x1 then max x1' x2', min x1' x2' else min x1' x2', max x1' x2' 591 | triggerEvent (x1', y1, x2', y2) (isx, sy) shape jse event 592 | 593 | | ScaledNestY(p1, p2, isy, shape) -> 594 | let y1' = projectOneY (y1, y2) sy p1 595 | let y2' = projectOneY (y1, y2) sy p2 596 | //let y1', y2' = if y2 < y1 then min y1' y2', max y1' y2' else max y1' y2', min y1' y2' 597 | triggerEvent (x1, y1', x2, y2') (sx, isy) shape jse event 598 | | ScaledPadding((t, r, b, l), isx, isy, shape) -> 599 | let calculateNestedRange rev (v1, v2) ins outs = 600 | match ins with 601 | | Continuous(CO l, CO h) -> 602 | let pos = 603 | [ projectOne rev (v1, v2) outs (COV (CO l)) 604 | projectOne rev (v1, v2) outs (COV (CO h)) ] 605 | Seq.min pos, Seq.max pos 606 | | Categorical(vals) -> 607 | let pos = vals |> Seq.collect (fun v -> 608 | [ projectOne rev (v1, v2) outs (CAR(v, 0.0)) 609 | projectOne rev (v1, v2) outs (CAR(v, 1.0)) ]) 610 | Seq.min pos, Seq.max pos 611 | //|> fun rs -> printfn "calculateNestedRange %A %A %A = %A" (v1, v2) ins outs rs; rs 612 | 613 | let x1', x2' = calculateNestedRange false (x1, x2) isx sx 614 | let y1', y2' = calculateNestedRange true (y1, y2) isy sy 615 | //printfn "PADDING: %A\nAXES: %A\nAREA: %A\nNEW AREA: %A\nAFTER PAD: %A\n\n" (t, r, b, l) (isx, isy) area (x1', y1', x2', y2') (x1' + l, y1' + t, x2' - r, y2' - b) 616 | triggerEvent (x1' + l, y1' + t, x2' - r, y2' - b) (isx, isy) shape jse event 617 | 618 | | ScaledLayered shapes -> for shape in shapes do triggerEvent area scales shape jse event 619 | | ScaledInteractive(handlers, sx, sy, shape) -> 620 | let localEvent = projectEvent area scales event 621 | if inScales scales localEvent then 622 | for handler in handlers do 623 | match localEvent, handler with 624 | | MouseEvent(MouseEventKind.Click, pt), EventHandler.Click(f) 625 | | MouseEvent(MouseEventKind.Move, pt), MouseMove(f) 626 | | MouseEvent(MouseEventKind.Up, pt), MouseUp(f) 627 | | MouseEvent(MouseEventKind.Down, pt), MouseDown(f) -> 628 | if jse <> null then jse.preventDefault() 629 | f (unbox jse) pt 630 | | TouchEvent(TouchEventKind.Move, pt), TouchMove(f) 631 | | TouchEvent(TouchEventKind.Start, pt), TouchStart(f) -> 632 | if jse <> null then jse.preventDefault() 633 | f (unbox jse) pt 634 | | TouchEnd, EventHandler.TouchEnd f -> f (unbox jse) 635 | | MouseLeave, EventHandler.MouseLeave f -> f (unbox jse) 636 | | MouseLeave, _ 637 | | TouchEnd, _ 638 | | TouchEvent(_, _), _ 639 | | MouseEvent(_, _), _ -> () 640 | triggerEvent area scales shape jse event 641 | 642 | 643 | // ------------------------------------------------------------------------------------------------ 644 | // Derived 645 | // ------------------------------------------------------------------------------------------------ 646 | 647 | module Derived = 648 | let StrokeColor(clr, s) = 649 | Shape.Style((fun s -> { s with StrokeColor = (1.0, HTML clr) }), s) 650 | 651 | let FillColor(clr, s) = 652 | Shape.Style((fun s -> { s with Fill = Solid(1.0, HTML clr) }), s) 653 | 654 | let Font(font, clr, s) = 655 | Shape.Style((fun s -> { s with Fill = Solid(1.0, HTML clr); StrokeColor = (0.0, HTML clr); Font = font }), s) 656 | 657 | let Area(line) = Shape <| seq { 658 | let line = Array.ofSeq line 659 | let firstX, lastX = fst line.[0], fst line.[line.Length - 1] 660 | yield firstX, COV (CO 0.0) 661 | yield! line 662 | yield lastX, COV (CO 0.0) 663 | yield firstX, COV (CO 0.0) } 664 | 665 | let VArea(line) = Shape <| seq { 666 | let line = Array.ofSeq line 667 | let firstY, lastY = snd line.[0], snd line.[line.Length - 1] 668 | yield COV (CO 0.0), firstY 669 | yield! line 670 | yield COV (CO 0.0), lastY 671 | yield COV (CO 0.0), firstY } 672 | 673 | let VShiftedArea(offs, line) = Shape <| seq { 674 | let line = Array.ofSeq line 675 | let firstY, lastY = snd line.[0], snd line.[line.Length - 1] 676 | yield COV (CO offs), firstY 677 | yield! line 678 | yield COV (CO offs), lastY 679 | yield COV (CO offs), firstY } 680 | 681 | let Bar(x, y) = Shape <| seq { 682 | yield COV x, CAR(y, 0.0) 683 | yield COV x, CAR(y, 1.0) 684 | yield COV (CO 0.0), CAR(y, 1.0) 685 | yield COV (CO 0.0), CAR(y, 0.0) } 686 | 687 | let Column(x, y) : Shape<1, 1> = Shape <| seq { 688 | yield CAR(x, 0.0), COV y 689 | yield CAR(x, 1.0), COV y 690 | yield CAR(x, 1.0), COV (CO 0.0) 691 | yield CAR(x, 0.0), COV (CO 0.0) } 692 | 693 | // ------------------------------------------------------------------------------------------------ 694 | // integration 695 | // ------------------------------------------------------------------------------------------------ 696 | 697 | module Compost = 698 | open Scales 699 | open Svg 700 | open Drawing 701 | open Events 702 | 703 | let niceNumber num decs = 704 | let str = string num 705 | let dot = str.IndexOf('.') 706 | let before, after = 707 | if dot = -1 then str, "" 708 | else str.Substring(0, dot), str.Substring(dot + 1, min decs (str.Length - dot - 1)) 709 | let after = 710 | if after.Length < decs then after + System.String [| for i in 1 .. (decs - after.Length) -> '0' |] 711 | else after 712 | let mutable res = before 713 | if before.Length > 5 then 714 | for i in before.Length-1 .. -1 .. 0 do 715 | let j = before.Length - i 716 | if i <> 0 && j % 3 = 0 then res <- res.Insert(i, ",") 717 | if Seq.forall ((=) '0') after then res 718 | else res + "." + after 719 | 720 | let defaultFormat scale value = 721 | match value with 722 | | CAR(CA s, _) -> s 723 | | COV(CO v) -> 724 | let dec = 725 | match scale with 726 | | Continuous(CO l, CO h) -> decimalPoints (unbox l, unbox h) 727 | | _ -> 0. 728 | niceNumber (System.Math.Round(unbox v, int dec)) (int dec) 729 | 730 | let defstyle = 731 | { Fill = Solid(1.0, RGB(196, 196, 196)) 732 | StrokeColor = (1.0, RGB(256, 0, 0)) 733 | StrokeDashArray = [] 734 | StrokeWidth = Pixels 2 735 | Animation = None 736 | Cursor = "default" 737 | Font = "10pt sans-serif" 738 | FormatAxisXLabel = defaultFormat 739 | FormatAxisYLabel = defaultFormat } 740 | 741 | let getRelativeLocation el x y = 742 | let rec getOffset (parent:HTMLElement) (x, y) = 743 | if parent = null then (x, y) 744 | else getOffset (unbox parent.offsetParent) (x-parent.offsetLeft, y-parent.offsetTop) 745 | let rec getParent (parent:HTMLElement) = 746 | // Safari: Skip over all the elements nested inside as they are weird 747 | // IE: Use parentNode when parentElement is not available (inside ?) 748 | if parent.namespaceURI = "http://www.w3.org/2000/svg" && parent.tagName <> "svg" then 749 | if parent.parentElement <> null then getParent parent.parentElement 750 | else getParent (unbox parent.parentNode) 751 | elif parent.offsetParent <> null then parent 752 | elif parent.parentElement <> null then getParent parent.parentElement 753 | else getParent (unbox parent.parentNode) 754 | getOffset (getParent el) (x, y) 755 | 756 | let createSvg revX revY (width, height) viz = 757 | let (sx, sy), shape = calculateScales defstyle viz 758 | 759 | let defs = ResizeArray<_>() 760 | let area = (0.0, 0.0, width, height) 761 | let svg = drawShape { Definitions = defs; Style = defstyle } area (sx, sy) shape 762 | 763 | let triggerEvent (e:Event) = triggerEvent area (sx, sy) shape e 764 | 765 | let mouseHandler kind el (evt:Event) = 766 | let evt = evt :?> MouseEvent 767 | let x, y = getRelativeLocation el evt.pageX evt.pageY 768 | triggerEvent evt (MouseEvent(kind, (COV(CO x), COV(CO y)))) 769 | 770 | let touchHandler kind el (evt:Event) = 771 | let evt = evt :?> TouchEvent 772 | let touch = evt.touches.[0] 773 | let x, y = getRelativeLocation el touch.pageX touch.pageY 774 | triggerEvent evt (TouchEvent(kind, (COV(CO x), COV(CO y)))) 775 | 776 | 777 | h?div [] [ 778 | s?svg [ 779 | "style"=>"overflow:visible" 780 | "width"=>string (int width); "height"=> string(int height); 781 | "click" =!> mouseHandler MouseEventKind.Click 782 | "mousemove" =!> mouseHandler MouseEventKind.Move 783 | "mousedown" =!> mouseHandler MouseEventKind.Down 784 | "mouseup" =!> mouseHandler MouseEventKind.Up 785 | "mouseleave" =!> fun _ evt -> triggerEvent evt MouseLeave 786 | "touchmove" =!> touchHandler TouchEventKind.Move 787 | "touchstart" =!> touchHandler TouchEventKind.Start 788 | "touchend" =!> fun _ evt -> triggerEvent evt TouchEnd 789 | ] [ 790 | let renderCtx = { Definitions = defs } 791 | let body = renderSvg renderCtx svg |> Array.ofSeq 792 | yield! defs 793 | yield! body 794 | ] 795 | ] 796 | -------------------------------------------------------------------------------- /src/compost/html.fs: -------------------------------------------------------------------------------- 1 | module Compost.Html 2 | 3 | open Fable.Core 4 | open Browser 5 | open Browser.Types 6 | open Fable.Core.JsInterop 7 | 8 | module FsOption = FSharp.Core.Option 9 | 10 | module Common = 11 | [] 12 | let keys<'T> (obj:obj) : string[] = failwith "never" 13 | 14 | [] 15 | let apply<'A, 'R> (f:obj) (args:'A[]) : 'R = failwith "never" 16 | 17 | [] 18 | let getProperty<'T> (obj:obj) (name:string) : 'T = failwith "never" 19 | 20 | [] 21 | let setProperty (o:obj) (s:string) (v:obj) = failwith "!" 22 | 23 | [] 24 | let event () : Event = failwith "JS" 25 | 26 | [] 27 | let parseInt (s:string) (b:int) : int = failwith "JS" 28 | 29 | [] 30 | let formatInt (i:int) (b:int) : string = failwith "JS" 31 | 32 | [] 33 | let typeOf(n:obj) : string = failwith "!" 34 | 35 | [] 36 | let isNumber(n:obj) : bool = failwith "!" 37 | 38 | [] 39 | let isDate(n:obj) : bool = failwith "!" 40 | 41 | [] 42 | let toISOString(o:obj) : string = failwith "!" 43 | 44 | [] 45 | let asDate(n:float) : System.DateTime = failwith "!" 46 | 47 | [] 48 | let dateOrNumberAsNumber(n:obj) : float = failwith "!" 49 | 50 | [] 51 | let formatDate(d:obj) : string = failwith "!" 52 | 53 | [] 54 | let formatLongDate(d:obj) : string = failwith "!" 55 | 56 | [] 57 | let formatTime(d:obj) : string = failwith "!" 58 | 59 | [] 61 | let formatDateTime(d:obj) : string = failwith "!" 62 | 63 | [] 64 | let isObject(n:obj) : bool = failwith "!" 65 | 66 | [] 67 | let isArray(n:obj) : bool = failwith "!" 68 | 69 | [] 70 | let isNaN(n:float) : bool = failwith "!" 71 | 72 | let niceNumber num decs = 73 | let str = string num 74 | let dot = str.IndexOf('.') 75 | let before, after = 76 | if dot = -1 then str, "" 77 | else str.Substring(0, dot), str.Substring(dot + 1, min decs (str.Length - dot - 1)) 78 | let after = 79 | if after.Length < decs then after + System.String [| for i in 1 .. (decs - after.Length) -> '0' |] 80 | else after 81 | let mutable res = before 82 | if before.Length > 5 then 83 | for i in before.Length-1 .. -1 .. 0 do 84 | let j = before.Length - i 85 | if i <> 0 && j % 3 = 0 then res <- res.Insert(i, ",") 86 | if Seq.forall ((=) '0') after then res 87 | else res + "." + after 88 | 89 | module Virtualdom = 90 | [] 91 | let h(arg1: string, arg2: obj, arg3: obj[]): obj = failwith "JS only" 92 | 93 | [] 94 | let diff (tree1:obj) (tree2:obj): obj = failwith "JS only" 95 | 96 | [] 97 | let patch (node:obj) (patches:obj): Node = failwith "JS only" 98 | 99 | [] 100 | let createElement (e:obj): Node = failwith "JS only" 101 | 102 | type DomAttribute = 103 | | Event of (HTMLElement -> Event -> unit) 104 | | Attribute of string 105 | | Property of obj 106 | 107 | type DomNode = 108 | | Text of string 109 | | Element of ns:string * tag:string * attributes:(string * DomAttribute)[] * children : DomNode[] 110 | 111 | let createTree ns tag args children = 112 | let attrs = ResizeArray<_>() 113 | let props = ResizeArray<_>() 114 | for k, v in args do 115 | match k, v with 116 | | k, Attribute v -> 117 | attrs.Add (k, box v) 118 | | k, Property o -> 119 | props.Add(k, o) 120 | | k, Event f -> 121 | props.Add ("on" + k, box (fun o -> f (Common.getProperty o "target") (Common.event()) )) 122 | let attrs = JsInterop.createObj attrs 123 | let ns = if ns = null || ns = "" then [] else ["namespace", box ns] 124 | let props = JsInterop.createObj (Seq.append (ns @ ["attributes", attrs]) props) 125 | let elem = Virtualdom.h(tag, props, children) 126 | elem 127 | 128 | let mutable counter = 0 129 | 130 | let rec renderVirtual node = 131 | match node with 132 | | Text(s) -> 133 | box s 134 | | Element(ns, tag, attrs, children) -> 135 | createTree ns tag attrs (Array.map renderVirtual children) 136 | 137 | let rec render node = 138 | match node with 139 | | Text(s) -> 140 | document.createTextNode(s) :> Node 141 | 142 | | Element(ns, tag, attrs, children) -> 143 | let el = 144 | if ns = null || ns = "" then document.createElement(tag) 145 | else document.createElementNS(ns, tag) :?> HTMLElement 146 | let rc = Array.map render children 147 | for c in rc do el.appendChild(c) |> ignore 148 | for k, a in attrs do 149 | match a with 150 | | Property(o) -> Common.setProperty el k o 151 | | Attribute(v) -> el.setAttribute(k, v) 152 | | Event(f) -> () //el.addEventListener(k, U2.Case1(EventListener(f el))) 153 | el :> Node 154 | 155 | let renderTo (node:HTMLElement) dom = 156 | while box node.lastChild <> null do ignore(node.removeChild(node.lastChild)) 157 | let el = render dom 158 | node.appendChild(el) |> ignore 159 | 160 | let createVirtualDomAsyncApp id initial r u = 161 | let event = new Event<'T>() 162 | let trigger e = event.Trigger(e) 163 | let mutable container = document.createElement("div") :> Node 164 | document.getElementById(id).innerHTML <- "" 165 | document.getElementById(id).appendChild(container) |> ignore 166 | let mutable tree = Fable.Core.JsInterop.createObj [] 167 | let mutable state = initial 168 | 169 | let handleEvent evt = Async.StartImmediate <| async { 170 | match evt with 171 | | Some e -> 172 | let! ns = u state e 173 | state <- ns 174 | | _ -> () 175 | let newTree = r trigger state |> renderVirtual 176 | let patches = Virtualdom.diff tree newTree 177 | container <- Virtualdom.patch container patches 178 | tree <- newTree } 179 | 180 | handleEvent None 181 | event.Publish.Add(Some >> handleEvent) 182 | 183 | let createVirtualDomApp id initial r u = 184 | let event = new Event<'T>() 185 | let trigger e = event.Trigger(e) 186 | let mutable container = document.createElement("div") :> Node 187 | document.getElementById(id).innerHTML <- "" 188 | document.getElementById(id).appendChild(container) |> ignore 189 | let mutable tree = Fable.Core.JsInterop.createObj [] 190 | let mutable state = initial 191 | 192 | let handleEvent evt = 193 | state <- match evt with Some e -> u state e | _ -> state 194 | let newTree = r trigger state |> renderVirtual 195 | let patches = Virtualdom.diff tree newTree 196 | container <- Virtualdom.patch container patches 197 | tree <- newTree 198 | 199 | handleEvent None 200 | event.Publish.Add(Some >> handleEvent) 201 | 202 | let text s = Text(s) 203 | let (=>) k v = k, Attribute(v) 204 | let (=!>) k f = k, Event(f) 205 | 206 | 207 | type El(ns) = 208 | member x.Namespace = ns 209 | static member (?) (el:El, n:string) = fun a b -> 210 | Element(el.Namespace, n, Array.ofList a, Array.ofList b) 211 | 212 | let h = El(null) 213 | let s = El("http://www.w3.org/2000/svg") 214 | -------------------------------------------------------------------------------- /src/project/data.js: -------------------------------------------------------------------------------- 1 | export let elections = 2 | [ { party:"Conservative", color:"#1F77B4", y17:317, y19:365}, 3 | { party:"Labour", color:"#D62728", y17:262, y19:202}, 4 | { party:"LibDem", color:"#FF7F0E", y17:12, y19:11}, 5 | { party:"SNP", color:"#BCBD22", y17:35, y19:48}, 6 | { party:"Green", color:"#2CA02C", y17:1, y19:1}, 7 | { party:"DUP", color:"#8C564B", y17:10, y19:8} ] 8 | 9 | export let gbpusd = 10 | [ 1.3206, 1.3267, 1.312, 1.3114, 1.3116, 1.3122, 1.3085, 1.3211, 1.3175, 11 | 1.3136, 1.3286, 1.3231, 1.3323, 1.3215, 1.3186, 1.2987, 1.296, 1.2932, 12 | 1.2885, 1.3048, 1.3287, 1.327, 1.3429, 1.3523, 1.3322, 1.3152, 1.3621, 13 | 1.4798, 1.4687, 1.467, 1.4694, 1.4293, 1.4064, 1.4196, 1.4114, 1.4282, 14 | 1.4334, 1.4465, 1.4552, 1.456, 1.4464, 1.4517, 1.4447, 1.4414 ].reverse() 15 | 16 | export let gbpeur = 17 | [ 1.1823, 1.1867, 1.1838, 1.1936, 1.1944, 1.1961, 1.1917, 1.2017, 1.1969, 18 | 1.193, 1.2006, 1.1952, 1.1998, 1.1903, 1.1909, 1.1759, 1.1743, 1.168, 19 | 1.1639, 1.175, 1.1929, 1.192, 1.2081, 1.2177, 1.2054, 1.1986, 1.2254, 20 | 1.3039, 1.3018, 1.3018, 1.296, 1.2709, 1.2617, 1.2634, 1.2589, 1.2639, 21 | 1.2687, 1.2771, 1.2773, 1.2823, 1.2726, 1.2814, 1.2947, 1.2898 ].reverse() 22 | 23 | export let iris = 24 | [ [5.1, 3.5, 1.4, 0.2, "Setosa"], [4.9, 3, 1.4, 0.2, "Setosa"], [4.7, 3.2, 1.3, 0.2, "Setosa"], 25 | [4.6, 3.1, 1.5, 0.2, "Setosa"], [5, 3.6, 1.4, 0.2, "Setosa"], [5.4, 3.9, 1.7, 0.4, "Setosa"], 26 | [4.6, 3.4, 1.4, 0.3, "Setosa"], [5, 3.4, 1.5, 0.2, "Setosa"], [4.4, 2.9, 1.4, 0.2, "Setosa"], 27 | [4.9, 3.1, 1.5, 0.1, "Setosa"], [5.4, 3.7, 1.5, 0.2, "Setosa"], [4.8, 3.4, 1.6, 0.2, "Setosa"], 28 | [4.8, 3, 1.4, 0.1, "Setosa"], [4.3, 3, 1.1, 0.1, "Setosa"], [5.8, 4, 1.2, 0.2, "Setosa"], 29 | [5.7, 4.4, 1.5, 0.4, "Setosa"], [5.4, 3.9, 1.3, 0.4, "Setosa"], [5.1, 3.5, 1.4, 0.3, "Setosa"], 30 | [5.7, 3.8, 1.7, 0.3, "Setosa"], [5.1, 3.8, 1.5, 0.3, "Setosa"], [5.4, 3.4, 1.7, 0.2, "Setosa"], 31 | [5.1, 3.7, 1.5, 0.4, "Setosa"], [4.6, 3.6, 1, 0.2, "Setosa"], [5.1, 3.3, 1.7, 0.5, "Setosa"], 32 | [4.8, 3.4, 1.9, 0.2, "Setosa"], [5, 3, 1.6, 0.2, "Setosa"], [5, 3.4, 1.6, 0.4, "Setosa"], 33 | [5.2, 3.5, 1.5, 0.2, "Setosa"], [5.2, 3.4, 1.4, 0.2, "Setosa"], [4.7, 3.2, 1.6, 0.2, "Setosa"], 34 | [4.8, 3.1, 1.6, 0.2, "Setosa"], [5.4, 3.4, 1.5, 0.4, "Setosa"], [5.2, 4.1, 1.5, 0.1, "Setosa"], 35 | [5.5, 4.2, 1.4, 0.2, "Setosa"], [4.9, 3.1, 1.5, 0.2, "Setosa"], [5, 3.2, 1.2, 0.2, "Setosa"], 36 | [5.5, 3.5, 1.3, 0.2, "Setosa"], [4.9, 3.6, 1.4, 0.1, "Setosa"], [4.4, 3, 1.3, 0.2, "Setosa"], 37 | [5.1, 3.4, 1.5, 0.2, "Setosa"], [5, 3.5, 1.3, 0.3, "Setosa"], [4.5, 2.3, 1.3, 0.3, "Setosa"], 38 | [4.4, 3.2, 1.3, 0.2, "Setosa"], [5, 3.5, 1.6, 0.6, "Setosa"], [5.1, 3.8, 1.9, 0.4, "Setosa"], 39 | [4.8, 3, 1.4, 0.3, "Setosa"], [5.1, 3.8, 1.6, 0.2, "Setosa"], [4.6, 3.2, 1.4, 0.2, "Setosa"], 40 | [5.3, 3.7, 1.5, 0.2, "Setosa"], [5, 3.3, 1.4, 0.2, "Setosa"], [7, 3.2, 4.7, 1.4, "Versicolor"], 41 | [6.4, 3.2, 4.5, 1.5, "Versicolor"], [6.9, 3.1, 4.9, 1.5, "Versicolor"], [5.5, 2.3, 4, 1.3, "Versicolor"], 42 | [6.5, 2.8, 4.6, 1.5, "Versicolor"], [5.7, 2.8, 4.5, 1.3, "Versicolor"], [6.3, 3.3, 4.7, 1.6, "Versicolor"], 43 | [4.9, 2.4, 3.3, 1, "Versicolor"], [6.6, 2.9, 4.6, 1.3, "Versicolor"], [5.2, 2.7, 3.9, 1.4, "Versicolor"], 44 | [5, 2, 3.5, 1, "Versicolor"], [5.9, 3, 4.2, 1.5, "Versicolor"], [6, 2.2, 4, 1, "Versicolor"], 45 | [6.1, 2.9, 4.7, 1.4, "Versicolor"], [5.6, 2.9, 3.6, 1.3, "Versicolor"], [6.7, 3.1, 4.4, 1.4, "Versicolor"], 46 | [5.6, 3, 4.5, 1.5, "Versicolor"], [5.8, 2.7, 4.1, 1, "Versicolor"], [6.2, 2.2, 4.5, 1.5, "Versicolor"], 47 | [5.6, 2.5, 3.9, 1.1, "Versicolor"], [5.9, 3.2, 4.8, 1.8, "Versicolor"], [6.1, 2.8, 4, 1.3, "Versicolor"], 48 | [6.3, 2.5, 4.9, 1.5, "Versicolor"], [6.1, 2.8, 4.7, 1.2, "Versicolor"], [6.4, 2.9, 4.3, 1.3, "Versicolor"], 49 | [6.6, 3, 4.4, 1.4, "Versicolor"], [6.8, 2.8, 4.8, 1.4, "Versicolor"], [6.7, 3, 5, 1.7, "Versicolor"], 50 | [6, 2.9, 4.5, 1.5, "Versicolor"], [5.7, 2.6, 3.5, 1, "Versicolor"], [5.5, 2.4, 3.8, 1.1, "Versicolor"], 51 | [5.5, 2.4, 3.7, 1, "Versicolor"], [5.8, 2.7, 3.9, 1.2, "Versicolor"], [6, 2.7, 5.1, 1.6, "Versicolor"], 52 | [5.4, 3, 4.5, 1.5, "Versicolor"], [6, 3.4, 4.5, 1.6, "Versicolor"], [6.7, 3.1, 4.7, 1.5, "Versicolor"], 53 | [6.3, 2.3, 4.4, 1.3, "Versicolor"], [5.6, 3, 4.1, 1.3, "Versicolor"], [5.5, 2.5, 4, 1.3, "Versicolor"], 54 | [5.5, 2.6, 4.4, 1.2, "Versicolor"], [6.1, 3, 4.6, 1.4, "Versicolor"], [5.8, 2.6, 4, 1.2, "Versicolor"], 55 | [5, 2.3, 3.3, 1, "Versicolor"], [5.6, 2.7, 4.2, 1.3, "Versicolor"], [5.7, 3, 4.2, 1.2, "Versicolor"], 56 | [5.7, 2.9, 4.2, 1.3, "Versicolor"], [6.2, 2.9, 4.3, 1.3, "Versicolor"], [5.1, 2.5, 3, 1.1, "Versicolor"], 57 | [5.7, 2.8, 4.1, 1.3, "Versicolor"], [6.3, 3.3, 6, 2.5, "Virginica"], [5.8, 2.7, 5.1, 1.9, "Virginica"], 58 | [7.1, 3, 5.9, 2.1, "Virginica"], [6.3, 2.9, 5.6, 1.8, "Virginica"], [6.5, 3, 5.8, 2.2, "Virginica"], 59 | [7.6, 3, 6.6, 2.1, "Virginica"], [4.9, 2.5, 4.5, 1.7, "Virginica"], [7.3, 2.9, 6.3, 1.8, "Virginica"], 60 | [6.7, 2.5, 5.8, 1.8, "Virginica"], [7.2, 3.6, 6.1, 2.5, "Virginica"], [6.5, 3.2, 5.1, 2, "Virginica"], 61 | [6.4, 2.7, 5.3, 1.9, "Virginica"], [6.8, 3, 5.5, 2.1, "Virginica"], [5.7, 2.5, 5, 2, "Virginica"], 62 | [5.8, 2.8, 5.1, 2.4, "Virginica"], [6.4, 3.2, 5.3, 2.3, "Virginica"], [6.5, 3, 5.5, 1.8, "Virginica"], 63 | [7.7, 3.8, 6.7, 2.2, "Virginica"], [7.7, 2.6, 6.9, 2.3, "Virginica"], [6, 2.2, 5, 1.5, "Virginica"], 64 | [6.9, 3.2, 5.7, 2.3, "Virginica"], [5.6, 2.8, 4.9, 2, "Virginica"], [7.7, 2.8, 6.7, 2, "Virginica"], 65 | [6.3, 2.7, 4.9, 1.8, "Virginica"], [6.7, 3.3, 5.7, 2.1, "Virginica"], [7.2, 3.2, 6, 1.8, "Virginica"], 66 | [6.2, 2.8, 4.8, 1.8, "Virginica"], [6.1, 3, 4.9, 1.8, "Virginica"], [6.4, 2.8, 5.6, 2.1, "Virginica"], 67 | [7.2, 3, 5.8, 1.6, "Virginica"], [7.4, 2.8, 6.1, 1.9, "Virginica"], [7.9, 3.8, 6.4, 2, "Virginica"], 68 | [6.4, 2.8, 5.6, 2.2, "Virginica"], [6.3, 2.8, 5.1, 1.5, "Virginica"], [6.1, 2.6, 5.6, 1.4, "Virginica"], 69 | [7.7, 3, 6.1, 2.3, "Virginica"], [6.3, 3.4, 5.6, 2.4, "Virginica"], [6.4, 3.1, 5.5, 1.8, "Virginica"], 70 | [6, 3, 4.8, 1.8, "Virginica"], [6.9, 3.1, 5.4, 2.1, "Virginica"], [6.7, 3.1, 5.6, 2.4, "Virginica"], 71 | [6.9, 3.1, 5.1, 2.3, "Virginica"], [5.8, 2.7, 5.1, 1.9, "Virginica"], [6.8, 3.2, 5.9, 2.3, "Virginica"], 72 | [6.7, 3.3, 5.7, 2.5, "Virginica"], [6.7, 3, 5.2, 2.3, "Virginica"], [6.3, 2.5, 5, 1.9, "Virginica"], 73 | [6.5, 3, 5.2, 2, "Virginica"], [6.2, 3.4, 5.4, 2.3, "Virginica"], [5.9, 3, 5.1, 1.8, "Virginica"] 74 | ].map(a => ({ sepal_length: a[0], sepal_width: a[1], petal_length: a[2], petal_width: a[3], species: a[4] })) 75 | -------------------------------------------------------------------------------- /src/project/demos.js: -------------------------------------------------------------------------------- 1 | import { scale as s, compost as c } from "../compost/compost.fs" 2 | import { elections, gbpusd, gbpeur, iris } from "./data.js" 3 | 4 | // Calculate bins of a histogram. The function splits the data into 10 5 | // equally sized bins, counts the values in each bin and returns an array 6 | // of three-element arrays with start of the bin, end of the bin and count 7 | function bins(data) { 8 | let lo = Math.min(...data), hi = Math.max(...data); 9 | let bins = {} 10 | for(var i=0; i k*1).sort() 15 | return keys.map(k => 16 | [ lo + (hi - lo) * (k / 10), 17 | lo + (hi - lo) * ((k + 1) / 10), bins[k]]); 18 | } 19 | 20 | // Makes a color given in "#rrggbb" format darker or lighter 21 | // (by multiplying each component by the specified number k) 22 | function adjust(color, k) { 23 | let r = parseInt(color.substr(1, 2), 16) 24 | let g = parseInt(color.substr(3, 2), 16) 25 | let b = parseInt(color.substr(5, 2), 16) 26 | let f = n => n*k > 255 ? 255 : n*k; 27 | return "#" + ((f(r) << 16) + (f(g) << 8) + (f(b) << 0)).toString(16); 28 | } 29 | 30 | // A derived Compost operation that adds a title to any given chart. 31 | // This works by creating text element and using 'nest' to allocate top 32 | // 15% of space for the title and the remaining 85% of space for the title. 33 | function title(text, chart) { 34 | let title = c.scale(s.continuous(0, 100), s.continuous(0, 100), 35 | c.font("11pt arial", "black", c.text(50, 80, text))) 36 | return c.overlay([ 37 | c.nest(0, 100, 85, 100, title), 38 | c.nest(0, 100, 0, 85, chart) 39 | ]) 40 | } 41 | 42 | // Creates a bar of height 'y' that is witin a categorical value 'x' 43 | // starting at the offset 'f' and ending at the offset 't'. 44 | function partColumn(f, t, x, y) { 45 | return c.shape([ [ [x,f], y ], [ [x,t], y ], [ [x,t], 0 ], [ [x,f], 0 ] ]) 46 | } 47 | 48 | // Create a line using array index as the X value and array value as the Y value 49 | function line(data) { 50 | return c.line(data.map((v, i) => [i, v])); 51 | } 52 | 53 | 54 | // ---------------------------------------------------------------------------- 55 | // DEMO #1: United Kingdom general elections (2017 vs 2019) 56 | // ---------------------------------------------------------------------------- 57 | 58 | let bars = 59 | c.axes("left bottom", c.scaleY(s.continuous(0, 410), c.overlay( 60 | elections.map(e => 61 | c.padding(0, 10, 0, 10, c.overlay([ 62 | c.fillColor(adjust(e.color, 0.8), partColumn(0, 0.5, e.party, e.y17)), 63 | c.fillColor(adjust(e.color, 1.2), partColumn(0.5, 1, e.party, e.y19)) 64 | ])) 65 | ) 66 | ))) 67 | 68 | c.render("out1", title("United Kingdom general elections (2017 vs 2019)", bars)) 69 | 70 | // ---------------------------------------------------------------------------- 71 | // DEMO #2: GBP-USD and GBP-EUR rates (June-July 2016) 72 | // ---------------------------------------------------------------------------- 73 | 74 | function body(lo, hi, data) { 75 | return c.axes("left right bottom", c.overlay([ 76 | c.fillColor("#1F77B460", c.shape( 77 | [ [0,lo], [16,lo], [16,hi], [0,hi] ])), 78 | c.fillColor("#D6272860", c.shape( 79 | [ [data.length-1,lo], [16,lo], [16,hi], [data.length-1,hi] ])), 80 | c.strokeColor("#202020", line(data)) 81 | ])) 82 | } 83 | 84 | let rates = c.overlay([ 85 | c.nestY(0, 50, body(1.25, 1.52, gbpusd)), 86 | c.nestY(50, 100, body(1.15, 1.32, gbpeur)), 87 | ]) 88 | 89 | c.render("out2", title("GBP-USD and GBP-EUR rates (June-July 2016)", rates)) 90 | 91 | // ---------------------------------------------------------------------------- 92 | // DEMO #3: Pairplot comparing features of the iris data set 93 | // ---------------------------------------------------------------------------- 94 | 95 | let irisColors = {Setosa:"blue", Virginica:"green", Versicolor:"red" } 96 | let cats = ["sepal_width", "petal_length", "petal_width"] 97 | 98 | let pairplot = 99 | c.overlay(cats.map(x => cats.map(y => 100 | c.nest([x, 0], [x, 1], [y, 0], [y, 1], 101 | c.axes("left bottom", c.overlay( 102 | x == y 103 | ? bins(iris.map(i => i[x])).map(b => 104 | c.fillColor("#808080", c.shape( 105 | [ [b[0], b[2]], [b[1], b[2]], [b[1], 0], [b[0], 0] ])) ) 106 | : iris.map(i => c.strokeColor(irisColors[i.species], 107 | c.bubble(i[x], i[y], 1, 1))) 108 | ))))).flat()) 109 | 110 | c.render("out3", title("Pairplot comparing sepal width, " + 111 | "petal length and petal width of irises", pairplot)) 112 | 113 | // ---------------------------------------------------------------------------- 114 | // DEMO #4: Interactive 'You Draw' chart that lets you resize bars 115 | // ---------------------------------------------------------------------------- 116 | 117 | let partyColors = {} 118 | for(var i = 0; i < elections.length; i++) 119 | partyColors[elections[i].party] = elections[i].color; 120 | 121 | function update1(state, evt) { 122 | switch (evt.kind) { 123 | case 'set': 124 | if (!state.enabled) return state; 125 | let newValues = state.values.map(kv => 126 | kv[0] == evt.party ? [kv[0], evt.newValue] : kv) 127 | return { ...state, values: newValues } 128 | case 'enable': 129 | return { ...state, enabled: evt.enabled } 130 | } 131 | } 132 | 133 | function render1(trigger, state) { 134 | return title("Drag the bars to guess UK election results!", 135 | c.axes("left bottom", c.scaleY(s.continuous(0, 400), 136 | c.on({ 137 | mousedown: () => trigger({ kind:'enable', enabled:true }), 138 | mouseup: () => trigger({ kind:'enable', enabled:false }), 139 | mousemove: (x, y) => trigger({ kind:'set', party:x[0], newValue:y }) 140 | }, c.overlay(state.values.map(kv => 141 | c.fillColor(partyColors[kv[0]], 142 | c.padding(0, 10, 0, 10, c.column(kv[0], kv[1]))) )) 143 | )))) 144 | } 145 | 146 | let init1 = { enabled:false, values: elections.map(e => [e.party, e.y19]) } 147 | c.interactive("out4", init1, update1, render1) 148 | 149 | // ---------------------------------------------------------------------------- 150 | // DEMO #5: Interactive 'You Draw' chart that lets you resize bars 151 | // ---------------------------------------------------------------------------- 152 | 153 | let data = 154 | [ ["Social protection", 14.10], ["Health", 7.40], ["Education", 4.50], 155 | ["General public services", 3.10], ["Economic affairs", 2.40] ] 156 | 157 | let colors = 158 | [ "#1f77b4", "#ff7f0e", "#2ca02c", "#d62728", "#9467bd", 159 | "#8c564b", "#e377c2", "#7f7f7f", "#bcbd22", "#17becf" ] 160 | 161 | let nums = data.map(v => v[1]) 162 | let sum = nums.reduce((a,b) => a + b) 163 | let max = Math.max.apply(null, nums) 164 | let avg = sum / data.length; 165 | 166 | let init = { 167 | animation:0, 168 | guessed:false, 169 | max:Math.floor(max * 1.2), 170 | values:data.map((v, i) => ({ 171 | moved: false, 172 | color: colors[i], 173 | category: v[0], 174 | value: avg, 175 | correct: v[1], 176 | random: Math.random() 177 | })).sort((a, b) => a.random - b.random) 178 | } 179 | 180 | function update(state, evt) { 181 | switch(evt.kind) { 182 | case "animate": 183 | return {...state, animation:state.animation + 0.02} 184 | case "set": 185 | if(state.animation > 0) return state; 186 | let newValues = state.values.map(v => 187 | ({ ...v, 188 | moved: v.category == evt.category ? true : v.moved, 189 | value: v.category == evt.category ? evt.value : v.value })) 190 | return {...state, values:newValues, guessed:newValues.every(v => v.moved) } 191 | } 192 | return state; 193 | } 194 | function render(trigger, state) { 195 | if (state.animation > 0 && state.animation < 1) 196 | window.setTimeout(() => trigger({kind:"animate"}), 10) 197 | let o = state.guessed ? {} : {disabled:""} 198 | function handler(x, y, e) { 199 | if (e.buttons > 0) trigger({kind:"set", value:x, category:y[0] }) 200 | } 201 | return c.html("div", { class:"youguess" }, [ 202 | c.svg(600, 400, c.axes("bottom", c.on({ 203 | mousemove: handler, mousedown: handler 204 | },c.overlay(state.values.map(v => { 205 | let av = v.correct * state.animation + v.value * (1 - state.animation); 206 | return c.padding(10,0,10,0,c.overlay([ 207 | c.font("13pt sans-serif", v.color, c.text(state.max*0.98, [v.category, 0.5], v.category, "end")), 208 | c.strokeColor(v.color, c.line([ [v.value, [v.category, 0]], [v.value, [v.category, 1]] ])), 209 | c.fillColor("#a0a0a030", c.bar(state.max, v.category)), 210 | c.fillColor(v.color + (v.moved?"90":"30"), c.bar(av, v.category)) 211 | ])); 212 | }) )))), 213 | c.html("div", {style:"width:600px;text-align:center"}, [ 214 | c.html("button", {...o, 215 | click:() => trigger({kind:"animate"}) }, [ "Show me how I did" ]) 216 | ]) 217 | ]); 218 | } 219 | 220 | c.interactive("out5", init, update, render) -------------------------------------------------------------------------------- /src/project/standalone.js: -------------------------------------------------------------------------------- 1 | import { scale as s, compost as c } from "../compost/compost.fs" 2 | 3 | window.c = c; 4 | window.s = s; 5 | -------------------------------------------------------------------------------- /src/webpack.dev.js: -------------------------------------------------------------------------------- 1 | // Note this only includes basic configuration for development mode. 2 | // For a more comprehensive configuration check: 3 | // https://github.com/fable-compiler/webpack-config-template 4 | 5 | var path = require("path"); 6 | 7 | module.exports = { 8 | mode: "development", 9 | entry: "./src/project/demos.js", 10 | output: { 11 | filename: "bundle.js", 12 | }, 13 | devServer: { 14 | publicPath: "/", 15 | contentBase: "./public", 16 | port: 8080, 17 | }, 18 | module: { 19 | rules: [{ 20 | test: /\.fs(x|proj)?$/, 21 | use: "fable-loader" 22 | }] 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/webpack.latest.js: -------------------------------------------------------------------------------- 1 | // Note this only includes basic configuration for development mode. 2 | // For a more comprehensive configuration check: 3 | // https://github.com/fable-compiler/webpack-config-template 4 | 5 | var path = require("path"); 6 | var pkg = require('../package.json') 7 | 8 | module.exports = { 9 | mode: "development", 10 | entry: "./src/project/standalone.js", 11 | output: { 12 | path: path.join(__dirname, "../docs/releases"), 13 | filename: "compost-latest.js", 14 | }, 15 | module: { 16 | rules: [{ 17 | test: /\.fs(x|proj)?$/, 18 | use: "fable-loader" 19 | }] 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/webpack.pub.js: -------------------------------------------------------------------------------- 1 | // Note this only includes basic configuration for development mode. 2 | // For a more comprehensive configuration check: 3 | // https://github.com/fable-compiler/webpack-config-template 4 | 5 | var path = require("path"); 6 | var pkg = require('../package.json') 7 | 8 | module.exports = { 9 | mode: "development", 10 | entry: "./src/project/standalone.js", 11 | output: { 12 | path: path.join(__dirname, "../docs/releases"), 13 | filename: "compost-" + pkg.version + ".js", 14 | }, 15 | module: { 16 | rules: [{ 17 | test: /\.fs(x|proj)?$/, 18 | use: "fable-loader" 19 | }] 20 | } 21 | } 22 | --------------------------------------------------------------------------------