Skip to content

Commit

Permalink
Merge pull request #836 from explorable-viz/example-fluid
Browse files Browse the repository at this point in the history
Bubble Chart First Impl
  • Loading branch information
JosephBond authored Nov 8, 2023
2 parents 4a8914e + 9ac684a commit 39578a2
Show file tree
Hide file tree
Showing 7 changed files with 184 additions and 8 deletions.
2 changes: 1 addition & 1 deletion fluid/example/linked-outputs/water-consumption-data.fld
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
[
{country: "Germany", cities: 1000, farms: 150, industry:500 , energy: 450, popMil: 81},
{country: "Germany", cities: 900, farms: 150, industry:500 , energy: 450, popMil: 81},
{country: "UK", cities: 800, farms: 200, industry: 400, energy: 700, popMil: 67}
]
6 changes: 2 additions & 4 deletions fluid/example/linked-outputs/water-ratio-chart.fld
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
let ratioFor country =
sum [row.popMil / row.farms | row <- data, row.country == country];
let countryData = [{x: country.country, y: ratioFor country.country} | country <- data]
in BarChart {
let countryData = [{x: country.cities, y: country.farms, z: country.popMil} | country <- data]
in BubbleChart {
caption: "Ratio of farmland consumption to population in millions",
data: countryData
}
2 changes: 1 addition & 1 deletion src/App/BarChart.js
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ function drawBarChart_ (
.enter()
.append('rect')
.attr('x', ([, d]) => x(d.x._1))
.attr('y', ([, d]) => y(d.y._1 + 1)) // ouch: bars overplot x-axis!
.attr('y', ([, d]) => (y(d.y._1))) // ouch: bars overplot x-axis!
.attr('width', x.bandwidth())
.attr('height', ([, d]) => height - y(d.y._1))
.attr('fill', ([, d]) => d.y._2 ? colorShade(barFill, -40) : barFill)
Expand Down
120 changes: 120 additions & 0 deletions src/App/BubbleChart.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
"use strict"

import * as d3 from "d3"
import * as d3tip from "d3-tip"

// This prelude currently duplicated across all FFI implementations.
function curry2 (f) {
return x1 => x2 => f(x1, x2)
}

function curry3 (f) {
return x1 => x2 => x3 => f(x1, x2, x3)
}

function curry4 (f) {
return x1 => x2 => x3 => x4 => f(x1, x2, x3, x4)
}

// https://stackoverflow.com/questions/5560248
function colorShade(col, amt) {
col = col.replace(/^#/, '')
if (col.length === 3) col = col[0] + col[0] + col[1] + col[1] + col[2] + col[2]

let [r, g, b] = col.match(/.{2}/g);
([r, g, b] = [parseInt(r, 16) + amt, parseInt(g, 16) + amt, parseInt(b, 16) + amt])

r = Math.max(Math.min(255, r), 0).toString(16)
g = Math.max(Math.min(255, g), 0).toString(16)
b = Math.max(Math.min(255, b), 0).toString(16)

const rr = (r.length < 2 ? '0' : '') + r
const gg = (g.length < 2 ? '0' : '') + g
const bb = (b.length < 2 ? '0' : '') + b

return `#${rr}${gg}${bb}`
}

function drawBubbleChart_ (
id,
childIndex,
{
caption, // String
data, // Array BubbleRecord
},
listener
) {
return () => {
const childId = id + '-' + childIndex
const margin = {top: 15, right: 0, bottom: 40, left: 30},
width = 200 - margin.left - margin.right,
height = 185 - margin.top - margin.bottom
const div = d3.select('#' + id)

div.selectAll('#' + childId).remove()

const svg = div
.append('svg')
.attr('width', width + margin.left + margin.right)
.attr('height', height + margin.top + margin.bottom)
.attr('id', childId)
.append('g')
.attr('transform', `translate(${margin.left}, ${margin.top})`)

const tip = d3tip.default()
.attr('class', 'd3-tip')
.offset([0, 0])
.html((_, d) => d.y_1)

svg.call(tip)

const x_max = Math.ceil(Math.max(...data.map(d => d.x._1 + d.z._1)))
const y_max = Math.ceil(Math.max(...data.map(d => d.y._1 + d.z._1)))
const z_max = Math.ceil(Math.max(...data.map(d => d.z._1)))
const x = d3.scaleLinear()
.domain([0,x_max])
.range([0, width])
svg.append('g')
.attr('transform', "translate(0," + height + ")")
.call(d3.axisBottom(x))
.selectAll('text')
.style('text-anchor', 'middle')

const y = d3.scaleLinear()
.domain([0, y_max])
.range([height,0])
svg.append('g')
.call(d3.axisLeft(y))

const z = d3.scaleLinear()
.domain([1, z_max])
.range([1, 30])

const dotFill = '#dcdcdc'
svg.append('g')
.selectAll('dot')
.data([...data.entries()])
.enter()
.append('circle')
.attr('cx', ([, d]) => x(d.x._1))
.attr('cy', ([, d]) => y(d.y._1))
.attr('r', ([, d]) => z(d.z._1))
.attr('stroke', 'black')
.style('fill', ([, d]) => d.y._2 ? colorShade(dotFill, -40) : dotFill)
.style('class', ([, d]) => d.y._2 ? 'dot-selected' : 'dot-unselected')
.on('mousedown', (e, d) => {
console.log(`mousedown ${d[0]}`)
listener(e)
})

svg.append('text')
.text(caption._1)
.attr('x', width / 2)
.attr('y', height + 35)
.attr('class', 'title-text')
.attr('dominant-baseline', 'bottom')
.attr('text-anchor', 'middle')
}
}

export var drawBubbleChart = curry4(drawBubbleChart_)
52 changes: 52 additions & 0 deletions src/App/BubbleChart.purs
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
module App.BubbleChart where

import Prelude hiding (absurd)

import App.Util (class Reflect, Renderer, Handler, from, get_intOrNumber, record)
import App.Util.Select (constrArg, field, listElement)
import Data.Maybe (Maybe)
import DataType (cBubbleChart, f_caption, f_data, f_x, f_y, f_z)
import Dict (Dict, get)
import Lattice (𝔹, neg)
import Primitive (string)
import Test.Util (Selector)
import Unsafe.Coerce (unsafeCoerce)
import Util (type (×), definitely', (!))
import Val (Val)
import Web.Event.Event (target)
import Web.Event.Internal.Types (EventTarget)

newtype BubbleChart = BubbleChart { caption :: String × 𝔹, data :: Array BubbleChartRecord }
newtype BubbleChartRecord = BubbleChartRecord { x :: Number × 𝔹, y :: Number × 𝔹, z :: Number × 𝔹 }

foreign import drawBubbleChart :: Renderer BubbleChart

instance Reflect (Dict (Val 𝔹)) BubbleChartRecord where
from r = BubbleChartRecord
{ x: get_intOrNumber f_x r
, y: get_intOrNumber f_y r
, z: get_intOrNumber f_z r
}

instance Reflect (Dict (Val 𝔹)) BubbleChart where
from r = BubbleChart
{ caption: string.unpack (get f_caption r)
, data: record from <$> from (get f_data r)
}

bubbleChartHandler :: Handler
bubbleChartHandler ev = toggleDot $ unsafeDotIndex $ target ev
where
toggleDot :: Int -> Selector Val
toggleDot i =
constrArg cBubbleChart 0
$ field f_data
$ listElement i
$ neg

unsafeDotIndex :: Maybe EventTarget -> Int
unsafeDotIndex tgt_opt =
let
tgt = definitely' $ tgt_opt
in
(unsafeCoerce tgt).__data__ ! 0
7 changes: 6 additions & 1 deletion src/App/Fig.purs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ module App.Fig where
import Prelude hiding (absurd)

import App.BarChart (BarChart, barChartHandler, drawBarChart)
import App.BubbleChart (BubbleChart, bubbleChartHandler, drawBubbleChart)
import App.CodeMirror (EditorView, addEditorView, dispatch, getContentsLength, update)
import App.LineChart (LineChart, drawLineChart, lineChartHandler)
import App.MatrixView (MatrixView(..), drawMatrix, matrixViewHandler, matrixRep)
Expand All @@ -18,7 +19,7 @@ import Data.Newtype (unwrap)
import Data.Set (singleton) as S
import Data.Traversable (sequence, sequence_)
import Data.Tuple (fst, uncurry)
import DataType (cBarChart, cCons, cLineChart, cNil)
import DataType (cBarChart, cBubbleChart, cCons, cLineChart, cNil)
import Desugarable (desug)
import Dict (get)
import Effect (Effect)
Expand Down Expand Up @@ -50,12 +51,14 @@ data View
| WaterTableView WaterTable
| LineChartFig LineChart
| BarChartFig BarChart
| BubbleChartFig BubbleChart

drawView :: HTMLId -> OnSel -> Int -> View -> Effect Unit
drawView divId onSel n (MatrixFig vw) = drawMatrix divId n vw =<< eventListener (onSel <<< matrixViewHandler)
drawView divId onSel n (WaterTableView vw) = drawTable divId n vw =<< eventListener (onSel <<< tableViewHandler)
drawView divId onSel n (LineChartFig vw) = drawLineChart divId n vw =<< eventListener (onSel <<< lineChartHandler)
drawView divId onSel n (BarChartFig vw) = drawBarChart divId n vw =<< eventListener (onSel <<< barChartHandler)
drawView divId onSel n (BubbleChartFig vw) = drawBubbleChart divId n vw =<< eventListener (onSel <<< bubbleChartHandler)

-- Convert sliced value to appropriate View, discarding top-level annotations for now.
-- 'from' is partial; encapsulate that here.
Expand All @@ -64,6 +67,8 @@ view _ (Constr _ c (u1 : Nil)) | c == cBarChart =
BarChartFig (unsafePartial $ record from u1)
view _ (Constr _ c (u1 : Nil)) | c == cLineChart =
LineChartFig (unsafePartial $ record from u1)
view _ (Constr _ c (u1 : Nil)) | c == cBubbleChart =
BubbleChartFig (unsafePartial $ record from u1)
view title u@(Constr _ c _) | c == cNil || c == cCons =
WaterTableView (WaterTable { title, table: unsafePartial $ record waterRecord <$> from u })
view title u@(Matrix _ _) =
Expand Down
3 changes: 2 additions & 1 deletion src/DataType.purs
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ f_name = "name" :: FieldName
f_plots = "plots" :: FieldName
f_x = "x" :: FieldName
f_y = "y" :: FieldName
f_z = "z" :: FieldName

dataTypes :: List DataType
dataTypes = L.fromFoldable
Expand Down Expand Up @@ -146,7 +147,7 @@ dataTypes = L.fromFoldable
]
, dataType "Plot"
[ cBarChart × 1 -- Record<caption: Str, data: List<Record<x: Str, y: Float>>>
, cBubbleChart × 1 -- Record<caption: Str>
, cBubbleChart × 1 -- Record<caption: Str, data: List<Record<x: Number, y: Number, z: Number>>>
, cLineChart × 1 -- Record<caption: Str, plots: List<LinePlot>>
, cLinePlot × 1 -- Record<name: Str, data: List<Record<x: Float, y: Float>>>
]
Expand Down

0 comments on commit 39578a2

Please sign in to comment.