The dialog that reads a delimited text file: the layout found, set
where it is wrong, on a preview of the channels as they will be read.
A text export says nothing about itself that a reader can trust
without looking: which column is time, at what rate, in which unit each
channel is. So importing one asks, every time. The dialog opens on what
io.delimited.text_layout found: the delimiter and the decimal point,
the time column (or none, and a sample rate in its place), and a table
of the channels, each with a box to keep it, its name and its unit as
the header gave them. Anything can be changed, and the preview under
it is read again from the file's head (PREVIEW_ROWS rows) by the same
read_delimited call the import makes, drawn by
plot.text_import.build_import_preview: each channel in its row, its
missing samples crossed, a dead channel named. The notes that read
gives are listed under the preview, and a refusal is shown in their
place with Import disabled, since the import would refuse the same way.
A file larger than PREVIEW_INLINE_BYTES is read for its preview on
the window's worker (MainWindow._run_long), so the dialog keeps
painting while the head of a wide file is parsed.
Import then reads the whole file with exactly those arguments, holding
only what differs from what was found, so the journal replays the
import as it was made: project.import_file(path, sample_rate=5120.0).
Classes:
| Name |
Description |
TextImportDialog |
How a delimited text file is read, chosen on a preview of its
|
Functions:
| Name |
Description |
ask_text_import |
Show the dialog and return the import options chosen, or None when
|
Classes
TextImportDialog
TextImportDialog(path: str | PathLike, colors: Mapping[str, str], parent: QWidget | None = None, runner: Callable[..., Any] | None = None)
Bases: QDialog
How a delimited text file is read, chosen on a preview of its
channels.
path is the file and colors the theme's colors. The dialog opens
on io.delimited.text_layout's reading of the file. runner runs
a slow read off the GUI thread, called as the window's _run_long
is (a status line, then the work, which takes progress=); without
one every preview is read in place.
options() is the answer in read_delimited's own keywords, holding
only what differs from what was found: an untouched dialog answers
{}, and the journal line is the plain import.
Methods:
| Name |
Description |
time_column |
The time column chosen, by index, or None.
|
options |
The import's keyword arguments, holding only what differs from
|
Source code in src/visualdynamics/gui/text_import.py
| def __init__(self, path: str | os.PathLike, colors: Mapping[str, str],
parent: QWidget | None = None,
runner: Callable[..., Any] | None = None) -> None:
super().__init__(parent)
import pyqtgraph as pg
self.path: str = str(path)
self.colors: dict[str, str] = dict(colors)
self.runner: Callable[..., Any] | None = runner
#: what the file was found to be, which `options` is measured from
self.found: delimited.TextLayout = delimited.text_layout(self.path)
#: the layout as the delimiter and decimal now chosen read it
self.layout: delimited.TextLayout = self.found
#: the last preview read, or None when it was refused
self.history: Any = None
#: what the last preview read noted, or the refusal
self.notes: list[str] = []
self.refusal: str | None = None
self._building = False
base = os.path.basename(self.path)
self.setWindowTitle(f'Import {base}')
self.header: QLabel = QLabel()
self.header.setWordWrap(True)
self.delimiter_box: QComboBox = QComboBox()
for label, value in DELIMITER_CHOICES:
self.delimiter_box.addItem(label, value)
self.decimal_box: QComboBox = QComboBox()
self.decimal_box.addItem('Point (0.25)', '.')
self.decimal_box.addItem('Comma (0,25)', ',')
self.time_box: QComboBox = QComboBox()
self.time_box.setToolTip('The column that holds each row\'s time, or '
'none when the rows are evenly sampled at '
'the sample rate')
self.rate_box: QDoubleSpinBox = QDoubleSpinBox()
self.rate_box.setRange(0.0, 1e9)
self.rate_box.setDecimals(3)
self.rate_box.setSuffix(' /s')
self.rate_box.setSpecialValueText('—')
self.rate_box.setKeyboardTracking(False)
self.rate_box.setToolTip('Samples per second. With a time column it '
'must agree with the column\'s step; '
'without one it is the rate the rows were '
'sampled at')
self.table: QTableWidget = QTableWidget(0, 3)
self.table.setHorizontalHeaderLabels(['Column', 'Name', 'Unit'])
self.table.horizontalHeader().setStretchLastSection(True)
self.table.verticalHeader().hide()
self.table.setToolTip('Uncheck a column to leave it out. A name that '
'is a DOF (101X+) becomes the channel\'s DOF; '
'a unit is declared and converted to SI')
self.plot: Any = pg.GraphicsLayoutWidget()
self.plot.setBackground(background_brush(self.colors))
self.plot.setMinimumSize(560, 360)
self.message: QLabel = QLabel()
self.message.setWordWrap(True)
self.message.setTextInteractionFlags(
Qt.TextInteractionFlag.TextSelectableByMouse)
buttons = QDialogButtonBox()
self.import_button: Any = buttons.addButton(
'Import', QDialogButtonBox.ButtonRole.AcceptRole)
buttons.addButton(QDialogButtonBox.StandardButton.Cancel)
buttons.accepted.connect(self.accept)
buttons.rejected.connect(self.reject)
form = QFormLayout()
form.addRow('Delimiter', self.delimiter_box)
form.addRow('Decimal point', self.decimal_box)
form.addRow('Time column', self.time_box)
form.addRow('Sample rate', self.rate_box)
side = QVBoxLayout()
side.addLayout(form)
side.addWidget(QLabel('Channels'))
side.addWidget(self.table, 1)
middle = QHBoxLayout()
middle.addWidget(self.plot, 1)
middle.addLayout(side)
layout = QVBoxLayout(self)
layout.addWidget(self.header)
layout.addLayout(middle, 1)
layout.addWidget(self.message)
layout.addWidget(buttons)
self._fill(self.found)
if self.found.sample_rate is not None:
self.rate_box.setValue(self.found.sample_rate)
self.delimiter_box.currentIndexChanged.connect(lambda _i: self._relayout())
self.decimal_box.currentIndexChanged.connect(lambda _i: self._relayout())
self.time_box.currentIndexChanged.connect(lambda _i: self._time_chosen())
self.rate_box.valueChanged.connect(lambda _v: self._restate())
self.table.itemChanged.connect(lambda _item: self._restate())
self._restate()
|
Methods:
time_column
time_column() -> int | None
The time column chosen, by index, or None.
Source code in src/visualdynamics/gui/text_import.py
| def time_column(self) -> int | None:
"""The time column chosen, by index, or None."""
return self.time_box.currentData()
|
options
options() -> dict[str, Any]
The import's keyword arguments, holding only what differs from
what was found: 'delimiter' and 'decimal', 'time_column' (a name,
or False for none), 'sample_rate', 'columns' when some are left
out, 'names' when one was changed and 'units' by column name
where one differs from the header's. A column whose name another
column shares is given by its index, which the reader would
otherwise refuse as naming two.
Source code in src/visualdynamics/gui/text_import.py
| def options(self) -> dict[str, Any]:
"""The import's keyword arguments, holding only what differs from
what was found: 'delimiter' and 'decimal', 'time_column' (a name,
or False for none), 'sample_rate', 'columns' when some are left
out, 'names' when one was changed and 'units' by column name
where one differs from the header's. A column whose name another
column shares is given by its index, which the reader would
otherwise refuse as naming two."""
out: dict[str, Any] = {}
layout = self.layout
def key(k: int) -> str | int:
name = layout.columns[k]
return name if layout.columns.count(name) == 1 else k
if layout.delimiter != self.found.delimiter:
out['delimiter'] = layout.delimiter
if layout.decimal != self.found.decimal:
out['decimal'] = layout.decimal
# measured from the layout as now read: the import reads it the
# same way from the delimiter and decimal stated
time = self.time_column()
if time != layout.time_column:
out['time_column'] = False if time is None else key(time)
rate = float(self.rate_box.value())
if rate > 0 and rate != (layout.sample_rate or 0.0):
out['sample_rate'] = rate
rows = self._rows()
kept = [(k, name, unit) for k, keep, name, unit in rows if keep]
if len(kept) < len(rows):
out['columns'] = [key(k) for k, _n, _u in kept]
if any(name != layout.columns[k] for k, name, _u in kept):
out['names'] = [name or layout.columns[k] for k, name, _u in kept]
units = {key(k): unit for k, _n, unit in kept
if unit and unit != (layout.units[k] or '')}
if units:
out['units'] = units
return out
|
Functions:
ask_text_import
ask_text_import(parent: QWidget | None, path: str | PathLike, colors: Mapping[str, str], runner: Callable[..., Any] | None = None) -> dict[str, Any] | None
Show the dialog and return the import options chosen, or None when
it is canceled and the file is skipped. runner is as the dialog
takes it.
Source code in src/visualdynamics/gui/text_import.py
| def ask_text_import(parent: QWidget | None, path: str | os.PathLike,
colors: Mapping[str, str],
runner: Callable[..., Any] | None = None
) -> dict[str, Any] | None:
"""Show the dialog and return the import options chosen, or None when
it is canceled and the file is skipped. `runner` is as the dialog
takes it."""
dialog = TextImportDialog(path, colors, parent, runner)
if dialog.exec() == QDialog.DialogCode.Accepted:
return dialog.options()
return None
|