Panics · P15 Charts: values within charts::LIMIT; scales, shares and tangents stay finite at the edges; entries past their lists are skipped
This commit is contained in:
1 parent
2ea16f4541
commit
defbec006e
23 files changed
+483
-95
No files matched your search
@@ -154,7 +154,7 @@ Ely GPUI Component. A component library for GPUI, in light and dark.
|
||||
- Taffy 0.13 collapses a child's vertical margin through a block parent with no padding or border on that side, and its size cache keeps only that block's size, so a box that measures it again leaves the margin out. A component moves a box with a relative offset, or with padding where what follows moves too, not with such a margin: `layout::Drawer` pulls and snaps its panel, and a fullscreen `overlays::Dialog` slides its card, with `top`; `motion::Stagger`, `Transition`, a centered Dialog and the palette's card rise with padding, so the box a scrim centers grows with them; the gallery's inspector sample is its probe's own child. A margin that stops at its parent is safe: a flex or grid item's own, or a child's of a flex item or of a box with padding or a border on that side. A flex wrapper stops the collapse but gives its items a definite height, so percent heights inside start to resolve (the Drawer's `max_h(relative(0.85))`).
|
||||
- Tiles in a picture's shape sit in flex rows of `flex_1` cells (`generative::GenerationGrid`). A cell's padding counts in its flex base, so a row of fewer cells gives each more width; cells that match across rows keep their inset on a box inside (`messaging::VideoCallGrid`).
|
||||
- Rise reads green and fall red unless a component's `red_up` swaps them, as markets in East Asia read. `finance::quotes::moves` picks the pair.
|
||||
- Charts lay out in `f32` pixels of their own box, tested without gpui, and paint through `charts::paint::at`: a canvas's paint gets window coordinates, and `with_element_offset` works only in prepaint. Their sizes come from `theme.chart()`.
|
||||
- Charts lay out in `f32` pixels of their own box, tested without gpui, and paint through `charts::paint::at`: a canvas's paint gets window coordinates, and `with_element_offset` works only in prepaint. Their sizes come from `theme.chart()`. A chart value is finite and within ±`charts::LIMIT` (1e30), so sums of values and their pixels stay inside `f32`; the builders refuse others, and a scale works in halves so a domain across the limit maps.
|
||||
- A label pinned to a point sits in a zero-size absolute box that centers it with flex (`charts::axes`), so no offset guesses at text size.
|
||||
- Tokens live in `src/theme`; the raw `px()` scan skips that folder only.
|
||||
- An owner's palette, edited or imported, reaches the theme per mode through `Theme::set_palette`, and `Theme::palette` names the one the shown mode fades to; high contrast strengthens Ely's own palettes alone. A color goes by its field's name through `token` and `token_mut`, on `Palette` and `Syntax` alike. Ely's palettes pass `tooling::A11yChecker`'s audit in both modes, with high contrast and without; a test holds them there.
|
||||
|
||||
@@ -11,6 +11,7 @@ use super::{
|
||||
parts::ChartTooltip,
|
||||
scale::{Linear, compact, nice},
|
||||
schedule::{days, marks},
|
||||
series::drawable,
|
||||
tiles::{Tiles, tracked},
|
||||
};
|
||||
use crate::{
|
||||
@@ -62,7 +63,10 @@ impl BurndownChart {
|
||||
/// A sprint from its first day through its last, with `scope` to do, in points or issues.
|
||||
pub fn new(id: impl Into<ElementId>, first: Date, last: Date, scope: f64) -> Self {
|
||||
assert!(last > first, "a sprint ends after its first day");
|
||||
assert!(scope.is_finite() && scope > 0.0, "a sprint has work to do");
|
||||
assert!(
|
||||
drawable(scope) && scope > 0.0,
|
||||
"a sprint has work to do, within charts::LIMIT"
|
||||
);
|
||||
Self {
|
||||
base: div(),
|
||||
id: id.into(),
|
||||
@@ -85,8 +89,8 @@ impl BurndownChart {
|
||||
assert!(
|
||||
self.left
|
||||
.iter()
|
||||
.all(|value| value.is_finite() && *value >= 0.0),
|
||||
"work left is a count, zero or more"
|
||||
.all(|value| drawable(*value) && *value >= 0.0),
|
||||
"work left is a count, zero or more, within charts::LIMIT"
|
||||
);
|
||||
self
|
||||
}
|
||||
|
||||
+13
-9
@@ -4,7 +4,10 @@ use gpui::{
|
||||
App, Div, ElementId, Entity, Hsla, InteractiveElement, IntoElement, ParentElement, Pixels,
|
||||
Refineable, RenderOnce, StyleRefinement, Styled, Window, canvas, div, fill,
|
||||
};
|
||||
use jiff::{Span, civil::Date};
|
||||
use jiff::{
|
||||
Span,
|
||||
civil::{Date, date},
|
||||
};
|
||||
|
||||
use super::{
|
||||
axes::{anchored, beside},
|
||||
@@ -14,6 +17,7 @@ use super::{
|
||||
parts::ChartTooltip,
|
||||
plot::Format,
|
||||
scale::compact,
|
||||
series::drawable,
|
||||
tiles::{Tiles, key, tracked},
|
||||
};
|
||||
use crate::theme::{ActiveTheme, Radius, TextSize};
|
||||
@@ -37,10 +41,13 @@ impl CalendarHeatmap {
|
||||
) -> Self {
|
||||
let counts: Vec<f64> = counts.into_iter().collect();
|
||||
assert!(
|
||||
counts
|
||||
.iter()
|
||||
.all(|count| count.is_finite() && *count >= 0.0),
|
||||
"a calendar needs counts of zero or more"
|
||||
counts.iter().all(|count| drawable(*count) && *count >= 0.0),
|
||||
"a calendar needs counts of zero or more, within charts::LIMIT"
|
||||
);
|
||||
let span = Span::new().days(counts.len().saturating_sub(1) as i64);
|
||||
assert!(
|
||||
start.checked_add(span).is_ok(),
|
||||
"a calendar's last day lies past the dates jiff holds"
|
||||
);
|
||||
Self {
|
||||
base: div(),
|
||||
@@ -119,10 +126,7 @@ impl RenderOnce for CalendarHeatmap {
|
||||
.child(text(day(ix).strftime("%b").to_string()))
|
||||
});
|
||||
let weekdays = [0usize, 2, 4].map(|weekday| {
|
||||
let date = (0..7)
|
||||
.map(day)
|
||||
.find(|date| date.weekday().to_monday_zero_offset() as usize == weekday)
|
||||
.expect("seven days hold every weekday");
|
||||
let date = date(2024, 1, 1 + weekday as i8);
|
||||
beside(
|
||||
top + side * (weekday as f32 + 0.5),
|
||||
sizes.gutter.to_pixels(rem),
|
||||
|
||||
+11
-6
@@ -10,6 +10,7 @@ use super::{
|
||||
geometry::{Categories, Ink, Mark, categories, scatter},
|
||||
plot::{Format, Layout, Pick, Plot, Scene, Tips, plot},
|
||||
scale::compact,
|
||||
series::drawable,
|
||||
series::{Points, Series, csv, points_csv},
|
||||
};
|
||||
use crate::theme::ActiveTheme;
|
||||
@@ -173,18 +174,22 @@ macro_rules! category_chart {
|
||||
|
||||
/// A dashed level across the chart with its label, as a target or a limit.
|
||||
pub fn rule(mut self, value: f64, label: impl Into<SharedString>) -> Self {
|
||||
assert!(value.is_finite(), "a rule needs a finite value");
|
||||
assert!(
|
||||
drawable(value),
|
||||
"a rule needs a value, within charts::LIMIT"
|
||||
);
|
||||
self.chart.rules.push((value, label.into()));
|
||||
self
|
||||
}
|
||||
|
||||
/// A mark at category `ix` with its label, as a release or an incident.
|
||||
pub fn note(mut self, ix: usize, label: impl Into<SharedString>) -> Self {
|
||||
assert!(
|
||||
ix < self.chart.labels.len(),
|
||||
"note {ix} is past the last label"
|
||||
);
|
||||
self.chart.notes.push((ix, label.into()));
|
||||
let label = label.into();
|
||||
if ix >= self.chart.labels.len() {
|
||||
log::error!("chart: note {label} at {ix} is past the last label; skipped");
|
||||
return self;
|
||||
}
|
||||
self.chart.notes.push((ix, label));
|
||||
self
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ use super::{
|
||||
plot::{Layout, Pick, Plot, Scene, Tips},
|
||||
scale::{Band, Linear, compact, nice},
|
||||
series::Series,
|
||||
series::drawable,
|
||||
stats::{Step, bins, density, five, steps},
|
||||
};
|
||||
|
||||
@@ -77,8 +78,8 @@ impl Histogram {
|
||||
pub fn new(id: impl Into<ElementId>, values: impl IntoIterator<Item = f64>) -> Self {
|
||||
let values: Vec<f64> = values.into_iter().collect();
|
||||
assert!(
|
||||
values.iter().all(|value| value.is_finite()),
|
||||
"a histogram needs finite values"
|
||||
values.iter().all(|value| drawable(*value)),
|
||||
"a histogram needs values, within charts::LIMIT"
|
||||
);
|
||||
Self {
|
||||
base: div(),
|
||||
@@ -130,7 +131,10 @@ impl WaterfallChart {
|
||||
|
||||
/// A change, up or down, from the running total.
|
||||
pub fn step(mut self, label: impl Into<SharedString>, change: f64) -> Self {
|
||||
assert!(change.is_finite(), "a step needs a finite change");
|
||||
assert!(
|
||||
drawable(change),
|
||||
"a step needs a change, within charts::LIMIT"
|
||||
);
|
||||
self.steps.push((label.into(), change, false));
|
||||
self
|
||||
}
|
||||
@@ -257,8 +261,8 @@ macro_rules! grouped {
|
||||
) -> Self {
|
||||
let values: Vec<f64> = values.into_iter().collect();
|
||||
assert!(
|
||||
values.len() >= 2 && values.iter().all(|value| value.is_finite()),
|
||||
"a group needs two finite values"
|
||||
values.len() >= 2 && values.iter().all(|value| drawable(*value)),
|
||||
"a group needs two values, within charts::LIMIT"
|
||||
);
|
||||
self.groups.push((label.into(), values));
|
||||
self
|
||||
|
||||
+23
-15
@@ -13,6 +13,7 @@ use super::{
|
||||
parts::ChartTooltip,
|
||||
plot::Format,
|
||||
scale::compact,
|
||||
series::drawable,
|
||||
};
|
||||
use crate::{
|
||||
theme::{ActiveTheme, Radius, TextSize},
|
||||
@@ -118,15 +119,18 @@ pub(crate) fn sankey(
|
||||
let columns = column.iter().max().map_or(1, |last| last + 1);
|
||||
let placed = &column;
|
||||
let members = |c: usize| (0..count).filter(move |ix| placed[*ix] == c);
|
||||
let total = |c: usize| members(c).map(|ix| through[ix]).sum::<f64>();
|
||||
let biggest = (0..columns).map(total).fold(0.0, f64::max);
|
||||
let scale = (0..columns)
|
||||
.map(|c| {
|
||||
let (total, gaps) = (
|
||||
members(c).map(|ix| through[ix]).sum::<f64>(),
|
||||
members(c).count().saturating_sub(1),
|
||||
);
|
||||
(frame.h - padding * gaps as f32).max(0.0) / total.max(f64::EPSILON) as f32
|
||||
let gaps = members(c).count().saturating_sub(1);
|
||||
f64::from((frame.h - padding * gaps as f32).max(0.0)) / (total(c) / biggest)
|
||||
})
|
||||
.fold(f32::MAX, f32::min);
|
||||
.fold(f64::INFINITY, f64::min);
|
||||
let height = |value: f64| match biggest > 0.0 {
|
||||
true => (value / biggest * scale) as f32,
|
||||
false => 0.0,
|
||||
};
|
||||
let mut nodes = vec![Rect::default(); count];
|
||||
for c in 0..columns {
|
||||
let x = if columns > 1 {
|
||||
@@ -134,7 +138,7 @@ pub(crate) fn sankey(
|
||||
} else {
|
||||
frame.x
|
||||
};
|
||||
let tall: f32 = members(c).map(|ix| through[ix] as f32 * scale).sum::<f32>()
|
||||
let tall: f32 = members(c).map(|ix| height(through[ix])).sum::<f32>()
|
||||
+ padding * members(c).count().saturating_sub(1) as f32;
|
||||
let mut y = frame.y + (frame.h - tall) / 2.0;
|
||||
for ix in members(c) {
|
||||
@@ -142,7 +146,7 @@ pub(crate) fn sankey(
|
||||
x,
|
||||
y,
|
||||
w: node,
|
||||
h: through[ix] as f32 * scale,
|
||||
h: height(through[ix]),
|
||||
};
|
||||
y += nodes[ix].h + padding;
|
||||
}
|
||||
@@ -152,7 +156,7 @@ pub(crate) fn sankey(
|
||||
.map(|link| Ribbon {
|
||||
from: (0.0, 0.0),
|
||||
to: (0.0, 0.0),
|
||||
thick: link.2 as f32 * scale,
|
||||
thick: height(link.2),
|
||||
})
|
||||
.collect();
|
||||
let mut order: Vec<usize> = (0..links.len()).collect();
|
||||
@@ -200,13 +204,16 @@ impl SankeyChart {
|
||||
|
||||
/// A flow of `value` from node `from` to node `to`, by their places in the list.
|
||||
pub fn link(mut self, from: usize, to: usize, value: f64) -> Self {
|
||||
if from >= self.nodes.len() || to >= self.nodes.len() || from == to {
|
||||
log::error!(
|
||||
"sankey: a link {from} -> {to} among {} nodes; skipped",
|
||||
self.nodes.len()
|
||||
);
|
||||
return self;
|
||||
}
|
||||
assert!(
|
||||
from < self.nodes.len() && to < self.nodes.len() && from != to,
|
||||
"a link joins two known nodes"
|
||||
);
|
||||
assert!(
|
||||
value.is_finite() && value > 0.0,
|
||||
"a link carries a positive value"
|
||||
drawable(value) && value > 0.0,
|
||||
"a link carries a positive value, within charts::LIMIT"
|
||||
);
|
||||
self.links.push((from, to, value));
|
||||
self
|
||||
@@ -243,6 +250,7 @@ impl RenderOnce for SankeyChart {
|
||||
w: (f32::from(bounds.size.width) - room * 2.0).max(0.0),
|
||||
h: (f32::from(bounds.size.height) - pixels(sizes.inset) * 2.0).max(0.0),
|
||||
};
|
||||
let hover = hover.filter(|_| frame.w > 0.0);
|
||||
let (nodes, ribbons) = if frame.w > 0.0 {
|
||||
sankey(
|
||||
self.nodes.len(),
|
||||
|
||||
@@ -13,6 +13,7 @@ use super::{
|
||||
parts::ChartTooltip,
|
||||
plot::Format,
|
||||
scale::compact,
|
||||
series::drawable,
|
||||
tiles::Tiles,
|
||||
};
|
||||
use crate::{
|
||||
@@ -41,8 +42,8 @@ impl FunnelChart {
|
||||
|
||||
pub fn stage(mut self, name: impl Into<SharedString>, value: f64) -> Self {
|
||||
assert!(
|
||||
value.is_finite() && value >= 0.0,
|
||||
"a stage needs a value of zero or more"
|
||||
drawable(value) && value >= 0.0,
|
||||
"a stage needs a value of zero or more, within charts::LIMIT"
|
||||
);
|
||||
self.stages.push((name.into(), value));
|
||||
self
|
||||
|
||||
@@ -43,6 +43,7 @@ pub use radar::RadarChart;
|
||||
pub use realtime::RealtimeChart;
|
||||
pub use relations::ChordDiagram;
|
||||
pub use schedule::{GanttChart, Task};
|
||||
pub use series::LIMIT;
|
||||
pub use series::{Points, Series};
|
||||
pub use sunburst::{Slice, Sunburst};
|
||||
pub use targets::{Bullet, BulletChart, ProgressChart};
|
||||
|
||||
@@ -129,10 +129,13 @@ impl NetworkGraph {
|
||||
|
||||
/// An edge between two nodes, by their places in the list.
|
||||
pub fn edge(mut self, a: usize, b: usize) -> Self {
|
||||
assert!(
|
||||
a < self.nodes.len() && b < self.nodes.len() && a != b,
|
||||
"an edge joins two known nodes"
|
||||
);
|
||||
if a >= self.nodes.len() || b >= self.nodes.len() || a == b {
|
||||
log::error!(
|
||||
"network: an edge {a} - {b} among {} nodes; skipped",
|
||||
self.nodes.len()
|
||||
);
|
||||
return self;
|
||||
}
|
||||
self.edges.push((a, b));
|
||||
self
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ use super::{
|
||||
parts::ChartTooltip,
|
||||
plot::Format,
|
||||
scale::{Linear, compact, nice},
|
||||
series::drawable,
|
||||
tiles::{Tiles, tracked},
|
||||
};
|
||||
use crate::theme::{ActiveTheme, TextSize};
|
||||
@@ -109,8 +110,8 @@ impl ParallelCoordinates {
|
||||
"a record needs a value per axis"
|
||||
);
|
||||
assert!(
|
||||
values.iter().all(|value| value.is_finite()),
|
||||
"a record needs finite values"
|
||||
values.iter().all(|value| drawable(*value)),
|
||||
"a record needs values, within charts::LIMIT"
|
||||
);
|
||||
self.records.push((name.into(), values));
|
||||
self
|
||||
|
||||
+3
-2
@@ -16,6 +16,7 @@ use super::{
|
||||
paint::{finish, measure, tint},
|
||||
parts::{ChartLegend, ChartTooltip},
|
||||
scale::compact,
|
||||
series::drawable,
|
||||
};
|
||||
use crate::{
|
||||
motion,
|
||||
@@ -191,8 +192,8 @@ impl PieChart {
|
||||
|
||||
pub fn slice(mut self, name: impl Into<SharedString>, value: f64) -> Self {
|
||||
assert!(
|
||||
value.is_finite() && value >= 0.0,
|
||||
"a slice needs a value of zero or more"
|
||||
drawable(value) && value >= 0.0,
|
||||
"a slice needs a value of zero or more, within charts::LIMIT"
|
||||
);
|
||||
self.slices.push((name.into(), value));
|
||||
self
|
||||
|
||||
@@ -10,6 +10,7 @@ use super::{
|
||||
geometry::{Rect, frame},
|
||||
paint::{at, finish, measure, place, ring, tint, trace},
|
||||
scale::{Linear, compact, nice},
|
||||
series::drawable,
|
||||
};
|
||||
use crate::{
|
||||
motion,
|
||||
@@ -43,8 +44,8 @@ impl RealtimeChart {
|
||||
) -> Self {
|
||||
let values: Vec<f64> = values.into_iter().collect();
|
||||
assert!(
|
||||
values.iter().all(|value| value.is_finite()),
|
||||
"a stream needs finite values"
|
||||
values.iter().all(|value| drawable(*value)),
|
||||
"a stream needs values, within charts::LIMIT"
|
||||
);
|
||||
Self {
|
||||
base: div(),
|
||||
|
||||
@@ -15,6 +15,7 @@ use super::{
|
||||
pie::{bearing, toward, wedge},
|
||||
plot::Format,
|
||||
scale::compact,
|
||||
series::drawable,
|
||||
tiles::{Tiles, tracked},
|
||||
};
|
||||
use crate::theme::{ActiveTheme, TextSize};
|
||||
@@ -26,10 +27,9 @@ type Arcs = Vec<(f32, f32)>;
|
||||
pub(crate) fn chords(matrix: &[Vec<f64>], pad: f32) -> (Arcs, Vec<Arcs>) {
|
||||
let grand: f64 = matrix.iter().flatten().sum();
|
||||
let room = (TAU - pad * matrix.len() as f32).max(0.0);
|
||||
let scale = if grand > 0.0 {
|
||||
room / grand as f32
|
||||
} else {
|
||||
0.0
|
||||
let share = |value: f64| match grand > 0.0 {
|
||||
true => (value / grand * f64::from(room)) as f32,
|
||||
false => 0.0,
|
||||
};
|
||||
let mut angle = -FRAC_PI_2 + pad / 2.0;
|
||||
let (mut groups, mut parts) = (Vec::new(), Vec::new());
|
||||
@@ -38,7 +38,7 @@ pub(crate) fn chords(matrix: &[Vec<f64>], pad: f32) -> (Arcs, Vec<Arcs>) {
|
||||
parts.push(
|
||||
row.iter()
|
||||
.map(|value| {
|
||||
let part = (angle, *value as f32 * scale);
|
||||
let part = (angle, share(*value));
|
||||
angle += part.1;
|
||||
part
|
||||
})
|
||||
@@ -126,8 +126,8 @@ impl ChordDiagram {
|
||||
matrix
|
||||
.iter()
|
||||
.flatten()
|
||||
.all(|value| value.is_finite() && *value >= 0.0),
|
||||
"flows are zero or more"
|
||||
.all(|value| drawable(*value) && *value >= 0.0),
|
||||
"flows are zero or more, within charts::LIMIT"
|
||||
);
|
||||
Self {
|
||||
base: div(),
|
||||
|
||||
+12
-7
@@ -1,3 +1,6 @@
|
||||
/// Shares past this many ranges lie off any screen; the bound keeps their pixels in `f32`.
|
||||
const FAR: f64 = 1e4;
|
||||
|
||||
/// A linear map from a domain of values onto a range of pixels.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub(crate) struct Linear {
|
||||
@@ -22,16 +25,17 @@ impl Linear {
|
||||
} else {
|
||||
(f64::from(at) - f64::from(self.range.0)) / f64::from(span)
|
||||
};
|
||||
self.domain.0 + (self.domain.1 - self.domain.0) * share
|
||||
let (low, high) = (self.domain.0 / 2.0, self.domain.1 / 2.0);
|
||||
(low + (high - low) * share) * 2.0
|
||||
}
|
||||
|
||||
/// Where `value` lands; a flat domain lands in the middle.
|
||||
/// Where `value` lands; a flat domain lands in the middle. Halves keep wide domains finite.
|
||||
pub(crate) fn at(&self, value: f64) -> f32 {
|
||||
let span = self.domain.1 - self.domain.0;
|
||||
let share = if span == 0.0 {
|
||||
let (low, high) = (self.domain.0 / 2.0, self.domain.1 / 2.0);
|
||||
let share = if high == low {
|
||||
0.5
|
||||
} else {
|
||||
(value - self.domain.0) / span
|
||||
((value / 2.0 - low) / (high - low)).clamp(-FAR, FAR)
|
||||
};
|
||||
self.range.0 + (self.range.1 - self.range.0) * share as f32
|
||||
}
|
||||
@@ -62,12 +66,13 @@ pub(crate) fn nice(low: f64, high: f64, count: usize) -> ((f64, f64), Vec<f64>)
|
||||
let (low, high) = (low.min(high), low.max(high));
|
||||
let (low, high) = if high - low <= high.abs().max(low.abs()) * 1e-12 {
|
||||
let pad = (high.abs().max(low.abs()) * 0.1).max(1.0);
|
||||
(low - pad, high + pad)
|
||||
((low - pad).max(f64::MIN), (high + pad).min(f64::MAX))
|
||||
} else {
|
||||
(low, high)
|
||||
};
|
||||
let step = nice_step(high - low, count);
|
||||
let (start, end) = ((low / step).floor() * step, (high / step).ceil() * step);
|
||||
let start = ((low / step).floor() * step).max(f64::MIN);
|
||||
let end = ((high / step).ceil() * step).min(f64::MAX);
|
||||
let ticks = (0..count.max(1) * 4 + 4)
|
||||
.map(|ix| start + step * ix as f64)
|
||||
.take_while(|tick| *tick <= end + step * 1e-9)
|
||||
|
||||
@@ -107,12 +107,15 @@ impl GanttChart {
|
||||
}
|
||||
|
||||
/// A task; the ones it waits for come before it.
|
||||
pub fn task(mut self, task: Task) -> Self {
|
||||
assert!(
|
||||
task.after.iter().all(|ix| *ix < self.tasks.len()),
|
||||
"task {} waits for one not yet given",
|
||||
task.name
|
||||
);
|
||||
pub fn task(mut self, mut task: Task) -> Self {
|
||||
let known = self.tasks.len();
|
||||
if task.after.iter().any(|ix| *ix >= known) {
|
||||
log::error!(
|
||||
"gantt: task {} waits for one not given; that wait is dropped",
|
||||
task.name
|
||||
);
|
||||
task.after.retain(|ix| *ix < known);
|
||||
}
|
||||
self.tasks.push(task);
|
||||
self
|
||||
}
|
||||
|
||||
+29
-8
@@ -7,12 +7,20 @@ pub struct Series {
|
||||
pub(crate) values: Vec<f64>,
|
||||
}
|
||||
|
||||
/// The largest value a chart draws: sums and pixels of such values stay within `f32`.
|
||||
pub const LIMIT: f64 = 1e30;
|
||||
|
||||
/// A value a chart can draw: finite and within ±`LIMIT`.
|
||||
pub(crate) fn drawable(value: f64) -> bool {
|
||||
value.is_finite() && value.abs() <= LIMIT
|
||||
}
|
||||
|
||||
impl Series {
|
||||
pub fn new(name: impl Into<SharedString>, values: impl IntoIterator<Item = f64>) -> Self {
|
||||
let values: Vec<f64> = values.into_iter().collect();
|
||||
assert!(
|
||||
values.iter().all(|value| value.is_finite()),
|
||||
"a series needs finite values"
|
||||
values.iter().all(|value| drawable(*value)),
|
||||
"a series needs values, within charts::LIMIT"
|
||||
);
|
||||
Self {
|
||||
name: name.into(),
|
||||
@@ -36,8 +44,8 @@ impl Points {
|
||||
) -> Self {
|
||||
let points: Vec<(f64, f64)> = points.into_iter().collect();
|
||||
assert!(
|
||||
points.iter().all(|(x, y)| x.is_finite() && y.is_finite()),
|
||||
"points need finite values"
|
||||
points.iter().all(|(x, y)| drawable(*x) && drawable(*y)),
|
||||
"points need values, within charts::LIMIT"
|
||||
);
|
||||
Self {
|
||||
name: name.into(),
|
||||
@@ -55,8 +63,8 @@ impl Points {
|
||||
"a bubble needs a size for each point"
|
||||
);
|
||||
assert!(
|
||||
sizes.iter().all(|size| size.is_finite() && *size >= 0.0),
|
||||
"bubble sizes are not negative"
|
||||
sizes.iter().all(|size| drawable(*size) && *size >= 0.0),
|
||||
"bubble sizes are not negative, within charts::LIMIT"
|
||||
);
|
||||
self.sizes = Some(sizes);
|
||||
self
|
||||
@@ -96,7 +104,7 @@ pub(crate) fn extent(values: impl Iterator<Item = f64>) -> (f64, f64) {
|
||||
})
|
||||
}
|
||||
|
||||
/// Tangents for a smooth line through `points` that never overshoots between them (Fritsch–Carlson).
|
||||
/// Tangents for a smooth line through `points` that never overshoots between them (Fritsch–Carlson); points at one x rise straight.
|
||||
pub(crate) fn tangents(points: &[(f32, f32)]) -> Vec<f32> {
|
||||
let count = points.len();
|
||||
if count < 2 {
|
||||
@@ -104,7 +112,10 @@ pub(crate) fn tangents(points: &[(f32, f32)]) -> Vec<f32> {
|
||||
}
|
||||
let slopes: Vec<f32> = points
|
||||
.windows(2)
|
||||
.map(|pair| (pair[1].1 - pair[0].1) / (pair[1].0 - pair[0].0))
|
||||
.map(|pair| {
|
||||
let slope = (pair[1].1 - pair[0].1) / (pair[1].0 - pair[0].0);
|
||||
if slope.is_finite() { slope } else { 0.0 }
|
||||
})
|
||||
.collect();
|
||||
let mut tangents: Vec<f32> = (0..count)
|
||||
.map(|ix| match ix {
|
||||
@@ -178,6 +189,16 @@ pub(crate) fn points_csv(clouds: &[Points]) -> String {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn points_at_one_x_rise_with_flat_tangents() {
|
||||
let rising = tangents(&[(10.0, 0.0), (10.0, 5.0), (10.0, 9.0), (11.0, 9.5)]);
|
||||
assert!(
|
||||
rising.iter().all(|tangent| tangent.is_finite()),
|
||||
"{rising:?}"
|
||||
);
|
||||
assert_eq!(&rising[..2], &[0.0, 0.0]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stacks_rise_from_zero_and_negatives_fall() {
|
||||
let stacks = stacked(&[&[2.0, -1.0], &[3.0, -2.0]]);
|
||||
|
||||
+17
-1
@@ -45,7 +45,16 @@ pub(crate) struct Five {
|
||||
fn quantile(sorted: &[f64], share: f64) -> f64 {
|
||||
let at = share * (sorted.len() - 1) as f64;
|
||||
let (below, above) = (at.floor() as usize, at.ceil() as usize);
|
||||
sorted[below] + (sorted[above] - sorted[below]) * (at - below as f64)
|
||||
let (low, high, part) = (sorted[below], sorted[above], at - below as f64);
|
||||
if low == high {
|
||||
return low;
|
||||
}
|
||||
let lerp = low + (high - low) * part;
|
||||
if lerp.is_finite() {
|
||||
lerp
|
||||
} else {
|
||||
low * (1.0 - part) + high * part
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn five(values: &[f64]) -> Five {
|
||||
@@ -149,6 +158,13 @@ pub(crate) fn steps(changes: &[(f64, bool)]) -> Vec<(f64, f64, Step)> {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn quartiles_across_all_of_f64_stay_finite() {
|
||||
let spread = five(&[-f64::MAX, f64::MAX]);
|
||||
assert!(spread.q1.is_finite() && spread.q3.is_finite());
|
||||
assert_eq!((spread.low, spread.high), (-f64::MAX, f64::MAX));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn values_fall_into_equal_bins() {
|
||||
let bins = bins(&[1.0, 2.0, 2.5, 9.0, 10.0], 5);
|
||||
|
||||
@@ -10,6 +10,7 @@ use super::{
|
||||
paint::{finish, measure, tint},
|
||||
pie::{bearing, wedge},
|
||||
scale::compact,
|
||||
series::drawable,
|
||||
tiles::Tiles,
|
||||
};
|
||||
use crate::{
|
||||
@@ -28,8 +29,8 @@ pub struct Slice {
|
||||
impl Slice {
|
||||
pub fn new(name: impl Into<SharedString>, value: f64) -> Self {
|
||||
assert!(
|
||||
value.is_finite() && value >= 0.0,
|
||||
"a slice needs a value of zero or more"
|
||||
drawable(value) && value >= 0.0,
|
||||
"a slice needs a value of zero or more, within charts::LIMIT"
|
||||
);
|
||||
Self {
|
||||
name: name.into(),
|
||||
|
||||
@@ -18,6 +18,7 @@ use super::{
|
||||
pie::{bearing, toward, wedge},
|
||||
plot::Format,
|
||||
scale::{compact, nice},
|
||||
series::drawable,
|
||||
tiles::{Tiles, tracked},
|
||||
};
|
||||
use crate::{
|
||||
@@ -38,8 +39,8 @@ pub struct Bullet {
|
||||
impl Bullet {
|
||||
pub fn new(label: impl Into<SharedString>, value: f64, target: f64) -> Self {
|
||||
assert!(
|
||||
value.is_finite() && target.is_finite() && value >= 0.0 && target >= 0.0,
|
||||
"a bullet reads values of zero or more"
|
||||
drawable(value) && drawable(target) && value >= 0.0 && target >= 0.0,
|
||||
"a bullet reads values of zero or more, within charts::LIMIT"
|
||||
);
|
||||
Self {
|
||||
label: label.into(),
|
||||
@@ -54,8 +55,8 @@ impl Bullet {
|
||||
self.bands = ends.into_iter().collect();
|
||||
assert!(
|
||||
self.bands.windows(2).all(|pair| pair[0] < pair[1])
|
||||
&& self.bands.iter().all(|end| *end > 0.0),
|
||||
"bands end in rising order"
|
||||
&& self.bands.iter().all(|end| *end > 0.0 && drawable(*end)),
|
||||
"bands end in rising order, within charts::LIMIT"
|
||||
);
|
||||
self
|
||||
}
|
||||
@@ -283,8 +284,8 @@ impl ProgressChart {
|
||||
/// A goal: how far it has come of its target.
|
||||
pub fn goal(mut self, name: impl Into<SharedString>, value: f64, target: f64) -> Self {
|
||||
assert!(
|
||||
value.is_finite() && value >= 0.0 && target.is_finite() && target > 0.0,
|
||||
"a goal has a positive target and a value of zero or more"
|
||||
drawable(value) && value >= 0.0 && drawable(target) && target > 0.0,
|
||||
"a goal has a positive target and a value of zero or more, within charts::LIMIT"
|
||||
);
|
||||
self.goals.push((name.into(), value, target));
|
||||
self
|
||||
@@ -313,9 +314,10 @@ impl RenderOnce for ProgressChart {
|
||||
.iter()
|
||||
.map(|(_, value, target)| value / target)
|
||||
.collect();
|
||||
let drawn: Vec<f64> = shares.iter().map(|share| share.min(1.0)).collect();
|
||||
let slow = motion::duration(motion::SLOW, cx);
|
||||
let (glided, moving) = glide.update(cx, |glide, _| {
|
||||
glide.follow(std::slice::from_ref(&shares), slow)
|
||||
glide.follow(std::slice::from_ref(&drawn), slow)
|
||||
});
|
||||
if moving {
|
||||
window.request_animation_frame();
|
||||
|
||||
@@ -0,0 +1,270 @@
|
||||
use std::{cell::Cell, rc::Rc};
|
||||
|
||||
use gpui::{
|
||||
AnyElement, Context, IntoElement, Modifiers, ParentElement, Render, Styled, TestAppContext,
|
||||
Window, div, point, px,
|
||||
};
|
||||
use jiff::civil::date;
|
||||
|
||||
use super::settle;
|
||||
use crate::{
|
||||
charts::{
|
||||
AreaChart, BarChart, BoxPlot, Bullet, BulletChart, CalendarHeatmap, ChordDiagram,
|
||||
FunnelChart, HeatmapChart, Histogram, LIMIT, LineChart, ParallelCoordinates, PieChart,
|
||||
ProgressChart, RadarChart, SankeyChart, Series, Slice, Sunburst, Treemap, ViolinPlot,
|
||||
WaterfallChart,
|
||||
},
|
||||
theme::{ActiveTheme, Theme},
|
||||
};
|
||||
|
||||
/// Every chart fed values at the edge of what it draws.
|
||||
struct Edge(u8);
|
||||
|
||||
impl Render for Edge {
|
||||
fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
|
||||
let top = Series::new("s", [LIMIT, LIMIT]);
|
||||
let chart: AnyElement = match self.0 {
|
||||
0 => SankeyChart::new("sankey", ["a", "b"])
|
||||
.link(0, 1, LIMIT)
|
||||
.into_any_element(),
|
||||
1 => ChordDiagram::new("chord", ["a", "b"], vec![vec![0.0, LIMIT], vec![1.0, 0.0]])
|
||||
.into_any_element(),
|
||||
2 => Histogram::new("histogram", [-LIMIT, LIMIT]).into_any_element(),
|
||||
3 => ViolinPlot::new("violin")
|
||||
.group("s", [LIMIT, LIMIT])
|
||||
.into_any_element(),
|
||||
4 => AreaChart::new("stack", ["a", "b"])
|
||||
.series(top.clone())
|
||||
.series(Series::new("t", [LIMIT, LIMIT]))
|
||||
.stacked()
|
||||
.into_any_element(),
|
||||
5 => WaterfallChart::new("waterfall")
|
||||
.step("a", LIMIT)
|
||||
.step("b", LIMIT)
|
||||
.total("all")
|
||||
.into_any_element(),
|
||||
6 => Sunburst::new(
|
||||
"sunburst",
|
||||
[Slice::new("root", 0.0)
|
||||
.children([Slice::new("a", LIMIT), Slice::new("b", LIMIT)])],
|
||||
)
|
||||
.into_any_element(),
|
||||
7 => Treemap::new("treemap")
|
||||
.tile("a", LIMIT)
|
||||
.tile("b", LIMIT)
|
||||
.into_any_element(),
|
||||
8 => RadarChart::new("radar", ["a", "b", "c"])
|
||||
.series(Series::new("s", [LIMIT; 3]))
|
||||
.into_any_element(),
|
||||
9 => LineChart::new("rule", ["a", "b"])
|
||||
.series(Series::new("s", [1.0, 2.0]))
|
||||
.rule(LIMIT, "target")
|
||||
.into_any_element(),
|
||||
10 => BoxPlot::new("box")
|
||||
.group("s", [-LIMIT, LIMIT])
|
||||
.into_any_element(),
|
||||
11 => CalendarHeatmap::new("calendar", date(9999, 12, 31), [1.0]).into_any_element(),
|
||||
12 => ChordDiagram::new("tiny", ["a", "b"], vec![vec![0.0, 1e-40], vec![1e-40, 0.0]])
|
||||
.into_any_element(),
|
||||
13 => LineChart::new("far", ["a", "b"])
|
||||
.series(Series::new("s", [1e-20, 2e-20]))
|
||||
.rule(LIMIT, "target")
|
||||
.into_any_element(),
|
||||
14 => ChordDiagram::new(
|
||||
"least",
|
||||
["a", "b"],
|
||||
vec![vec![0.0, 1e-310], vec![1e-310, 0.0]],
|
||||
)
|
||||
.into_any_element(),
|
||||
15 => Treemap::new("least-tiles")
|
||||
.tile("a", 1e-310)
|
||||
.tile("b", 1e-310)
|
||||
.into_any_element(),
|
||||
16 => SankeyChart::new("least-flow", ["a", "b", "c"])
|
||||
.link(0, 1, 1e-310)
|
||||
.link(1, 2, 1e-310)
|
||||
.into_any_element(),
|
||||
_ => LineChart::new("top", ["a", "b"])
|
||||
.series(top)
|
||||
.into_any_element(),
|
||||
};
|
||||
div().w(px(400.0)).h(px(240.0)).child(chart)
|
||||
}
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn every_chart_draws_values_at_its_limit(cx: &mut TestAppContext) {
|
||||
cx.update(Theme::init);
|
||||
for case in 0..=17 {
|
||||
let (_, cx) = cx.add_window_view(move |_, _| Edge(case));
|
||||
settle(cx);
|
||||
}
|
||||
}
|
||||
|
||||
/// A line that turns from the top limit to the bottom one, and a goal from far past its target back to twice it.
|
||||
struct Turning(Rc<Cell<bool>>);
|
||||
|
||||
impl Render for Turning {
|
||||
fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
|
||||
let turned = self.0.get();
|
||||
let value = if turned { -LIMIT } else { LIMIT };
|
||||
let (goal, target) = if turned { (2.0, 1.0) } else { (LIMIT, 1e-30) };
|
||||
div()
|
||||
.w(px(400.0))
|
||||
.child(LineChart::new("line", ["a", "b"]).series(Series::new("s", [value, value])))
|
||||
.child(ProgressChart::new("progress").goal("g", goal, target))
|
||||
}
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn a_chart_glides_between_its_limits(cx: &mut TestAppContext) {
|
||||
cx.update(Theme::init);
|
||||
let turned = Rc::new(Cell::new(false));
|
||||
let seen = turned.clone();
|
||||
let (view, cx) = cx.add_window_view(|_, _| Turning(seen));
|
||||
settle(cx);
|
||||
turned.set(true);
|
||||
view.update(cx, |_, cx| cx.notify());
|
||||
settle(cx);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic(expected = "within charts::LIMIT")]
|
||||
fn a_value_past_the_limit_is_refused() {
|
||||
let _ = Series::new("s", [LIMIT * 10.0]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic(expected = "within charts::LIMIT")]
|
||||
fn a_band_past_the_limit_is_refused() {
|
||||
let _ = Bullet::new("b", 1.0, 2.0).bands([LIMIT * 10.0]);
|
||||
}
|
||||
|
||||
/// A smooth line in a box barely wider than its gutter.
|
||||
struct Sliver;
|
||||
|
||||
impl Render for Sliver {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let chart = cx.theme().chart();
|
||||
let rem = window.rem_size();
|
||||
let width = chart.gutter.to_pixels(rem) + chart.inset.to_pixels(rem) + px(0.000015);
|
||||
div().w(width).h(px(240.0)).child(
|
||||
LineChart::new("sliver", ["a", "b", "c", "d", "e"])
|
||||
.series(Series::new("s", [1.0, 2.0, 3.0, 4.0, 5.0]))
|
||||
.smooth(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn a_smooth_line_in_a_sliver_draws(cx: &mut TestAppContext) {
|
||||
cx.update(Theme::init);
|
||||
let (_, cx) = cx.add_window_view(|_, _| Sliver);
|
||||
settle(cx);
|
||||
}
|
||||
|
||||
/// A centered sankey that shrinks under a resting pointer.
|
||||
struct Shrinking(Rc<Cell<bool>>);
|
||||
|
||||
impl Render for Shrinking {
|
||||
fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
|
||||
let width = if self.0.get() { 20.0 } else { 400.0 };
|
||||
div()
|
||||
.w(px(400.0))
|
||||
.h(px(240.0))
|
||||
.flex()
|
||||
.flex_col()
|
||||
.items_center()
|
||||
.child(
|
||||
SankeyChart::new("shrinking", ["a", "b"])
|
||||
.link(0, 1, 5.0)
|
||||
.w(px(width))
|
||||
.h(px(240.0)),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn a_hovered_sankey_that_shrinks_drops_its_hover(cx: &mut TestAppContext) {
|
||||
cx.update(Theme::init);
|
||||
let shrunk = Rc::new(Cell::new(false));
|
||||
let seen = shrunk.clone();
|
||||
let (view, cx) = cx.add_window_view(|_, _| Shrinking(seen));
|
||||
settle(cx);
|
||||
cx.simulate_mouse_move(point(px(200.0), px(120.0)), None, Modifiers::none());
|
||||
settle(cx);
|
||||
shrunk.set(true);
|
||||
view.update(cx, |_, cx| cx.notify());
|
||||
settle(cx);
|
||||
}
|
||||
|
||||
const LEAST: f64 = 1e-310;
|
||||
|
||||
/// Every other chart fed the smallest values it takes.
|
||||
struct Least(u8);
|
||||
|
||||
impl Render for Least {
|
||||
fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
|
||||
let tiny = || Series::new("s", [LEAST, LEAST * 2.0, LEAST]);
|
||||
let chart: AnyElement = match self.0 {
|
||||
0 => PieChart::new("pie")
|
||||
.slice("a", LEAST)
|
||||
.slice("b", LEAST)
|
||||
.into_any_element(),
|
||||
1 => FunnelChart::new("funnel")
|
||||
.stage("a", LEAST)
|
||||
.stage("b", LEAST)
|
||||
.into_any_element(),
|
||||
2 => ParallelCoordinates::new("parallel", ["a", "b"])
|
||||
.record("r", [LEAST, LEAST * 2.0])
|
||||
.record("q", [LEAST * 2.0, LEAST])
|
||||
.into_any_element(),
|
||||
3 => BulletChart::new("bullets")
|
||||
.bullet(Bullet::new("b", LEAST, LEAST * 2.0).bands([LEAST, LEAST * 3.0]))
|
||||
.into_any_element(),
|
||||
4 => ProgressChart::new("goals")
|
||||
.goal("g", LEAST, LEAST)
|
||||
.into_any_element(),
|
||||
5 => HeatmapChart::new("heat", ["a", "b"])
|
||||
.row("r", [LEAST, LEAST * 2.0])
|
||||
.into_any_element(),
|
||||
6 => Histogram::new("histogram", [LEAST, LEAST * 2.0, LEAST * 3.0]).into_any_element(),
|
||||
7 => BoxPlot::new("box")
|
||||
.group("s", [LEAST, LEAST * 2.0, LEAST * 4.0])
|
||||
.into_any_element(),
|
||||
8 => ViolinPlot::new("violin")
|
||||
.group("s", [LEAST, LEAST * 2.0])
|
||||
.into_any_element(),
|
||||
9 => WaterfallChart::new("waterfall")
|
||||
.step("a", LEAST)
|
||||
.total("all")
|
||||
.into_any_element(),
|
||||
10 => RadarChart::new("radar", ["a", "b", "c"])
|
||||
.series(tiny())
|
||||
.into_any_element(),
|
||||
11 => Sunburst::new(
|
||||
"sunburst",
|
||||
[Slice::new("root", LEAST).children([Slice::new("a", LEAST)])],
|
||||
)
|
||||
.into_any_element(),
|
||||
12 => AreaChart::new("area", ["a", "b", "c"])
|
||||
.series(tiny())
|
||||
.stacked()
|
||||
.into_any_element(),
|
||||
_ => BarChart::new("bars", ["a", "b", "c"])
|
||||
.series(tiny())
|
||||
.horizontal()
|
||||
.into_any_element(),
|
||||
};
|
||||
div().w(px(400.0)).h(px(240.0)).child(chart)
|
||||
}
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn every_chart_draws_its_smallest_values(cx: &mut TestAppContext) {
|
||||
cx.update(Theme::init);
|
||||
for case in 0..=13 {
|
||||
let (_, cx) = cx.add_window_view(move |_, _| Least(case));
|
||||
settle(cx);
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
mod limits;
|
||||
|
||||
use std::{
|
||||
cell::{Cell, RefCell},
|
||||
rc::Rc,
|
||||
@@ -138,7 +140,7 @@ impl Render for Picked {
|
||||
}
|
||||
}
|
||||
|
||||
fn settle(cx: &mut VisualTestContext) {
|
||||
pub(super) fn settle(cx: &mut VisualTestContext) {
|
||||
for _ in 0..3 {
|
||||
cx.run_until_parked();
|
||||
cx.update(|window, _| window.refresh());
|
||||
@@ -373,3 +375,36 @@ fn a_long_band_name_widens_the_gutter_to_half_the_chart(cx: &mut TestAppContext)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A note, a link, an edge and a wait that name what their chart lacks.
|
||||
struct Dangling;
|
||||
|
||||
impl Render for Dangling {
|
||||
fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
|
||||
let line = LineChart::new("line", ["a", "b"])
|
||||
.series(Series::new("s", [1.0, 2.0]))
|
||||
.note(7, "late");
|
||||
let sankey = SankeyChart::new("sankey", ["a", "b"])
|
||||
.link(0, 9, 5.0)
|
||||
.link(0, 1, 5.0);
|
||||
let network = NetworkGraph::new("network")
|
||||
.node("a", 0)
|
||||
.node("b", 0)
|
||||
.edge(0, 9);
|
||||
let gantt = GanttChart::new("gantt")
|
||||
.task(Task::new("t", date(2026, 3, 1), date(2026, 3, 9)).after(4));
|
||||
div()
|
||||
.w(px(400.0))
|
||||
.child(line)
|
||||
.child(sankey)
|
||||
.child(network)
|
||||
.child(gantt)
|
||||
}
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn entries_past_their_lists_are_skipped(cx: &mut TestAppContext) {
|
||||
cx.update(Theme::init);
|
||||
let (_, cx) = cx.add_window_view(|_, _| Dangling);
|
||||
settle(cx);
|
||||
}
|
||||
+5
-4
@@ -14,6 +14,7 @@ use super::{
|
||||
parts::ChartTooltip,
|
||||
plot::Format,
|
||||
scale::compact,
|
||||
series::drawable,
|
||||
};
|
||||
use crate::{
|
||||
theme::{ActiveTheme, Radius, TextSize},
|
||||
@@ -105,8 +106,8 @@ impl Treemap {
|
||||
|
||||
pub fn tile(mut self, name: impl Into<SharedString>, value: f64) -> Self {
|
||||
assert!(
|
||||
value.is_finite() && value >= 0.0,
|
||||
"a tile needs a value of zero or more"
|
||||
drawable(value) && value >= 0.0,
|
||||
"a tile needs a value of zero or more, within charts::LIMIT"
|
||||
);
|
||||
self.tiles.push((name.into(), value));
|
||||
self
|
||||
@@ -282,8 +283,8 @@ impl HeatmapChart {
|
||||
"a heatmap row needs a value per column"
|
||||
);
|
||||
assert!(
|
||||
values.iter().all(|value| value.is_finite()),
|
||||
"a heatmap needs finite values"
|
||||
values.iter().all(|value| drawable(*value)),
|
||||
"a heatmap needs values, within charts::LIMIT"
|
||||
);
|
||||
self.rows.push((label.into(), values));
|
||||
self
|
||||
|
||||
+2
-1
@@ -29,7 +29,7 @@ Each item reads every site in its folders, fixes what the rules name, updates ca
|
||||
- [x] P12 Data Display — `src/data_display`
|
||||
- [x] P13 Lists & Trees — `src/lists`
|
||||
- [x] P14 Tables — `src/tables`
|
||||
- [ ] P15 Charts — `src/charts`
|
||||
- [x] P15 Charts — `src/charts`
|
||||
- [ ] P16 Finance — `src/finance`
|
||||
- [ ] P17 Code Editor — `src/editor`
|
||||
- [ ] P18 Terminal — `src/terminal`
|
||||
@@ -79,3 +79,4 @@ One line per item: what changed, what stayed and why, review rounds.
|
||||
- P12: a featured column past the columns features none; a gauge past its scale pegs; usage parts past the total fill it and repeat hues past eight; stars above max log and fill. Kept: one-value contracts, a comparison row's shape, construction. Review: 3 rounds (round 2 met an edit that had not applied).
|
||||
- P13: a directory listing's selected name gone from its entries picks none. Kept: one-value contracts; activation, folder split, single-select sections and tree rows hold by construction. Review: 1 round.
|
||||
- P14: a pivot field missing from its data draws an error line; a gone group-by column leaves the table ungrouped; a merge past the sheet is skipped; each logs. Kept: row shapes of one snapshot, one-value contracts, builder misuse, construction. Review: 1 round.
|
||||
- P15: chart values are finite and within ±`charts::LIMIT` (1e30), so sums and pixels stay in `f32`; scales work in halves and bound a share to ±1e4 ranges; chord and sankey size by shares; quartiles, weekday labels, flat tangents and a zero-width sankey's hover hold; notes, links, edges and waits past their lists are skipped. Review: 5 rounds, the cap; round 5's finding (points at one x) is fixed and tested, and P16's review checks it.
|
||||
Reference in new issue
Block a user