Line charts. Chart plugin sub command extraction. (#2627)

This commit is contained in:
Andrés N. Robalino 2020-10-01 19:23:10 -05:00 committed by GitHub
parent f0b7ab5ecc
commit ddf9d61346
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 664 additions and 65 deletions

View File

@ -174,8 +174,13 @@ path = "src/plugins/nu_plugin_extra_s3.rs"
required-features = ["s3"]
[[bin]]
name = "nu_plugin_extra_chart"
path = "src/plugins/nu_plugin_extra_chart.rs"
name = "nu_plugin_extra_chart_bar"
path = "src/plugins/nu_plugin_extra_chart_bar.rs"
required-features = ["chart"]
[[bin]]
name = "nu_plugin_extra_chart_line"
path = "src/plugins/nu_plugin_extra_chart_line.rs"
required-features = ["chart"]
# Main nu binary

View File

@ -114,6 +114,8 @@ pub fn create_default_context(interactive: bool) -> Result<EvaluationContext, Bo
whole_stream_command(Shells),
whole_stream_command(Enter),
whole_stream_command(Exit),
// Viz
whole_stream_command(Chart),
// Viewers
whole_stream_command(Autoview),
whole_stream_command(Table),

View File

@ -17,6 +17,7 @@ pub(crate) mod build_string;
pub(crate) mod cal;
pub(crate) mod cd;
pub(crate) mod char_;
pub(crate) mod chart;
pub(crate) mod classified;
#[cfg(feature = "clipboard-cli")]
pub(crate) mod clip;
@ -142,6 +143,7 @@ pub(crate) use benchmark::Benchmark;
pub(crate) use build_string::BuildString;
pub(crate) use cal::Cal;
pub(crate) use char_::Char;
pub(crate) use chart::Chart;
pub(crate) use compact::Compact;
pub(crate) use config::{
Config, ConfigClear, ConfigGet, ConfigLoad, ConfigPath, ConfigRemove, ConfigSet, ConfigSetInto,

View File

@ -0,0 +1,52 @@
use crate::commands::WholeStreamCommand;
use crate::prelude::*;
use nu_errors::ShellError;
use nu_protocol::{ReturnSuccess, Signature, UntaggedValue};
#[derive(Clone)]
pub struct Chart;
#[async_trait]
impl WholeStreamCommand for Chart {
fn name(&self) -> &str {
"chart"
}
fn signature(&self) -> Signature {
Signature::build("chart")
}
fn usage(&self) -> &str {
"Displays charts."
}
async fn run(
&self,
_args: CommandArgs,
registry: &CommandRegistry,
) -> Result<OutputStream, ShellError> {
if registry.get_command("chart bar").is_none() {
return Err(ShellError::untagged_runtime_error(
"nu_plugin_chart not installed.",
));
}
let registry = registry.clone();
Ok(OutputStream::one(Ok(ReturnSuccess::Value(
UntaggedValue::string(crate::commands::help::get_help(&Chart, &registry))
.into_value(Tag::unknown()),
))))
}
}
#[cfg(test)]
mod tests {
use super::Chart;
#[test]
fn examples_work_as_expected() {
use crate::examples::test as test_examples;
test_examples(Chart {})
}
}

View File

@ -72,7 +72,6 @@ pub fn report(
)?;
let group_labels = planes.grouping_total();
let split_labels = planes.splits_total();
let reduced = reduce(&evaluated, options.reduction, &tag)?;
@ -89,7 +88,7 @@ pub fn report(
},
Range {
start: UntaggedValue::int(0).into_untagged_value(),
end: split_labels,
end: maxima,
},
),
data: reduced,
@ -295,7 +294,7 @@ mod tests {
},
Range {
start: int(0),
end: int(3),
end: int(30),
},
),
data: table(&[

View File

@ -0,0 +1,6 @@
use nu_plugin::serve_plugin;
use nu_plugin_chart::ChartBar;
fn main() {
serve_plugin(&mut ChartBar::new());
}

View File

@ -0,0 +1,6 @@
use nu_plugin::serve_plugin;
use nu_plugin_chart::ChartLine;
fn main() {
serve_plugin(&mut ChartLine::new());
}

View File

@ -1,52 +1,22 @@
use nu_data::utils::Model;
use nu_errors::ShellError;
use nu_protocol::Value;
use nu_source::Tagged;
use tui::{
layout::{Constraint, Direction, Layout},
style::{Color, Modifier, Style},
widgets::{BarChart as TuiBarChart, Block, Borders},
widgets::BarChart,
};
pub enum Columns {
One(Tagged<String>),
Two(Tagged<String>, Tagged<String>),
None,
}
const DEFAULT_COLOR: Color = Color::Green;
#[allow(clippy::type_complexity)]
pub struct Chart {
pub reduction: nu_data::utils::Reduction,
pub columns: Columns,
pub eval: Option<Box<dyn Fn(usize, &Value) -> Result<Value, ShellError> + Send>>,
pub format: Option<String>,
}
impl Default for Chart {
fn default() -> Self {
Self::new()
}
}
impl Chart {
pub fn new() -> Chart {
Chart {
reduction: nu_data::utils::Reduction::Count,
columns: Columns::None,
eval: None,
format: None,
}
}
}
pub struct BarChart<'a> {
pub struct Bar<'a> {
pub title: &'a str,
pub data: Vec<(&'a str, u64)>,
pub enhanced_graphics: bool,
}
impl<'a> BarChart<'a> {
pub fn from_model(model: &'a nu_data::utils::Model) -> Result<BarChart<'a>, ShellError> {
impl<'a> Bar<'a> {
pub fn from_model(model: &'a Model) -> Result<Bar<'a>, ShellError> {
let mut data = Vec::new();
let mut data_points = Vec::new();
@ -109,7 +79,7 @@ impl<'a> BarChart<'a> {
}
}
Ok(BarChart {
Ok(Bar {
title: "Bar Chart",
data: (&data[..]).to_vec(),
enhanced_graphics: true,
@ -127,15 +97,13 @@ impl<'a> BarChart<'a> {
.constraints([Constraint::Percentage(100)].as_ref())
.split(f.size());
let barchart = TuiBarChart::default()
.block(Block::default().title("Chart").borders(Borders::ALL))
let barchart = BarChart::default()
.data(&self.data)
.bar_width(9)
.bar_style(Style::default().fg(Color::Green))
.bar_style(Style::default().fg(DEFAULT_COLOR))
.value_style(
Style::default()
.fg(Color::Black)
.bg(Color::Green)
.bg(Color::Black)
.add_modifier(Modifier::BOLD),
);

View File

@ -1,4 +1,5 @@
mod chart;
mod bar;
mod line;
mod nu;
pub use chart::Chart;
pub use nu::{ChartBar, ChartLine};

View File

@ -0,0 +1,146 @@
use nu_data::utils::Model;
use nu_errors::ShellError;
use tui::{
layout::{Constraint, Direction, Layout},
style::{Color, Modifier, Style},
symbols,
text::Span,
widgets::{Axis, Chart, Dataset, GraphType},
};
const DEFAULT_COLOR: Color = Color::Green;
const DEFAULT_LINE_COLORS: [Color; 5] = [
Color::Green,
Color::Cyan,
Color::Magenta,
Color::Yellow,
Color::Red,
];
#[derive(Debug)]
pub struct Line<'a> {
title: &'a str,
x_labels: Vec<String>,
x_range: [f64; 2],
y_range: [f64; 2],
datasets_names: Vec<String>,
data: Vec<Vec<(f64, f64)>>,
}
impl<'a> Line<'a> {
pub fn from_model(model: &'a Model) -> Result<Line<'a>, ShellError> {
Ok(Line {
title: "Line Chart",
x_labels: model.labels.x.to_vec(),
x_range: [
model.ranges.0.start.as_u64()? as f64,
model.labels.x.len() as f64,
],
y_range: [
model.ranges.1.start.as_u64()? as f64,
model.ranges.1.end.as_u64()? as f64,
],
datasets_names: if model.labels.y.len() == 1 {
vec!["".to_string()]
} else {
model.labels.y.to_vec()
},
data: model
.data
.table_entries()
.collect::<Vec<_>>()
.iter()
.map(|subset| {
subset
.table_entries()
.enumerate()
.map(|(idx, data_point)| {
(
idx as f64,
if let Ok(point) = data_point.as_u64() {
point as f64
} else {
0.0
},
)
})
.collect::<Vec<_>>()
})
.collect::<Vec<_>>(),
})
}
pub fn draw<T>(&mut self, ui: &mut tui::Terminal<T>) -> std::io::Result<()>
where
T: tui::backend::Backend,
{
ui.draw(|f| {
let chunks = Layout::default()
.direction(Direction::Vertical)
.margin(1)
.constraints([Constraint::Percentage(100)].as_ref())
.split(f.size());
let x_labels = self
.x_labels
.iter()
.map(move |label| {
Span::styled(label, Style::default().add_modifier(Modifier::BOLD))
})
.collect::<Vec<_>>();
let y_labels = vec![
Span::styled(
format!("{}", self.y_range[0]),
Style::default().add_modifier(Modifier::BOLD),
),
Span::raw(format!("{}", (self.y_range[0] + self.y_range[1]) / 2.0)),
Span::styled(
format!("{}", self.y_range[1]),
Style::default().add_modifier(Modifier::BOLD),
),
];
let marker = if x_labels.len() > 60 {
symbols::Marker::Braille
} else {
symbols::Marker::Dot
};
let datasets = self
.data
.iter()
.enumerate()
.map(|(idx, data_series)| {
Dataset::default()
.name(&self.datasets_names[idx])
.marker(marker)
.graph_type(GraphType::Line)
.style(
Style::default().fg(*DEFAULT_LINE_COLORS
.get(idx)
.unwrap_or_else(|| &DEFAULT_COLOR)),
)
.data(data_series)
})
.collect();
let chart = Chart::new(datasets)
.x_axis(
Axis::default()
.style(Style::default().fg(Color::Gray))
.labels(x_labels)
.bounds(self.x_range),
)
.y_axis(
Axis::default()
.style(Style::default().fg(Color::Gray))
.labels(y_labels)
.bounds(self.y_range),
);
f.render_widget(chart, chunks[0]);
})
}
}

View File

@ -0,0 +1,5 @@
mod bar;
mod line;
pub use bar::SubCommand as ChartBar;
pub use line::SubCommand as ChartLine;

View File

@ -1,10 +1,11 @@
use nu_data::utils::{report as build_report, Model};
use nu_errors::ShellError;
use nu_plugin::Plugin;
use nu_protocol::{CallInfo, ColumnPath, Primitive, Signature, SyntaxShape, UntaggedValue, Value};
use nu_source::TaggedItem;
use nu_source::{Tagged, TaggedItem};
use nu_value_ext::ValueExt;
use crate::chart::{BarChart, Chart, Columns};
use crate::bar::Bar;
use std::{
error::Error,
@ -27,8 +28,39 @@ enum Event<I> {
Tick,
}
fn display(model: &nu_data::utils::Model) -> Result<(), Box<dyn Error>> {
let mut app = BarChart::from_model(&model)?;
pub enum Columns {
One(Tagged<String>),
Two(Tagged<String>, Tagged<String>),
None,
}
#[allow(clippy::type_complexity)]
pub struct SubCommand {
pub reduction: nu_data::utils::Reduction,
pub columns: Columns,
pub eval: Option<Box<dyn Fn(usize, &Value) -> Result<Value, ShellError> + Send>>,
pub format: Option<String>,
}
impl Default for SubCommand {
fn default() -> Self {
Self::new()
}
}
impl SubCommand {
pub fn new() -> SubCommand {
SubCommand {
reduction: nu_data::utils::Reduction::Count,
columns: Columns::None,
eval: None,
format: None,
}
}
}
fn display(model: &Model) -> Result<(), Box<dyn Error>> {
let mut app = Bar::from_model(&model)?;
enable_raw_mode()?;
@ -94,10 +126,10 @@ fn display(model: &nu_data::utils::Model) -> Result<(), Box<dyn Error>> {
Ok(())
}
impl Plugin for Chart {
impl Plugin for SubCommand {
fn config(&mut self) -> Result<Signature, ShellError> {
Ok(Signature::build("chart")
.desc("Displays bar charts")
Ok(Signature::build("chart bar")
.desc("Bar charts")
.switch("acc", "accumuate values", Some('a'))
.optional(
"columns",
@ -131,7 +163,7 @@ impl Plugin for Chart {
}
}
impl Chart {
impl SubCommand {
fn run(&mut self, call_info: CallInfo, input: Vec<Value>) -> Result<(), ShellError> {
let args = call_info.args;
let name = call_info.name_tag;
@ -269,7 +301,7 @@ impl Chart {
reduction: &self.reduction,
};
let _ = display(&nu_data::utils::report(&data, options, &name)?);
let _ = display(&build_report(&data, options, &name)?);
}
Columns::One(col) => {
let key = col.clone();
@ -316,7 +348,7 @@ impl Chart {
reduction: &self.reduction,
};
let _ = display(&nu_data::utils::report(&data, options, &name)?);
let _ = display(&build_report(&data, options, &name)?);
}
_ => {}
}

View File

@ -0,0 +1,369 @@
use nu_data::utils::{report as build_report, Model};
use nu_errors::ShellError;
use nu_plugin::Plugin;
use nu_protocol::{CallInfo, ColumnPath, Primitive, Signature, SyntaxShape, UntaggedValue, Value};
use nu_source::{Tagged, TaggedItem};
use nu_value_ext::ValueExt;
use crate::line::Line;
use std::{
error::Error,
io::{stdout, Write},
sync::mpsc,
thread,
time::{Duration, Instant},
};
use crossterm::{
event::{self, DisableMouseCapture, EnableMouseCapture, Event as CEvent, KeyCode},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use tui::{backend::CrosstermBackend, Terminal};
enum Event<I> {
Input(I),
Tick,
}
pub enum Columns {
One(Tagged<String>),
Two(Tagged<String>, Tagged<String>),
None,
}
#[allow(clippy::type_complexity)]
pub struct SubCommand {
pub reduction: nu_data::utils::Reduction,
pub columns: Columns,
pub eval: Option<Box<dyn Fn(usize, &Value) -> Result<Value, ShellError> + Send>>,
pub format: Option<String>,
}
impl Default for SubCommand {
fn default() -> Self {
Self::new()
}
}
impl SubCommand {
pub fn new() -> SubCommand {
SubCommand {
reduction: nu_data::utils::Reduction::Count,
columns: Columns::None,
eval: None,
format: None,
}
}
}
fn display(model: &Model) -> Result<(), Box<dyn Error>> {
let mut app = Line::from_model(&model)?;
enable_raw_mode()?;
let mut stdout = stdout();
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
let backend = CrosstermBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
let (tx, rx) = mpsc::channel();
let tick_rate = Duration::from_millis(250);
thread::spawn(move || {
let mut last_tick = Instant::now();
loop {
if event::poll(tick_rate - last_tick.elapsed()).is_ok() {
if let Ok(CEvent::Key(key)) = event::read() {
let _ = tx.send(Event::Input(key));
}
}
if last_tick.elapsed() >= tick_rate {
let _ = tx.send(Event::Tick);
last_tick = Instant::now();
}
}
});
terminal.clear()?;
loop {
app.draw(&mut terminal)?;
match rx.recv()? {
Event::Input(event) => match event.code {
KeyCode::Char('q') => {
disable_raw_mode()?;
execute!(
terminal.backend_mut(),
LeaveAlternateScreen,
DisableMouseCapture
)?;
terminal.show_cursor()?;
break;
}
_ => {
disable_raw_mode()?;
execute!(
terminal.backend_mut(),
LeaveAlternateScreen,
DisableMouseCapture
)?;
terminal.show_cursor()?;
break;
}
},
Event::Tick => {}
}
}
Ok(())
}
impl Plugin for SubCommand {
fn config(&mut self) -> Result<Signature, ShellError> {
Ok(Signature::build("chart line")
.desc("Line charts")
.switch("acc", "accumuate values", Some('a'))
.optional(
"columns",
SyntaxShape::Any,
"the columns to chart [x-axis y-axis]",
)
.named(
"use",
SyntaxShape::ColumnPath,
"column to use for evaluation",
Some('u'),
)
.named(
"format",
SyntaxShape::String,
"Specify date and time formatting",
Some('f'),
))
}
fn sink(&mut self, call_info: CallInfo, input: Vec<Value>) {
if let Some(Value {
value: UntaggedValue::Primitive(Primitive::Boolean(true)),
..
}) = call_info.args.get("acc")
{
self.reduction = nu_data::utils::Reduction::Accumulate;
}
let _ = self.run(call_info, input);
}
}
impl SubCommand {
fn run(&mut self, call_info: CallInfo, input: Vec<Value>) -> Result<(), ShellError> {
let args = call_info.args;
let name = call_info.name_tag;
self.eval = if let Some(path) = args.get("use") {
Some(evaluator(path.as_column_path()?.item))
} else {
None
};
self.format = if let Some(fmt) = args.get("format") {
Some(fmt.as_string()?)
} else {
None
};
for arg in args.positional_iter() {
match arg {
Value {
value: UntaggedValue::Primitive(Primitive::String(column)),
tag,
} => {
let column = column.clone();
self.columns = Columns::One(column.tagged(tag));
}
Value {
value: UntaggedValue::Table(arguments),
tag,
} => {
if arguments.len() > 1 {
let col1 = arguments
.get(0)
.ok_or_else(|| {
ShellError::labeled_error(
"expected file and replace strings eg) [find replace]",
"missing find-replace values",
tag,
)
})?
.as_string()?
.tagged(tag);
let col2 = arguments
.get(1)
.ok_or_else(|| {
ShellError::labeled_error(
"expected file and replace strings eg) [find replace]",
"missing find-replace values",
tag,
)
})?
.as_string()?
.tagged(tag);
self.columns = Columns::Two(col1, col2);
} else {
let col1 = arguments
.get(0)
.ok_or_else(|| {
ShellError::labeled_error(
"expected file and replace strings eg) [find replace]",
"missing find-replace values",
tag,
)
})?
.as_string()?
.tagged(tag);
self.columns = Columns::One(col1);
}
}
_ => {}
}
}
let data = UntaggedValue::table(&input).into_value(&name);
match &self.columns {
Columns::Two(col1, col2) => {
let key = col1.clone();
let fmt = self.format.clone();
let grouper = Box::new(move |_: usize, row: &Value| {
let key = key.clone();
let fmt = fmt.clone();
match row.get_data_by_key(key.borrow_spanned()) {
Some(key) => {
if let Some(fmt) = fmt {
let callback = nu_data::utils::helpers::date_formatter(fmt);
callback(&key, "nothing".to_string())
} else {
nu_value_ext::as_string(&key)
}
}
None => Err(ShellError::labeled_error(
"unknown column",
"unknown column",
key.tag(),
)),
}
});
let key = col2.clone();
let splitter = Box::new(move |_: usize, row: &Value| {
let key = key.clone();
match row.get_data_by_key(key.borrow_spanned()) {
Some(key) => nu_value_ext::as_string(&key),
None => Err(ShellError::labeled_error(
"unknown column",
"unknown column",
key.tag(),
)),
}
});
let formatter = if self.format.is_some() {
let default = String::from("%b-%Y");
let string_fmt = self.format.as_ref().unwrap_or_else(|| &default);
Some(nu_data::utils::helpers::date_formatter(
string_fmt.to_string(),
))
} else {
None
};
let options = nu_data::utils::Operation {
grouper: Some(grouper),
splitter: Some(splitter),
format: &formatter,
eval: &self.eval,
reduction: &self.reduction,
};
let _ = display(&build_report(&data, options, &name)?);
}
Columns::One(col) => {
let key = col.clone();
let fmt = self.format.clone();
let grouper = Box::new(move |_: usize, row: &Value| {
let key = key.clone();
let fmt = fmt.clone();
match row.get_data_by_key(key.borrow_spanned()) {
Some(key) => {
if let Some(fmt) = fmt {
let callback = nu_data::utils::helpers::date_formatter(fmt);
callback(&key, "nothing".to_string())
} else {
nu_value_ext::as_string(&key)
}
}
None => Err(ShellError::labeled_error(
"unknown column",
"unknown column",
key.tag(),
)),
}
});
let formatter = if self.format.is_some() {
let default = String::from("%b-%Y");
let string_fmt = self.format.as_ref().unwrap_or_else(|| &default);
Some(nu_data::utils::helpers::date_formatter(
string_fmt.to_string(),
))
} else {
None
};
let options = nu_data::utils::Operation {
grouper: Some(grouper),
splitter: None,
format: &formatter,
eval: &self.eval,
reduction: &self.reduction,
};
let _ = display(&build_report(&data, options, &name)?);
}
_ => {}
}
Ok(())
}
}
pub fn evaluator(by: ColumnPath) -> Box<dyn Fn(usize, &Value) -> Result<Value, ShellError> + Send> {
Box::new(move |_: usize, value: &Value| {
let path = by.clone();
let eval = nu_value_ext::get_data_by_column_path(value, &path, move |_, _, error| error);
match eval {
Ok(with_value) => Ok(with_value),
Err(reason) => Err(reason),
}
})
}

View File

@ -1,6 +0,0 @@
use nu_plugin::serve_plugin;
use nu_plugin_chart::Chart;
fn main() {
serve_plugin(&mut Chart::new());
}

View File

@ -0,0 +1,6 @@
use nu_plugin::serve_plugin;
use nu_plugin_chart::ChartBar;
fn main() {
serve_plugin(&mut ChartBar::new());
}

View File

@ -0,0 +1,6 @@
use nu_plugin::serve_plugin;
use nu_plugin_chart::ChartLine;
fn main() {
serve_plugin(&mut ChartLine::new());
}