Skip to content

Commit

Permalink
Histogram and Scatterplot visualizations support for Table (#1608)
Browse files Browse the repository at this point in the history
  • Loading branch information
mwu-tow authored Mar 25, 2021
1 parent 78ab5ee commit 5b57960
Show file tree
Hide file tree
Showing 9 changed files with 466 additions and 0 deletions.
2 changes: 2 additions & 0 deletions .github/workflows/scala.yml
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,7 @@ jobs:
$ENGINE_DIST_DIR/bin/enso --run test/Table_Tests
$ENGINE_DIST_DIR/bin/enso --run test/Database_Tests
$ENGINE_DIST_DIR/bin/enso --run test/Geo_Tests
$ENGINE_DIST_DIR/bin/enso --run test/Visualization_Tests
- name: Test Engine Distribution (Windows)
shell: bash
Expand All @@ -245,6 +246,7 @@ jobs:
$ENGINE_DIST_DIR/bin/enso.bat --run test/Table_Tests
$ENGINE_DIST_DIR/bin/enso.bat --run test/Database_Tests
$ENGINE_DIST_DIR/bin/enso.bat --run test/Geo_Tests
$ENGINE_DIST_DIR/bin/enso.bat --run test/Visualization_Tests
# Publish
- name: Publish the Engine Distribution Artifact
Expand Down
32 changes: 32 additions & 0 deletions distribution/std-lib/Standard/src/Visualization/Helpers.enso
Original file line number Diff line number Diff line change
@@ -1,8 +1,40 @@
from Standard.Base import all

import Standard.Table.Data.Column
import Standard.Table.Data.Storage
import Standard.Table.Data.Table

## PRIVATE
Any.catch_: Any -> Any
Any.catch_ ~val = this.catch (_-> val)

## PRIVATE
Any.catch_ : Any -> Any
Error.catch_ ~val = this.catch (_-> val)

## PRIVATE
recover_errors : Any -> Any
recover_errors ~body =
result = Panic.recover body
result.catch err->
Json.from_pairs [["error", err.to_display_text]] . to_text

## PRIVATE

Returns all the columns in the table, including indices.
Index columns are placed before other columns.
Table.Table.all_columns : Vector
Table.Table.all_columns =
index = this.index.catch_ []
index_columns = case index of
Vector.Vector _ -> index
a -> [a]
index_columns + this.columns


## PRIVATE

Checks if the column stores numbers.
Column.Column.is_numeric : Boolean
Column.Column.is_numeric =
[Storage.Integer,Storage.Decimal].contains this.storage_type
69 changes: 69 additions & 0 deletions distribution/std-lib/Standard/src/Visualization/Histogram.enso
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
from Standard.Base import all

import Standard.Table.Data.Column
import Standard.Table.Data.Table
import Standard.Visualization.Helpers

## PRIVATE
Get first numeric column of the table.
Table.Table.first_numeric : Table -> Column ! Nothing
Table.Table.first_numeric = this.all_columns.find _.is_numeric

## PRIVATE
Get the value column - the column that will be used to create histogram.
Table.Table.value_column : Table -> Column ! Nothing
Table.Table.value_column =
named_col = this.at 'value'
named_col.catch_ <| this.first_numeric

## PRIVATE
Information that are placed in an update sent to a visualization.
type Update
## PRIVATE
type Update values label

## PRIVATE
Generate JSON that can be consumed by the visualization.
to_json : Object
to_json =
data = ['data', Json.from_pairs [['values', this.values]]]
axis = ['axis', Json.from_pairs [['x', Json.from_pairs [['label', this.label]]]]]
ret_pairs = case this.label of
Nothing -> [data]
_ -> [axis,data]
Json.from_pairs ret_pairs

## PRIVATE
from_table : Table -> Update
from_table table =
col = table.value_column
label = col.name.catch_ Nothing
values = col.to_vector.catch_ []
Update values label

## PRIVATE
from_vector : Vector -> Update
from_vector vector =
Update vector Nothing

## PRIVATE
from_value : Any -> Update
from_value value =
case value of
Table.Table _ -> here.from_table value
Vector.Vector _ -> here.from_vector value
Column.Column _ -> here.from_table value.to_table
_ -> here.from_vector value.to_vector

## PRIVATE

Default preprocessor for the histogram visualization.

Generates JSON text describing the histogram visualization.

Arguments:
- value: the value to be visualized.
process_to_json_text : Any -> Text
process_to_json_text value =
update = here.from_value value
update.to_json.to_text
151 changes: 151 additions & 0 deletions distribution/std-lib/Standard/src/Visualization/Scatter_Plot.enso
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
from Standard.Base import all

import Standard.Table.Data.Column
import Standard.Table.Data.Table
import Standard.Visualization.Helpers

## PRIVATE
Name of the index column that may be generated to plot against.
index_name : Text
index_name = 'index'

## PRIVATE
data_field : Text
data_field = 'data'

## PRIVATE
axis_field : Text
axis_field = 'axis'

## PRIVATE
label_field : Text
label_field = 'label'

## PRIVATE
Represents a recognized point data field for a scatter plot visualization.
type PointData
## PRIVATE
type PointData
## PRIVATE
type X
## PRIVATE
type Y
## PRIVATE
type Color
## PRIVATE
type Shape
## PRIVATE
type Label
## PRIVATE
type Size

## PRIVATE
Returns all recognized point data fields.
all_fields : Vector
all_fields = [X,Y,Color,Shape,Label,Size]

## PRIVATE
recognized_names : Vector
recognized_names = this.all_fields.map _.name

## PRIVATE
is_recognized : Column -> Boolean
is_recognized column = this.recognized_names.contains column.name

## PRIVATE
name : Text
name = this.to_text.to_lower_case

## PRIVATE
fallback_column : Table -> Column ! Nothing
fallback_column table = case this of
X -> table.index.catch_ <| this.iota table.row_count
Y ->
x_column = X.lookup_in table
candidates = table.all_columns
is_good_enough c = c.is_numeric && c.name != x_column.name
is_good c = is_good_enough c && (this.is_recognized c).not

candidates.find is_good . catch_ <| candidates.find is_good_enough
_ -> Error.throw Nothing

## PRIVATE
Returns a vector of subsequent integers beginning from 0.
iota : Number -> Vector
iota count =
# FIXME [mwu]: Adjust once https://github.com/enso-org/enso/issues/1439
# is addressed.
range = 0.up_to <| count + 1
Column.from_vector here.index_name range.to_vector

## PRIVATE
lookup_in : Table -> Column
lookup_in table =
named = table.at this.name
named.catch_ <| this.fallback_column table

## PRIVATE
Generates JSON that describes points data.
Table.Table.point_data : Table -> Object
Table.Table.point_data =
get_point_data field = field.lookup_in this . rename field.name
is_valid column = column.is_error.not
columns = PointData.all_fields.map get_point_data . filter is_valid
(0.up_to <| this.row_count + 1).to_vector.map <| row_n->
pairs = columns.map column->
value = column.at row_n . catch_ Nothing
[column.name, value]
Json.from_pairs pairs

## PRIVATE
Generates JSON that describes plot axes.
Table.Table.axes : Table -> Object
Table.Table.axes =
describe_axis field =
col_name = field.lookup_in this . name
label = Json.from_pairs [[here.label_field, col_name]]
[field.name, label]
x_axis = describe_axis X
y_axis = describe_axis Y
is_valid axis_pair =
label = axis_pair.at 1
label.is_error.not && (this.all_columns.length > 0)
axes_obj = Json.from_pairs <| [x_axis, y_axis].filter is_valid
if axes_obj.fields.size > 0 then axes_obj else Nothing

## PRIVATE
Vector.Vector.point_data : Vector -> Object
Vector.Vector.point_data =
this.map_with_index <| i-> elem->
Json.from_pairs [[X.name,i],[Y.name,elem]]

## PRIVATE

json_from_table table =
data = table.point_data
axes = table.axes
Json.from_pairs <| [[here.data_field,data], [here.axis_field, axes]]

## PRIVATE
json_from_vector vec =
data = [here.data_field, vec.point_data]
axes = [here.axis_field, Nothing]
Json.from_pairs [data, axes]

## PRIVATE

Default preprocessor for the scatterplot visualization.

Generates JSON text describing the scatterplot visualization.

Arguments:
- value: the value to be visualized.
process_to_json_text : Any -> Text
process_to_json_text value =
json = case value of
Column.Column _ -> here.json_from_table value.to_table
Table.Table _ -> here.json_from_table value
Vector.Vector _ -> here.json_from_vector value
_ -> here.json_from_vector value.to_vector

json.to_text
6 changes: 6 additions & 0 deletions test/Visualization_Tests/package.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
name: Visualization_Tests
version: 0.0.1
enso-version: default
license: MIT
author: [email protected]
maintainer: [email protected]
34 changes: 34 additions & 0 deletions test/Visualization_Tests/src/Helpers_Spec.enso
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
from Standard.Base import all

import Standard.Table.Data.Table
import Standard.Test
import Standard.Visualization.Helpers

import Visualization_Tests

spec =
Test.group "Table.all_columns" <|
Test.specify "works with empty table" <|
table = Table.from_rows [] []
table.all_columns.map (_.name) . should_equal []

Test.specify "works when there is no index set" <|
header = ['a', 'b']
row_1 = [11 , 10 ]
row_2 = [21 , 20 ]
table = Table.from_rows header [row_1, row_2]
table.all_columns.map (_.name) . should_equal ['a','b']

Test.specify "works when there is nothing but index" <|
header = ['a']
row_1 = [11 ]
row_2 = [21 ]
table = Table.from_rows header [row_1, row_2]
table.all_columns.map (_.name) . should_equal ['a']

Test.specify "includes both normal and index columns" <|
header = ['a', 'b']
row_1 = [11 , 10 ]
row_2 = [21 , 20 ]
table = Table.from_rows header [row_1, row_2] . set_index 'a'
table.all_columns.map (_.name) . should_equal ['a','b']
60 changes: 60 additions & 0 deletions test/Visualization_Tests/src/Histogram_Spec.enso
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
from Standard.Base import all

import Standard.Table.Data.Column
import Standard.Table.Data.Table
import Standard.Test
import Standard.Visualization.Histogram

import Visualization_Tests

spec =
expect value expected_label expected_values =
text = Histogram.process_to_json_text value
json = Json.parse text
expected_data = Json.from_pairs [['values', expected_values]]
expected_json = case expected_label of
Nothing -> Json.from_pairs [['data', expected_data]]
_ ->
expected_x = Json.from_pairs [['label', expected_label]]
expected_axis = ['axis', Json.from_pairs [['x', expected_x]]]
Json.from_pairs [['data', expected_data], expected_axis]
json.should_equal expected_json

Test.group "Histogram Visualization" <|

Test.specify "deals with an empty table" <|
table = Table.from_rows [] []
expect table Nothing []

Test.specify "plots first column if none recognized" <|
header = ['α', 'ω']
row_1 = [11 , 10 ]
row_2 = [21 , 20 ]
table = Table.from_rows header [row_1, row_2]
expect table 'α' [11,21]

Test.specify "plots first column if none recognized even if index" <|
header = ['α']
row_1 = [11 ]
row_2 = [21 ]
table = Table.from_rows header [row_1, row_2] . set_index 'α'
expect table 'α' [11,21]

Test.specify "plots 'value' numeric column if present" <|
header = ['α', 'value']
row_1 = [11 , 10 ]
row_2 = [21 , 20 ]
table = Table.from_rows header [row_1, row_2]
expect table 'value' [10,20]

Test.specify "plots column" <|
column = Column.from_vector 'my_name' [1,4,6]
expect column 'my_name' [1,4,6]

Test.specify "plots vector" <|
vector = [1,2,3]
expect vector Nothing vector

Test.specify "plots range" <|
vector = 2.up_to 4
expect vector Nothing [2,3,4]
12 changes: 12 additions & 0 deletions test/Visualization_Tests/src/Main.enso
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
from Standard.Base import all

import Standard.Test

import Visualization_Tests.Helpers_Spec
import Visualization_Tests.Histogram_Spec
import Visualization_Tests.Scatter_Plot_Spec

main = Test.Suite.runMain <|
Helpers_Spec.spec
Histogram_Spec.spec
Scatter_Plot_Spec.spec
Loading

0 comments on commit 5b57960

Please sign in to comment.