Bar Chart baseline. (#2621)

Bar Chart ready.
This commit is contained in:
Andrés N. Robalino 2020-09-30 13:27:52 -05:00 committed by GitHub
parent 892a416211
commit a56abb6502
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 803 additions and 136 deletions

53
Cargo.lock generated
View File

@ -607,6 +607,12 @@ dependencies = [
"zip", "zip",
] ]
[[package]]
name = "cassowary"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53"
[[package]] [[package]]
name = "cc" name = "cc"
version = "1.0.59" version = "1.0.59"
@ -940,6 +946,22 @@ dependencies = [
"winapi 0.3.9", "winapi 0.3.9",
] ]
[[package]]
name = "crossterm"
version = "0.18.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2fcdc3c9cf8ee446222e8ee8691a6d21b563b8fe1a64b1873080db7b5b23cf0"
dependencies = [
"bitflags",
"crossterm_winapi",
"lazy_static 1.4.0",
"libc",
"mio 0.7.0",
"parking_lot 0.11.0",
"signal-hook",
"winapi 0.3.9",
]
[[package]] [[package]]
name = "crossterm_winapi" name = "crossterm_winapi"
version = "0.6.1" version = "0.6.1"
@ -2850,6 +2872,7 @@ dependencies = [
"nu-test-support", "nu-test-support",
"nu-value-ext", "nu-value-ext",
"nu_plugin_binaryview", "nu_plugin_binaryview",
"nu_plugin_chart",
"nu_plugin_fetch", "nu_plugin_fetch",
"nu_plugin_from_bson", "nu_plugin_from_bson",
"nu_plugin_from_sqlite", "nu_plugin_from_sqlite",
@ -3116,7 +3139,7 @@ name = "nu_plugin_binaryview"
version = "0.20.0" version = "0.20.0"
dependencies = [ dependencies = [
"ansi_term 0.12.1", "ansi_term 0.12.1",
"crossterm", "crossterm 0.18.0",
"image", "image",
"neso", "neso",
"nu-errors", "nu-errors",
@ -3127,6 +3150,21 @@ dependencies = [
"rawkey", "rawkey",
] ]
[[package]]
name = "nu_plugin_chart"
version = "0.20.0"
dependencies = [
"crossterm 0.18.0",
"nu-cli",
"nu-data",
"nu-errors",
"nu-plugin",
"nu-protocol",
"nu-source",
"nu-value-ext",
"tui",
]
[[package]] [[package]]
name = "nu_plugin_fetch" name = "nu_plugin_fetch"
version = "0.20.0" version = "0.20.0"
@ -5471,6 +5509,19 @@ dependencies = [
"cfg-if", "cfg-if",
] ]
[[package]]
name = "tui"
version = "0.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2eaeee894a1e9b90f80aa466fe59154fdb471980b5e104d8836fcea309ae17e"
dependencies = [
"bitflags",
"cassowary",
"crossterm 0.17.8",
"unicode-segmentation",
"unicode-width",
]
[[package]] [[package]]
name = "typed-arena" name = "typed-arena"
version = "1.7.0" version = "1.7.0"

View File

@ -27,6 +27,7 @@ nu-protocol = {version = "0.20.0", path = "./crates/nu-protocol"}
nu-source = {version = "0.20.0", path = "./crates/nu-source"} nu-source = {version = "0.20.0", path = "./crates/nu-source"}
nu-value-ext = {version = "0.20.0", path = "./crates/nu-value-ext"} nu-value-ext = {version = "0.20.0", path = "./crates/nu-value-ext"}
nu_plugin_chart = {version = "0.20.0", path = "./crates/nu_plugin_chart", optional = true}
nu_plugin_binaryview = {version = "0.20.0", path = "./crates/nu_plugin_binaryview", optional = true} nu_plugin_binaryview = {version = "0.20.0", path = "./crates/nu_plugin_binaryview", optional = true}
nu_plugin_fetch = {version = "0.20.0", path = "./crates/nu_plugin_fetch", optional = true} nu_plugin_fetch = {version = "0.20.0", path = "./crates/nu_plugin_fetch", optional = true}
nu_plugin_from_bson = {version = "0.20.0", path = "./crates/nu_plugin_from_bson", optional = true} nu_plugin_from_bson = {version = "0.20.0", path = "./crates/nu_plugin_from_bson", optional = true}
@ -59,6 +60,16 @@ serde = {version = "1.0.115", features = ["derive"]}
toml = "0.5.6" toml = "0.5.6"
[features] [features]
ctrlc-support = ["nu-cli/ctrlc"]
directories-support = ["nu-cli/directories", "nu-cli/dirs", "nu-data/directories", "nu-data/dirs"]
git-support = ["nu-cli/git2"]
ptree-support = ["nu-cli/ptree"]
rich-benchmark = ["nu-cli/rich-benchmark"]
rustyline-support = ["nu-cli/rustyline-support"]
term-support = ["nu-cli/term"]
uuid-support = ["nu-cli/uuid_crate"]
which-support = ["nu-cli/ichwh", "nu-cli/which"]
default = [ default = [
"sys", "sys",
"ps", "ps",
@ -77,38 +88,30 @@ default = [
"fetch", "fetch",
"rich-benchmark", "rich-benchmark",
] ]
extra = ["default", "binaryview", "tree", "clipboard-cli", "trash-support", "start", "bson", "sqlite", "s3"] extra = ["default", "binaryview", "tree", "clipboard-cli", "trash-support", "start", "bson", "sqlite", "s3", "chart"]
stable = ["default"] stable = ["default"]
# Default trace = ["nu-parser/trace"]
# Stable (Default)
inc = ["nu_plugin_inc"] inc = ["nu_plugin_inc"]
ps = ["nu_plugin_ps"] ps = ["nu_plugin_ps"]
sys = ["nu_plugin_sys"] sys = ["nu_plugin_sys"]
textview = ["nu_plugin_textview"] textview = ["nu_plugin_textview"]
# Stable
binaryview = ["nu_plugin_binaryview"]
bson = ["nu_plugin_from_bson", "nu_plugin_to_bson"]
fetch = ["nu_plugin_fetch"] fetch = ["nu_plugin_fetch"]
match = ["nu_plugin_match"] match = ["nu_plugin_match"]
post = ["nu_plugin_post"] post = ["nu_plugin_post"]
# Extra
bson = ["nu_plugin_from_bson", "nu_plugin_to_bson"]
chart = ["nu_plugin_chart"]
binaryview = ["nu_plugin_binaryview"]
clipboard-cli = ["nu-cli/clipboard-cli"]
trash-support = ["nu-cli/trash-support"]
start = ["nu_plugin_start"]
tree = ["nu_plugin_tree"]
s3 = ["nu_plugin_s3"] s3 = ["nu_plugin_s3"]
sqlite = ["nu_plugin_from_sqlite", "nu_plugin_to_sqlite"] sqlite = ["nu_plugin_from_sqlite", "nu_plugin_to_sqlite"]
start = ["nu_plugin_start"]
trace = ["nu-parser/trace"]
tree = ["nu_plugin_tree"]
clipboard-cli = ["nu-cli/clipboard-cli"]
ctrlc-support = ["nu-cli/ctrlc"]
directories-support = ["nu-cli/directories", "nu-cli/dirs", "nu-data/directories", "nu-data/dirs"]
git-support = ["nu-cli/git2"]
ptree-support = ["nu-cli/ptree"]
rich-benchmark = ["nu-cli/rich-benchmark"]
rustyline-support = ["nu-cli/rustyline-support"]
term-support = ["nu-cli/term"]
trash-support = ["nu-cli/trash-support"]
uuid-support = ["nu-cli/uuid_crate"]
which-support = ["nu-cli/ichwh", "nu-cli/which"]
# Core plugins that ship with `cargo install nu` by default # Core plugins that ship with `cargo install nu` by default
# Currently, Cargo limits us to installing only one binary # Currently, Cargo limits us to installing only one binary
@ -170,6 +173,11 @@ name = "nu_plugin_extra_s3"
path = "src/plugins/nu_plugin_extra_s3.rs" path = "src/plugins/nu_plugin_extra_s3.rs"
required-features = ["s3"] required-features = ["s3"]
[[bin]]
name = "nu_plugin_extra_chart"
path = "src/plugins/nu_plugin_extra_chart.rs"
required-features = ["chart"]
# Main nu binary # Main nu binary
[[bin]] [[bin]]
name = "nu" name = "nu"

View File

@ -112,8 +112,9 @@ pub async fn histogram(
nu_data::utils::Operation { nu_data::utils::Operation {
grouper: Some(Box::new(move |_, _| Ok(String::from("frequencies")))), grouper: Some(Box::new(move |_, _| Ok(String::from("frequencies")))),
splitter: Some(splitter(column_grouper)), splitter: Some(splitter(column_grouper)),
format: None, format: &None,
eval: &evaluate_with, eval: &evaluate_with,
reduction: &nu_data::utils::Reduction::Count,
}, },
&name, &name,
)?; )?;
@ -123,17 +124,33 @@ pub async fn histogram(
Ok(futures::stream::iter( Ok(futures::stream::iter(
results results
.percentages .data
.table_entries() .table_entries()
.map(move |value| { .cloned()
let values = value.table_entries().cloned().collect::<Vec<_>>();
let occurrences = values.len();
(occurrences, values[occurrences - 1].clone())
})
.collect::<Vec<_>>() .collect::<Vec<_>>()
.into_iter() .into_iter()
.map(move |(occurrences, value)| { .zip(
results
.percentages
.table_entries()
.cloned()
.collect::<Vec<_>>()
.into_iter(),
)
.map(move |(counts, percentages)| {
let percentage = percentages
.table_entries()
.cloned()
.last()
.unwrap_or_else(|| {
UntaggedValue::decimal_from_float(0.0, name.span).into_value(&name)
});
let value = counts
.table_entries()
.cloned()
.last()
.unwrap_or_else(|| UntaggedValue::int(0).into_value(&name));
let mut fact = TaggedDictBuilder::new(&name); let mut fact = TaggedDictBuilder::new(&name);
let column_value = labels let column_value = labels
.get(idx) .get(idx)
@ -147,19 +164,19 @@ pub async fn histogram(
.clone(); .clone();
fact.insert_value(&column.item, column_value); fact.insert_value(&column.item, column_value);
fact.insert_untagged("occurrences", UntaggedValue::int(occurrences)); fact.insert_untagged("count", value);
let percentage = format!( let fmt_percentage = format!(
"{}%", "{}%",
// Some(2) < the number of digits // Some(2) < the number of digits
// true < group the digits // true < group the digits
crate::commands::str_::from::action(&value, &name, Some(2), true)? crate::commands::str_::from::action(&percentage, &name, Some(2), true)?
.as_string()? .as_string()?
); );
fact.insert_untagged("percentage", UntaggedValue::string(percentage)); fact.insert_untagged("percentage", UntaggedValue::string(fmt_percentage));
let string = std::iter::repeat("*") let string = std::iter::repeat("*")
.take(value.as_u64().map_err(|_| { .take(percentage.as_u64().map_err(|_| {
ShellError::labeled_error("expected a number", "expected a number", &name) ShellError::labeled_error("expected a number", "expected a number", &name)
})? as usize) })? as usize)
.collect::<String>(); .collect::<String>();

View File

@ -54,7 +54,7 @@ fn summarizes_by_values() {
| get rusty_at | get rusty_at
| histogram | histogram
| where value == "Estados Unidos" | where value == "Estados Unidos"
| get occurrences | get count
| echo $it | echo $it
"# "#
)); ));
@ -93,20 +93,19 @@ fn help() {
} }
#[test] #[test]
fn occurrences() { fn count() {
let actual = nu!( let actual = nu!(
cwd: ".", pipeline( cwd: ".", pipeline(
r#" r#"
echo "[{"bit":1},{"bit":0},{"bit":0},{"bit":0},{"bit":0},{"bit":0},{"bit":0},{"bit":1}]" echo [[bit]; [1] [0] [0] [0] [0] [0] [0] [1]]
| from json
| histogram bit | histogram bit
| sort-by occurrences | sort-by count
| reject frequency | reject frequency
| to json | to json
"# "#
)); ));
let bit_json = r#"[{"bit":"1","occurrences":2,"percentage":"33.33%"},{"bit":"0","occurrences":6,"percentage":"100.00%"}]"#; let bit_json = r#"[{"bit":"1","count":2,"percentage":"33.33%"},{"bit":"0","count":6,"percentage":"100.00%"}]"#;
assert_eq!(actual.out, bit_json); assert_eq!(actual.out, bit_json);
} }

View File

@ -1,6 +1,6 @@
#![allow(clippy::type_complexity)] #![allow(clippy::type_complexity)]
use crate::value::compute_values; use crate::value::unsafe_compute_values;
use derive_new::new; use derive_new::new;
use nu_errors::ShellError; use nu_errors::ShellError;
use nu_protocol::hir::Operator; use nu_protocol::hir::Operator;
@ -23,6 +23,14 @@ impl Labels {
} }
} }
pub fn at_split(&self, idx: usize) -> Option<&str> {
if let Some(k) = self.y.get(idx) {
Some(&k[..])
} else {
None
}
}
pub fn grouped(&self) -> impl Iterator<Item = &String> { pub fn grouped(&self) -> impl Iterator<Item = &String> {
self.x.iter() self.x.iter()
} }
@ -51,7 +59,7 @@ fn formula(
calculator: Box<dyn Fn(Vec<&Value>) -> Result<Value, ShellError> + Send + Sync + 'static>, calculator: Box<dyn Fn(Vec<&Value>) -> Result<Value, ShellError> + Send + Sync + 'static>,
) -> Box<dyn Fn(&Value, Vec<&Value>) -> Result<Value, ShellError> + Send + Sync + 'static> { ) -> Box<dyn Fn(&Value, Vec<&Value>) -> Result<Value, ShellError> + Send + Sync + 'static> {
Box::new(move |acc, datax| -> Result<Value, ShellError> { Box::new(move |acc, datax| -> Result<Value, ShellError> {
let result = match compute_values(Operator::Multiply, &acc, &acc_begin) { let result = match unsafe_compute_values(Operator::Multiply, &acc, &acc_begin) {
Ok(v) => v.into_untagged_value(), Ok(v) => v.into_untagged_value(),
Err((left_type, right_type)) => { Err((left_type, right_type)) => {
return Err(ShellError::coerce_error( return Err(ShellError::coerce_error(
@ -62,43 +70,100 @@ fn formula(
}; };
match calculator(datax) { match calculator(datax) {
Ok(total) => Ok(match compute_values(Operator::Plus, &result, &total) { Ok(total) => Ok(
Ok(v) => v.into_untagged_value(), match unsafe_compute_values(Operator::Plus, &result, &total) {
Err((left_type, right_type)) => { Ok(v) => v.into_untagged_value(),
return Err(ShellError::coerce_error( Err((left_type, right_type)) => {
left_type.spanned_unknown(), return Err(ShellError::coerce_error(
right_type.spanned_unknown(), left_type.spanned_unknown(),
)) right_type.spanned_unknown(),
} ))
}), }
},
),
Err(reason) => Err(reason), Err(reason) => Err(reason),
} }
}) })
} }
pub fn reducer_for( pub fn reducer_for(
command: Reduction, command: &Reduction,
) -> Box<dyn Fn(&Value, Vec<&Value>) -> Result<Value, ShellError> + Send + Sync + 'static> { ) -> Box<dyn Fn(&Value, Vec<&Value>) -> Result<Value, ShellError> + Send + Sync + 'static> {
match command { match command {
Reduction::Accumulate => Box::new(formula( Reduction::Accumulate => Box::new(formula(
UntaggedValue::int(1).into_untagged_value(), UntaggedValue::int(1).into_untagged_value(),
Box::new(sum), Box::new(sum),
)), )),
_ => Box::new(formula( Reduction::Count => Box::new(formula(
UntaggedValue::int(0).into_untagged_value(), UntaggedValue::int(0).into_untagged_value(),
Box::new(sum), Box::new(sum),
)), )),
} }
} }
pub fn max(values: &Value, tag: impl Into<Tag>) -> Result<&Value, ShellError> { pub fn max(values: &Value, tag: impl Into<Tag>) -> Result<Value, ShellError> {
let tag = tag.into(); let tag = tag.into();
values let mut x = UntaggedValue::int(0);
.table_entries()
.filter_map(|dataset| dataset.table_entries().max()) for split in values.table_entries() {
.max() match split.value {
.ok_or_else(|| ShellError::labeled_error("err", "err", &tag)) UntaggedValue::Table(ref values) => {
let inner = inner_max(values)?;
if let Ok(greater_than) =
crate::value::compare_values(Operator::GreaterThan, &inner.value, &x)
{
if greater_than {
x = inner.value.clone();
}
} else {
return Err(ShellError::unexpected(format!(
"Could not compare\nleft: {:?}\nright: {:?}",
inner.value, x
)));
}
}
_ => {
return Err(ShellError::labeled_error(
"Attempted to compute the sum of a value that cannot be summed.",
"value appears here",
split.tag.span,
))
}
}
}
Ok(x.into_value(&tag))
}
pub fn inner_max(data: &[Value]) -> Result<Value, ShellError> {
let mut biggest = data
.first()
.ok_or_else(|| {
ShellError::unexpected("Cannot perform aggregate math operation on empty data")
})?
.value
.clone();
for value in data.iter() {
if let Ok(greater_than) =
crate::value::compare_values(Operator::GreaterThan, &value.value, &biggest)
{
if greater_than {
biggest = value.value.clone();
}
} else {
return Err(ShellError::unexpected(format!(
"Could not compare\nleft: {:?}\nright: {:?}",
biggest, value.value
)));
}
}
Ok(Value {
value: biggest,
tag: Tag::unknown(),
})
} }
pub fn sum(data: Vec<&Value>) -> Result<Value, ShellError> { pub fn sum(data: Vec<&Value>) -> Result<Value, ShellError> {
@ -107,7 +172,7 @@ pub fn sum(data: Vec<&Value>) -> Result<Value, ShellError> {
for value in data { for value in data {
match value.value { match value.value {
UntaggedValue::Primitive(_) => { UntaggedValue::Primitive(_) => {
acc = match compute_values(Operator::Plus, &acc, &value) { acc = match unsafe_compute_values(Operator::Plus, &acc, &value) {
Ok(v) => v, Ok(v) => v,
Err((left_type, right_type)) => { Err((left_type, right_type)) => {
return Err(ShellError::coerce_error( return Err(ShellError::coerce_error(
@ -133,19 +198,12 @@ pub fn sort_columns(
values: &[String], values: &[String],
format: &Option<Box<dyn Fn(&Value, String) -> Result<String, ShellError>>>, format: &Option<Box<dyn Fn(&Value, String) -> Result<String, ShellError>>>,
) -> Result<Vec<String>, ShellError> { ) -> Result<Vec<String>, ShellError> {
let mut keys = vec![]; let mut keys = values.to_vec();
if let Some(fmt) = format { if format.is_none() {
for k in values.iter() { keys.sort();
let k = k.clone().tagged_unknown();
let v = crate::value::Date::naive_from_str(k.borrow_tagged())?.into_untagged_value();
keys.push(fmt(&v, k.to_string())?);
}
} else {
keys = values.to_vec();
} }
keys.sort();
Ok(keys) Ok(keys)
} }
@ -167,17 +225,13 @@ pub fn sort(planes: &Labels, values: &Value, tag: impl Into<Tag>) -> Result<Valu
let grouped = groups.get_data_by_key(key.borrow_spanned()); let grouped = groups.get_data_by_key(key.borrow_spanned());
if let Some(grouped) = grouped { if let Some(grouped) = grouped {
y.push(grouped.table_entries().cloned().collect::<Vec<_>>()); y.push(grouped);
} else { } else {
let empty = UntaggedValue::table(&[]).into_value(&tag); y.push(UntaggedValue::Table(vec![]).into_value(&tag));
y.push(empty.table_entries().cloned().collect::<Vec<_>>());
} }
} }
x.push( x.push(UntaggedValue::table(&y).into_value(&tag));
UntaggedValue::table(&y.iter().cloned().flatten().collect::<Vec<Value>>())
.into_value(&tag),
);
} }
Ok(UntaggedValue::table(&x).into_value(&tag)) Ok(UntaggedValue::table(&x).into_value(&tag))
@ -195,17 +249,27 @@ pub fn evaluate(
let mut y = vec![]; let mut y = vec![];
for (idx, subset) in split.table_entries().enumerate() { for (idx, subset) in split.table_entries().enumerate() {
let mut set = vec![]; if let UntaggedValue::Table(values) = &subset.value {
if let Some(ref evaluator) = evaluator {
let mut evaluations = vec![];
if let Some(ref evaluator) = evaluator { for set in values.iter() {
let value = evaluator(idx, subset)?; evaluations.push(evaluator(idx, set)?);
}
set.push(value); y.push(UntaggedValue::Table(evaluations).into_value(&tag));
} else { } else {
set.push(UntaggedValue::int(1).into_value(&tag)); y.push(
UntaggedValue::Table(
values
.iter()
.map(|_| UntaggedValue::int(1).into_value(&tag))
.collect::<Vec<_>>(),
)
.into_value(&tag),
);
}
} }
y.push(UntaggedValue::table(&set).into_value(&tag));
} }
x.push(UntaggedValue::table(&y).into_value(&tag)); x.push(UntaggedValue::table(&y).into_value(&tag));
@ -215,14 +279,17 @@ pub fn evaluate(
} }
pub enum Reduction { pub enum Reduction {
#[allow(dead_code)]
Count, Count,
Accumulate, Accumulate,
} }
pub fn reduce(values: &Value, tag: impl Into<Tag>) -> Result<Value, ShellError> { pub fn reduce(
values: &Value,
reduction_with: &Reduction,
tag: impl Into<Tag>,
) -> Result<Value, ShellError> {
let tag = tag.into(); let tag = tag.into();
let reduce_with = reducer_for(Reduction::Accumulate); let reduce_with = reducer_for(reduction_with);
let mut datasets = vec![]; let mut datasets = vec![];
for dataset in values.table_entries() { for dataset in values.table_entries() {
@ -255,8 +322,8 @@ pub fn percentages(
.filter_map(|s| { .filter_map(|s| {
let hundred = UntaggedValue::decimal_from_float(100.0, tag.span); let hundred = UntaggedValue::decimal_from_float(100.0, tag.span);
match compute_values(Operator::Divide, &hundred, &maxima) { match unsafe_compute_values(Operator::Divide, &hundred, &maxima) {
Ok(v) => match compute_values(Operator::Multiply, &s, &v) { Ok(v) => match unsafe_compute_values(Operator::Multiply, &s, &v) {
Ok(v) => Some(v.into_untagged_value()), Ok(v) => Some(v.into_untagged_value()),
Err(_) => None, Err(_) => None,
}, },

View File

@ -6,6 +6,7 @@ mod internal;
pub use crate::utils::group::group; pub use crate::utils::group::group;
pub use crate::utils::split::split; pub use crate::utils::split::split;
pub use crate::utils::internal::Reduction;
use crate::utils::internal::*; use crate::utils::internal::*;
use derive_new::new; use derive_new::new;
@ -27,8 +28,9 @@ pub struct Model {
pub struct Operation<'a> { pub struct Operation<'a> {
pub grouper: Option<Box<dyn Fn(usize, &Value) -> Result<String, ShellError> + Send>>, pub grouper: Option<Box<dyn Fn(usize, &Value) -> Result<String, ShellError> + Send>>,
pub splitter: Option<Box<dyn Fn(usize, &Value) -> Result<String, ShellError> + Send>>, pub splitter: Option<Box<dyn Fn(usize, &Value) -> Result<String, ShellError> + Send>>,
pub format: Option<Box<dyn Fn(&Value, String) -> Result<String, ShellError>>>, pub format: &'a Option<Box<dyn Fn(&Value, String) -> Result<String, ShellError>>>,
pub eval: &'a Option<Box<dyn Fn(usize, &Value) -> Result<Value, ShellError> + Send>>, pub eval: &'a Option<Box<dyn Fn(usize, &Value) -> Result<Value, ShellError> + Send>>,
pub reduction: &'a Reduction,
} }
pub fn report( pub fn report(
@ -46,19 +48,17 @@ pub fn report(
.map(|(key, _)| key.clone()) .map(|(key, _)| key.clone())
.collect::<Vec<_>>(); .collect::<Vec<_>>();
let x = if options.format.is_some() { let x = sort_columns(&x, &options.format)?;
sort_columns(&x, &options.format)
} else {
sort_columns(&x, &None)
}?;
let mut y = splitted let mut y = splitted
.row_entries() .row_entries()
.map(|(key, _)| key.clone()) .map(|(key, _)| key.clone())
.collect::<Vec<_>>(); .collect::<Vec<_>>();
y.sort(); y.sort();
let planes = Labels { x, y }; let planes = Labels { x, y };
let sorted = sort(&planes, &splitted, &tag)?; let sorted = sort(&planes, &splitted, &tag)?;
let evaluated = evaluate( let evaluated = evaluate(
@ -72,11 +72,11 @@ pub fn report(
)?; )?;
let group_labels = planes.grouping_total(); let group_labels = planes.grouping_total();
let split_labels = planes.splits_total();
let reduced = reduce(&evaluated, &tag)?; let reduced = reduce(&evaluated, options.reduction, &tag)?;
let max = max(&reduced, &tag)?.clone(); let maxima = max(&reduced, &tag)?;
let maxima = max.clone();
let percents = percentages(&maxima, &reduced, &tag)?; let percents = percentages(&maxima, &reduced, &tag)?;
@ -89,7 +89,7 @@ pub fn report(
}, },
Range { Range {
start: UntaggedValue::int(0).into_untagged_value(), start: UntaggedValue::int(0).into_untagged_value(),
end: max, end: split_labels,
}, },
), ),
data: reduced, data: reduced,
@ -99,7 +99,6 @@ pub fn report(
pub mod helpers { pub mod helpers {
use super::Model; use super::Model;
use bigdecimal::BigDecimal;
use indexmap::indexmap; use indexmap::indexmap;
use nu_errors::ShellError; use nu_errors::ShellError;
use nu_protocol::{UntaggedValue, Value}; use nu_protocol::{UntaggedValue, Value};
@ -113,10 +112,6 @@ pub mod helpers {
UntaggedValue::int(s).into_untagged_value() UntaggedValue::int(s).into_untagged_value()
} }
pub fn decimal(f: impl Into<BigDecimal>) -> Value {
UntaggedValue::decimal(f.into()).into_untagged_value()
}
pub fn decimal_from_float(f: f64, span: Span) -> Value { pub fn decimal_from_float(f: f64, span: Span) -> Value {
UntaggedValue::decimal_from_float(f, span).into_untagged_value() UntaggedValue::decimal_from_float(f, span).into_untagged_value()
} }
@ -216,9 +211,12 @@ pub mod helpers {
} }
pub fn date_formatter( pub fn date_formatter(
fmt: &'static str, fmt: String,
) -> Box<dyn Fn(&Value, String) -> Result<String, ShellError>> { ) -> Box<dyn Fn(&Value, String) -> Result<String, ShellError>> {
Box::new(move |date: &Value, _: String| date.format(&fmt)) Box::new(move |date: &Value, _: String| {
let fmt = fmt.clone();
date.format(&fmt)
})
} }
pub fn assert_without_checking_percentages(report_a: Model, report_b: Model) { pub fn assert_without_checking_percentages(report_a: Model, report_b: Model) {
@ -232,23 +230,24 @@ pub mod helpers {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::helpers::{ use super::helpers::{
assert_without_checking_percentages, committers, date_formatter, decimal, assert_without_checking_percentages, committers, date_formatter, decimal_from_float, int,
decimal_from_float, int, table, table,
}; };
use super::{report, Labels, Model, Operation, Range}; use super::{report, Labels, Model, Operation, Range, Reduction};
use nu_errors::ShellError; use nu_errors::ShellError;
use nu_protocol::Value; use nu_protocol::Value;
use nu_source::{Span, Tag, TaggedItem}; use nu_source::{Span, Tag, TaggedItem};
use nu_value_ext::ValueExt; use nu_value_ext::ValueExt;
#[test] #[test]
fn prepares_report_using_accumulating_value() { fn prepares_report_using_counting_value() {
let committers = table(&committers()); let committers = table(&committers());
let by_date = Box::new(move |_, row: &Value| { let by_date = Box::new(move |_, row: &Value| {
let key = String::from("date").tagged_unknown(); let key = String::from("date").tagged_unknown();
let key = row.get_data_by_key(key.borrow_spanned()).unwrap(); let key = row.get_data_by_key(key.borrow_spanned()).unwrap();
let callback = date_formatter("%Y-%m-%d"); let callback = date_formatter("%Y-%m-%d".to_string());
callback(&key, "nothing".to_string()) callback(&key, "nothing".to_string())
}); });
@ -261,7 +260,7 @@ mod tests {
let options = Operation { let options = Operation {
grouper: Some(by_date), grouper: Some(by_date),
splitter: Some(by_country), splitter: Some(by_country),
format: Some(date_formatter("%Y-%m-%d")), format: &None,
eval: /* value to be used for accumulation */ &Some(Box::new(move |_, value: &Value| { eval: /* value to be used for accumulation */ &Some(Box::new(move |_, value: &Value| {
let chickens_key = String::from("chickens").tagged_unknown(); let chickens_key = String::from("chickens").tagged_unknown();
@ -275,6 +274,7 @@ mod tests {
) )
}) })
})), })),
reduction: &Reduction::Count
}; };
assert_without_checking_percentages( assert_without_checking_percentages(
@ -295,29 +295,29 @@ mod tests {
}, },
Range { Range {
start: int(0), start: int(0),
end: int(60), end: int(3),
}, },
), ),
data: table(&[ data: table(&[
table(&[int(10), int(30), int(60)]), table(&[int(10), int(20), int(30)]),
table(&[int(5), int(15), int(30)]), table(&[int(5), int(10), int(15)]),
table(&[int(2), int(6), int(12)]), table(&[int(2), int(4), int(6)]),
]), ]),
percentages: table(&[ percentages: table(&[
table(&[
decimal_from_float(33.33, Span::unknown()),
decimal_from_float(66.66, Span::unknown()),
decimal_from_float(99.99, Span::unknown()),
]),
table(&[ table(&[
decimal_from_float(16.66, Span::unknown()), decimal_from_float(16.66, Span::unknown()),
decimal(50), decimal_from_float(33.33, Span::unknown()),
decimal(100), decimal_from_float(49.99, Span::unknown()),
]), ]),
table(&[ table(&[
decimal_from_float(8.33, Span::unknown()), decimal_from_float(6.66, Span::unknown()),
decimal(25), decimal_from_float(13.33, Span::unknown()),
decimal(50), decimal_from_float(19.99, Span::unknown()),
]),
table(&[
decimal_from_float(3.33, Span::unknown()),
decimal(10),
decimal(20),
]), ]),
]), ]),
}, },

View File

@ -16,7 +16,7 @@ pub fn split(
let mut out = TaggedDictBuilder::new(&tag); let mut out = TaggedDictBuilder::new(&tag);
if splitter.is_none() { if splitter.is_none() {
out.insert_untagged("table", UntaggedValue::table(&[value.clone()])); out.insert_untagged("table", value.clone());
return Ok(out.into_value()); return Ok(out.into_value());
} }

View File

@ -1,10 +1,12 @@
use crate::value::{UntaggedValue, Value}; use crate::value::{UntaggedValue, Value};
#[derive(Debug)]
pub enum RowValueIter<'a> { pub enum RowValueIter<'a> {
Empty, Empty,
Entries(indexmap::map::Iter<'a, String, Value>), Entries(indexmap::map::Iter<'a, String, Value>),
} }
#[derive(Debug)]
pub enum TableValueIter<'a> { pub enum TableValueIter<'a> {
Empty, Empty,
Entries(std::slice::Iter<'a, Value>), Entries(std::slice::Iter<'a, Value>),

View File

@ -11,7 +11,7 @@ doctest = false
[dependencies] [dependencies]
ansi_term = "0.12.1" ansi_term = "0.12.1"
crossterm = "0.17" crossterm = "0.18"
image = {version = "0.22.4", default_features = false, features = ["png_codec", "jpeg"]} image = {version = "0.22.4", default_features = false, features = ["png_codec", "jpeg"]}
neso = "0.5.0" neso = "0.5.0"
nu-errors = {path = "../nu-errors", version = "0.20.0"} nu-errors = {path = "../nu-errors", version = "0.20.0"}

View File

@ -0,0 +1,22 @@
[package]
authors = ["The Nu Project Contributors"]
description = "A plugin to display charts"
edition = "2018"
license = "MIT"
name = "nu_plugin_chart"
version = "0.20.0"
[lib]
doctest = false
[dependencies]
nu-data = {path = "../nu-data", version = "0.20.0"}
nu-errors = {path = "../nu-errors", version = "0.20.0"}
nu-plugin = {path = "../nu-plugin", version = "0.20.0"}
nu-protocol = {path = "../nu-protocol", version = "0.20.0"}
nu-source = {path = "../nu-source", version = "0.20.0"}
nu-cli = {path = "../nu-cli", version = "0.20.0"}
nu-value-ext = {path = "../nu-value-ext", version = "0.20.0"}
crossterm = "0.18"
tui = {version = "0.12.0", default-features = false, features = ["crossterm"]}

View File

@ -0,0 +1,155 @@
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},
};
pub enum Columns {
One(Tagged<String>),
Two(Tagged<String>, Tagged<String>),
None,
}
#[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 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> {
let mut data = Vec::new();
let mut data_points = Vec::new();
for percentages in model
.percentages
.table_entries()
.cloned()
.collect::<Vec<_>>()
.into_iter()
{
let mut percentages_collected = vec![];
for percentage in percentages
.table_entries()
.cloned()
.collect::<Vec<_>>()
.into_iter()
{
percentages_collected.push(percentage.as_u64()?);
}
data_points.push(percentages_collected);
}
let mark_in = if model.labels.y.len() <= 1 {
0
} else {
(model.labels.y.len() as f64 / 2.0).floor() as usize
};
for idx in 0..model.labels.x.len() {
let mut current = 0;
loop {
let label = if current == mark_in {
model
.labels
.at(idx)
.ok_or_else(|| ShellError::untagged_runtime_error("Could not load data"))?
} else {
""
};
let percentages_collected = data_points
.get(current)
.ok_or_else(|| ShellError::untagged_runtime_error("Could not load data"))?;
data.push((
label,
*percentages_collected
.get(idx)
.ok_or_else(|| ShellError::untagged_runtime_error("Could not load data"))?,
));
current += 1;
if current == model.labels.y.len() {
break;
}
}
}
Ok(BarChart {
title: "Bar Chart",
data: (&data[..]).to_vec(),
enhanced_graphics: true,
})
}
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(2)
.constraints([Constraint::Percentage(100)].as_ref())
.split(f.size());
let barchart = TuiBarChart::default()
.block(Block::default().title("Chart").borders(Borders::ALL))
.data(&self.data)
.bar_width(9)
.bar_style(Style::default().fg(Color::Green))
.value_style(
Style::default()
.bg(Color::Green)
.add_modifier(Modifier::BOLD),
);
f.render_widget(barchart, chunks[0]);
})
}
pub fn on_right(&mut self) {
let one_bar = self.data.remove(0);
self.data.push(one_bar);
}
pub fn on_left(&mut self) {
if let Some(one_bar) = self.data.pop() {
self.data.insert(0, one_bar);
}
}
}

View File

@ -0,0 +1,4 @@
mod chart;
mod nu;
pub use chart::Chart;

View File

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

View File

@ -0,0 +1,330 @@
use nu_errors::ShellError;
use nu_plugin::Plugin;
use nu_protocol::{CallInfo, ColumnPath, Primitive, Signature, SyntaxShape, UntaggedValue, Value};
use nu_source::TaggedItem;
use nu_value_ext::ValueExt;
use crate::chart::{BarChart, Chart, Columns};
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,
}
fn display(model: &nu_data::utils::Model) -> Result<(), Box<dyn Error>> {
let mut app = BarChart::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;
}
KeyCode::Left => app.on_left(),
KeyCode::Right => app.on_right(),
_ => {}
},
Event::Tick => {}
}
}
Ok(())
}
impl Plugin for Chart {
fn config(&mut self) -> Result<Signature, ShellError> {
Ok(Signature::build("chart")
.desc("Displays bar 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 Chart {
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(&nu_data::utils::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(&nu_data::utils::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

@ -35,7 +35,7 @@ If we now want to see how often the different numbers were generated, we can use
```shell ```shell
> open random_numbers.csv | histogram "random numbers" > open random_numbers.csv | histogram "random numbers"
───┬────────────────┬─────────────┬────────────┬────────────────────────────────────────────────────────────────────────────────────────────────────── ───┬────────────────┬─────────────┬────────────┬──────────────────────────────────────────────────────────────────────────────────────────────────────
# │ random numbers │ occurrences │ percentage │ frequency # │ random numbers │ count │ percentage │ frequency
───┼────────────────┼─────────────┼────────────┼────────────────────────────────────────────────────────────────────────────────────────────────────── ───┼────────────────┼─────────────┼────────────┼──────────────────────────────────────────────────────────────────────────────────────────────────────
0 │ 0 │ 8 │ 57.14% │ ********************************************************* 0 │ 0 │ 8 │ 57.14% │ *********************************************************
1 │ 1 │ 14 │ 100.00% │ **************************************************************************************************** 1 │ 1 │ 14 │ 100.00% │ ****************************************************************************************************
@ -51,7 +51,7 @@ We can also set the name of the second column or sort the table:
```shell ```shell
> open random_numbers.csv | histogram "random numbers" probability > open random_numbers.csv | histogram "random numbers" probability
───┬────────────────┬─────────────┬────────────┬────────────────────────────────────────────────────────────────────────────────────────────────────── ───┬────────────────┬─────────────┬────────────┬──────────────────────────────────────────────────────────────────────────────────────────────────────
# │ random numbers │ occurrences │ percentage │ probability # │ random numbers │ count │ percentage │ probability
───┼────────────────┼─────────────┼────────────┼────────────────────────────────────────────────────────────────────────────────────────────────────── ───┼────────────────┼─────────────┼────────────┼──────────────────────────────────────────────────────────────────────────────────────────────────────
0 │ 0 │ 8 │ 57.14% │ ********************************************************* 0 │ 0 │ 8 │ 57.14% │ *********************************************************
1 │ 1 │ 14 │ 100.00% │ **************************************************************************************************** 1 │ 1 │ 14 │ 100.00% │ ****************************************************************************************************
@ -66,7 +66,7 @@ We can also set the name of the second column or sort the table:
```shell ```shell
> open random_numbers.csv | histogram "random numbers" probability | sort-by probability > open random_numbers.csv | histogram "random numbers" probability | sort-by probability
───┬────────────────┬─────────────┬────────────┬────────────────────────────────────────────────────────────────────────────────────────────────────── ───┬────────────────┬─────────────┬────────────┬──────────────────────────────────────────────────────────────────────────────────────────────────────
# │ random numbers │ occurrences │ percentage │ probability # │ random numbers │ count │ percentage │ probability
───┼────────────────┼─────────────┼────────────┼────────────────────────────────────────────────────────────────────────────────────────────────────── ───┼────────────────┼─────────────┼────────────┼──────────────────────────────────────────────────────────────────────────────────────────────────────
0 │ 4 │ 3 │ 21.43% │ ********************* 0 │ 4 │ 3 │ 21.43% │ *********************
1 │ 3 │ 6 │ 42.86% │ ****************************************** 1 │ 3 │ 6 │ 42.86% │ ******************************************
@ -81,9 +81,9 @@ We can also set the name of the second column or sort the table:
Of course, histogram operations are not restricted to just analyzing numbers in files, you can also analyze your directories Of course, histogram operations are not restricted to just analyzing numbers in files, you can also analyze your directories
```shell ```shell
> ls -la | histogram type | sort-by occurrences > ls -la | histogram type | sort-by count
───┬─────────┬─────────────┬────────────┬────────────────────────────────────────────────────────────────────────────────────────────────────── ───┬─────────┬─────────────┬────────────┬──────────────────────────────────────────────────────────────────────────────────────────────────────
# │ type │ occurrences │ percentage │ frequency # │ type │ count │ percentage │ frequency
───┼─────────┼─────────────┼────────────┼────────────────────────────────────────────────────────────────────────────────────────────────────── ───┼─────────┼─────────────┼────────────┼──────────────────────────────────────────────────────────────────────────────────────────────────────
0 │ Dir │ 5 │ 4.76% │ **** 0 │ Dir │ 5 │ 4.76% │ ****
1 │ Symlink │ 28 │ 26.67% │ ************************** 1 │ Symlink │ 28 │ 26.67% │ **************************

View File

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