refactor: move CPU graph over to new system

This commit is contained in:
ClementTsang 2022-05-04 04:41:32 -04:00
parent 9e63642e9c
commit c97126df22
7 changed files with 251 additions and 267 deletions

View File

@ -2228,8 +2228,8 @@ impl App {
.cpu_state
.get_mut_widget_state(self.current_widget.widget_id - 1)
{
cpu_widget_state.scroll_state.current_scroll_position = 0;
cpu_widget_state.scroll_state.scroll_direction = ScrollDirection::Up;
cpu_widget_state.table_state.current_scroll_position = 0;
cpu_widget_state.table_state.scroll_direction = ScrollDirection::Up;
}
}
@ -2306,8 +2306,8 @@ impl App {
{
let cap = self.canvas_data.cpu_data.len();
if cap > 0 {
cpu_widget_state.scroll_state.current_scroll_position = cap - 1;
cpu_widget_state.scroll_state.scroll_direction = ScrollDirection::Down;
cpu_widget_state.table_state.current_scroll_position = cap - 1;
cpu_widget_state.table_state.scroll_direction = ScrollDirection::Down;
}
}
}
@ -2380,7 +2380,7 @@ impl App {
.get_mut(&(self.current_widget.widget_id - 1))
{
cpu_widget_state
.scroll_state
.table_state
.update_position(num_to_change_by, self.canvas_data.cpu_data.len());
}
}
@ -2957,7 +2957,7 @@ impl App {
.get_widget_state(self.current_widget.widget_id - 1)
{
if let Some(visual_index) =
cpu_widget_state.scroll_state.table_state.selected()
cpu_widget_state.table_state.table_state.selected()
{
self.change_cpu_legend_position(
offset_clicked_entry as i64 - visual_index as i64,

View File

@ -61,8 +61,19 @@ pub enum WidthBounds {
impl WidthBounds {
pub const fn soft_from_str(name: &'static str, max_percentage: Option<f32>) -> WidthBounds {
let len = name.len() as u16;
WidthBounds::Soft {
min_width: name.len() as u16,
min_width: len,
desired: len,
max_percentage,
}
}
pub const fn soft_from_str_with_alt(
name: &'static str, alt: &'static str, max_percentage: Option<f32>,
) -> WidthBounds {
WidthBounds::Soft {
min_width: alt.len() as u16,
desired: name.len() as u16,
max_percentage,
}
@ -75,6 +86,9 @@ pub struct TableComponentColumn {
/// A restriction on this column's width, if desired.
pub width_bounds: WidthBounds,
/// The calculated width of the column.
pub calculated_width: u16,
}
impl TableComponentColumn {
@ -92,8 +106,13 @@ impl TableComponentColumn {
CellContent::Simple(name.into())
},
width_bounds,
calculated_width: 0,
}
}
pub fn should_skip(&self) -> bool {
self.calculated_width == 0
}
}
/// [`TableComponentState`] deals with fields for a scrollable's current state.
@ -104,7 +123,6 @@ pub struct TableComponentState {
pub scroll_direction: ScrollDirection,
pub table_state: TableState,
pub columns: Vec<TableComponentColumn>,
pub calculated_widths: Vec<u16>,
}
impl TableComponentState {
@ -115,7 +133,6 @@ impl TableComponentState {
scroll_direction: ScrollDirection::Down,
table_state: Default::default(),
columns,
calculated_widths: Vec::default(),
}
}
@ -132,16 +149,18 @@ impl TableComponentState {
let mut total_width_left = total_width;
let column_widths = &mut self.calculated_widths;
*column_widths = vec![0; self.columns.len()];
for column in self.columns.iter_mut() {
column.calculated_width = 0;
}
let columns = if left_to_right {
Either::Left(self.columns.iter().enumerate())
Either::Left(self.columns.iter_mut())
} else {
Either::Right(self.columns.iter().enumerate().rev())
Either::Right(self.columns.iter_mut().rev())
};
for (itx, column) in columns {
let mut num_columns = 0;
for column in columns {
match &column.width_bounds {
WidthBounds::Soft {
min_width,
@ -161,9 +180,10 @@ impl TableComponentState {
if *min_width > space_taken {
break;
} else {
} else if space_taken > 0 {
total_width_left = total_width_left.saturating_sub(space_taken + 1);
column_widths[itx] = space_taken;
column.calculated_width = space_taken;
num_columns += 1;
}
}
WidthBounds::Hard(width) => {
@ -171,27 +191,34 @@ impl TableComponentState {
if *width > space_taken {
break;
} else {
} else if space_taken > 0 {
total_width_left = total_width_left.saturating_sub(space_taken + 1);
column_widths[itx] = space_taken;
column.calculated_width = space_taken;
num_columns += 1;
}
}
}
}
while let Some(0) = column_widths.last() {
column_widths.pop();
}
if !column_widths.is_empty() {
if num_columns > 0 {
// Redistribute remaining.
let amount_per_slot = total_width_left / column_widths.len() as u16;
total_width_left %= column_widths.len() as u16;
for (index, width) in column_widths.iter_mut().enumerate() {
if index < total_width_left.into() {
*width += amount_per_slot + 1;
} else {
*width += amount_per_slot;
let mut num_dist = num_columns;
let amount_per_slot = total_width_left / num_dist;
total_width_left %= num_dist;
for column in self.columns.iter_mut() {
if num_dist == 0 {
break;
}
if column.calculated_width > 0 {
if total_width_left > 0 {
column.calculated_width += amount_per_slot + 1;
total_width_left -= 1;
} else {
column.calculated_width += amount_per_slot;
}
num_dist -= 1;
}
}
}
@ -871,29 +898,13 @@ impl ProcState {
pub struct NetWidgetState {
pub current_display_time: u64,
pub autohide_timer: Option<Instant>,
// pub draw_max_range_cache: f64,
// pub draw_labels_cache: Vec<String>,
// pub draw_time_start_cache: f64,
// TODO: Re-enable these when we move net details state-side!
// pub unit_type: DataUnitTypes,
// pub scale_type: AxisScaling,
}
impl NetWidgetState {
pub fn init(
current_display_time: u64,
autohide_timer: Option<Instant>,
// unit_type: DataUnitTypes,
// scale_type: AxisScaling,
) -> Self {
pub fn init(current_display_time: u64, autohide_timer: Option<Instant>) -> Self {
NetWidgetState {
current_display_time,
autohide_timer,
// draw_max_range_cache: 0.0,
// draw_labels_cache: vec![],
// draw_time_start_cache: 0.0,
// unit_type,
// scale_type,
}
}
}
@ -924,20 +935,33 @@ pub struct CpuWidgetState {
pub current_display_time: u64,
pub is_legend_hidden: bool,
pub autohide_timer: Option<Instant>,
pub scroll_state: TableComponentState,
pub table_state: TableComponentState,
pub is_multi_graph_mode: bool,
pub table_width_state: CanvasTableWidthState,
}
impl CpuWidgetState {
pub fn init(current_display_time: u64, autohide_timer: Option<Instant>) -> Self {
const CPU_LEGEND_HEADER: [(Cow<'static, str>, Option<Cow<'static, str>>); 2] =
[(Cow::Borrowed("CPU"), None), (Cow::Borrowed("Use%"), None)];
const WIDTHS: [WidthBounds; CPU_LEGEND_HEADER.len()] = [
WidthBounds::soft_from_str("CPU", Some(0.5)),
WidthBounds::soft_from_str("Use%", Some(0.5)),
];
let table_state = TableComponentState::new(
CPU_LEGEND_HEADER
.iter()
.zip(WIDTHS)
.map(|(c, width)| TableComponentColumn::new(c.0.clone(), c.1.clone(), width))
.collect(),
);
CpuWidgetState {
current_display_time,
is_legend_hidden: false,
autohide_timer,
scroll_state: TableComponentState::default(),
table_state,
is_multi_graph_mode: false,
table_width_state: CanvasTableWidthState::default(),
}
}
}
@ -1166,7 +1190,6 @@ mod test {
scroll_direction: ScrollDirection::Down,
table_state: Default::default(),
columns: vec![],
calculated_widths: vec![],
};
let s = &mut scroll;

View File

@ -17,6 +17,11 @@ use crate::{
data_conversion::{CellContent, TableData, TableRow},
};
pub struct TextTableTitle<'a> {
pub title: Cow<'a, str>,
pub is_expanded: bool,
}
pub struct TextTable<'a> {
pub table_gap: u16,
pub is_force_redraw: bool,
@ -31,11 +36,8 @@ pub struct TextTable<'a> {
/// The highlighted text style.
pub highlighted_text_style: Style,
/// The graph title.
pub title: Cow<'a, str>,
/// Whether this graph is expanded.
pub is_expanded: bool,
/// The graph title and whether it is expanded (if there is one).
pub title: Option<TextTableTitle<'a>>,
/// Whether this widget is selected.
pub is_on_widget: bool,
@ -58,42 +60,46 @@ pub struct TextTable<'a> {
impl<'a> TextTable<'a> {
/// Generates a title for the [`TextTable`] widget, given the available space.
fn generate_title(&self, draw_loc: Rect, pos: usize, total: usize) -> Spans<'_> {
let title = if self.show_table_scroll_position {
let title_string = concat_string!(
self.title,
"(",
pos.to_string(),
" of ",
total.to_string(),
") "
);
fn generate_title(&self, draw_loc: Rect, pos: usize, total: usize) -> Option<Spans<'_>> {
self.title
.as_ref()
.map(|TextTableTitle { title, is_expanded }| {
let title = if self.show_table_scroll_position {
let title_string = concat_string!(
title,
"(",
pos.to_string(),
" of ",
total.to_string(),
") "
);
if title_string.len() + 2 <= draw_loc.width.into() {
title_string
} else {
self.title.to_string()
}
} else {
self.title.to_string()
};
if title_string.len() + 2 <= draw_loc.width.into() {
title_string
} else {
title.to_string()
}
} else {
title.to_string()
};
if self.is_expanded {
let title_base = concat_string!(title, "── Esc to go back ");
let esc = concat_string!(
"",
"".repeat(usize::from(draw_loc.width).saturating_sub(
UnicodeSegmentation::graphemes(title_base.as_str(), true).count() + 2
)),
"─ Esc to go back "
);
Spans::from(vec![
Span::styled(title, self.title_style),
Span::styled(esc, self.border_style),
])
} else {
Spans::from(Span::styled(title, self.title_style))
}
if *is_expanded {
let title_base = concat_string!(title, "── Esc to go back ");
let esc = concat_string!(
"",
"".repeat(usize::from(draw_loc.width).saturating_sub(
UnicodeSegmentation::graphemes(title_base.as_str(), true).count() + 2
)),
"─ Esc to go back "
);
Spans::from(vec![
Span::styled(title, self.title_style),
Span::styled(esc, self.border_style),
])
} else {
Spans::from(Span::styled(title, self.title_style))
}
})
}
pub fn draw_text_table<B: Backend>(
&self, f: &mut Frame<'_, B>, draw_loc: Rect, state: &mut TableComponentState,
@ -108,16 +114,19 @@ impl<'a> TextTable<'a> {
.split(draw_loc)[0];
let disk_block = if self.draw_border {
let title = self.generate_title(
let block = Block::default()
.borders(Borders::ALL)
.border_style(self.border_style);
if let Some(title) = self.generate_title(
draw_loc,
state.current_scroll_position.saturating_add(1),
table_data.data.len(),
);
Block::default()
.title(title)
.borders(Borders::ALL)
.border_style(self.border_style)
) {
block.title(title)
} else {
block
}
} else if self.is_on_widget {
Block::default()
.borders(SIDE_BORDERS)
@ -179,30 +188,32 @@ impl<'a> TextTable<'a> {
}
let columns = &state.columns;
let widths = &state.calculated_widths;
let header = Row::new(
columns
.iter()
.zip(widths)
.map(|(c, width)| truncate_text(&c.name, (*width).into(), None)),
)
let header = Row::new(columns.iter().filter_map(|c| {
if c.calculated_width == 0 {
None
} else {
Some(truncate_text(&c.name, c.calculated_width.into(), None))
}
}))
.style(self.header_style)
.bottom_margin(table_gap);
let disk_rows = sliced_vec.iter().map(|row| {
let table_rows = sliced_vec.iter().map(|row| {
let (row, style) = match row {
TableRow::Raw(row) => (row, None),
TableRow::Styled(row, style) => (row, Some(*style)),
};
Row::new(
row.iter()
.zip(widths)
.map(|(cell, width)| truncate_text(cell, (*width).into(), style)),
)
Row::new(row.iter().zip(columns).filter_map(|(cell, c)| {
if c.calculated_width == 0 {
None
} else {
Some(truncate_text(cell, c.calculated_width.into(), style))
}
}))
});
let widget = {
let mut table = Table::new(disk_rows)
let mut table = Table::new(table_rows)
.block(disk_block)
.highlight_style(self.highlighted_text_style)
.style(self.text_style);
@ -216,9 +227,15 @@ impl<'a> TextTable<'a> {
f.render_stateful_widget(
widget.widths(
&(widths
&(columns
.iter()
.map(|w| Constraint::Length(*w))
.filter_map(|c| {
if c.calculated_width == 0 {
None
} else {
Some(Constraint::Length(c.calculated_width))
}
})
.collect::<Vec<_>>()),
),
margined_draw_loc,

View File

@ -1,38 +1,34 @@
use std::borrow::Cow;
use std::{borrow::Cow, iter};
use crate::{
app::{layout_manager::WidgetDirection, App, CpuWidgetState},
canvas::{
components::{GraphData, TimeGraph},
drawing_utils::{get_column_widths, get_start_position, should_hide_x_label},
components::{GraphData, TextTable, TimeGraph},
drawing_utils::should_hide_x_label,
Painter,
},
constants::*,
data_conversion::ConvertedCpuData,
data_conversion::{CellContent, ConvertedCpuData, TableData, TableRow},
};
use concat_string::concat_string;
use itertools::Either;
use tui::{
backend::Backend,
layout::{Constraint, Direction, Layout, Rect},
terminal::Frame,
text::Text,
widgets::{Block, Borders, Row, Table},
};
const CPU_LEGEND_HEADER: [&str; 2] = ["CPU", "Use%"];
const AVG_POSITION: usize = 1;
const ALL_POSITION: usize = 0;
static CPU_LEGEND_HEADER_LENS: [usize; 2] =
[CPU_LEGEND_HEADER[0].len(), CPU_LEGEND_HEADER[1].len()];
impl Painter {
pub fn draw_cpu<B: Backend>(
&self, f: &mut Frame<'_, B>, app_state: &mut App, draw_loc: Rect, widget_id: u64,
) {
if draw_loc.width as f64 * 0.15 <= 6.0 {
let legend_width = (draw_loc.width as f64 * 0.15) as u16;
if legend_width < 6 {
// Skip drawing legend
if app_state.current_widget.widget_id == (widget_id + 1) {
if app_state.app_config_fields.left_legend {
@ -55,18 +51,25 @@ impl Painter {
}
}
} else {
let graph_width = draw_loc.width - legend_width;
let (graph_index, legend_index, constraints) =
if app_state.app_config_fields.left_legend {
(
1,
0,
[Constraint::Percentage(15), Constraint::Percentage(85)],
[
Constraint::Length(legend_width),
Constraint::Length(graph_width),
],
)
} else {
(
0,
1,
[Constraint::Percentage(85), Constraint::Percentage(15)],
[
Constraint::Length(graph_width),
Constraint::Length(legend_width),
],
)
};
@ -121,7 +124,7 @@ impl Painter {
) -> Vec<GraphData<'a>> {
let show_avg_offset = if show_avg_cpu { AVG_POSITION } else { 0 };
let current_scroll_position = cpu_widget_state.scroll_state.current_scroll_position;
let current_scroll_position = cpu_widget_state.table_state.current_scroll_position;
if current_scroll_position == ALL_POSITION {
// This case ensures the other cases cannot have the position be equal to 0.
cpu_data
@ -224,149 +227,81 @@ impl Painter {
let recalculate_column_widths = app_state.should_get_widget_bounds();
if let Some(cpu_widget_state) = app_state.cpu_state.widget_states.get_mut(&(widget_id - 1))
{
cpu_widget_state.is_legend_hidden = false; // TODO: This line (and the one above, see caller) is pretty dumb.
let cpu_data: &mut [ConvertedCpuData] = &mut app_state.canvas_data.cpu_data;
let cpu_table_state = &mut cpu_widget_state.scroll_state.table_state;
let is_on_widget = widget_id == app_state.current_widget.widget_id;
let table_gap = if draw_loc.height < TABLE_GAP_HEIGHT_LIMIT {
0
} else {
app_state.app_config_fields.table_gap
};
let start_position = get_start_position(
usize::from(
(draw_loc.height + (1 - table_gap)).saturating_sub(self.table_height_offset),
),
&cpu_widget_state.scroll_state.scroll_direction,
&mut cpu_widget_state.scroll_state.scroll_bar,
cpu_widget_state.scroll_state.current_scroll_position,
app_state.is_force_redraw,
);
cpu_table_state.select(Some(
cpu_widget_state
.scroll_state
.current_scroll_position
.saturating_sub(start_position),
));
// TODO: This line (and the one above, see caller) is pretty dumb but I guess needed.
cpu_widget_state.is_legend_hidden = false;
let sliced_cpu_data = &cpu_data[start_position..];
let offset_scroll_index = cpu_widget_state
.scroll_state
.current_scroll_position
.saturating_sub(start_position);
let show_avg_cpu = app_state.app_config_fields.show_average_cpu;
// Calculate widths
if recalculate_column_widths {
cpu_widget_state.table_width_state.desired_column_widths = vec![6, 4];
cpu_widget_state.table_width_state.calculated_column_widths = get_column_widths(
draw_loc.width,
&[None, None],
&(CPU_LEGEND_HEADER_LENS
.iter()
.map(|width| Some(*width as u16))
.collect::<Vec<_>>()),
&[Some(0.5), Some(0.5)],
&(cpu_widget_state
.table_width_state
.desired_column_widths
.iter()
.map(|width| Some(*width))
.collect::<Vec<_>>()),
false,
);
}
let dcw = &cpu_widget_state.table_width_state.desired_column_widths;
let ccw = &cpu_widget_state.table_width_state.calculated_column_widths;
let cpu_rows = sliced_cpu_data.iter().enumerate().map(|(itx, cpu)| {
let mut truncated_name =
if let (Some(desired_column_width), Some(calculated_column_width)) =
(dcw.get(0), ccw.get(0))
{
if *desired_column_width > *calculated_column_width {
Text::raw(&cpu.short_cpu_name)
} else {
Text::raw(&cpu.cpu_name)
}
} else {
Text::raw(&cpu.cpu_name)
};
let is_first_column_hidden = if let Some(calculated_column_width) = ccw.get(0) {
*calculated_column_width == 0
let cpu_data = {
let row_widths = vec![1, 3]; // TODO: Should change this to take const generics (usize) and an array.
let colour_iter = if show_avg_cpu {
Either::Left(
iter::once(&self.colours.all_colour_style)
.chain(iter::once(&self.colours.avg_colour_style))
.chain(self.colours.cpu_colour_styles.iter().cycle()),
)
} else {
false
Either::Right(
iter::once(&self.colours.all_colour_style)
.chain(self.colours.cpu_colour_styles.iter().cycle()),
)
};
let truncated_legend = if is_first_column_hidden && cpu.legend_value.is_empty() {
// For the case where we only have room for one column, display "All" in the normally blank area.
Text::raw("All")
} else {
Text::raw(&cpu.legend_value)
};
if !is_first_column_hidden
&& itx == offset_scroll_index
&& itx + start_position == ALL_POSITION
{
truncated_name.patch_style(self.colours.currently_selected_text_style);
Row::new(vec![truncated_name, truncated_legend])
} else {
let cpu_string_row = vec![truncated_name, truncated_legend];
Row::new(cpu_string_row).style(if itx == offset_scroll_index {
self.colours.currently_selected_text_style
} else if itx + start_position == ALL_POSITION {
self.colours.all_colour_style
} else if show_avg_cpu {
if itx + start_position == AVG_POSITION {
self.colours.avg_colour_style
} else {
self.colours.cpu_colour_styles[(itx + start_position
- AVG_POSITION
- 1)
% self.colours.cpu_colour_styles.len()]
}
let data = {
let iter = app_state.canvas_data.cpu_data.iter().zip(colour_iter);
const CPU_WIDTH_CHECK: u16 = 10; // This is hard-coded, it's terrible.
if draw_loc.width < CPU_WIDTH_CHECK {
Either::Left(iter.map(|(cpu, style)| {
let row = vec![
CellContent::Simple("".into()),
CellContent::Simple(if cpu.legend_value.is_empty() {
cpu.cpu_name.clone().into()
} else {
cpu.legend_value.clone().into()
}),
];
TableRow::Styled(row, *style)
}))
} else {
self.colours.cpu_colour_styles[(itx + start_position - ALL_POSITION - 1)
% self.colours.cpu_colour_styles.len()]
})
Either::Right(iter.map(|(cpu, style)| {
let row = vec![
CellContent::HasAlt {
alt: cpu.short_cpu_name.clone().into(),
main: cpu.cpu_name.clone().into(),
},
CellContent::Simple(cpu.legend_value.clone().into()),
];
TableRow::Styled(row, *style)
}))
}
}
});
.collect();
// Note we don't set highlight_style, as it should always be shown for this widget.
let border_and_title_style = if is_on_widget {
TableData { data, row_widths }
};
let is_on_widget = widget_id == app_state.current_widget.widget_id;
let border_style = if is_on_widget {
self.colours.highlighted_border_style
} else {
self.colours.border_style
};
// Draw
f.render_stateful_widget(
Table::new(cpu_rows)
.block(
Block::default()
.borders(Borders::ALL)
.border_style(border_and_title_style),
)
.header(
Row::new(CPU_LEGEND_HEADER.to_vec())
.style(self.colours.table_header_style)
.bottom_margin(table_gap),
)
.widths(
&(cpu_widget_state
.table_width_state
.calculated_column_widths
.iter()
.map(|calculated_width| Constraint::Length(*calculated_width as u16))
.collect::<Vec<_>>()),
),
draw_loc,
cpu_table_state,
);
TextTable {
table_gap: app_state.app_config_fields.table_gap,
is_force_redraw: app_state.is_force_redraw,
recalculate_column_widths,
header_style: self.colours.table_header_style,
border_style,
highlighted_text_style: self.colours.currently_selected_text_style, // We always highlight the selected CPU entry... not sure if I like this though.
title: None,
is_on_widget,
draw_border: true,
show_table_scroll_position: app_state.app_config_fields.show_table_scroll_position,
title_style: self.colours.widget_title_style,
text_style: self.colours.text_style,
left_to_right: false,
}
.draw_text_table(f, draw_loc, &mut cpu_widget_state.table_state, &cpu_data);
}
}
}

View File

@ -2,7 +2,10 @@ use tui::{backend::Backend, layout::Rect, terminal::Frame};
use crate::{
app,
canvas::{components::TextTable, Painter},
canvas::{
components::{TextTable, TextTableTitle},
Painter,
},
};
impl Painter {
@ -13,7 +16,6 @@ impl Painter {
let recalculate_column_widths = app_state.should_get_widget_bounds();
if let Some(disk_widget_state) = app_state.disk_state.widget_states.get_mut(&widget_id) {
let is_on_widget = app_state.current_widget.widget_id == widget_id;
let (border_style, highlighted_text_style) = if is_on_widget {
(
self.colours.highlighted_border_style,
@ -29,8 +31,10 @@ impl Painter {
header_style: self.colours.table_header_style,
border_style,
highlighted_text_style,
title: " Disks ".into(),
is_expanded: app_state.is_expanded,
title: Some(TextTableTitle {
title: " Disks ".into(),
is_expanded: app_state.is_expanded,
}),
is_on_widget,
draw_border,
show_table_scroll_position: app_state.app_config_fields.show_table_scroll_position,

View File

@ -2,7 +2,10 @@ use tui::{backend::Backend, layout::Rect, terminal::Frame};
use crate::{
app,
canvas::{components::TextTable, Painter},
canvas::{
components::{TextTable, TextTableTitle},
Painter,
},
};
impl Painter {
@ -29,8 +32,10 @@ impl Painter {
header_style: self.colours.table_header_style,
border_style,
highlighted_text_style,
title: " Temperatures ".into(),
is_expanded: app_state.is_expanded,
title: Some(TextTableTitle {
title: " Temperatures ".into(),
is_expanded: app_state.is_expanded,
}),
is_on_widget,
draw_border,
show_table_scroll_position: app_state.app_config_fields.show_table_scroll_position,

View File

@ -238,7 +238,7 @@ pub fn convert_cpu_data_points(
if data.cpu_data.len() + 1 != existing_cpu_data.len() {
*existing_cpu_data = vec![ConvertedCpuData {
cpu_name: "All".to_string(),
short_cpu_name: "All".to_string(),
short_cpu_name: "".to_string(),
cpu_data: vec![],
legend_value: String::new(),
}];