Case Insensitive Dataframe Support in Visualizations (#1634)

Ref https://github.com/enso-org/ide/issues/1391
This commit is contained in:
Michał Wawrzyniec Urbańczyk 2021-04-01 10:05:17 +02:00 committed by GitHub
parent 547db918e5
commit 8d77a565eb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 207 additions and 17 deletions

View File

@ -0,0 +1,35 @@
from Standard.Base import all
import Standard.Table.Data.Table
import Standard.Test
import Standard.Visualization.Helpers
## PRIVATE
Construct JSON describing table geo map visualization.
Arguments:
- table: the Table to be visualized.
json_from_table : Table.Table -> Object
json_from_table table =
names = ['label', 'latitude', 'longitude', 'radius', 'color']
pairs = names.filter_map <| name->
column = table.lookup_ignore_case name
column.when_valid ["df_" + name, column.to_vector]
Json.from_pairs pairs
## PRIVATE
Default preprocessor for the geo map visualization, generating JSON text
describing the geo map visualization.
Arguments:
- value: the value to be visualized.
process_to_json_text : Any -> Text
process_to_json_text value =
json = case value of
Table.Table _ -> here.json_from_table value
_ -> value.to_json
json.to_text

View File

@ -5,11 +5,85 @@ import Standard.Table.Data.Storage
import Standard.Table.Data.Table
## PRIVATE
Any.catch_: Any -> Any
Maps the vector using the given function. Filters out all error values.
Arguments:
- f: unary invokable that is applied to each vector element. Non-error
values are returned in the resulting vector. Error values are dropped.
Vector.Vector.filter_map : Any -> Vector
Vector.Vector.filter_map f = this.map f . filter .is_valid
## PRIVATE
Returns the given value if this is not an error. Propagates error otherwise.
Arguments:
- val: a value that will be evaluated and returned if `this` is an error.
Any.when_valid : Any -> Any
Any.when_valid ~val = this.map_valid (_-> val)
## PRIVATE
Returns the given value if this is not an error. Propagates error otherwise.
Arguments:
- val: a value that will be evaluated and returned if `this` is an error.
Error.when_valid : Any -> Any
Error.when_valid ~val = this.map_valid (_-> val)
## PRIVATE
Checks if the value is not an error.
Any.is_valid : Any
Any.is_valid = this.is_error.not
## PRIVATE
Checks if the value is not an error.
Error.is_valid : Any
Error.is_valid = this.is_error.not
## PRIVATE
Maps over non-error value.
Arguments:
- f: a function that will be used to generate return value from a non-error
`this` value.
Any.map_valid : Any -> Any
Any.map_valid f = f this
## PRIVATE
Maps over non-error value.
Arguments:
- _: a function that will be used to generate return value from a non-error
`this` value.
Error.map_valid : Any -> Any
Error.map_valid _ = this
## PRIVATE
Recovers from the error by returning the parameter value.
The error contents will be ignored.
Arguments:
- val: a value that will be evaluated and returned if `this` is an error.
Any.catch_ : Any -> Any
Any.catch_ ~val = this.catch (_-> val)
## PRIVATE
Any.catch_ : Any -> Any
Recovers from the error by returning the parameter value.
The error contents will be ignored.
Arguments:
- val: a value that will be evaluated and returned if `this` is an error.
Error.catch_ : Any -> Any
Error.catch_ ~val = this.catch (_-> val)
## PRIVATE
@ -21,7 +95,8 @@ recover_errors ~body =
## PRIVATE
Returns all the columns in the table, including indices.
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 =
@ -31,6 +106,19 @@ Table.Table.all_columns =
a -> [a]
index_columns + this.columns
## PRIVATE
Looks for a column by a given name.
Unlike `Table.at` looks into index columns and name comparison is case-insensitive.
Arguments:
- text: the case-insensitive name of the searched column.
Table.Table.lookup_ignore_case : Text -> Column ! Nothing
Table.Table.lookup_ignore_case name =
ret = this.all_columns.find <| col->
col.name.equals_ignore_case name
ret
## PRIVATE

View File

@ -13,8 +13,8 @@ Table.Table.first_numeric = this.all_columns.find _.is_numeric
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
named_col = this.lookup_ignore_case 'value'
named_col.catch_ this.first_numeric
## PRIVATE
Information that are placed in an update sent to a visualization.

View File

@ -81,7 +81,7 @@ type PointData
## PRIVATE
lookup_in : Table -> Column
lookup_in table =
named = table.at this.name
named = table.lookup_ignore_case this.name
named.catch_ <| this.fallback_column table
## PRIVATE
@ -89,8 +89,7 @@ type PointData
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
columns = PointData.all_fields.filter_map get_point_data
(0.up_to <| this.row_count + 1).to_vector.map <| row_n->
pairs = columns.map column->
value = column.at row_n . catch_ Nothing
@ -109,7 +108,7 @@ Table.Table.axes =
y_axis = describe_axis Y
is_valid axis_pair =
label = axis_pair.at 1
label.is_error.not && (this.all_columns.length > 0)
label.is_valid && (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

View File

@ -0,0 +1,36 @@
from Standard.Base import all
import Standard.Table.Data.Table
import Standard.Test
import Standard.Visualization.Geo_Map
import Visualization_Tests.Helpers
spec =
expect value expected_json_text =
result = Geo_Map.process_to_json_text value
Json.parse result . should_equal <| Json.parse expected_json_text
Test.group "Geo_Map" <|
Test.specify "works with empty table" <|
table = Table.from_rows [] []
expect table '{}'
Test.specify "skips unrecognized columns" <|
header = ['α' , 'β' , 'ω']
row_1 = [11 , 10 , 09 ]
row_2 = [21 , 20 , 19 ]
table = Table.from_rows header [row_1, row_2]
expect table '{}'
Test.specify "recognizes relevant columns" <|
header = ['latitude' , 'longitude' , 'color' , 'label' , 'radius']
row_1 = [11 , 10 , 'red' , 'name' , 195 ]
table = Table.from_rows header [row_1]
expect table '{"df_color":["red"],"df_label":["name"],"df_latitude":[11],"df_longitude":[10],"df_radius":[195]}'
Test.specify "is case insensitive" <|
header = ['latitude' , 'LONGITUDE' , 'LaBeL']
row_1 = [11 , 10 , 09 ]
row_2 = [21 , 20 , 19 ]
table = Table.from_rows header [row_1, row_2]
expect table '{"df_label":[9,19],"df_latitude":[11,21],"df_longitude":[10,20]}'

View File

@ -0,0 +1,9 @@
from Standard.Base import all
import Standard.Table.Data.Column
import Standard.Test
Column.Column.expect : Text -> Vector -> Test.Success
Column.Column.expect name contents =
this.name.should_equal name
this.to_vector.should_equal contents

View File

@ -3,8 +3,7 @@ from Standard.Base import all
import Standard.Table.Data.Table
import Standard.Test
import Standard.Visualization.Helpers
import Visualization_Tests
import Visualization_Tests.Helpers
spec =
Test.group "Table.all_columns" <|
@ -26,9 +25,22 @@ spec =
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" <|
Test.specify "includes the index first and then normal 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']
Test.group "Table.lookup_ignore_case" <|
Test.specify "ignores case and takes first matching" <|
header = ['A', 'a' , 'ω' , 'Ω']
row_1 = [11 , 10 , 12 , 13]
row_2 = [21 , 20 , 22 , 23]
table = Table.from_rows header [row_1, row_2]
table.lookup_ignore_case 'a' . expect 'A' [11,21]
table.lookup_ignore_case 'A' . expect 'A' [11,21]
table.lookup_ignore_case 'ω' . expect 'ω' [12,22]
table.lookup_ignore_case 'Ω' . expect 'ω' [12,22]
table.lookup_ignore_case 'b' . is_error . should_equal True
table.lookup_ignore_case 'B' . is_error . should_equal True

View File

@ -4,7 +4,6 @@ import Standard.Table.Data.Column
import Standard.Table.Data.Table
import Standard.Test
import Standard.Visualization.Histogram
import Visualization_Tests
spec =
@ -47,6 +46,13 @@ spec =
table = Table.from_rows header [row_1, row_2]
expect table 'value' [10,20]
Test.specify "is case insensitive" <|
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]

View File

@ -2,6 +2,7 @@ from Standard.Base import all
import Standard.Test
import Visualization_Tests.Geo_Map_Spec
import Visualization_Tests.Helpers_Spec
import Visualization_Tests.Histogram_Spec
import Visualization_Tests.Scatter_Plot_Spec
@ -9,6 +10,7 @@ import Visualization_Tests.Sql_Spec
import Visualization_Tests.Table_Spec
main = Test.Suite.runMain <|
Geo_Map_Spec.spec
Helpers_Spec.spec
Histogram_Spec.spec
Scatter_Plot_Spec.spec

View File

@ -49,6 +49,12 @@ spec =
table = Table.from_rows header [row_1]
expect table (labels 'x' 'y') '[{"color":"ff0000","label":"label","shape":"square","size":50,"x":11,"y":10}]'
Test.specify "is case insensitive" <|
header = ['X' , 'Y' , 'Size' , 'Shape' , 'Label' , 'Color' ]
row_1 = [11 , 10 , 50 , 'square' , 'label' , 'ff0000']
table = Table.from_rows header [row_1]
expect table (labels 'X' 'Y') '[{"color":"ff0000","label":"label","shape":"square","size":50,"x":11,"y":10}]'
Test.specify "uses first unrecognized numeric column as `y` fallback" <|
header = ['x' , 'size' , 'name' , 'z' , 'ω']
row_1 = [11 , 50 , 'circul' , 20 , 30]
@ -69,16 +75,13 @@ spec =
table = Table.from_rows header [row_1, row_2] . set_index 'baz'
# [TODO] mwu: When it is possible to set multiple index columns, test such case.
expect table (labels 'baz' 'y') '[{"size":40,"x":14,"y":10},{"size":50,"x":15,"y":20}]'
Test.specify "prefers explicit 'x' to index and looks into indices for recognized fields" <|
header = [ 'x' , 'size']
row_1 = [ 10 , 21 ]
row_2 = [ 20 , 22 ]
table = Table.from_rows header [row_1, row_2] . set_index 'size'
# FIXME [mwu] Below the `size` field should be present. Depends on
# https://github.com/enso-org/enso/issues/1602
expect table (labels 'x' 'size') '[{"x":10,"y":21},{"x":20,"y":22}]'
expect table (labels 'x' 'size') '[{"size":21,"x":10,"y":21},{"size":22,"x":20,"y":22}]'
Test.specify "used default index for `x` if none set" <|
header = [ 'y' , 'bar' , 'size']