diff --git a/src/runtime-arr/chart.arr b/src/runtime-arr/chart.arr index b0931a2ae4..5362b14336 100644 --- a/src/runtime-arr/chart.arr +++ b/src/runtime-arr/chart.arr @@ -3,21 +3,18 @@ provide-types * import global as G import chart-lib as CL +import either as E import image as IM import list as L import option as O include from O: type Option end -################################################################################ -# CONSTANTS -################################################################################ - - ################################################################################ # TYPE SYNONYMS ################################################################################ +type PlottableFunction = (Number -> Number) type Posn = RawArray type TableIntern = RawArray> @@ -25,8 +22,13 @@ type TableIntern = RawArray> # HELPERS ################################################################################ +fun check-num(v :: Number) -> Nothing: nothing end +fun check-string(v :: String) -> Nothing: nothing end + posn = {(x :: Number, y :: Number): [G.raw-array: x, y]} +unsafe-equal = {(x :: Number, y :: Number): (x <= y) and (y <= x)} + fun map2(xs :: L.List, ys :: L.List): L.map2({(x, y): [G.raw-array: x, y]}, xs, ys) end @@ -39,12 +41,11 @@ fun to-table2(xs :: L.List, ys :: L.List) -> TableIntern: L.to-raw-array(L.map2({(x, y): [G.raw-array: x, y]}, xs, ys)) end -# TODO(tiffany): add in get-vs-from-img after VS is implemented - -################################################################################ -# METHODS -################################################################################ +fun to-table3(xs :: L.List, ys :: L.List, zs :: L.List) -> TableIntern: + L.to-raw-array(L.map3({(x, y, z): [G.raw-array: x, y, z]}, xs, ys, zs)) +end +# TODO(tiffany): add in get-vs-from-img after VS is implemented ################################################################################ # BOUNDING BOX @@ -90,10 +91,44 @@ fun get-bounding-box(ps :: L.List) -> BoundingBox: end end +fun merge-bounding-box(bs :: L.List) -> BoundingBox: + for L.fold(prev from default-bounding-box, e from bs): + ask: + | e.is-valid and prev.is-valid then: + default-bounding-box.{ + x-min: G.num-min(e.x-min, prev.x-min), + x-max: G.num-max(e.x-max, prev.x-max), + y-min: G.num-min(e.y-min, prev.y-min), + y-max: G.num-max(e.y-max, prev.y-max), + is-valid: true, + } + | e.is-valid then: e + | prev.is-valid then: prev + | otherwise: default-bounding-box + end + end +end + ################################################################################ # DEFAULT VALUES ################################################################################ +type BoxChartSeries = { + tab :: TableIntern, + height :: Number, + horizontal :: Boolean +} + +default-box-plot-series = { + horizontal: false +} + +type PieChartSeries = { + tab :: TableIntern, +} + +default-pie-chart-series = {} + type BarChartSeries = { tab :: TableIntern, legends :: RawArray, @@ -102,6 +137,54 @@ type BarChartSeries = { default-bar-chart-series = {} +type HistogramSeries = { + tab :: TableIntern, + bin-width :: Option, + max-num-bins :: Option, + min-num-bins :: Option, +} + +default-histogram-series = { + bin-width: O.none, + max-num-bins: O.none, + min-num-bins: O.none, +} + +type LinePlotSeries = { + ps :: L.List, + color :: Option, + legend :: String, +} + +default-line-plot-series = { + color: O.none, + legend: '', +} + +type ScatterPlotSeries = { + ps :: L.List, + color :: Option, + legend :: String, + point-size :: Number, +} + +default-scatter-plot-series = { + color: O.none, + legend: '', + point-size: 7, +} + +type FunctionPlotSeries = { + f :: PlottableFunction, + color :: Option, + legend :: String, +} + +default-function-plot-series = { + color: O.none, + legend: '', +} + ########### type ChartWindowObject = { @@ -118,6 +201,29 @@ default-chart-window-object :: ChartWindowObject = { render: method(self): G.raise('unimplemented') end, } +type BoxChartWindowObject = { + title :: String, + width :: Number, + height :: Number, + x-axis :: String, + y-axis :: String, + render :: ( -> IM.Image), +} + +default-box-plot-chart-window-object :: BoxChartWindowObject = default-chart-window-object.{ + x-axis: '', + y-axis: '', +} + +type PieChartWindowObject = { + title :: String, + width :: Number, + height :: Number, + render :: ( -> IM.Image), +} + +default-pie-chart-window-object :: PieChartWindowObject = default-chart-window-object + type BarChartWindowObject = { title :: String, width :: Number, @@ -136,14 +242,110 @@ default-bar-chart-window-object :: BarChartWindowObject = default-chart-window-o y-max: O.none, } +type HistogramChartWindowObject = { + title :: String, + width :: Number, + height :: Number, + render :: ( -> IM.Image), + x-axis :: String, + y-axis :: String, + x-min :: Option, + x-max :: Option, + y-max :: Option, +} + +default-histogram-chart-window-object :: HistogramChartWindowObject = + default-chart-window-object.{ + x-axis: '', + y-axis: '', + x-min: O.none, + x-max: O.none, + y-max: O.none, + } + +type PlotChartWindowObject = { + title :: String, + width :: Number, + height :: Number, + render :: ( -> IM.Image), + x-axis :: String, + y-axis :: String, + x-min :: Option, + x-max :: Option, + x-max :: Option, + y-max :: Option, + num-samples :: Number, +} + +default-plot-chart-window-object :: PlotChartWindowObject = default-chart-window-object.{ + x-axis: '', + y-axis: '', + x-min: O.none, + x-max: O.none, + y-min: O.none, + y-max: O.none, + num-samples: 1000, +} + ################################################################################ # DATA DEFINITIONS ################################################################################ data DataSeries: + | line-plot-series(obj :: LinePlotSeries) with: + is-single: false, + color: method(self, color :: IM.Color): + line-plot-series(self.obj.{color: O.some(color)}) + end, + legend: method(self, legend :: String): + line-plot-series(self.obj.{legend: legend}) + end, + | scatter-plot-series(obj :: ScatterPlotSeries) with: + is-single: false, + color: method(self, color :: IM.Color): + scatter-plot-series(self.obj.{color: O.some(color)}) + end, + legend: method(self, legend :: String): + scatter-plot-series(self.obj.{legend: legend}) + end, + point-size: method(self, point-size :: Number): + scatter-plot-series(self.obj.{point-size: point-size}) + end, + | function-plot-series(obj :: FunctionPlotSeries) with: + is-single: false, + color: method(self, color :: IM.Color): + function-plot-series(self.obj.{color: O.some(color)}) + end, + legend: method(self, legend :: String): + function-plot-series(self.obj.{legend: legend}) + end, + | pie-chart-series(obj :: PieChartSeries) with: + is-single: true, | bar-chart-series(obj :: BarChartSeries) with: is-single: true, - constr: {(): bar-chart-series}, + # TODO(tiffany): add box-plot-series in by adding a | + # box-plot-series(obj :: BoxChartSeries) with: + # is-single: true, + # horizontal: method(self, h): + # box-plot-series(self.obj.{horizontal: h}) + # end + | histogram-series(obj :: HistogramSeries) with: + is-single: true, + method bin-width(self, bin-width :: Number): + histogram-series(self.obj.{bin-width: O.some(bin-width)}) + end, + method max-num-bins(self, max-num-bins :: Number): + histogram-series(self.obj.{max-num-bins: O.some(max-num-bins)}) + end, + method min-num-bins(self, min-num-bins :: Number): + histogram-series(self.obj.{min-num-bins: O.some(min-num-bins)}) + end, + method num-bins(self, num-bins :: Number): + histogram-series(self.obj.{ + min-num-bins: O.some(num-bins), + max-num-bins: O.some(num-bins) + }) + end, # TODO(tiffany): add _output and test get-vs-from-img after VS is implemented end @@ -156,30 +358,79 @@ fun check-chart-window(p :: ChartWindowObject) -> Nothing: end data ChartWindow: + | pie-chart-window(obj :: PieChartWindowObject) with: + title: method(self, title :: String): pie-chart-window(self.obj.{title: title}) end, + width: method(self, width :: Number): pie-chart-window(self.obj.{width: width}) end, + height: method(self, height :: Number): pie-chart-window(self.obj.{height: height}) end, + display: method(self): + _ = check-chart-window(self.obj) + self.obj.{interact: true}.render() + end, + get-image: method(self): + _ = check-chart-window(self.obj) + self.obj.{interact: false}.render() + end, + # TODO(tiffany): add box-plot-chart-window in with a | + # box-plot-chart-window(obj :: BoxChartWindowObject) with: + # x-axis: method(self, x-axis :: String): box-plot-chart-window(self.obj.{x-axis: x-axis}) end, + # y-axis: method(self, y-axis :: String): box-plot-chart-window(self.obj.{y-axis: y-axis}) end, + # title: method(self, title :: String): box-plot-chart-window(self.obj.{title: title}) end, + # width: method(self, width :: Number): box-plot-chart-window(self.obj.{width: width}) end, + # height: method(self, height :: Number): box-plot-chart-window(self.obj.{height: height}) end, + # display: method(self): + # _ = check-chart-window(self.obj) + # self.obj.{interact: true}.render() + # end, + # get-image: method(self): + # _ = check-chart-window(self.obj) + # self.obj.{interact: false}.render() + # end, | bar-chart-window(obj :: BarChartWindowObject) with: - constr: {(): bar-chart-window}, - x-axis: method(self, x-axis :: String): self.constr()(self.obj.{x-axis: x-axis}) end, - y-axis: method(self, y-axis :: String): self.constr()(self.obj.{y-axis: y-axis}) end, - y-min: method(self, y-min :: Number): self.constr()(self.obj.{y-min: O.some(y-min)}) end, - y-max: method(self, y-max :: Number): self.constr()(self.obj.{y-max: O.some(y-max)}) end, -sharing: - method display(self): - _ = check-chart-window(self.obj) - self.obj.{interact: true}.render() - end, - method get-image(self): - _ = check-chart-window(self.obj) - self.obj.{interact: false}.render() - end, - method title(self, title :: String): - self.constr()(self.obj.{title: title}) - end, - method width(self, width :: Number): - self.constr()(self.obj.{width: width}) - end, - method height(self, height :: Number): - self.constr()(self.obj.{height: height}) - end, + x-axis: method(self, x-axis :: String): bar-chart-window(self.obj.{x-axis: x-axis}) end, + y-axis: method(self, y-axis :: String): bar-chart-window(self.obj.{y-axis: y-axis}) end, + y-min: method(self, y-min :: Number): bar-chart-window(self.obj.{y-min: O.some(y-min)}) end, + y-max: method(self, y-max :: Number): bar-chart-window(self.obj.{y-max: O.some(y-max)}) end, + title: method(self, title :: String): bar-chart-window(self.obj.{title: title}) end, + width: method(self, width :: Number): bar-chart-window(self.obj.{width: width}) end, + height: method(self, height :: Number): bar-chart-window(self.obj.{height: height}) end, + display: method(self): + _ = check-chart-window(self.obj) + self.obj.{interact: true}.render() + end, + get-image: method(self): + _ = check-chart-window(self.obj) + self.obj.{interact: false}.render() + end, + | histogram-chart-window(obj :: HistogramChartWindowObject) with: + x-axis: method(self, x-axis :: String): histogram-chart-window(self.obj.{x-axis: x-axis}) end, + y-axis: method(self, y-axis :: String): histogram-chart-window(self.obj.{y-axis: y-axis}) end, + x-min: method(self, x-min :: Number): histogram-chart-window(self.obj.{x-min: O.some(x-min)}) end, + x-max: method(self, x-max :: Number): histogram-chart-window(self.obj.{x-max: O.some(x-max)}) end, + y-max: method(self, y-max :: Number): histogram-chart-window(self.obj.{y-max: O.some(y-max)}) end, + | plot-chart-window(obj :: PlotChartWindowObject) with: + x-axis: method(self, x-axis :: String): plot-chart-window(self.obj.{x-axis: x-axis}) end, + y-axis: method(self, y-axis :: String): plot-chart-window(self.obj.{y-axis: y-axis}) end, + x-min: method(self, x-min :: Number): plot-chart-window(self.obj.{x-min: O.some(x-min)}) end, + x-max: method(self, x-max :: Number): plot-chart-window(self.obj.{x-max: O.some(x-max)}) end, + y-min: method(self, y-min :: Number): plot-chart-window(self.obj.{y-min: O.some(y-min)}) end, + y-max: method(self, y-max :: Number): plot-chart-window(self.obj.{y-max: O.some(y-max)}) end, + num-samples: method(self, num-samples :: Number) block: + when (num-samples <= 0) or (num-samples > 100000) or G.not(G.num-is-integer(num-samples)): + G.raise('num-samples: value must be an ineger between 1 and 100000') + end + plot-chart-window(self.obj.{num-samples: num-samples}) + end, + title: method(self, title :: String): plot-chart-window(self.obj.{title: title}) end, + width: method(self, width :: Number): plot-chart-window(self.obj.{width: width}) end, + height: method(self, height :: Number): plot-chart-window(self.obj.{height: height}) end, + display: method(self): + _ = check-chart-window(self.obj) + self.obj.{interact: true}.render() + end, + get-image: method(self): + _ = check-chart-window(self.obj) + self.obj.{interact: false}.render() + end, # TODO(tiffany): add _output and test get-vs-from-img after VS is implemented end @@ -187,6 +438,58 @@ end # FUNCTIONS ################################################################################ +fun function-plot-from-list(f :: PlottableFunction) -> DataSeries: + function-plot-series(default-function-plot-series.{ + f: f, + }) +end + +fun line-plot-from-list(xs :: L.List, ys :: L.List) -> DataSeries block: + when L.length(xs) <> L.length(ys): + G.raise('line-plot: xs and ys should have the same length') + end + L.each(check-num, xs) + L.each(check-num, ys) + line-plot-series(default-line-plot-series.{ + ps: L.map2({(x, y): [G.raw-array: x, y]}, xs, ys) + }) +end + +fun scatter-plot-from-list(xs :: L.List, ys :: L.List) -> DataSeries block: + when L.length(xs) <> L.length(ys): + G.raise('scatter-plot: xs and ys should have the same length') + end + L.each(check-num, xs) + L.each(check-num, ys) + scatter-plot-series(default-scatter-plot-series.{ + ps: L.map2({(x, y): [G.raw-array: x, y]}, xs, ys) + }) +end + +# TODO(tiffany): implement labeled-scatter-plot-from-list + +# TODO(tiffany): implement exploding-pie-chart-from-list + +fun pie-chart-from-list(labels :: L.List, values :: L.List) -> DataSeries block: + doc: ``` + Consume labels, a list of string, and values, a list of numbers + and construct a pie chart + ``` + label-length = L.length(labels) + value-length = L.length(values) + when label-length <> value-length: + G.raise('pie-chart: labels and values should have the same length') + end + when label-length == 0: + G.raise('pie-chart: need at least one data') + end + L.each(check-num, values) + L.each(check-string, labels) + pie-chart-series(default-pie-chart-series.{ + tab: to-table2(labels, values) + }) +end + fun bar-chart-from-list(labels :: L.List, values :: L.List) -> DataSeries block: doc: ``` Consume labels, a list of string, and values, a list of numbers @@ -197,9 +500,8 @@ fun bar-chart-from-list(labels :: L.List, values :: L.List) -> D when label-length <> value-length: G.raise('bar-chart: labels and values should have the same length') end - # TODO(tiffany): uncomment after implementing each - #values.each(check-num) - #labels.each(check-string) + L.each(check-num, values) + L.each(check-string, labels) bar-chart-series(default-bar-chart-series.{ tab: to-table2(labels, values), legends: [G.raw-array: ''], @@ -207,6 +509,26 @@ fun bar-chart-from-list(labels :: L.List, values :: L.List) -> D }) end +# TODO(tiffany): implement grouped-bar-chart-from-list + +# TODO(tiffany): implement box-plot-from-list + +# TODO(tiffany): implement labeled-box-plot-from-list + +# TODO(tiffany): implement freq-bar-chart-from-list + +fun histogram-from-list(values :: L.List) -> DataSeries block: + doc: ``` + Consume a list of numbers and construct a histogram + ``` + L.each(check-num, values) + histogram-series(default-histogram-series.{ + tab: to-table2(L.map({(_): ''}, values), values), + }) +end + +# TODO(tiffany): implement labeled-histogram-from-list + ################################################################################ # PLOTS ################################################################################ @@ -214,6 +536,14 @@ end fun render-chart(s :: DataSeries) -> ChartWindow: doc: 'Render it!' cases (DataSeries) s: + # TODO(tiffany): fix scatter-plot-series + | line-plot-series(_) => plot-chart-window(default-plot-chart-window-object) + | function-plot-series(_) => plot-chart-window(default-plot-chart-window-object) + | scatter-plot-series(_) => plot-chart-window(default-plot-chart-window-object) + | pie-chart-series(obj) => + pie-chart-window(default-pie-chart-window-object.{ + render: method(self): CL.pie-chart(obj.tab) end + }) | bar-chart-series(obj) => bar-chart-window(default-bar-chart-window-object.{ render: method(self): @@ -233,8 +563,62 @@ fun render-chart(s :: DataSeries) -> ChartWindow: CL.bar-chart(obj.tab) end }) + # TODO(tiffany): implement CL.box-plot + # box-plot-series(obj) => + # box-plot-chart-window(default-box-plot-chart-window-object.{ + # render: method(self): + # CL.box-plot(self, obj) + # end + # }) + | histogram-series(obj) => + histogram-chart-window(default-histogram-chart-window-object.{ + render: method(self): + shadow self = self.{y-min: O.none} + _ = cases (Option) self.x-min: + | some(x-min) => + cases (Option) self.x-max: + | some(x-max) => + if x-min >= x-max: + G.raise("render: x-min must be strictly less than x-max") + else: + G.nothing + end + | else => G.nothing + end + | else => G.nothing + end + _ = cases (Option) self.y-min: + | some(y-min) => + cases (Option) self.y-max: + | some(y-max) => + if y-min >= y-max: + G.raise("render: y-min must be strictly less than y-max") + else: + G.nothing + end + | else => G.nothing + end + | else => G.nothing + end + CL.histogram(obj.tab) + end + }) end #where: # render-now = {(x): render-chart(x).get-image()} end + +# TODO(tiffany): implement inbound-x + +# TODO(tiffany): implement inbound-y + +# TODO(tiffany): implement inbound-xy + +# TODO(tiffany): implement dist + +# TODO(tiffany): implement nearest + +# TODO(tiffany): implement find-pt-on-edge + +# TODO(tiffany): implement line-plot-edge-cut diff --git a/src/runtime/chart-lib.arr.js b/src/runtime/chart-lib.arr.js index fb4f7fd42e..6ff0800a46 100644 --- a/src/runtime/chart-lib.arr.js +++ b/src/runtime/chart-lib.arr.js @@ -20,7 +20,6 @@ function checkColor(val) { /* @stopify flat */ function barChart(tableFromRawArray) { headers = ["",""]; - return { "$brand": "chart", "chartType": "BarChart", @@ -39,6 +38,16 @@ function barChartFromTable(table) { }; } +/* @stopify flat */ +function pieChart(tableFromRawArray) { + return { + "$brand": "chart", + "chartType": "PieChart", + "_headers": ["",""], + "_rows": tableFromRawArray + }; +} + /* @stopify flat */ function pieChartFromTable(table) { return { @@ -49,6 +58,16 @@ function pieChartFromTable(table) { }; } +/* @stopify flat */ +function histogram(tableFromRawArray) { + return { + "$brand": "chart", + "chartType": "Histogram", + "_headers": ["",""], + "_rows": tableFromRawArray + }; +} + /* @stopify flat */ function histogramFromTable(table) { return { @@ -89,9 +108,15 @@ return module.exports = { "bar-chart-from-table": /* @stopify flat */ function (table) { return barChartFromTable(table); }, + "pie-chart": /* @stopify flat */ function (tableFromRawArray) { + return pieChart(tableFromRawArray); + }, "pie-chart-from-table": /* @stopify flat */ function (table) { return pieChartFromTable(table); }, + "histogram": /* @stopify flat */ function (tableFromRawArray) { + return histogram(tableFromRawArray); + }, "histogram-from-table": /* @stopify flat */ function (table) { return histogramFromTable(table); }, diff --git a/src/runtime/chart-lib.arr.json b/src/runtime/chart-lib.arr.json index ef0ec1aa29..123f8ef891 100644 --- a/src/runtime/chart-lib.arr.json +++ b/src/runtime/chart-lib.arr.json @@ -19,7 +19,9 @@ "check-color": ["arrow", ["Any"], "Any"], "bar-chart": ["arrow", ["rOfR"], "Chart"], "bar-chart-from-table": ["arrow", ["Table"], "Chart"], + "pie-chart": ["arrow", ["rOfR"], "Chart"], "pie-chart-from-table": ["arrow", ["Table"], "Chart"], + "histogram": ["arrow", ["rOfR"], "Chart"], "histogram-from-table": ["arrow", ["Table"], "Chart"], "scatter-chart-from-table": ["arrow", ["Table"], "Chart"], "line-chart-from-table": ["arrow", ["Table"], "Chart"] diff --git a/src/runtime/list.arr.js b/src/runtime/list.arr.js index 13a6c7271b..61a63a8348 100644 --- a/src/runtime/list.arr.js +++ b/src/runtime/list.arr.js @@ -20,6 +20,11 @@ module.exports = { } return arr; }, + 'each': function( fun, lst ) { + for (let i = 0; i < lst.length; i++) { + lst[i] = fun(lst[i]); + } + }, 'map': function( fun, list ) { return list.map( fun ); }, diff --git a/src/runtime/list.arr.json b/src/runtime/list.arr.json index 2eea1cc2be..2dbb99e5a7 100644 --- a/src/runtime/list.arr.json +++ b/src/runtime/list.arr.json @@ -21,6 +21,7 @@ "length": ["forall", ["a"], ["arrow", ["lOfA"], "Number"]], "contains": ["forall", ["a"], ["arrow", ["lOfA", "tva"], "Boolean"]], "to-raw-array": ["forall", ["a"], ["arrow", ["lOfA"], "rOfA"]], + "each": ["forall", ["a"], ["arrow", [["arrow", ["tva"], "Nothing"], "lOfA"], "Nothing"]], "map": ["forall", ["a", "b"], ["arrow", [["arrow", ["tva"], "tvb"], "lOfA"], "lOfB"]], "map2": ["forall", ["a", "b", "c"], ["arrow", [["arrow", ["tva", "tvb"], "tvc"], "lOfA", "lOfB"], "lOfC"]], "map3": ["forall", ["a", "b", "c", "d"], ["arrow", [["arrow", ["tva", "tvb", "tvc"], "tvd"], "lOfA", "lOfB", "lOfC"], "lOfD"]],