h5tab 1.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
h5tab-1.0.0/PKG-INFO ADDED
@@ -0,0 +1,7 @@
1
+ Metadata-Version: 2.4
2
+ Name: h5tab
3
+ Version: 1.0.0
4
+ Summary: Simple HDF5 file viewer.
5
+ Author-email: John Helly <j.c.helly@durham.ac.uk>
6
+ Requires-Python: >=3.7
7
+ Requires-Dist: h5py
h5tab-1.0.0/README.md ADDED
@@ -0,0 +1,35 @@
1
+ # h5tab - view tabular data stored as HDF5 datasets
2
+
3
+ This is a simple utility to display multiple HDF5 datasets as columns in a
4
+ table. This is useful when you need to look up elements with the same index
5
+ between different datasets.
6
+
7
+ ## Usage
8
+
9
+ Run the script with the names of one or more HDF5 files as arguments.
10
+ ```
11
+ h5tab.py filename ...
12
+ ```
13
+ This brings up a tree view where you can select datasets to open. Clicking
14
+ a dataset toggles it between selected and not selected.
15
+
16
+ Once you've selected all of the datasets to open click the "Open
17
+ selected datasets" button at the bottom of the window. This opens a new
18
+ window with a table where the columns correspond to the selected datasets
19
+ and the rows are the dataset element index (in the first dimension in case
20
+ of multidimensional datasets).
21
+
22
+ In the case of compound data types, individual fields can be designated
23
+ as table columns.
24
+
25
+ Only the displayed portions of the datasets are read in, so very large
26
+ datasets can be viewed.
27
+
28
+ ## Limitations
29
+
30
+ This is not a general purpose HDF5 file viewer.
31
+
32
+ * Table rows always correspond to the first dimension so datasets with
33
+ large dimensions other than the first can't be displayed
34
+ * The program has no good way to display variable length datasets
35
+ * The tree view can be slow for large numbers of datasets
@@ -0,0 +1,7 @@
1
+ Metadata-Version: 2.4
2
+ Name: h5tab
3
+ Version: 1.0.0
4
+ Summary: Simple HDF5 file viewer.
5
+ Author-email: John Helly <j.c.helly@durham.ac.uk>
6
+ Requires-Python: >=3.7
7
+ Requires-Dist: h5py
@@ -0,0 +1,9 @@
1
+ README.md
2
+ h5tab.py
3
+ pyproject.toml
4
+ h5tab.egg-info/PKG-INFO
5
+ h5tab.egg-info/SOURCES.txt
6
+ h5tab.egg-info/dependency_links.txt
7
+ h5tab.egg-info/entry_points.txt
8
+ h5tab.egg-info/requires.txt
9
+ h5tab.egg-info/top_level.txt
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ h5tab = h5tab:main
@@ -0,0 +1 @@
1
+ h5py
@@ -0,0 +1 @@
1
+ h5tab
h5tab-1.0.0/h5tab.py ADDED
@@ -0,0 +1,724 @@
1
+ #!/usr/bin/env python
2
+
3
+ #
4
+ # TODO:
5
+ #
6
+ # More sensible way to determine no. of rows
7
+ # Add attribute/info icons
8
+ #
9
+
10
+ # Avoid warnings if h5py has MPI support
11
+ try:
12
+ import mpi4py
13
+ except ImportError:
14
+ pass
15
+ else:
16
+ mpi4py.rc.initialize = False
17
+
18
+ import h5py
19
+
20
+ import os.path
21
+ import sys
22
+
23
+ from tkinter import *
24
+ from idlelib.tree import TreeItem, TreeNode
25
+
26
+ import tkinter.font as tkFont
27
+
28
+ ITEM_TOP = 0
29
+ ITEM_GROUP = 1
30
+ ITEM_DATASET = 2
31
+ ITEM_ATTR = 3
32
+ ITEM_DESCR = 4
33
+
34
+ icon_images = None
35
+
36
+ # Nodes which are highlighted
37
+ selected_nodes = {}
38
+
39
+ # Format for displaying values
40
+ float_fmt = "%14.6e"
41
+ int_fmt = "%d"
42
+
43
+ # Minimum column width
44
+ min_width = 16
45
+
46
+ # Maximum elements per row
47
+ max_elements = 6
48
+
49
+ # Maximum column width
50
+ max_width = max_elements * min_width
51
+
52
+ def get_shape(data):
53
+ """Return shape of array or [1] if its a scalar"""
54
+ if len(data.shape) > 0:
55
+ return data.shape
56
+ else:
57
+ return [1]
58
+
59
+ def get_format(dtype):
60
+ # Check for variable length data types
61
+ if h5py.check_string_dtype(dtype) is not None:
62
+ return "%s"
63
+ elif h5py.check_vlen_dtype(dtype) is not None:
64
+ dtype = h5py.check_vlen_dtype(dtype)
65
+ # Choose format string depending on base type
66
+ if dtype.kind == "i" or dtype.kind == "u":
67
+ fmt = int_fmt
68
+ elif dtype.kind == "f":
69
+ fmt = float_fmt
70
+ elif dtype.kind == "S" or dtype.kind == "U" or dtype.kind == "a":
71
+ fmt = "%s"
72
+ else:
73
+ fmt = None
74
+ return fmt
75
+
76
+ def get_width(dtype):
77
+ if h5py.check_string_dtype(dtype) is not None or h5py.check_vlen_dtype(dtype) is not None:
78
+ return max_width
79
+ elif dtype.kind == "S" or dtype.kind == "U" or dtype.kind == "a":
80
+ return min(max_width, max((dtype.itemsize, min_width)))
81
+ else:
82
+ return min_width
83
+
84
+ def is_too_big(shape):
85
+ if len(shape) < 2:
86
+ # 1D arrays are always ok
87
+ return False
88
+ elif len(shape) == 2:
89
+ # 2D is ok if second dim is small
90
+ return (shape[1] > max_elements)
91
+ else:
92
+ # Multidimensional array - check no. of elements
93
+ i = shape[1]
94
+ for j in shape[2:]:
95
+ i *= j
96
+ return (i > max_elements)
97
+
98
+ def argsort(seq):
99
+ return sorted(range(len(seq)), key=seq.__getitem__)
100
+
101
+ def get_icons():
102
+ # idlelib doesn't have appropriate icons for datasets/attributes
103
+ # so encode some here (data strings are base64 encoded .gif files)
104
+ dataset = PhotoImage(data=r'R0lGODlhEAANAPIEADAwMAD//8PDw9zc3P///6usr'+
105
+ r'QAAAAAAACH5BAEAAAUALAAAAAAQAA0AAAMqOLDcrSHKORe9'+
106
+ r'0uktuv/fIhBkaZKCCK6deL5ECrCsC5syvaq6BygbzSABADs=')
107
+ node = PhotoImage(data=r'R0lGODlhCwALAPECAAAAAH9/f////6usrSH5BAEAAA'+
108
+ r'MALAAAAAALAAsAAAIanI8Wy6wCowhPQjqCvdVivX3b1Elf0y'+
109
+ r'TqUAAAOw==')
110
+ return {"data":dataset, "attr":node}
111
+
112
+ def value_to_text(value, fmt):
113
+ """Write out a multidimensional array as text"""
114
+ if isinstance(value, str):
115
+ return value
116
+ elif value.dtype.kind == "O" and value.shape == ():
117
+ # Value is a single object reference
118
+ return value_to_text(value[()], fmt)
119
+ elif len(value.shape) > 0:
120
+ # Value is an array, possibly of objects
121
+ s = []
122
+ for i in range(value.shape[0]):
123
+ s.append(value_to_text(value[i,...], fmt))
124
+ return "[" + (",".join(s)) + "]"
125
+ else:
126
+ # Value is a non-object scalar
127
+ if fmt is None:
128
+ return str(value)
129
+ else:
130
+ return fmt % value
131
+
132
+ def type_string(dtype):
133
+ """Return a description of a HDF5 data type"""
134
+ if h5py.check_string_dtype(dtype) is not None:
135
+ return "vlen string"
136
+ elif h5py.check_vlen_dtype(dtype) is not None:
137
+ return "vlen "+str(h5py.check_vlen_dtype(dtype))
138
+ else:
139
+ return str(dtype)
140
+
141
+ class MyTreeNode(TreeNode):
142
+ def __init__(self, canvas, parent, item):
143
+ TreeNode.__init__(self, canvas, parent, item)
144
+ self.item = item
145
+ def select(self, event=None, value=None):
146
+ if self.item.item_type == ITEM_DATASET:
147
+ if value is None:
148
+ self.selected = not(self.selected)
149
+ else:
150
+ self.selected = value
151
+ self.canvas.delete(self.image_id)
152
+ self.drawicon()
153
+ self.drawtext()
154
+ global selected_nodes
155
+ my_id = (self.item.file.filename, self.item.path, self.item.field)
156
+ if self.selected:
157
+ selected_nodes[my_id] = self.item
158
+ else:
159
+ if my_id in selected_nodes:
160
+ del selected_nodes[my_id]
161
+
162
+ def geticonimage(self, name):
163
+ global icon_images
164
+ if icon_images is None:
165
+ icon_images = get_icons()
166
+ if name in icon_images:
167
+ return icon_images[name]
168
+ else:
169
+ return TreeNode.geticonimage(self, name)
170
+
171
+ class TopItem(TreeItem):
172
+ """Top level tree widget item with one sub-item for each file"""
173
+ def __init__(self):
174
+ self.nodes = []
175
+ self.item_type = ITEM_TOP
176
+ def GetText(self):
177
+ return "HDF5 files"
178
+ def IsExpandable(self):
179
+ return True
180
+ def GetSubList(self):
181
+ return self.nodes
182
+ def add_child(self, child):
183
+ self.nodes.append(child)
184
+
185
+ class AttrItem(TreeItem):
186
+ """Tree widget item which represents a HDF5 attribute"""
187
+ def __init__(self, name, value):
188
+ self.name = name
189
+ self.value = value_to_text(value, None)
190
+ self.item_type = ITEM_ATTR
191
+ def GetText(self):
192
+ return self.name + " = " + self.value
193
+ def IsExpandable(self):
194
+ return False
195
+ def GetSubList(self):
196
+ return []
197
+ def GetIconName(self):
198
+ return "attr"
199
+
200
+ class DescrItem(TreeItem):
201
+ """Tree widget item which represents a description of a dataset"""
202
+ def __init__(self, id, dtype, field):
203
+ self.item_type = ITEM_DESCR
204
+ self.text = "shape="+str(id.shape)+", dtype="+type_string(dtype)
205
+ if field is None:
206
+ # Don't show layout and chunks for dataset sub-fields
207
+ plist = id.get_create_plist()
208
+ if plist.get_layout() == h5py.h5d.CHUNKED:
209
+ chunk = plist.get_chunk()
210
+ self.text += ", chunks="+str(chunk)
211
+ for i in range(plist.get_nfilters()):
212
+ (code, flags, value, name) = plist.get_filter(i)
213
+ self.text += ", "+str(name.decode())
214
+ def GetText(self):
215
+ return self.text
216
+ def IsExpandable(self):
217
+ return False
218
+ def GetSubList(self):
219
+ return []
220
+ def GetIconName(self):
221
+ return "attr"
222
+
223
+ #
224
+ # Use low level interface instead of instantiating h5py objects for every
225
+ # tree item in here to avoid performance issue with datasets with
226
+ # many chunks if using h5py < 2.0.
227
+ #
228
+ class HDF5Item(TreeItem):
229
+ """Tree widget item which represents a HDF5 file, group or dataset"""
230
+ def __init__(self, id, name="", file=None, is_file=False, parent_name="", field=None):
231
+ self.id = id
232
+ self.file = file
233
+ self.is_file = is_file
234
+ self.name = name
235
+ if field is None:
236
+ self.path = (parent_name+"/"+name).lstrip("/")
237
+ else:
238
+ self.path = parent_name
239
+ self.field = field if field is not None else ()
240
+ if isinstance(self.id, h5py.h5d.DatasetID):
241
+ self.item_type = ITEM_DATASET
242
+ else:
243
+ self.item_type = ITEM_GROUP
244
+ def GetText(self):
245
+ if self.is_file:
246
+ return self.file.filename
247
+ else:
248
+ return self.name
249
+ def IsExpandable(self):
250
+ return True
251
+ def GetSubList(self):
252
+ sublist = []
253
+ if isinstance(self.id, h5py.h5d.DatasetID):
254
+ # This is a dataset or a sub-field of a dataset. Find its data type.
255
+ dtype = self.id.get_type().dtype
256
+ if self.field is not None:
257
+ for f in self.field:
258
+ dtype = dtype[f]
259
+ # Add its description to the tree.
260
+ sublist.append(DescrItem(self.id, dtype, field=self.field))
261
+ # Check for compound type and add sub fields
262
+ if dtype.fields is not None:
263
+ for field in dtype.fields:
264
+ sublist.append(HDF5Item(self.id, field, self.file,
265
+ parent_name=self.path, field=self.field+(field,)))
266
+ if isinstance(self.id, h5py.h5g.GroupID):
267
+ # This is a group. Add its members to the tree.
268
+ n = self.id.get_num_objs()
269
+ for i in range(n):
270
+ name = self.id.get_objname_by_idx(i)
271
+ ot = self.id.get_objtype_by_idx(i)
272
+ if ot == h5py.h5g.DATASET:
273
+ subid = h5py.h5d.open(self.id, name)
274
+ sublist.append(HDF5Item(subid, name.decode(), self.file,
275
+ parent_name=self.path))
276
+ elif ot == h5py.h5g.GROUP:
277
+ subid = h5py.h5g.open(self.id, name)
278
+ sublist.append(HDF5Item(subid, name.decode(), self.file,
279
+ parent_name=self.path))
280
+ else:
281
+ # Unsupported object type
282
+ pass
283
+ # Get number of attributes on this object
284
+ try:
285
+ nr_attrs = h5py.h5a.get_num_attrs(self.id)
286
+ except ValueError:
287
+ # Workaround for old HDF5 versions: need to open root group
288
+ if self.is_file:
289
+ nr_attrs = h5py.h5a.get_num_attrs(self.id["/"])
290
+ else:
291
+ raise
292
+ # Add attributes of this object to the tree
293
+ if nr_attrs > 0:
294
+ for name in self.get_data().attrs.keys():
295
+ try:
296
+ value = self.get_data().attrs[name]
297
+ except IOError as e:
298
+ pass
299
+ else:
300
+ sublist.append(AttrItem(name, value))
301
+
302
+ return sublist
303
+ def GetIconName(self):
304
+ if self.item_type == ITEM_DATASET:
305
+ return "data"
306
+ else:
307
+ return "folder"
308
+ def get_data(self):
309
+ if len(self.path) > 0:
310
+ return self.file[self.path]
311
+ else:
312
+ return self.file
313
+
314
+ class AutoScrollbar(Scrollbar):
315
+ def set(self, lo, hi):
316
+ if float(lo) <= 0.0 and float(hi) >= 1.0:
317
+ self.tk.call("grid", "remove", self)
318
+ else:
319
+ self.grid()
320
+ Scrollbar.set(self, lo, hi)
321
+
322
+ class DataWindow():
323
+ def __init__(self, data):
324
+
325
+ global windows_open
326
+ windows_open += 1
327
+
328
+ self.ids = data.keys()
329
+ self.filenames = [id[0] for id in self.ids]
330
+ self.names = [id[1] for id in self.ids]
331
+ self.fields = [id[2] for id in self.ids]
332
+ self.datasets = [data[k].get_data() for k in self.ids]
333
+ self.nrows_tot = max([get_shape(arr)[0] for arr in self.datasets])
334
+
335
+ # Sort by file name
336
+ #idx = argsort(self.filenames)
337
+ sort_key = [fn + n for (fn,n) in zip(self.filenames, self.names)]
338
+ idx = argsort(sort_key)
339
+ self.names = [self.names[i] for i in idx]
340
+ self.filenames = [self.filenames[i] for i in idx]
341
+ self.datasets = [self.datasets[i] for i in idx]
342
+ self.fields = [self.fields[i] for i in idx]
343
+
344
+ # Figure out which columns need to show file names
345
+ self.nspan = [1 for _ in range(len(self.names))]
346
+ for i in range(len(self.names)-1):
347
+ for j in range(i+1,len(self.names)):
348
+ if self.filenames[i] == self.filenames[j]:
349
+ self.nspan[i] += 1
350
+ for i in range(1, len(self.names)):
351
+ if self.filenames[i] == self.filenames[i-1]:
352
+ self.nspan[i] = 0
353
+
354
+ # Make top level window
355
+ self.toplevel = Toplevel()
356
+ self.toplevel.title("HDF5 data table")
357
+ self.toplevel.grid_rowconfigure(0,weight=1)
358
+ self.toplevel.grid_columnconfigure(0,weight=1)
359
+
360
+ # Add label, entry and button for jumping to row
361
+ lframe = Frame(self.toplevel)
362
+ lframe.grid(row=2, column=0, sticky=E+W)
363
+ lframe.columnconfigure(0, weight=0)
364
+ lframe.columnconfigure(1, weight=0)
365
+ lframe.columnconfigure(2, weight=1)
366
+ self.label = Label(lframe, text="Find index: ")
367
+ self.label.grid(row=2, column=0)
368
+ self.entry = Entry(lframe)
369
+ self.entry.grid(row=2, column=1)
370
+ self.entry.bind("<Return>", self.on_return)
371
+
372
+ # Add row count
373
+ self.rowlabel = Label(lframe, text="Total rows: "+str(self.nrows_tot))
374
+ self.rowlabel.grid(row=2, column=2, sticky=E)
375
+
376
+ # Make vertical scroll bar
377
+ self.vsbar = AutoScrollbar(self.toplevel, orient=VERTICAL,
378
+ command=self.scrollbar_moved)
379
+ self.vsbar.grid(row=0, column=1, sticky=N+S)
380
+
381
+ # Make horizontal scroll bar
382
+ hsbar = AutoScrollbar(self.toplevel, orient=HORIZONTAL)
383
+ hsbar.grid(row=1, column=0, sticky=E+W)
384
+
385
+ # Put a canvas in the window
386
+ self.canvas = Canvas(self.toplevel, xscrollcommand=hsbar.set)
387
+ self.canvas.grid(row=0, column=0, sticky=N+S+E+W)
388
+ hsbar.config(command=self.canvas.xview)
389
+
390
+ # Put a frame in the canvas
391
+ self.frame = Frame(self.canvas, bd=3, relief=SUNKEN)
392
+ self.frame.rowconfigure(2,weight=1)
393
+
394
+ # Calculate column widths
395
+ self.widths = []
396
+ for (name,dset,fname,fields) in zip(self.names,self.datasets,self.filenames,self.fields):
397
+ dtype = dset.dtype
398
+ for f in fields:
399
+ dtype = dtype[f]
400
+ width = get_width(dtype)
401
+ if len(dset.shape) > 1:
402
+ width += 3
403
+ for s in dset.shape[1:]:
404
+ width *= s
405
+ if is_too_big(dset.shape):
406
+ width = min_width
407
+ width = min((max_width, max(width, len(name))))
408
+ self.widths.append(width)
409
+
410
+ # Calculate file name widths
411
+ self.fname_widths = []
412
+ icol = 0
413
+ for (width, nspan) in zip(self.widths, self.nspan):
414
+ w = 0
415
+ for i in range(nspan):
416
+ w += self.widths[icol+i]
417
+ self.fname_widths.append(w)
418
+ icol += 1
419
+
420
+ # Add text widgets with column headings and data columns
421
+ font = tkFont.Font(family="courier", size=-16)
422
+ self.title = []
423
+ self.column = []
424
+ self.fname = []
425
+ icol = 1
426
+ self.index = Text(self.frame, font=font, wrap=NONE,
427
+ width=12, bg="light cyan")
428
+ self.index.grid(row=2, column=0, sticky=W+N+S)
429
+ for (name,dset,fname,width,fw,fields) in zip(self.names,self.datasets,
430
+ self.filenames, self.widths,
431
+ self.fname_widths, self.fields):
432
+ if len(fields) == 0:
433
+ # Basic type - just display the dataset name
434
+ display_name = name
435
+ else:
436
+ # Compound type - need to display the field name too
437
+ display_name = "|".join((name,)+fields)
438
+
439
+ self.title.append(Text(self.frame, font=font, wrap=NONE,
440
+ width=width, height=1, bg="light yellow"))
441
+ self.title[-1].insert("end", display_name+" ")
442
+ self.title[-1].grid(row=1, column=icol,sticky=N+W)
443
+ self.title[-1].config(state=DISABLED)
444
+ self.column.append(Text(self.frame, font=font, wrap=NONE,
445
+ width=width, bg="white", setgrid=1))
446
+ self.column[-1].grid(row=2, column=icol, sticky=W+N+S)
447
+ self.column[-1].config(state=DISABLED)
448
+
449
+ # Add file name spanning columns from same file
450
+ if self.nspan[icol-1] > 0:
451
+ self.fname.append(Text(self.frame, font=font, wrap=NONE,
452
+ height=1, width=fw))
453
+ self.fname[-1].grid(row=0, column=icol, sticky=E+W,
454
+ columnspan=self.nspan[icol-1])
455
+ self.fname[-1].insert("end", fname)
456
+ self.fname[-1].config(state=DISABLED)
457
+
458
+ icol += 1
459
+
460
+ self.win = self.canvas.create_window(0, 0, anchor=NW,
461
+ window=self.frame)
462
+ self.frame.update_idletasks()
463
+ self.canvas.config(scrollregion=self.canvas.bbox("all"))
464
+
465
+ # Resize window if size of canvas changes
466
+ self.canvas.bind( '<Configure>', self.on_resize)
467
+
468
+ # Scrolling with keys/mouse wheel
469
+ # TODO - see if there's a way to get events passed to the parent
470
+ # frame instead of binding everything!
471
+ self.bind_scrolling(self.canvas)
472
+ self.bind_scrolling(self.frame)
473
+ self.bind_scrolling(self.index)
474
+ self.bind_scrolling(self.vsbar)
475
+ self.bind_scrolling(self.entry)
476
+ self.bind_scrolling(self.label)
477
+ for c in self.column:
478
+ self.bind_scrolling(c)
479
+ for t in self.title:
480
+ self.bind_scrolling(t)
481
+ for f in self.fname:
482
+ self.bind_scrolling(f)
483
+
484
+ # Store height of one row of text
485
+ for i in range(1000):
486
+ self.index.insert("end", str(i)+"\n")
487
+ height = self.index.winfo_height()
488
+ nrows = int(self.index.index("@0,%d" % height).split(".")[0])
489
+ self.row_height = int(height//nrows)
490
+
491
+ # Set up initial content
492
+ self.update_nrows()
493
+ self.move_to_row(0)
494
+ self.frame.focus_set()
495
+
496
+ self.toplevel.protocol("WM_DELETE_WINDOW", self.destroy)
497
+
498
+ def bind_scrolling(self, widget):
499
+ widget.bind("<Prior>", self.pageup)
500
+ widget.bind("<Next>", self.pagedown)
501
+ widget.bind("<Button-4>", self.pageup)
502
+ widget.bind("<Button-5>", self.pagedown)
503
+
504
+ def on_resize(self, event):
505
+ # Resize the data window
506
+ self.canvas.itemconfig(self.win, height=event.height)
507
+ self.canvas.update_idletasks()
508
+ self.update_nrows()
509
+ # Resize text widgets
510
+ #self.index.configure(height=self.nrows_display)
511
+ #for c in self.column:
512
+ # c.configure(height=self.nrows_display)
513
+ # Redraw values
514
+ self.update_scroll_bar()
515
+ self.update_data()
516
+
517
+ def update_data(self):
518
+ # Redraw array index
519
+ self.index.config(state=NORMAL)
520
+ self.index.delete(1.0, END)
521
+ self.ilast = min(self.ifirst + self.nrows_display, self.nrows_tot)
522
+ for i in range(self.ifirst, self.ilast):
523
+ if i < self.ilast -1:
524
+ self.index.insert("end", str(i)+"\n")
525
+ else:
526
+ self.index.insert("end", str(i))
527
+ self.index.config(state=DISABLED)
528
+ # Redraw columns
529
+ for col, dat, fields in zip(self.column, self.datasets, self.fields):
530
+ # Find data type of this column
531
+ dtype = dat.dtype
532
+ for f in fields:
533
+ dtype = dtype[f]
534
+ # Clear this column
535
+ col.config(state=NORMAL)
536
+ col.delete(1.0, END)
537
+ # Check the data is something we can sensibly display
538
+ if is_too_big(dat.shape):
539
+ col.insert("end", "Unable to display!")
540
+ else:
541
+ # Check at least one value is to be displayed
542
+ if len(dat.shape) == 0:
543
+ # Its a scalar, so only visible if scrolled up to the top
544
+ if self.ifirst == 0:
545
+ # Decide format to use
546
+ fmt = get_format(dat.dtype)
547
+ col.insert("end", value_to_text(dat[...], fmt)+"\n")
548
+ else:
549
+ # Its an array
550
+ if self.ifirst < dat.shape[0] and self.ilast > self.ifirst:
551
+ ilast = min(self.ilast, dat.shape[0])
552
+ # Read the data
553
+ d = dat[self.ifirst:ilast, ...]
554
+ for f in fields:
555
+ d = d[f]
556
+ # Decide format to use
557
+ fmt = get_format(d.dtype)
558
+ # Add values to display
559
+ for i in range(d.shape[0]):
560
+ if i < d.shape[0] - 1:
561
+ col.insert("end", value_to_text(d[i,...],fmt)+
562
+ "\n")
563
+ else:
564
+ col.insert("end", value_to_text(d[i,...],fmt))
565
+
566
+ col.config(state=DISABLED)
567
+
568
+
569
+ def update_scroll_bar(self):
570
+ lo = float(self.ifirst) / float(self.nrows_tot)
571
+ hi = float(self.ilast) / float(self.nrows_tot)
572
+ self.vsbar.set(lo, hi)
573
+
574
+ def scrollbar_moved(self, action, *args):
575
+ if action == "moveto":
576
+ offset = float(args[0])
577
+ i = int(offset * self.nrows_tot)
578
+ elif action == "scroll":
579
+ step = int(args[0])
580
+ what = args[1]
581
+ if what == "units":
582
+ i = self.ifirst + step
583
+ else:
584
+ i = self.ifirst + step * self.nrows_display
585
+ self.move_to_row(i)
586
+
587
+ def on_return(self, event):
588
+ try:
589
+ i = int(self.entry.get())
590
+ except ValueError:
591
+ pass
592
+ else:
593
+ self.move_to_row(i)
594
+
595
+ def move_to_row(self, i):
596
+ self.ifirst = i
597
+ if self.ifirst >= self.nrows_tot - self.nrows_display:
598
+ self.ifirst = self.nrows_tot - self.nrows_display
599
+ if self.ifirst < 0:
600
+ self.ifirst = 0
601
+ self.ilast = min((self.ifirst + self.nrows_display,
602
+ self.nrows_tot))
603
+ self.update_scroll_bar()
604
+ self.update_data()
605
+
606
+ def pageup(self, event):
607
+ self.move_to_row(self.ifirst - self.nrows_display)
608
+ return "break"
609
+
610
+ def pagedown(self, event):
611
+ self.move_to_row(self.ifirst + self.nrows_display)
612
+ return "break"
613
+
614
+ def destroy(self, *args):
615
+ global windows_open
616
+ global root
617
+ self.toplevel.destroy()
618
+ windows_open -= 1
619
+ if windows_open == 0:
620
+ root.destroy()
621
+
622
+ def update_nrows(self):
623
+ n = ((self.index.winfo_height()-0.5*self.row_height)/
624
+ self.row_height)
625
+ n = max((0, n))
626
+ self.nrows_display = int(n)
627
+
628
+ class BrowserWindow():
629
+ def __init__(self, filenames):
630
+
631
+ global windows_open
632
+ windows_open += 1
633
+
634
+ nloaded = 0
635
+
636
+ self.windows = []
637
+
638
+ self.toplevel = Toplevel()
639
+ self.toplevel.title("HDF5 Browser")
640
+ self.toplevel.config(bg="white")
641
+
642
+ canvas = Canvas(self.toplevel)
643
+ canvas.config(bg='white')
644
+ canvas.grid(sticky=N+S+E+W)
645
+ top_item = TopItem()
646
+ top_node = MyTreeNode(canvas, None, top_item)
647
+
648
+ vsbar = AutoScrollbar(self.toplevel)
649
+ vsbar.config(command=canvas.yview)
650
+ canvas.config(yscrollcommand=vsbar.set)
651
+ vsbar.grid(row=0, column=1, sticky=N+S)
652
+
653
+ hsbar = AutoScrollbar(self.toplevel, orient=HORIZONTAL)
654
+ hsbar.config(command=canvas.xview)
655
+ canvas.config(xscrollcommand=hsbar.set)
656
+ hsbar.grid(row=1, column=0, sticky=E+W)
657
+ canvas.bind('<4>', lambda event : canvas.yview('scroll', -1, 'units'))
658
+ canvas.bind('<5>', lambda event : canvas.yview('scroll', 1, 'units'))
659
+
660
+ self.toplevel.grid_rowconfigure(0, weight=1)
661
+ self.toplevel.grid_columnconfigure(0, weight=1)
662
+
663
+ for fname in sys.argv[1:]:
664
+ try:
665
+ f = h5py.File(fname, "r")
666
+ except IOError:
667
+ print("Unable to read file: ", fname)
668
+ continue
669
+ top_item.add_child(HDF5Item(f.id, file=f, is_file=True))
670
+ nloaded += 1
671
+
672
+ if nloaded == 0:
673
+ raise IOError("No files loaded!")
674
+
675
+ bframe = Frame(self.toplevel, relief="raised", borderwidth=2)
676
+ bframe.grid(row=2, column=0, sticky=N+S+E+W)
677
+ b1 = Button(bframe, text="Open selected datasets as table",
678
+ command=self.open_datasets)
679
+ b1.grid(row=0, column=0)
680
+
681
+ b2 = Button(bframe, text="Deselect all",
682
+ command=self.deselect_all)
683
+ b2.grid(row=0, column=1)
684
+
685
+ self.canvas = canvas
686
+ self.top_node = top_node
687
+ self.top_item = top_item
688
+
689
+ top_node.update()
690
+ top_node.expand()
691
+
692
+ self.toplevel.protocol("WM_DELETE_WINDOW", self.destroy)
693
+
694
+ def open_datasets(self):
695
+ if len(selected_nodes.keys()) > 0:
696
+ self.windows.append(DataWindow(selected_nodes))
697
+
698
+ def deselect_all(self):
699
+ global selected_nodes
700
+ self.top_node.deselecttree()
701
+ selected_nodes = {}
702
+
703
+ def destroy(self, *args):
704
+ global windows_open
705
+ global root
706
+ self.toplevel.destroy()
707
+ windows_open -= 1
708
+ if windows_open == 0:
709
+ root.destroy()
710
+
711
+ def main():
712
+
713
+ global root
714
+ global windows_open
715
+
716
+ root = Tk()
717
+ root.withdraw()
718
+
719
+ windows_open = 0
720
+
721
+ filenames = sys.argv[1:]
722
+ mainwin = BrowserWindow(filenames)
723
+
724
+ root.mainloop()
@@ -0,0 +1,18 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "h5tab"
7
+ version = "1.0.0"
8
+ authors = [
9
+ { name="John Helly", email="j.c.helly@durham.ac.uk" },
10
+ ]
11
+ description = "Simple HDF5 file viewer."
12
+ requires-python = ">=3.7"
13
+ dependencies = [
14
+ "h5py",
15
+ ]
16
+
17
+ [project.scripts]
18
+ h5tab = "h5tab:main"
h5tab-1.0.0/setup.cfg ADDED
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+