#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# data_table.py
"""
A class to sync a data object and list_ctrl
"""
# Copyright (c) 2021 Dan Cutright
# This file is part of IQDM-Analytics, released under a MIT license.
# See the file LICENSE included with this distribution, also
# available at https://github.com/IQDM/IQDM-Analytics
from copy import deepcopy
import wx
from iqdma.utilities import get_selected_listctrl_items, get_sorted_indices
[docs]class DataTable:
"""Helper class for ``wx.ListCtrl``"""
def __init__(
self,
list_ctrl: wx.ListCtrl,
data: dict = None,
columns: list = None,
widths: list = None,
formats: list = None,
):
"""Init DataTable class
Parameters
----------
list_ctrl : wx.ListCtrl
the list_ctrl in the GUI to be updated with data in this class
data : dict
data should be formatted in a dictionary with keys being the
column names and values being lists
columns : list
the keys of the data object to be visible in the list_ctrl
widths : list
optionally specify the widths of the columns
formats : list
optionally specify wx Format values (e.g., wx.LIST_FORMAT_LEFT)
"""
self.layout = list_ctrl
self.sort_indices = None
self.data = deepcopy(data)
self.columns = deepcopy(columns)
self.widths = widths
if formats:
self.formats = formats
else:
if not self.columns:
column_length = 0
else:
column_length = len(self.columns)
self.formats = [wx.LIST_FORMAT_LEFT] * column_length
if data:
# TODO: Initializing class with duplicates data in view?
self.set_data(data, columns, formats=formats)
self.set_data_in_layout()
[docs] def set_data(
self,
data: dict,
columns: list,
formats: list = None,
ignore_layout: bool = False,
):
"""Set data and update layout
Parameters
----------
data : dict
data should be formatted in a dictionary with keys being the
column names and values being lists
columns : list
the keys of the data object to be visible in the list_ctrl
formats : list
optionally specify wx Format values (e.g., wx.LIST_FORMAT_LEFT)
ignore_layout : bool
If true, do not update layout
"""
if formats:
self.formats = formats
elif columns and len(columns) != len(self.formats):
self.formats = [wx.LIST_FORMAT_LEFT] * len(columns)
delete_rows = bool(self.row_count)
self.data = deepcopy(data)
self.columns = deepcopy(columns)
if delete_rows:
self.delete_all_rows(layout_only=True)
if not ignore_layout:
self.set_layout_columns()
self.set_data_in_layout()
if self.widths:
self.set_column_widths()
# If len of new data is different than previous, sorting may crash
self.sort_indices = None
[docs] def set_layout_columns(self):
"""Clear layout and re-add columns"""
self.layout.DeleteAllColumns()
for i, col in enumerate(self.columns):
self.layout.AppendColumn(col, format=self.formats[i])
@property
def keys(self) -> list:
"""Column names
Returns
-------
list
A copy of ``columns``
"""
return [col for col in self.columns]
@property
def column_count(self) -> int:
"""Number of columns
Returns
-------
int
Length of ``columns``
"""
if self.columns:
return len(self.columns)
return 0
@property
def row_count(self) -> int:
"""Number of rows
Returns
-------
int
Length of first column in ``data``
"""
if self.data:
return len(self.data[self.columns[0]])
return 0
[docs] def data_to_list_of_rows(self) -> list:
"""Convert ``data`` into a list of rows as needed for list_ctrl
Returns
-------
list
data in the format of list of rows
"""
if self.data and self.keys:
return [
[self.data[col][row] for col in self.columns]
for row in range(self.row_count)
]
else:
return []
[docs] def set_data_in_layout(self):
"""Set data in layout from ``data``"""
row_data = self.data_to_list_of_rows()
for row in row_data:
self.append_row(row, layout_only=True)
[docs] def append_row(self, row: list, layout_only: bool = False):
"""Add a row of data
Parameters
----------
row : list
data ordered by self.columns
layout_only : bool
If true, only add row to the GUI
"""
if not layout_only:
self.append_row_to_data(row)
if self.layout:
index = self.layout.InsertItem(50000, str(row[0]))
for i in range(len(row))[1:]:
if isinstance(row[i], int):
value = "%d" % row[i]
elif isinstance(row[i], float):
value = "%0.2f" % row[i]
else:
value = str(row[i])
self.layout.SetItem(index, i, value)
[docs] def append_row_to_data(self, row: list):
"""Add a row of data to self.data
Parameters
----------
row : list
data ordered by self.columns
"""
if not self.data:
columns = self.keys
self.data = {columns[i]: [value] for i, value in enumerate(row)}
else:
for i, key in enumerate(self.keys):
self.data[key].append(row[i])
[docs] def delete_all_rows(
self, layout_only: bool = False, force_delete_data: bool = False
):
"""Clear all data from ``data`` and the layout view
Parameters
----------
layout_only : bool
If True, do not remove the row from self.data
force_delete_data : bool
If true, force deletion even if layout is not set
"""
if self.layout:
self.layout.DeleteAllItems()
if self.layout or force_delete_data:
if not layout_only:
if self.data:
for key in self.keys:
self.data[key] = []
[docs] def get_value(self, row_index: int, column_index: int):
"""Get a specific table value with a column name and row index
Parameters
----------
row_index : int
retrieve value from row with this index
column_index : int
retrieve value from column with this index
Returns
-------
any
value corresponding to provided indices
"""
return self.data[self.keys[column_index]][row_index]
[docs] def get_row(self, row_index: int) -> list:
"""Get a row of data from self.data with the given row index
Parameters
----------
row_index : int
retrieve all values from row with this index
Returns
-------
list
values for the specified row
"""
return [self.data[key][row_index] for key in self.keys]
[docs] def set_column_width(self, index: int, width: int):
"""Change the column width in the view
Parameters
----------
index : int
index of column
width : int
the specified width
"""
self.layout.SetColumnWidth(index, width)
[docs] def set_column_widths(self, auto: bool = False):
"""Set all widths in layout based on ``widths``
Parameters
----------
auto : bool
Use ``wx.LIST_AUTOSIZE_USEHEADER`` rather than ``widths``
"""
if auto:
for i in range(len(self.columns)):
self.set_column_width(i, wx.LIST_AUTOSIZE_USEHEADER)
else:
if self.widths is not None:
for i, width in enumerate(self.widths):
self.set_column_width(i, width)
[docs] def clear(self):
"""Delete all data in self.data and clear the table view"""
self.delete_all_rows()
self.layout.DeleteAllColumns()
[docs] def get_csv_rows(self) -> list:
"""Convert ``data`` to a list of strings for CSV writing
Returns
-------
list of str
Each item is a str for a CSV file
"""
csv_data = []
for row in self.data_for_csv:
row = [str(el).replace("\n", "<>") for el in row]
csv_data.append(",".join(row))
return csv_data
@property
def data_for_csv(self) -> list:
"""Iterate through ``data`` to get a list of csv rows
Returns
-------
list of lists
list of rows. Each row is a list of column data
"""
data = [self.columns]
for row_index in range(self.row_count):
row = []
for key in self.keys:
raw_value = self.data[key][row_index]
if isinstance(raw_value, float):
row.append("%0.5f" % raw_value)
else:
row.append(raw_value)
data.append(row)
return data
@property
def selected_row_data(self) -> list:
"""Row data from the current selection in ``wx.ListCtrl``
Returns
-------
list
row data of the currently selected row in the GUI
"""
return [
self.get_row(index)
for index in get_selected_listctrl_items(self.layout)
]
@property
def selected_row_index(self) -> list:
"""Get the indices of selected rows in ``wx.ListCtrl``
Returns
-------
list
List of indices
"""
return get_selected_listctrl_items(self.layout)
@property
def has_data(self) -> bool:
"""Check if there are any rows of data
Returns
-------
bool
True if ``row_count`` > 0
"""
return bool(self.row_count)
[docs] def sort_table(self, evt: wx.EVT_LIST_COL_CLICK):
"""Sort the data based on the clicked column header
Parameters
----------
evt : wx.EVT_LIST_COL_CLICK
Event from a ListCtrl column header click
"""
if self.data:
key = self.columns[
evt.Column
] # get the column name from the column index (evt.Column)
sort_indices = get_sorted_indices(
self.data[key]
) # handles str and float mixtures
if self.sort_indices is None:
self.sort_indices = list(range(len(self.data[key])))
# reverse order if already sorted
if sort_indices == list(range(len(sort_indices))):
sort_indices = sort_indices[::-1]
self.sort_indices = [
self.sort_indices[i] for i in sort_indices
] # keep original order
# reorder data and reinitialize table view
self.data = {
column: [self.data[column][i] for i in sort_indices]
for column in self.columns
}
self.set_data(self.data, self.columns, self.formats)
[docs] def get_data_in_original_order(self) -> dict:
"""Get ``data`` in the order it was original set
Returns
-------
dict
keys are column names with voalues of row data
"""
if self.sort_indices is None:
return self.data
return {
column: [self.data[column][i] for i in self.sort_indices]
for column in self.columns
}
[docs] def increment_index(self, evt: wx.Event = None, increment: int = None):
"""Increment the ListCtrl selection with an event or fixed increment
Parameters
----------
evt : wx.Event
An event with a ``GetKeyCode`` method
increment : int
If no event is passed, use a fixed index increment
"""
if self.has_data:
if hasattr(evt, "GetKeyCode"):
keycode = evt.GetKeyCode()
if keycode == wx.WXK_UP:
evt.Skip()
increment = -1
elif keycode == wx.WXK_DOWN:
evt.Skip()
increment = 1
else:
return
if increment is None:
increment = 1
current_index = self.selected_row_index
if len(current_index):
new_index = current_index[0] + increment
if new_index > self.row_count - 1:
new_index = 0
else:
new_index = -1 + increment if increment > 0 else -increment
self.layout.Select(new_index)