diff --git a/abacus-core/src/dataframe.rs b/abacus-core/src/dataframe.rs index 679679b..889b0a2 100644 --- a/abacus-core/src/dataframe.rs +++ b/abacus-core/src/dataframe.rs @@ -6,7 +6,7 @@ use std::{ str::FromStr, }; -use rhai::EvalAltResult; +use rhai::{Dynamic, EvalAltResult, EvalContext, Expression, Position}; pub fn setup_engine(engine: &mut rhai::Engine) { //polar data frame @@ -52,6 +52,18 @@ pub fn setup_engine(engine: &mut rhai::Engine) { engine.register_fn("min", script_functions::min); engine.register_fn("max", script_functions::max); engine.register_fn("first", script_functions::first); + let _ = engine.register_custom_operator("gt", 200); + let _ = engine.register_custom_operator("gte", 200); + engine.register_fn("gt", script_functions::gt_op); + engine.register_fn("gte", script_functions::gte_op); + + engine + .register_custom_syntax( + ["from", "$ident$", "$expr$", ":", "$expr$"], // the custom syntax + true, // variables declared within this custom syntax + implementation_df_select, + ) + .unwrap(); } #[derive(Clone, Debug, PartialEq)] @@ -339,6 +351,82 @@ mod script_functions { Ok(Series(s)) } } + pub fn gt_op(a: &str, b: rhai::Dynamic) -> DataFrameExpression { + DataFrameExpression(polars::prelude::col(a).gt(DataFrameExpression::from(b))) + } + + pub fn gte_op(a: &str, b: rhai::Dynamic) -> DataFrameExpression { + DataFrameExpression(polars::prelude::col(a).gt_eq(DataFrameExpression::from(b))) + } +} + +fn implementation_df_select( + context: &mut EvalContext, + inputs: &[Expression], +) -> Result> { + let df_name = inputs[0].get_string_value().ok_or_else(|| { + Box::new(EvalAltResult::ErrorVariableNotFound( + "variable not found".to_string(), + Position::default(), + )) + })?; + + let df = context + .scope() + .get(df_name) + .ok_or_else(|| { + Box::new(EvalAltResult::ErrorVariableNotFound( + format!("{} not found", df_name), + Position::default(), + )) + })? + .clone() + .try_cast::() + .ok_or_else(|| { + Box::new(EvalAltResult::ErrorVariableNotFound( + format!("{} not found", df_name), + Position::default(), + )) + })?; + + let raw_filter_array = context.eval_expression_tree(&inputs[2])?; + let filter_array = raw_filter_array + .into_array() + .map_err(|e| { + Box::new(EvalAltResult::ErrorVariableNotFound( + format!("{} value not an array", e), + Position::default(), + )) + })? + .into_iter() + .map(|i| i.cast::()) + .collect::>(); + + let raw_select_array = context.eval_expression_tree(&inputs[1])?; + let select_array = raw_select_array.into_array().map_err(|e| { + Box::new(EvalAltResult::ErrorVariableNotFound( + format!("{} value not an array", e), + Position::default(), + )) + })?; + + let select_expressions = select_array + .iter() + .map(|s| { + filter_array + .iter() + .fold(polars::prelude::col(&s.to_string()), |acc, i| { + acc.filter(i.0.clone()) + }) + }) + .collect::>(); + + Ok(Dynamic::from(DataFrame( + df.0.lazy() + .select(&select_expressions) + .collect() + .map_err(|e| e.to_string())?, + ))) } #[cfg(test)] @@ -494,4 +582,26 @@ s1 + s2 .collect::>(); assert_eq!(s, vec![Some(18)]); } + + #[test] + pub fn test_dataframe_select_syntax() { + let res = process( + r#" +let data = load_csv("test/data.csv"); +from data ["age"] : ["age" gt 18]; +"#, + ); + + dbg!(&res); + + let s = res + .into_frame() + .column("age") + .unwrap() + .i64() + .unwrap() + .into_iter() + .collect::>(); + assert_eq!(s, vec![Some(22), Some(32)]); + } } diff --git a/abacus-core/test/data.csv b/abacus-core/test/data.csv new file mode 100644 index 0000000..ade1481 --- /dev/null +++ b/abacus-core/test/data.csv @@ -0,0 +1,4 @@ +name,age, +alice,18 +sasha,22 +lacey,32 \ No newline at end of file diff --git a/abacus-ui/src/app_delegate.rs b/abacus-ui/src/app_delegate.rs index f4591fb..8b528a1 100644 --- a/abacus-ui/src/app_delegate.rs +++ b/abacus-ui/src/app_delegate.rs @@ -63,7 +63,7 @@ impl AppDelegate for Delegate { if cmd.is(commands::DELETE_BLOCK) { if let Some(index) = cmd.get(commands::DELETE_BLOCK) { - data.blocks.remove(*index); + data.remove_block(*index); return druid::Handled::Yes; } } diff --git a/abacus-ui/src/data/app_data.rs b/abacus-ui/src/data/app_data.rs index fce2f4d..e5ec0de 100644 --- a/abacus-ui/src/data/app_data.rs +++ b/abacus-ui/src/data/app_data.rs @@ -14,6 +14,15 @@ pub struct AppData { pub modals: Modals, } +impl AppData { + pub fn remove_block(&mut self, idx: usize) { + self.blocks.remove(idx); + for (idx, block) in self.blocks.iter_mut().enumerate() { + block.index = idx; + } + } +} + impl Default for AppData { fn default() -> Self { Self { diff --git a/abacus-ui/src/editor.rs b/abacus-ui/src/editor.rs index 8408024..ae4e8cb 100644 --- a/abacus-ui/src/editor.rs +++ b/abacus-ui/src/editor.rs @@ -41,10 +41,10 @@ impl AbacusEditor { let rects = layout.rects_for_range(0..1); let rect = rects.first().unwrap(); let rect = Rect::new( + rect.min_x() - 1.0, + rect.min_y(), rect.min_x() + 1.0, - rect.min_y() + 2.0, - rect.min_x() + 3.0, - rect.max_y() - 2.0, + rect.max_y(), ); ctx.fill( rect, @@ -55,10 +55,10 @@ impl AbacusEditor { .last() { let cursor_rect = Rect::new( - char_rect.max_x() + 3.0, - char_rect.min_y() + 2.0, - char_rect.max_x() + 5.0, - char_rect.max_y() - 2.0, + char_rect.max_x() - 1.0, + char_rect.min_y(), + char_rect.max_x() + 1.0, + char_rect.max_y(), ); ctx.fill( @@ -204,6 +204,10 @@ impl Widget for AbacusEditor { data.mode = EditMode::Insert; data.cursor_to_end_of_line(); } + "I" => { + data.cursor_to_start_of_line(); + data.mode = EditMode::Insert; + } "a" => { if e.mods.ctrl() { data.select_all(); diff --git a/abacus-ui/src/output_block.rs b/abacus-ui/src/output_block.rs index 2865d77..09c6eed 100644 --- a/abacus-ui/src/output_block.rs +++ b/abacus-ui/src/output_block.rs @@ -61,6 +61,47 @@ pub fn output_block() -> impl Widget { .expand_width(), )) } + Output::DataFrame(frame) => { + let mut flex = Flex::row(); + for series in frame.iter() { + let mut col = Flex::column(); + col.add_child( + Label::new(series.name()) + .with_font( + FontDescriptor::new(FontFamily::MONOSPACE) + .with_weight(FontWeight::BLACK), + ) + .with_text_size(OUTPUT_FONT_SIZE) + .expand_width() + .padding(3.0) + .border(Color::rgb8(60, 60, 60), 1.0), + ); + + for v in series.iter() { + col.add_child( + Label::new(v.to_string()) + .with_font( + FontDescriptor::new(FontFamily::MONOSPACE) + .with_weight(FontWeight::MEDIUM), + ) + .with_text_size(OUTPUT_FONT_SIZE) + .expand_width() + .padding(3.0) + .border(Color::rgb8(60, 60, 60), 1.0), + ); + } + + flex.add_flex_child(col, 1.0); + } + + Box::new(Padding::new( + 25.0, + flex.padding(10.0) + .background(Color::rgb8(30, 30, 30)) + .rounded(4.0) + .expand_width(), + )) + } _ => Box::new(Padding::new( 0.0, Label::new("") diff --git a/frame b/frame new file mode 100644 index 0000000..82b6468 --- /dev/null +++ b/frame @@ -0,0 +1,8 @@ +{ + "blocks": [ + { + "name": "Block #1", + "content": "dataframe(#{ names: [\"Alice\", \"Bob\", \"Charles\"], ages: [18,21,35] })" + } + ] +} \ No newline at end of file diff --git a/frame.abacus b/frame.abacus new file mode 100644 index 0000000..7caed60 --- /dev/null +++ b/frame.abacus @@ -0,0 +1,8 @@ +{ + "blocks": [ + { + "name": "Things", + "content": "let df = dataframe(#{\n names: [\"Alice\", \"Charles\", \"Bob\"], \n ages: [18,25,31],\n id: [1,2,3],\n amount: [10,0,12]\n});\n\n//df.select([column(\"ages\").filter(column(\"ages\") gt 20)]);\n\nlet df2 = from df [\"names\", \"ages\"] : [\"ages\" gt 20];\ndf2" + } + ] +} \ No newline at end of file diff --git a/test.abacus b/test.abacus new file mode 100644 index 0000000..9f42184 --- /dev/null +++ b/test.abacus @@ -0,0 +1,8 @@ +{ + "blocks": [ + { + "name": "Things", + "content": "let x = 1;\nx+1" + } + ] +} \ No newline at end of file