Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Histogram and Scatterplot visualizations support for Table #1608

Merged
merged 5 commits into from
Mar 25, 2021
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
29 changes: 29 additions & 0 deletions distribution/std-lib/Standard/src/Visualization/Helpers.enso
Original file line number Diff line number Diff line change
@@ -1,8 +1,37 @@
from Standard.Base import all

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

## PRIVATE
Any.catch_ ~val = this.catch (_-> val)
iamrecursion marked this conversation as resolved.
Show resolved Hide resolved

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

## PRIVATE
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 after other columns.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please wrap this at 80 rather than on sentences.

Why the decision to place index columns after the other columns? My instinct would've been to put them before.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Incidentally this is the order that scatter plot logic wants them in such order. Then histogram needs all columns in any order —so I've put method with this order to helpers, rather then implement both variants.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But I will split them.

Table.Table.all_columns : Vector
Table.Table.all_columns =
index = this.index.catch (_-> [])
index_columns = case index of
Vector.Vector _ -> index
_ -> [this.index]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
_ -> [this.index]
a -> [a]

this.columns + index_columns


## PRIVATE

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

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As a general comment for this file, you could define most of these as extension methods which would be much more idiomatic.


from Standard.Table.Data.Column import Column

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

## PRIVATE
Get first numeric column of the table.
first_numeric : Table -> Column ! Nothing
first_numeric table = table.columns.find _.is_numeric
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not make this an extension method on Table?


## PRIVATE
Get the value column - the column that will be used to create histogram.
value_column : Table -> Column ! Nothing
value_column table =
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This too.

named_col = table.at 'value'
named_col.catch_ <| here.first_numeric table

## 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.
from_table : Object
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Type signature is wrong.

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 : Vector -> Update
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wrong type signature?

from_table table =
col = here.value_column table
label = col.name.catch_ Nothing
values = col.to_vector.catch_ []
Update values label

## PRIVATE
from_value : Vector -> Update
from_vector vector =
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wrong type signature.

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
146 changes: 146 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,146 @@
from Standard.Base import all
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're missing type signatures for most of this file. Also, many things here could be done as extension methods rather than module-level statics. Again, the former is more idiomatic.


from Standard.Table.Data.Column import 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 = 'index'

## PRIVATE
data_field = 'data'

## PRIVATE
axis_field = 'axis'

## PRIVATE
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 = [X,Y,Color,Shape,Label,Size]

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

## PRIVATE
is_recognized column = this.recognized_names.contains column.name

## PRIVATE
name = this.to_text.to_lower_case
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
name = this.to_text.to_lower_case
name = this.to_text.to_lower_case


## PRIVATE
fallback_column table = case this of
X -> table.index.catch_ <| this.iota table.row_count
Y ->
x_column = X.lookup 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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Everywhere else we don't align assignments.


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 : Table -> Column
lookup table =
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

rename to lookup_in

named = table.at this.name
named.catch_ <| this.fallback_column table

## PRIVATE
Generates JSON that describes points data.
data_from_table : Table -> Object
data_from_table df =
# IO.println <| "data from table: " + df.to_text
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

commented code

get_point_data field = field.lookup df . rename field.name
is_valid column = column.is_error.not
columns = PointData.all_fields.map get_point_data . filter is_valid
(0.up_to (df.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.
axes_from_table : Table -> Object
axes_from_table table =
describe_axis field =
col_name = field.lookup table . 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 && (table.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
data_from_vector : Vector -> Object
data_from_vector vec =
vec.map_with_index <| i-> elem->
Json.from_pairs [[X.name,i],[Y.name,elem]]

## PRIVATE
json_from_table table =
data = here.data_from_table table
# IO.println <| "data: " + data.to_text
axes = here.axes_from_table table
# IO.println <| "axes: " + axes.to_text
Json.from_pairs <| [[here.data_field,data], [here.axis_field, axes]]

## PRIVATE
json_from_vector vec =
data = [here.data_field,here.data_from_vector vec]
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]
35 changes: 35 additions & 0 deletions test/Visualization_Tests/src/Helpers_Spec.enso
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
from Standard.Base import all

from Standard.Table.Data.Table import Table
from Standard.Table.Data.Column import Column
import Standard.Test
import Standard.Visualization.Helpers
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Qualified imports should be above unqualified imports, separated by a line.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we have any written coding guidelines for Enso code?


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 ['b','a']
53 changes: 53 additions & 0 deletions test/Visualization_Tests/src/Histogram_Spec.enso
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
from Standard.Base import all

from Standard.Table.Data.Table import Table
from Standard.Table.Data.Column import Column
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 '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