From 069cf61b957549b9466d6f2e50f612f383f1af48 Mon Sep 17 00:00:00 2001 From: marthasharkey Date: Wed, 28 Aug 2024 14:39:55 +0100 Subject: [PATCH] Initial enso changes to ScatterPlot (#10871) --- .../0.0.0-dev/src/Scatter_Plot.enso | 59 +++++++++++++++---- .../src/Scatter_Plot_Spec.enso | 18 ++---- 2 files changed, 50 insertions(+), 27 deletions(-) diff --git a/distribution/lib/Standard/Visualization/0.0.0-dev/src/Scatter_Plot.enso b/distribution/lib/Standard/Visualization/0.0.0-dev/src/Scatter_Plot.enso index b3d05fa6489..818731638ca 100644 --- a/distribution/lib/Standard/Visualization/0.0.0-dev/src/Scatter_Plot.enso +++ b/distribution/lib/Standard/Visualization/0.0.0-dev/src/Scatter_Plot.enso @@ -1,5 +1,5 @@ from Standard.Base import all - +import Standard.Base.Data.Vector.Builder from Standard.Table import Column, Table import project.Helpers @@ -33,6 +33,9 @@ type Point_Data ## PRIVATE Y + ## PRIVATE + Y_multi number:Integer=0 + ## PRIVATE Color @@ -66,7 +69,10 @@ type Point_Data ## PRIVATE fallback_column : Table -> Column ! No_Fallback_Column fallback_column self table = case self of - Point_Data.X -> Point_Data.iota table.row_count + Point_Data.X -> + candidate = table.columns.first + is_good c = c.is_numeric && c.name != Point_Data.Y.name + if is_good candidate then candidate else Point_Data.iota table.row_count Point_Data.Y -> x_column = Point_Data.X.lookup_in table candidates = table.columns @@ -74,6 +80,15 @@ type Point_Data is_good c = is_good_enough c && (self.is_recognized c).not candidates.find if_missing=(Error.throw Nothing) is_good . catch_ <| candidates.find is_good_enough + Point_Data.Y_multi _ -> + x_column = Point_Data.X.lookup_in table + y_column = Point_Data.Y.lookup_in table + check_for_size c = c.name.equals_ignore_case Point_Data.Size.name != True + candidates_filtered = table.columns.filter (c-> c.is_numeric && c.name != x_column.name && c.name != y_column.name && check_for_size c) + candidates = candidates_filtered.drop self.number + is_good c = (self.is_recognized c).not + + candidates.find if_missing=candidates.first is_good _ -> Error.throw No_Fallback_Column ## PRIVATE @@ -100,34 +115,41 @@ type No_Fallback_Column ## PRIVATE Generates JSON that describes points data. -Table.point_data : Vector -Table.point_data self = +Table.point_data : Vector -> Vector +Table.point_data self all_fields = get_point_data field = field.lookup_in self . rename field.name . catch Any (_->Nothing) is_not_nothing x = case x of Nothing -> False _ -> True - columns = Point_Data.all_fields.map get_point_data . filter is_not_nothing + columns = all_fields.map get_point_data . filter is_not_nothing (0.up_to self.row_count).to_vector.map <| row_n-> pairs = columns.map column-> value = column.at row_n . catch_ Nothing [column.name, value] JS_Object.from_pairs pairs +## PRIVATE + + Returns the number of numeric columns for the plot. +Table.numeric_column_count : Number +Table.numeric_column_count self = + columns = self.columns . filter (c-> c.is_numeric && c.name != Point_Data.Size.name) + columns.length + ## PRIVATE Generates JSON that describes plot axes. -Table.axes : JS_Object -Table.axes self = +Table.axes : Vector -> JS_Object +Table.axes self all_fields = describe_axis field = col_name = field.lookup_in self . name label = JS_Object.from_pairs [[label_field, col_name]] [field.name, label] - x_axis = describe_axis Point_Data.X - y_axis = describe_axis Point_Data.Y + axis = all_fields.map describe_axis is_valid axis_pair = label = axis_pair.at 1 label.is_valid && (self.columns.length > 0) - axes_obj = JS_Object.from_pairs <| [x_axis, y_axis].filter is_valid + axes_obj = JS_Object.from_pairs <| axis.filter is_valid if axes_obj.length > 0 then axes_obj else Nothing ## PRIVATE @@ -178,11 +200,22 @@ limit_data limit data = case limit of if limit <= extreme.length then extreme.take (..First limit) else extreme + data.take (..Sample (limit - extreme.length)) + +## PRIVATE +get_axes_field : Integer -> Vector +get_axes_field number_of_numeric = + fields_for_multiseries = ((0.up_to number_of_numeric).map idx-> Point_Data.Y_multi idx) + fields_for_multiseries . insert item=Point_Data.X . insert item=Point_Data.Y + ## PRIVATE json_from_table : Table -> Vector Integer | Nothing -> Integer | Nothing -> Text json_from_table table bounds limit = - data = table.point_data |> bound_data bounds |> limit_data limit - axes = table.axes + number_of_numeric_cols = table.numeric_column_count-2 + fields_for_multiseries = Point_Data.all_fields + ((0.up_to number_of_numeric_cols).map idx-> Point_Data.Y_multi idx) + fields_for_plot = if number_of_numeric_cols > 0 then fields_for_multiseries else Point_Data.all_fields + data = table.point_data fields_for_plot |> bound_data bounds |> limit_data limit + fields_for_axes = get_axes_field number_of_numeric_cols + axes = table.axes fields_for_axes JS_Object.from_pairs [[data_field, data], [axis_field, axes]] . to_json ## PRIVATE @@ -202,9 +235,9 @@ json_from_vector vec bounds limit = process_to_json_text : Any -> Integer | Nothing -> Integer | Nothing -> Text process_to_json_text value bounds=Nothing limit=Nothing = json = case value of - _ : Column -> json_from_table value.to_table bounds limit _ : Table -> json_from_table value bounds limit _ : Vector -> json_from_vector value bounds limit + _ : Column -> json_from_table value.to_table bounds limit _ -> json_from_vector value.to_vector bounds limit json diff --git a/test/Visualization_Tests/src/Scatter_Plot_Spec.enso b/test/Visualization_Tests/src/Scatter_Plot_Spec.enso index 7ab673d92b3..8c7d2c57b35 100644 --- a/test/Visualization_Tests/src/Scatter_Plot_Spec.enso +++ b/test/Visualization_Tests/src/Scatter_Plot_Spec.enso @@ -26,22 +26,16 @@ add_specs suite_builder = index = Scatter_Plot.index_name axis label = JS_Object.from_pairs [['label',label]] labels x y = JS_Object.from_pairs [['x', axis x], ['y', axis y]] . to_text + labels_multi x y z = JS_Object.from_pairs [['x', axis x], ['y', axis y], ['(y_multi 0)', axis z]] . to_text no_labels = 'null' suite_builder.group "Scatter Plot Visualization" group_builder-> - group_builder.specify "plots first column if none recognized" <| + group_builder.specify "plots left most column for x if numeric" <| header = ['α', 'ω'] row_1 = [11 , 10 ] row_2 = [21 , 20 ] table = Table.from_rows header [row_1, row_2] - expect table (labels index 'α') '[{"x":0,"y":11},{"x":1,"y":21}]' - - group_builder.specify "plots 'y' against indices when no 'x' recognized" <| - header = ['α', 'y'] - row_1 = [11 , 10 ] - row_2 = [21 , 20 ] - table = Table.from_rows header [row_1, row_2] - expect table (labels index 'y') '[{"x":0,"y":10},{"x":1,"y":20}]' + expect table (labels 'α' 'ω') '[{"x":11,"y":10},{"x":21,"y":20}]' group_builder.specify "recognizes all relevant columns" <| header = ['x' , 'y' , 'size' , 'shape' , 'label' , 'color' ] @@ -59,7 +53,7 @@ add_specs suite_builder = header = ['x' , 'size' , 'name' , 'z' , 'ω'] row_1 = [11 , 50 , 'circul' , 20 , 30] table = Table.from_rows header [row_1] - expect table (labels 'x' 'z') '[{"size":50,"x":11,"y":20}]' + expect table (labels_multi 'x' 'z' 'ω') '[{"size":50,"x":11,"y":20,"(y_multi 0)":30}]' group_builder.specify "provided only recognized columns" <| header = ['x', 'y' , 'bar' , 'size'] @@ -115,10 +109,6 @@ add_specs suite_builder = text = Scatter_Plot.process_to_json_text vector bounds expect_text text no_labels '[{"x":1,"y":10},{"x":2,"y":20}]' - group_builder.specify "using indices for x if given a column" <| - column = Column.from_vector 'some_col' [10,2,3] - expect column (labels 'index' 'some_col') '[{"x":0,"y":10},{"x":1,"y":2},{"x":2,"y":3}]' - group_builder.specify "using indices for x if given a range" <| value = 2.up_to 5 expect value no_labels '[{"x":0,"y":2},{"x":1,"y":3},{"x":2,"y":4}]'