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",
]
[[package]]
name = "cassowary"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53"
[[package]]
name = "cc"
version = "1.0.59"
@ -940,6 +946,22 @@ dependencies = [
"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]]
name = "crossterm_winapi"
version = "0.6.1"
@ -2850,6 +2872,7 @@ dependencies = [
"nu-test-support",
"nu-value-ext",
"nu_plugin_binaryview",
"nu_plugin_chart",
"nu_plugin_fetch",
"nu_plugin_from_bson",
"nu_plugin_from_sqlite",
@ -3116,7 +3139,7 @@ name = "nu_plugin_binaryview"
version = "0.20.0"
dependencies = [
"ansi_term 0.12.1",
"crossterm",
"crossterm 0.18.0",
"image",
"neso",
"nu-errors",
@ -3127,6 +3150,21 @@ dependencies = [
"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]]
name = "nu_plugin_fetch"
version = "0.20.0"
@ -5471,6 +5509,19 @@ dependencies = [
"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]]
name = "typed-arena"
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-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_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}
@ -59,6 +60,16 @@ serde = {version = "1.0.115", features = ["derive"]}
toml = "0.5.6"
[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 = [
"sys",
"ps",
@ -77,38 +88,30 @@ default = [
"fetch",
"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"]
# Default
trace = ["nu-parser/trace"]
# Stable (Default)
inc = ["nu_plugin_inc"]
ps = ["nu_plugin_ps"]
sys = ["nu_plugin_sys"]
textview = ["nu_plugin_textview"]
# Stable
binaryview = ["nu_plugin_binaryview"]
bson = ["nu_plugin_from_bson", "nu_plugin_to_bson"]
fetch = ["nu_plugin_fetch"]
match = ["nu_plugin_match"]
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"]
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
# 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"
required-features = ["s3"]
[[bin]]
name = "nu_plugin_extra_chart"
path = "src/plugins/nu_plugin_extra_chart.rs"
required-features = ["chart"]
# Main nu binary
[[bin]]
name = "nu"

View File

@ -112,8 +112,9 @@ pub async fn histogram(
nu_data::utils::Operation {
grouper: Some(Box::new(move |_, _| Ok(String::from("frequencies")))),
splitter: Some(splitter(column_grouper)),
format: None,
format: &None,
eval: &evaluate_with,
reduction: &nu_data::utils::Reduction::Count,
},
&name,
)?;
@ -123,17 +124,33 @@ pub async fn histogram(
Ok(futures::stream::iter(
results
.percentages
.data
.table_entries()
.map(move |value| {
let values = value.table_entries().cloned().collect::<Vec<_>>();
let occurrences = values.len();
(occurrences, values[occurrences - 1].clone())
})
.cloned()
.collect::<Vec<_>>()
.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 column_value = labels
.get(idx)
@ -147,19 +164,19 @@ pub async fn histogram(
.clone();
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
// 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()?
);
fact.insert_untagged("percentage", UntaggedValue::string(percentage));
fact.insert_untagged("percentage", UntaggedValue::string(fmt_percentage));
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)
})? as usize)
.collect::<String>();

View File

@ -54,7 +54,7 @@ fn summarizes_by_values() {
| get rusty_at
| histogram
| where value == "Estados Unidos"
| get occurrences
| get count
| echo $it
"#
));
@ -93,20 +93,19 @@ fn help() {
}
#[test]
fn occurrences() {
fn count() {
let actual = nu!(
cwd: ".", pipeline(
r#"
echo "[{"bit":1},{"bit":0},{"bit":0},{"bit":0},{"bit":0},{"bit":0},{"bit":0},{"bit":1}]"
| from json
echo [[bit]; [1] [0] [0] [0] [0] [0] [0] [1]]
| histogram bit
| sort-by occurrences
| sort-by count
| reject frequency
| 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);
}

View File

@ -1,6 +1,6 @@
#![allow(clippy::type_complexity)]
use crate::value::compute_values;
use crate::value::unsafe_compute_values;
use derive_new::new;
use nu_errors::ShellError;
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> {
self.x.iter()
}
@ -51,7 +59,7 @@ fn formula(
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::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(),
Err((left_type, right_type)) => {
return Err(ShellError::coerce_error(
@ -62,7 +70,8 @@ fn formula(
};
match calculator(datax) {
Ok(total) => Ok(match compute_values(Operator::Plus, &result, &total) {
Ok(total) => Ok(
match unsafe_compute_values(Operator::Plus, &result, &total) {
Ok(v) => v.into_untagged_value(),
Err((left_type, right_type)) => {
return Err(ShellError::coerce_error(
@ -70,35 +79,91 @@ fn formula(
right_type.spanned_unknown(),
))
}
}),
},
),
Err(reason) => Err(reason),
}
})
}
pub fn reducer_for(
command: Reduction,
command: &Reduction,
) -> Box<dyn Fn(&Value, Vec<&Value>) -> Result<Value, ShellError> + Send + Sync + 'static> {
match command {
Reduction::Accumulate => Box::new(formula(
UntaggedValue::int(1).into_untagged_value(),
Box::new(sum),
)),
_ => Box::new(formula(
Reduction::Count => Box::new(formula(
UntaggedValue::int(0).into_untagged_value(),
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();
values
.table_entries()
.filter_map(|dataset| dataset.table_entries().max())
.max()
.ok_or_else(|| ShellError::labeled_error("err", "err", &tag))
let mut x = UntaggedValue::int(0);
for split in values.table_entries() {
match split.value {
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> {
@ -107,7 +172,7 @@ pub fn sum(data: Vec<&Value>) -> Result<Value, ShellError> {
for value in data {
match value.value {
UntaggedValue::Primitive(_) => {
acc = match compute_values(Operator::Plus, &acc, &value) {
acc = match unsafe_compute_values(Operator::Plus, &acc, &value) {
Ok(v) => v,
Err((left_type, right_type)) => {
return Err(ShellError::coerce_error(
@ -133,19 +198,12 @@ pub fn sort_columns(
values: &[String],
format: &Option<Box<dyn Fn(&Value, String) -> Result<String, ShellError>>>,
) -> Result<Vec<String>, ShellError> {
let mut keys = vec![];
if let Some(fmt) = format {
for k in values.iter() {
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();
}
let mut keys = values.to_vec();
if format.is_none() {
keys.sort();
}
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());
if let Some(grouped) = grouped {
y.push(grouped.table_entries().cloned().collect::<Vec<_>>());
y.push(grouped);
} else {
let empty = UntaggedValue::table(&[]).into_value(&tag);
y.push(empty.table_entries().cloned().collect::<Vec<_>>());
y.push(UntaggedValue::Table(vec![]).into_value(&tag));
}
}
x.push(
UntaggedValue::table(&y.iter().cloned().flatten().collect::<Vec<Value>>())
.into_value(&tag),
);
x.push(UntaggedValue::table(&y).into_value(&tag));
}
Ok(UntaggedValue::table(&x).into_value(&tag))
@ -195,17 +249,27 @@ pub fn evaluate(
let mut y = vec![];
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 value = evaluator(idx, subset)?;
let mut evaluations = vec![];
set.push(value);
} else {
set.push(UntaggedValue::int(1).into_value(&tag));
for set in values.iter() {
evaluations.push(evaluator(idx, set)?);
}
y.push(UntaggedValue::table(&set).into_value(&tag));
y.push(UntaggedValue::Table(evaluations).into_value(&tag));
} else {
y.push(
UntaggedValue::Table(
values
.iter()
.map(|_| UntaggedValue::int(1).into_value(&tag))
.collect::<Vec<_>>(),
)
.into_value(&tag),
);
}
}
}
x.push(UntaggedValue::table(&y).into_value(&tag));
@ -215,14 +279,17 @@ pub fn evaluate(
}
pub enum Reduction {
#[allow(dead_code)]
Count,
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 reduce_with = reducer_for(Reduction::Accumulate);
let reduce_with = reducer_for(reduction_with);
let mut datasets = vec![];
for dataset in values.table_entries() {
@ -255,8 +322,8 @@ pub fn percentages(
.filter_map(|s| {
let hundred = UntaggedValue::decimal_from_float(100.0, tag.span);
match compute_values(Operator::Divide, &hundred, &maxima) {
Ok(v) => match compute_values(Operator::Multiply, &s, &v) {
match unsafe_compute_values(Operator::Divide, &hundred, &maxima) {
Ok(v) => match unsafe_compute_values(Operator::Multiply, &s, &v) {
Ok(v) => Some(v.into_untagged_value()),
Err(_) => None,
},

View File

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

View File

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

View File

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

View File

@ -11,7 +11,7 @@ doctest = false
[dependencies]
ansi_term = "0.12.1"
crossterm = "0.17"
crossterm = "0.18"
image = {version = "0.22.4", default_features = false, features = ["png_codec", "jpeg"]}
neso = "0.5.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
> open random_numbers.csv | histogram "random numbers"
───┬────────────────┬─────────────┬────────────┬──────────────────────────────────────────────────────────────────────────────────────────────────────
# │ random numbers │ occurrences │ percentage │ frequency
# │ random numbers │ count │ percentage │ frequency
───┼────────────────┼─────────────┼────────────┼──────────────────────────────────────────────────────────────────────────────────────────────────────
0 │ 0 │ 8 │ 57.14% │ *********************************************************
1 │ 1 │ 14 │ 100.00% │ ****************************************************************************************************
@ -51,7 +51,7 @@ We can also set the name of the second column or sort the table:
```shell
> open random_numbers.csv | histogram "random numbers" probability
───┬────────────────┬─────────────┬────────────┬──────────────────────────────────────────────────────────────────────────────────────────────────────
# │ random numbers │ occurrences │ percentage │ probability
# │ random numbers │ count │ percentage │ probability
───┼────────────────┼─────────────┼────────────┼──────────────────────────────────────────────────────────────────────────────────────────────────────
0 │ 0 │ 8 │ 57.14% │ *********************************************************
1 │ 1 │ 14 │ 100.00% │ ****************************************************************************************************
@ -66,7 +66,7 @@ We can also set the name of the second column or sort the table:
```shell
> 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% │ *********************
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
```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% │ ****
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());
}