mapdata 1.10.1__tar.gz → 2.0.2__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.
- {mapdata-1.10.1/mapdata.egg-info → mapdata-2.0.2}/PKG-INFO +8 -1
- {mapdata-1.10.1 → mapdata-2.0.2}/README.md +6 -0
- {mapdata-1.10.1 → mapdata-2.0.2}/mapdata/mapdata.py +565 -87
- {mapdata-1.10.1 → mapdata-2.0.2/mapdata.egg-info}/PKG-INFO +8 -1
- {mapdata-1.10.1 → mapdata-2.0.2}/setup.py +2 -2
- {mapdata-1.10.1 → mapdata-2.0.2}/LICENSE.txt +0 -0
- {mapdata-1.10.1 → mapdata-2.0.2}/MANIFEST.in +0 -0
- {mapdata-1.10.1 → mapdata-2.0.2}/mapdata.egg-info/SOURCES.txt +0 -0
- {mapdata-1.10.1 → mapdata-2.0.2}/mapdata.egg-info/dependency_links.txt +0 -0
- {mapdata-1.10.1 → mapdata-2.0.2}/mapdata.egg-info/top_level.txt +0 -0
- {mapdata-1.10.1 → mapdata-2.0.2}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: mapdata
|
|
3
|
-
Version:
|
|
3
|
+
Version: 2.0.2
|
|
4
4
|
Summary: An interactive map and table explorer for geographic coordinates in a CSV file
|
|
5
5
|
Home-page: https://osdn.net/project/mapdata/
|
|
6
6
|
Author: Dreas Nielsen
|
|
@@ -27,6 +27,7 @@ Requires: pyproj
|
|
|
27
27
|
Requires: odfpy
|
|
28
28
|
Requires: openpyxl
|
|
29
29
|
Requires: xlrd
|
|
30
|
+
Requires: matplotlib
|
|
30
31
|
Requires-Python: >=3.8
|
|
31
32
|
Description-Content-Type: text/markdown
|
|
32
33
|
License-File: LICENSE.txt
|
|
@@ -63,6 +64,12 @@ export the map to an image file, and quit.
|
|
|
63
64
|
|
|
64
65
|
Selected rows in the data table can be exported to a CSV or spreadsheet file.
|
|
65
66
|
|
|
67
|
+
Data can also be displayed in several different types of plots: box plots, scatter
|
|
68
|
+
plots, line charts, and counts of categorical and quantitative variables. Plots
|
|
69
|
+
can use either all data or only data values that are selected in the map and
|
|
70
|
+
table. Plots have a live connection to the data table, so when selections are
|
|
71
|
+
changed the plots are automatically updated.
|
|
72
|
+
|
|
66
73
|
|
|
67
74
|
Complete documentation is at [https://mapdata.osdn.io](https://mapdata.osdn.io).
|
|
68
75
|
|
|
@@ -30,6 +30,12 @@ export the map to an image file, and quit.
|
|
|
30
30
|
|
|
31
31
|
Selected rows in the data table can be exported to a CSV or spreadsheet file.
|
|
32
32
|
|
|
33
|
+
Data can also be displayed in several different types of plots: box plots, scatter
|
|
34
|
+
plots, line charts, and counts of categorical and quantitative variables. Plots
|
|
35
|
+
can use either all data or only data values that are selected in the map and
|
|
36
|
+
table. Plots have a live connection to the data table, so when selections are
|
|
37
|
+
changed the plots are automatically updated.
|
|
38
|
+
|
|
33
39
|
|
|
34
40
|
Complete documentation is at [https://mapdata.osdn.io](https://mapdata.osdn.io).
|
|
35
41
|
|
|
@@ -3,7 +3,9 @@
|
|
|
3
3
|
# mapdata.py
|
|
4
4
|
#
|
|
5
5
|
# PURPOSE
|
|
6
|
-
#
|
|
6
|
+
# Display a simple interactive map of data points, allowing points to
|
|
7
|
+
# be highlighted by clicking on the map or table or by querying,
|
|
8
|
+
# and allowing some simple data plots.
|
|
7
9
|
#
|
|
8
10
|
# COPYRIGHT AND LICENSE
|
|
9
11
|
# Copyright (c) 2023, R. Dreas Nielsen
|
|
@@ -17,72 +19,13 @@
|
|
|
17
19
|
# GNU General Public License for more details.
|
|
18
20
|
# The GNU General Public License is available at <http://www.gnu.org/licenses/>
|
|
19
21
|
#
|
|
20
|
-
# NOTES
|
|
21
|
-
# 1.
|
|
22
|
-
#
|
|
23
22
|
# AUTHOR
|
|
24
23
|
# Dreas Nielsen (RDN)
|
|
25
24
|
#
|
|
26
|
-
# HISTORY
|
|
27
|
-
# Date Remarks
|
|
28
|
-
# ---------- -----------------------------------------------------
|
|
29
|
-
# 2023-03-27 Created. RDN.
|
|
30
|
-
# 2023-04-16 Added CsvFile() and treeview_table(), and began
|
|
31
|
-
# MapUI(). RDN.
|
|
32
|
-
# 2023-04-24 Completed MapUI() and tested to success.
|
|
33
|
-
# 2023-04-25 Added map control buttons to zoom, focus, and un-select. RDN.
|
|
34
|
-
# 2023-04-27 Adapted to missing coordinates in the data table and
|
|
35
|
-
# to avoid an exception when table columns are resized but
|
|
36
|
-
# no row selected. RDN.
|
|
37
|
-
# 2023-04-29 Allowed single or multiple selection, and zoom to
|
|
38
|
-
# selected markers when multiple are selected.
|
|
39
|
-
# Added menu items to save selected table rows and to save
|
|
40
|
-
# the map as a Postscript document. Added menu item to
|
|
41
|
-
# change the marker used. RDN.
|
|
42
|
-
# 2023-04-30 Reduced the set of colors that can be selected for the marker. RDN.
|
|
43
|
-
# 2023-05-01 Added a Quit option to the file menu and removed the bottom
|
|
44
|
-
# button frame. Implemented reading of default and custom
|
|
45
|
-
# configuration files, and import of an .xbm symbol file.
|
|
46
|
-
# Enabled operation as a full GUI application--note that this
|
|
47
|
-
# requires a change to the command-line filename from an argument
|
|
48
|
-
# to an option. When the application is started without a command
|
|
49
|
-
# line specification, PIL emits error messages to stderr but
|
|
50
|
-
# ordinarily this is not seen and the program's operation is
|
|
51
|
-
# unaffected. RDN.
|
|
52
|
-
# 2023-05-02 Changed the default label color and locations, added global
|
|
53
|
-
# settings for the location symbol, color, font, font size,
|
|
54
|
-
# font color, and font location, and allowed all of those to
|
|
55
|
-
# be changed via the configuration file. RDN.
|
|
56
|
-
# 2023-05-08 Added conversion of other coordinate reference systems to 4326. RDN.
|
|
57
|
-
# 2023-05-09 Added ability to switch CRSs for the same data. RDN.
|
|
58
|
-
# 2023-05-11 Fixed map controls when resizing. Added command-line arguments
|
|
59
|
-
# to export the map and quit. RDN.
|
|
60
|
-
# 2023-05-12 Added export of configuration settings. Cleaned up help
|
|
61
|
-
# dialogs. RDN.
|
|
62
|
-
# 2023-05-14 Put the map and table in a PanedWindow. RDN.
|
|
63
|
-
# 2023-05-16 Fixed label wrapping in Windows on the CSV open dialog. Corrected
|
|
64
|
-
# binding of Return and Escape keys to dialog button actions. Adujsted
|
|
65
|
-
# button position in MsgDialog. RDN.
|
|
66
|
-
# 2023-05-24 Modified dialogs, added 'Help' buttons. RDN.
|
|
67
|
-
# 2023-05-30 Partially implemented data query dialog--functional, but no sidebar
|
|
68
|
-
# listing column values. RDN.
|
|
69
|
-
# 2023-05-31 Added sidebar listing column values to the data query dialog.
|
|
70
|
-
# Double-clicking on a listed column value adds it to the end of the
|
|
71
|
-
# SQL entry widget. RDN.
|
|
72
|
-
# 2023-06-01 Modified to select all rows in the table with the same lat/lon as
|
|
73
|
-
# a clicked map location, even when multiselect=0. RDN.
|
|
74
|
-
# 2023-06-02 Added 'Invert' menu option on new 'Selections' menu. RDN.
|
|
75
|
-
# 2023-06-03 Centered dialogs, made dialogs un-resizable, and added menu
|
|
76
|
-
# hotkeys. RDN.
|
|
77
|
-
# 2023-06-04 Added menu option to import data from spreadsheets. RDN.
|
|
78
|
-
# 2023-06-08 Added menu option to import data from databases. RDN.
|
|
79
|
-
# 2023-06-09 Added DuckDB as a data source and modifed startup to
|
|
80
|
-
# allow spreadsheets to be selected on the command line. RDN.
|
|
81
|
-
# 2023-06-12 Corrected Help links for spreadsheet and database dialogs. RDN.
|
|
82
25
|
# ==================================================================
|
|
83
26
|
|
|
84
|
-
version = "
|
|
85
|
-
vdate = "2023-06-
|
|
27
|
+
version = "2.0.2"
|
|
28
|
+
vdate = "2023-06-15"
|
|
86
29
|
|
|
87
30
|
copyright = "2023"
|
|
88
31
|
|
|
@@ -96,8 +39,13 @@ from configparser import ConfigParser
|
|
|
96
39
|
import csv
|
|
97
40
|
import re
|
|
98
41
|
import datetime
|
|
42
|
+
import dateutil.parser as dateparser
|
|
99
43
|
import time
|
|
44
|
+
import math
|
|
45
|
+
import collections
|
|
100
46
|
import webbrowser
|
|
47
|
+
import threading
|
|
48
|
+
import queue
|
|
101
49
|
import sqlite3
|
|
102
50
|
import tempfile
|
|
103
51
|
import tkinter as tk
|
|
@@ -113,6 +61,10 @@ import odf.number
|
|
|
113
61
|
import odf.style
|
|
114
62
|
import xlrd
|
|
115
63
|
import openpyxl
|
|
64
|
+
import matplotlib
|
|
65
|
+
matplotlib.use('TkAgg')
|
|
66
|
+
from matplotlib.figure import Figure
|
|
67
|
+
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg, NavigationToolbar2Tk
|
|
116
68
|
|
|
117
69
|
|
|
118
70
|
# Default name of configuration file. Files with other names may be read.
|
|
@@ -558,13 +510,105 @@ def db_colnames(tbl_hdrs):
|
|
|
558
510
|
colnames.append(dquote(hdr))
|
|
559
511
|
return colnames
|
|
560
512
|
|
|
513
|
+
def isint(v):
|
|
514
|
+
if v is None or (type(v) is str and v.strip() == ''):
|
|
515
|
+
return None
|
|
516
|
+
if type(v) == int:
|
|
517
|
+
return True
|
|
518
|
+
if type(v) == float:
|
|
519
|
+
return False
|
|
520
|
+
try:
|
|
521
|
+
int(v)
|
|
522
|
+
return True
|
|
523
|
+
except ValueError:
|
|
524
|
+
return False
|
|
525
|
+
|
|
561
526
|
def isfloat(v):
|
|
527
|
+
if v is None or (type(v) is str and v.strip() == ''):
|
|
528
|
+
return None
|
|
562
529
|
try:
|
|
563
530
|
float(v)
|
|
564
531
|
return True
|
|
565
532
|
except ValueError:
|
|
566
533
|
return False
|
|
567
534
|
|
|
535
|
+
def dt_type(v):
|
|
536
|
+
# Type of date/time: timestamp, date, time, or None
|
|
537
|
+
if v is None or (type(v) is str and v.strip() == ''):
|
|
538
|
+
return None
|
|
539
|
+
t = None
|
|
540
|
+
try:
|
|
541
|
+
t = dateparser.isoparse(v)
|
|
542
|
+
except:
|
|
543
|
+
try:
|
|
544
|
+
t = dateparser.parse(v, ignoretz=True)
|
|
545
|
+
except:
|
|
546
|
+
pass
|
|
547
|
+
if t is None:
|
|
548
|
+
return None
|
|
549
|
+
if t.time() == datetime.time(0,0):
|
|
550
|
+
return "date"
|
|
551
|
+
else:
|
|
552
|
+
return "timestamp"
|
|
553
|
+
|
|
554
|
+
def data_type(v):
|
|
555
|
+
# Characterizes the value v as one of a simple set of data types.
|
|
556
|
+
# Returns "timestamp", "date", "int", "float", or "string"
|
|
557
|
+
if v is None or (type(v) is str and v == ''):
|
|
558
|
+
return None
|
|
559
|
+
if isint(v):
|
|
560
|
+
return "int"
|
|
561
|
+
if isfloat(v):
|
|
562
|
+
return "float"
|
|
563
|
+
dt = dt_type(v)
|
|
564
|
+
if dt is not None:
|
|
565
|
+
return dt
|
|
566
|
+
return "string"
|
|
567
|
+
|
|
568
|
+
def data_type_cast_fn(data_type_str):
|
|
569
|
+
if data_type_str == "string":
|
|
570
|
+
return str
|
|
571
|
+
elif data_type_str == "date":
|
|
572
|
+
return datetime.date
|
|
573
|
+
elif data_type_str == "timestamp":
|
|
574
|
+
return datetime.timestamp
|
|
575
|
+
elif data_type_str == "int":
|
|
576
|
+
return int
|
|
577
|
+
elif data_type_str == "float":
|
|
578
|
+
return float
|
|
579
|
+
|
|
580
|
+
def common_data_type(values):
|
|
581
|
+
# Returns a data type common to all the values in the list.
|
|
582
|
+
# This is "string" unlees all types are the same.
|
|
583
|
+
# Null (None) values are ignored. If all values are null, return "string".
|
|
584
|
+
val2 = [v for v in values if v is not None and not (type(v) is str and v.strip() == '')]
|
|
585
|
+
if len(val2) == 0:
|
|
586
|
+
return "string"
|
|
587
|
+
else:
|
|
588
|
+
types = [data_type(v) for v in val2]
|
|
589
|
+
if len(set(types)) == 1:
|
|
590
|
+
return types[0]
|
|
591
|
+
else:
|
|
592
|
+
return "string"
|
|
593
|
+
|
|
594
|
+
def set_data_types(headers, rows, data_type_list):
|
|
595
|
+
return_queue = queue.Queue()
|
|
596
|
+
def eval_columns(headers, rows, return_queue):
|
|
597
|
+
coltypes = []
|
|
598
|
+
for i, colname in enumerate(headers):
|
|
599
|
+
datavals = [row[i] for row in rows]
|
|
600
|
+
non_null = [d for d in datavals if d is not None and not (type(d) is str and d.strip() == '')]
|
|
601
|
+
nullcount = len(datavals) - len(non_null)
|
|
602
|
+
uniquevals = len(set(non_null))
|
|
603
|
+
coltypes.append((colname, common_data_type(datavals), nullcount, uniquevals))
|
|
604
|
+
return_queue.put(coltypes)
|
|
605
|
+
t = threading.Thread(target=eval_columns, args=(headers, rows, return_queue))
|
|
606
|
+
t.start()
|
|
607
|
+
return (t, return_queue)
|
|
608
|
+
|
|
609
|
+
# Translations to SQLite type affinity names
|
|
610
|
+
sqlite_type_x = {'int': 'INTEGER', 'float': 'REAL', 'string': 'TEXT', 'timestamp': 'TEXT', 'date': 'TEXT'}
|
|
611
|
+
|
|
568
612
|
def center_window(win):
|
|
569
613
|
win.update_idletasks()
|
|
570
614
|
m = re.match(r"(\d+)x(\d+)\+(-?\d+)\+(-?\d+)", win.geometry())
|
|
@@ -577,6 +621,11 @@ def center_window(win):
|
|
|
577
621
|
ypos = (sht/2) - (wht/2)
|
|
578
622
|
win.geometry("%dx%d+%d+%d" % (wwd, wht, xpos, ypos))
|
|
579
623
|
|
|
624
|
+
def raise_window(win):
|
|
625
|
+
win.attributes('-topmost', 1)
|
|
626
|
+
win.attributes('-topmost', 0)
|
|
627
|
+
|
|
628
|
+
|
|
580
629
|
|
|
581
630
|
class MapUI(object):
|
|
582
631
|
def __init__(self, src_name, message, lat_col, lon_col, crs=4326, sheet=None,
|
|
@@ -593,7 +642,6 @@ class MapUI(object):
|
|
|
593
642
|
else:
|
|
594
643
|
# src_name is a filename, either CSV or spreadsheet
|
|
595
644
|
fn, ext = os.path.splitext(src_name)
|
|
596
|
-
print("data source extension = %s" % ext)
|
|
597
645
|
if ext.lower() == ".csv":
|
|
598
646
|
try:
|
|
599
647
|
headers, rows = file_data(src_name)
|
|
@@ -637,6 +685,8 @@ class MapUI(object):
|
|
|
637
685
|
self.max_lat = None
|
|
638
686
|
self.min_lon = None
|
|
639
687
|
self.max_lon = None
|
|
688
|
+
# List of PlotDialog objects, so they can have data pushed or be deleted.
|
|
689
|
+
self.plot_list = []
|
|
640
690
|
# Database connection is set in 'add_data()'; variables are initialized here
|
|
641
691
|
self.dbtmpdir = None
|
|
642
692
|
self.dbname = None
|
|
@@ -795,8 +845,12 @@ class MapUI(object):
|
|
|
795
845
|
new_marker = self.map_widget.set_marker(lat_val, lon_val, icon=self.sel_marker_icon)
|
|
796
846
|
new_markers.append(new_marker)
|
|
797
847
|
for m in self.sel_map_markers:
|
|
798
|
-
|
|
848
|
+
m.delete()
|
|
799
849
|
self.sel_map_markers = new_markers
|
|
850
|
+
else:
|
|
851
|
+
for m in self.sel_map_markers:
|
|
852
|
+
m.delete()
|
|
853
|
+
self.update_plot_data()
|
|
800
854
|
self.set_status()
|
|
801
855
|
def set_sel_marker(self, symbol, color):
|
|
802
856
|
select_marker = tk.BitmapImage(data=icon_xbm[symbol], foreground=color)
|
|
@@ -879,7 +933,12 @@ class MapUI(object):
|
|
|
879
933
|
self.loc_map_markers.append(mkr)
|
|
880
934
|
else:
|
|
881
935
|
self.missing_latlon += 1
|
|
936
|
+
self.update_plot_data()
|
|
882
937
|
def add_data(self, rows, headers, lat_col, lon_col, label_col, symbol_col, color_col):
|
|
938
|
+
# Launch separate process to determine data types
|
|
939
|
+
self.data_types = []
|
|
940
|
+
(dt_thread, dt_queue) = set_data_types(headers, rows, self.data_types)
|
|
941
|
+
# Re-set all data-specific variables and widgets
|
|
883
942
|
self.lat_col = lat_col
|
|
884
943
|
self.lon_col = lon_col
|
|
885
944
|
self.src_lat_col = lat_col
|
|
@@ -969,6 +1028,9 @@ class MapUI(object):
|
|
|
969
1028
|
cur.close()
|
|
970
1029
|
# Initial value for user-entered WHERE clause
|
|
971
1030
|
self.whereclause = ""
|
|
1031
|
+
# Save data types for use in column selection for plotting
|
|
1032
|
+
dt_thread.join()
|
|
1033
|
+
self.data_types = dt_queue.get(block=True)
|
|
972
1034
|
# Return frame and data table
|
|
973
1035
|
return tframe, tdata
|
|
974
1036
|
def remove_data(self):
|
|
@@ -977,6 +1039,7 @@ class MapUI(object):
|
|
|
977
1039
|
while len(self.loc_map_markers) > 0:
|
|
978
1040
|
self.loc_map_markers.pop().delete()
|
|
979
1041
|
self.map_widget.delete_all_marker()
|
|
1042
|
+
self.close_all_plots()
|
|
980
1043
|
self.tableframe.destroy()
|
|
981
1044
|
self.tbl.destroy()
|
|
982
1045
|
def set_tbl_selectmode(self):
|
|
@@ -1023,15 +1086,6 @@ class MapUI(object):
|
|
|
1023
1086
|
self.replace_data(rows, headers, lat_col, lon_col, id_col, sym_col, col_col)
|
|
1024
1087
|
if desc is not None and desc != '':
|
|
1025
1088
|
self.msg_label['text'] = desc
|
|
1026
|
-
#def first_data_file(self):
|
|
1027
|
-
# dfd = DataFileDialog()
|
|
1028
|
-
# fn, id_col, lat_col, lon_col, crs, sym_col, col_col, title = dfd.get_datafile()
|
|
1029
|
-
# if fn is None or fn == '':
|
|
1030
|
-
# self.cancel()
|
|
1031
|
-
# else:
|
|
1032
|
-
# self.crs = crs
|
|
1033
|
-
# headers, rows = file_data(fn)
|
|
1034
|
-
# self.tableframe, self.tbl = self.add_data(rows, headers, lat_col, lon_col, id_col, sym_col, col_col)
|
|
1035
1089
|
def zoom_full(self):
|
|
1036
1090
|
self.map_widget.fit_bounding_box((self.max_lat, self.min_lon), (self.min_lat, self.max_lon))
|
|
1037
1091
|
def zoom_selected(self):
|
|
@@ -1061,6 +1115,7 @@ class MapUI(object):
|
|
|
1061
1115
|
self.map_widget.delete(m)
|
|
1062
1116
|
self.tbl.selection_remove(*self.tbl.selection())
|
|
1063
1117
|
self.sel_map_markers = []
|
|
1118
|
+
self.update_plot_data()
|
|
1064
1119
|
self.set_status()
|
|
1065
1120
|
def change_basemap(self, *args):
|
|
1066
1121
|
new_map = self.basemap_var.get()
|
|
@@ -1091,6 +1146,7 @@ class MapUI(object):
|
|
|
1091
1146
|
new_marker = self.map_widget.set_marker(lat, lon, icon=self.sel_marker_icon)
|
|
1092
1147
|
if not new_marker in self.sel_map_markers:
|
|
1093
1148
|
self.sel_map_markers.append(new_marker)
|
|
1149
|
+
self.update_plot_data()
|
|
1094
1150
|
self.set_status()
|
|
1095
1151
|
def set_status(self):
|
|
1096
1152
|
statusmsg = " %d rows" % self.table_row_count
|
|
@@ -1101,6 +1157,45 @@ class MapUI(object):
|
|
|
1101
1157
|
if self.multiselect_var.get() == "1":
|
|
1102
1158
|
statusmsg = statusmsg + " | Ctrl-click to select multiple rows"
|
|
1103
1159
|
self.tblframe.statusbar.config(text = statusmsg)
|
|
1160
|
+
|
|
1161
|
+
def get_all_data(self, column_list):
|
|
1162
|
+
# Plotting support. Return all data for the specified columns as a list of column-oriented lists.
|
|
1163
|
+
res = []
|
|
1164
|
+
for c in column_list:
|
|
1165
|
+
i = self.headers.index(c)
|
|
1166
|
+
res.append([row[i] for row in self.rows])
|
|
1167
|
+
return res
|
|
1168
|
+
def get_sel_data(self, column_list):
|
|
1169
|
+
# Plotting support. Return data from selected rows for the specified columns, as a list of lists.
|
|
1170
|
+
res = [[] for _ in column_list]
|
|
1171
|
+
indices = [self.headers.index(c) for c in column_list]
|
|
1172
|
+
for sel_row in self.tbl.selection():
|
|
1173
|
+
datarow = self.tbl.item(sel_row)["values"]
|
|
1174
|
+
for i, index in enumerate(indices):
|
|
1175
|
+
res[i].append(datarow[index])
|
|
1176
|
+
return res
|
|
1177
|
+
def update_plot_data(self, all_data=False):
|
|
1178
|
+
for plot in self.plot_list:
|
|
1179
|
+
if all_data or plot.sel_only_var.get() == "1":
|
|
1180
|
+
plot.q_redraw()
|
|
1181
|
+
def remove_plot(self, plot_obj):
|
|
1182
|
+
# For use by the plot 'do_close()' method.
|
|
1183
|
+
try:
|
|
1184
|
+
self.plot_list.remove(plot_obj)
|
|
1185
|
+
except:
|
|
1186
|
+
pass
|
|
1187
|
+
def close_plot(self, plot_obj):
|
|
1188
|
+
try:
|
|
1189
|
+
plot_obj.do_close()
|
|
1190
|
+
self.remove_plot()
|
|
1191
|
+
except:
|
|
1192
|
+
pass
|
|
1193
|
+
def close_all_plots(self):
|
|
1194
|
+
while len(self.plot_list) > 0:
|
|
1195
|
+
self.plot_list[0].do_close()
|
|
1196
|
+
# The callback will remove the plot.
|
|
1197
|
+
self.plot_list = []
|
|
1198
|
+
|
|
1104
1199
|
def change_crs(self):
|
|
1105
1200
|
crsdlg = NewCrsDialog(self.crs)
|
|
1106
1201
|
new_crs = crsdlg.get_crs()
|
|
@@ -1187,11 +1282,13 @@ class MapUI(object):
|
|
|
1187
1282
|
tbl_menu = tk.Menu(mnu, tearoff=0)
|
|
1188
1283
|
map_menu = tk.Menu(mnu, tearoff=0)
|
|
1189
1284
|
sel_menu = tk.Menu(mnu, tearoff=0)
|
|
1285
|
+
plot_menu = tk.Menu(mnu, tearoff=0)
|
|
1190
1286
|
help_menu = tk.Menu(mnu, tearoff=0)
|
|
1191
1287
|
mnu.add_cascade(label="File", menu=file_menu, underline=0)
|
|
1192
1288
|
mnu.add_cascade(label="Table", menu=tbl_menu, underline=0)
|
|
1193
1289
|
mnu.add_cascade(label="Map", menu=map_menu, underline=0)
|
|
1194
1290
|
mnu.add_cascade(label="Selections", menu=sel_menu, underline=0)
|
|
1291
|
+
mnu.add_cascade(label="Plot", menu=plot_menu, underline=0)
|
|
1195
1292
|
mnu.add_cascade(label="Help", menu=help_menu, underline=0)
|
|
1196
1293
|
def save_table():
|
|
1197
1294
|
if table_object.selection():
|
|
@@ -1309,6 +1406,11 @@ class MapUI(object):
|
|
|
1309
1406
|
f.write("label_size=%s\n" % label_size)
|
|
1310
1407
|
f.write("label_bold=%s\n" % ('No' if not label_bold else 'Yes'))
|
|
1311
1408
|
f.write("label_position=%s\n" % label_position)
|
|
1409
|
+
def show_data_types():
|
|
1410
|
+
dlg = MsgDialog("Data Types", "Data types, data completeness, and number of unique\nnon-missing values for columns of the data table:")
|
|
1411
|
+
tframe, tdata = treeview_table(dlg.content_frame, self.data_types, ["Column", "Type", "Missing", "Unique"], "browse")
|
|
1412
|
+
tframe.grid(row=0, column=0, sticky=tk.NSEW)
|
|
1413
|
+
dlg.show()
|
|
1312
1414
|
def run_query():
|
|
1313
1415
|
dlg = QueryDialog(self.headers, self.db, self.whereclause)
|
|
1314
1416
|
whereclause = dlg.get_where()
|
|
@@ -1343,6 +1445,11 @@ class MapUI(object):
|
|
|
1343
1445
|
self.tbl.selection_set(tuple(new_selections))
|
|
1344
1446
|
self.mark_map(None)
|
|
1345
1447
|
self.set_status()
|
|
1448
|
+
def new_plot():
|
|
1449
|
+
dlg = PlotDialog(self, self.data_types)
|
|
1450
|
+
self.plot_list.append(dlg)
|
|
1451
|
+
dlg.show
|
|
1452
|
+
|
|
1346
1453
|
def online_help():
|
|
1347
1454
|
webbrowser.open("https://mapdata.osdn.io", new=2, autoraise=True)
|
|
1348
1455
|
def show_config_files():
|
|
@@ -1376,6 +1483,7 @@ Copyright %s, R Dreas Nielsen
|
|
|
1376
1483
|
file_menu.add_command(label="Quit", command = self.cancel, underline=0)
|
|
1377
1484
|
tbl_menu.add_command(label="Un-select all", command = self.unselect_map, underline=0)
|
|
1378
1485
|
tbl_menu.add_command(label="Export selected", command = save_table, underline=1)
|
|
1486
|
+
tbl_menu.add_command(label="Data types", command = show_data_types, underline=5)
|
|
1379
1487
|
map_menu.add_command(label="Change marker", command = change_marker, underline=7)
|
|
1380
1488
|
map_menu.add_command(label="Zoom selected", command = self.zoom_selected, underline=5)
|
|
1381
1489
|
map_menu.add_command(label="Zoom full", command = self.zoom_full, underline=5)
|
|
@@ -1386,6 +1494,8 @@ Copyright %s, R Dreas Nielsen
|
|
|
1386
1494
|
sel_menu.add_command(label="Invert", command = invert_selections, underline=0)
|
|
1387
1495
|
sel_menu.add_command(label="Un-select all", command = self.unselect_map, underline=0)
|
|
1388
1496
|
sel_menu.add_command(label="Set by query", command = run_query, underline=7)
|
|
1497
|
+
plot_menu.add_command(label="New", command = new_plot, underline=0)
|
|
1498
|
+
plot_menu.add_command(label="Close all", command = self.close_all_plots, underline=0)
|
|
1389
1499
|
help_menu.add_command(label="Online help", command = online_help, underline=7)
|
|
1390
1500
|
help_menu.add_command(label="Config files", command = show_config_files, underline=0)
|
|
1391
1501
|
help_menu.add_command(label="About", command = show_about, underline=0)
|
|
@@ -1749,10 +1859,11 @@ class ImportSpreadsheetDialog(object):
|
|
|
1749
1859
|
def w1_next():
|
|
1750
1860
|
# Open spreadsheet, get sheet names
|
|
1751
1861
|
fn, ext = os.path.splitext(self.fn_var.get())
|
|
1862
|
+
ext = ext.lower()
|
|
1752
1863
|
try:
|
|
1753
|
-
if ext
|
|
1864
|
+
if ext == '.ods':
|
|
1754
1865
|
sso = OdsFile()
|
|
1755
|
-
elif ext
|
|
1866
|
+
elif ext == '.xlsx':
|
|
1756
1867
|
sso = XlsxFile()
|
|
1757
1868
|
else:
|
|
1758
1869
|
sso = XlsFile()
|
|
@@ -1762,10 +1873,17 @@ class ImportSpreadsheetDialog(object):
|
|
|
1762
1873
|
sso.open(self.fn_var.get())
|
|
1763
1874
|
self.sheet_list = sso.sheetnames()
|
|
1764
1875
|
self.sheet_sel["values"] = self.sheet_list
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1876
|
+
if ext in ('.ods', '.xlsx'):
|
|
1877
|
+
try:
|
|
1878
|
+
sso.close()
|
|
1879
|
+
except:
|
|
1880
|
+
pass
|
|
1881
|
+
else:
|
|
1882
|
+
try:
|
|
1883
|
+
sso.release_resources()
|
|
1884
|
+
del sso
|
|
1885
|
+
except:
|
|
1886
|
+
pass
|
|
1769
1887
|
wiz2_frame.lift()
|
|
1770
1888
|
|
|
1771
1889
|
w1btn_frame = tk.Frame(wiz1_frame, borderwidth=3, relief=tk.RIDGE)
|
|
@@ -2525,9 +2643,366 @@ class QueryDialog(object):
|
|
|
2525
2643
|
return None
|
|
2526
2644
|
|
|
2527
2645
|
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
|
|
2646
|
+
class PlotDialog(object):
|
|
2647
|
+
def __init__(self, parent, column_specs):
|
|
2648
|
+
self.parent = parent
|
|
2649
|
+
self.column_specs = column_specs
|
|
2650
|
+
self.dataset = None
|
|
2651
|
+
self.data_labels = None
|
|
2652
|
+
self.plot_data = None
|
|
2653
|
+
self.plot_data_labels = None
|
|
2654
|
+
self.dlg = tk.Toplevel()
|
|
2655
|
+
self.dlg.title("Plot")
|
|
2656
|
+
self.dlg.columnconfigure(0, weight=1)
|
|
2657
|
+
|
|
2658
|
+
# Message
|
|
2659
|
+
prompt_frame = tk.Frame(self.dlg)
|
|
2660
|
+
prompt_frame.grid(row=0, column=0, sticky=tk.NSEW, pady=(3,3))
|
|
2661
|
+
prompt_frame.columnconfigure(0, weight=1)
|
|
2662
|
+
msg_lbl = ttk.Label(prompt_frame, width=70, text="Select the type of plot, columns for X and Y data, and whether to show all data or only selected data.")
|
|
2663
|
+
msg_lbl.grid(row=0, column=0, sticky=tk.W, padx=(6,6), pady=(3,3))
|
|
2664
|
+
def wrap_msg(event):
|
|
2665
|
+
msg_lbl.configure(wraplength=event.width - 5)
|
|
2666
|
+
msg_lbl.bind("<Configure>", wrap_msg)
|
|
2667
|
+
|
|
2668
|
+
# Controls
|
|
2669
|
+
ctrl_frame = tk.Frame(self.dlg)
|
|
2670
|
+
ctrl_frame.grid(row=1, column=0, sticky=tk.N+tk.EW)
|
|
2671
|
+
|
|
2672
|
+
self.type_var = tk.StringVar(ctrl_frame, "")
|
|
2673
|
+
type_lbl = ttk.Label(ctrl_frame, text="Plot type:")
|
|
2674
|
+
type_lbl.grid(row=0, column=0, sticky=tk.E, padx=(6,3), pady=(3,3))
|
|
2675
|
+
self.type_sel = ttk.Combobox(ctrl_frame, state="readonly", textvariable=self.type_var, width=20,
|
|
2676
|
+
values=["Box plot", "Category counts", "Line plot", "Scatter plot", "Value counts", "Y range plot"])
|
|
2677
|
+
self.type_sel.grid(row=0, column=1, sticky=tk.W, padx=(3,6), pady=(3,3))
|
|
2678
|
+
self.type_sel.bind("<<ComboboxSelected>>", self.set_xy)
|
|
2679
|
+
|
|
2680
|
+
self.sel_only_var = tk.StringVar(ctrl_frame, "0")
|
|
2681
|
+
self.sel_only_ck = ttk.Checkbutton(ctrl_frame, text="Selected data only", command=self.q_redraw, variable=self.sel_only_var,
|
|
2682
|
+
onvalue="1", offvalue="0")
|
|
2683
|
+
self.sel_only_ck.grid(row=1, column=0, columnspan=2, sticky=tk.W, padx=(6,6), pady=(3,3))
|
|
2684
|
+
|
|
2685
|
+
self.x_var = tk.StringVar(ctrl_frame, "")
|
|
2686
|
+
x_lbl = ttk.Label(ctrl_frame, text="X column:")
|
|
2687
|
+
x_lbl.grid(row=0, column=2, sticky=tk.E, padx=(6,3), pady=(3,3))
|
|
2688
|
+
self.x_sel = ttk.Combobox(ctrl_frame, state="disabled", textvariable=self.x_var, width=20)
|
|
2689
|
+
self.x_sel.grid(row=0, column=3, sticky=tk.W, padx=(3,6), pady=(3,3))
|
|
2690
|
+
self.x_sel.bind("<<ComboboxSelected>>", self.q_redraw)
|
|
2691
|
+
|
|
2692
|
+
self.y_var = tk.StringVar(ctrl_frame, "")
|
|
2693
|
+
y_lbl = ttk.Label(ctrl_frame, text="Y column:")
|
|
2694
|
+
y_lbl.grid(row=1, column=2, sticky=tk.E, padx=(6,3), pady=(3,3))
|
|
2695
|
+
self.y_sel = ttk.Combobox(ctrl_frame, state="disabled", textvariable=self.y_var, width=20)
|
|
2696
|
+
self.y_sel.grid(row=1, column=3, sticky=tk.W, padx=(3,6), pady=(3,3))
|
|
2697
|
+
self.y_sel.bind("<<ComboboxSelected>>", self.q_redraw)
|
|
2698
|
+
|
|
2699
|
+
self.xlog_var = tk.StringVar(ctrl_frame, "0")
|
|
2700
|
+
self.xlog_ck = ttk.Checkbutton(ctrl_frame, text="Log X", state="disabled", command=self.q_redraw, variable=self.xlog_var,
|
|
2701
|
+
onvalue="1", offvalue="0")
|
|
2702
|
+
self.xlog_ck.grid(row=0, column=4, sticky=tk.W, padx=(6,6), pady=(3,3))
|
|
2703
|
+
|
|
2704
|
+
self.ylog_var = tk.StringVar(ctrl_frame, "0")
|
|
2705
|
+
self.ylog_ck = ttk.Checkbutton(ctrl_frame, text="Log Y", state="disabled", command=self.q_redraw, variable=self.ylog_var,
|
|
2706
|
+
onvalue="1", offvalue="0")
|
|
2707
|
+
self.ylog_ck.grid(row=1, column=4, sticky=tk.W, padx=(6,6), pady=(3,3))
|
|
2708
|
+
|
|
2709
|
+
# Plot
|
|
2710
|
+
self.content_frame = tk.Frame(self.dlg, borderwidth=3, relief=tk.RIDGE)
|
|
2711
|
+
self.content_frame.grid(row=2, column=0, sticky=tk.NSEW)
|
|
2712
|
+
self.dlg.rowconfigure(2, weight=1)
|
|
2713
|
+
self.dlg.columnconfigure(0, weight=1)
|
|
2714
|
+
self.content_frame.rowconfigure(0, weight=1)
|
|
2715
|
+
self.content_frame.columnconfigure(0, weight=1)
|
|
2716
|
+
self.plotfig = Figure(dpi=100)
|
|
2717
|
+
self.plotfig.set_figheight(5)
|
|
2718
|
+
self.plotfig_canvas = FigureCanvasTkAgg(self.plotfig, self.content_frame)
|
|
2719
|
+
self.plot_nav = NavigationToolbar2Tk(self.plotfig_canvas, self.content_frame)
|
|
2720
|
+
self.plot_axes = self.plotfig.add_subplot(111)
|
|
2721
|
+
self.plotfig_canvas.get_tk_widget().pack(side=tk.TOP, fill=tk.BOTH, expand=1)
|
|
2722
|
+
self.plot_nav.update()
|
|
2723
|
+
|
|
2724
|
+
# Buttons
|
|
2725
|
+
btn_frame = tk.Frame(self.dlg, borderwidth=3, relief=tk.RIDGE)
|
|
2726
|
+
btn_frame.columnconfigure(0, weight=1)
|
|
2727
|
+
btn_frame.grid(row=3, column=0, sticky=tk.EW, pady=(3,3))
|
|
2728
|
+
btn_frame.columnconfigure(0, weight=0)
|
|
2729
|
+
btn_frame.columnconfigure(1, weight=0)
|
|
2730
|
+
btn_frame.columnconfigure(2, weight=0)
|
|
2731
|
+
btn_frame.columnconfigure(3, weight=1)
|
|
2732
|
+
self.canceled = False
|
|
2733
|
+
self.help_btn = ttk.Button(btn_frame, text="Help", command=self.do_help)
|
|
2734
|
+
self.help_btn.grid(row=0, column=0, sticky=tk.W, padx=(6,3))
|
|
2735
|
+
self.data_btn = ttk.Button(btn_frame, text="Source Data", state="disabled", command=self.show_data)
|
|
2736
|
+
self.data_btn.grid(row=0, column=1, sticky=tk.W, padx=(3,3))
|
|
2737
|
+
self.plot_data_btn = ttk.Button(btn_frame, text="Plot Data", state="disabled", command=self.show_plot_data)
|
|
2738
|
+
self.plot_data_btn.grid(row=0, column=2, sticky=tk.W, padx=(3,6))
|
|
2739
|
+
close_btn = ttk.Button(btn_frame, text="Close", command=self.do_close)
|
|
2740
|
+
close_btn.grid(row=0, column=3, sticky=tk.E, padx=(6,6))
|
|
2741
|
+
self.dlg.bind("<Escape>", self.do_close)
|
|
2742
|
+
center_window(self.dlg)
|
|
2743
|
+
raise_window(self.dlg)
|
|
2744
|
+
|
|
2745
|
+
def do_help(self):
|
|
2746
|
+
webbrowser.open("https://mapdata.osdn.io/dialogs.html#plot-dialog", new=2, autoraise=True)
|
|
2747
|
+
|
|
2748
|
+
def show_data(self):
|
|
2749
|
+
# Show data that have been collected for plotting, but not summarized as needed for a particular plot type.
|
|
2750
|
+
if self.dataset is not None:
|
|
2751
|
+
dlg = MsgDialog("Source Data", "Original data:")
|
|
2752
|
+
variables = len(self.dataset)
|
|
2753
|
+
rowwise_data = []
|
|
2754
|
+
for i in range(len(self.dataset[0])):
|
|
2755
|
+
row = []
|
|
2756
|
+
for j in range(variables):
|
|
2757
|
+
row.append(self.dataset[j][i])
|
|
2758
|
+
rowwise_data.append(row)
|
|
2759
|
+
tframe, tdata = treeview_table(dlg.content_frame, rowwise_data, self.data_labels)
|
|
2760
|
+
tframe.grid(row=0, column=0, sticky=tk.NSEW)
|
|
2761
|
+
dlg.show()
|
|
2762
|
+
|
|
2763
|
+
def show_plot_data(self):
|
|
2764
|
+
# Show data that have been collected for plotting, but not summarized as needed for a particular plot type.
|
|
2765
|
+
if self.plot_data is not None:
|
|
2766
|
+
dlg = MsgDialog("Data for Plotting", "Data to be plotted:")
|
|
2767
|
+
variables = len(self.plot_data)
|
|
2768
|
+
rowwise_data = []
|
|
2769
|
+
max_data_len = max([len(self.plot_data[i]) for i in range(variables)])
|
|
2770
|
+
for i in range(max_data_len):
|
|
2771
|
+
row = []
|
|
2772
|
+
for j in range(variables):
|
|
2773
|
+
try:
|
|
2774
|
+
# Boxplot data are not necessarily a full matrix
|
|
2775
|
+
row.append(self.plot_data[j][i])
|
|
2776
|
+
except:
|
|
2777
|
+
row.append(None)
|
|
2778
|
+
rowwise_data.append(row)
|
|
2779
|
+
tframe, tdata = treeview_table(dlg.content_frame, rowwise_data, self.plot_data_labels)
|
|
2780
|
+
tframe.grid(row=0, column=0, sticky=tk.NSEW)
|
|
2781
|
+
dlg.show()
|
|
2782
|
+
|
|
2783
|
+
def set_xy(self, *args):
|
|
2784
|
+
# Enable X and Y value selection, and set values based on plot type and column types.
|
|
2785
|
+
# Conditionally (re)draw the plot.
|
|
2786
|
+
self.plotfig.clear()
|
|
2787
|
+
self.plot_axes = self.plotfig.add_subplot(111)
|
|
2788
|
+
self.plotfig_canvas.draw()
|
|
2789
|
+
self.dataset = None
|
|
2790
|
+
self.data_labels = None
|
|
2791
|
+
self.plot_data = None
|
|
2792
|
+
self.plot_data_labels = None
|
|
2793
|
+
self.data_btn["state"] = "disabled"
|
|
2794
|
+
self.plot_data_btn["state"] = "disabled"
|
|
2795
|
+
categ_columns = [c[0] for c in self.column_specs if c[1] == "string"]
|
|
2796
|
+
quant_columns = [c[0] for c in self.column_specs if c[1] != "string"]
|
|
2797
|
+
plot_type = self.type_var.get()
|
|
2798
|
+
self.x_var.set('')
|
|
2799
|
+
self.y_var.set('')
|
|
2800
|
+
if plot_type == "Category counts":
|
|
2801
|
+
self.x_sel["values"] = categ_columns
|
|
2802
|
+
self.xlog_ck["state"] = "disabled"
|
|
2803
|
+
self.x_sel["state"] = "readonly"
|
|
2804
|
+
self.y_sel["state"] = "disabled"
|
|
2805
|
+
self.ylog_ck["state"] = "disabled"
|
|
2806
|
+
elif plot_type == "Value counts":
|
|
2807
|
+
self.x_sel["values"] = quant_columns
|
|
2808
|
+
self.xlog_ck["state"] = "normal"
|
|
2809
|
+
self.x_sel["state"] = "readonly"
|
|
2810
|
+
self.y_sel["state"] = "disabled"
|
|
2811
|
+
self.ylog_ck["state"] = "disabled"
|
|
2812
|
+
elif plot_type == "Box plot":
|
|
2813
|
+
self.x_sel["values"] = categ_columns
|
|
2814
|
+
self.xlog_ck["state"] = "disabled"
|
|
2815
|
+
self.y_sel["values"] = quant_columns
|
|
2816
|
+
self.x_sel["state"] = "readonly"
|
|
2817
|
+
self.y_sel["state"] = "readonly"
|
|
2818
|
+
self.ylog_ck["state"] = "normal"
|
|
2819
|
+
else:
|
|
2820
|
+
self.x_sel["values"] = quant_columns
|
|
2821
|
+
self.xlog_ck["state"] = "normal"
|
|
2822
|
+
self.y_sel["values"] = quant_columns
|
|
2823
|
+
self.x_sel["state"] = "readonly"
|
|
2824
|
+
self.y_sel["state"] = "readonly"
|
|
2825
|
+
self.ylog_ck["state"] = "normal"
|
|
2826
|
+
|
|
2827
|
+
def q_redraw(self, *args):
|
|
2828
|
+
# Conditionally (re)draw the plot.
|
|
2829
|
+
plot_type = self.type_var.get()
|
|
2830
|
+
can_redraw = (plot_type in ("Category counts", "Value counts") and self.x_var.get() != '') \
|
|
2831
|
+
or (plot_type in ("Scatter plot", "Line plot", "Box plot", "Y range plot") and self.x_var.get() != '' and self.y_var.get() != '')
|
|
2832
|
+
if can_redraw:
|
|
2833
|
+
self.plotfig.clear()
|
|
2834
|
+
self.plot_axes = self.plotfig.add_subplot(111)
|
|
2835
|
+
self.plotfig_canvas.draw()
|
|
2836
|
+
self.get_data()
|
|
2837
|
+
if self.dataset is not None:
|
|
2838
|
+
self.redraw()
|
|
2839
|
+
|
|
2840
|
+
def get_data(self):
|
|
2841
|
+
self.data_btn["state"] = "disabled"
|
|
2842
|
+
self.plot_data_btn["state"] = "disabled"
|
|
2843
|
+
self.dataset = None
|
|
2844
|
+
plot_type = self.type_var.get()
|
|
2845
|
+
column_list = [self.x_var.get()]
|
|
2846
|
+
if self.y_var.get() != '':
|
|
2847
|
+
column_list.append(self.y_var.get())
|
|
2848
|
+
if self.sel_only_var.get() == "1":
|
|
2849
|
+
dataset = self.parent.get_sel_data(column_list)
|
|
2850
|
+
else:
|
|
2851
|
+
dataset = self.parent.get_all_data(column_list)
|
|
2852
|
+
if dataset is None or len(dataset[0]) == 0:
|
|
2853
|
+
self.dataset = None
|
|
2854
|
+
self.data_labels = None
|
|
2855
|
+
self.plot_data = None
|
|
2856
|
+
self.plot_data_labels = None
|
|
2857
|
+
self.data_btn["state"] = "disabled"
|
|
2858
|
+
self.plot_data_btn["state"] = "disabled"
|
|
2859
|
+
else:
|
|
2860
|
+
# Remove missing data
|
|
2861
|
+
column_indexes = range(len(dataset))
|
|
2862
|
+
clean_data = [[] for _ in dataset]
|
|
2863
|
+
for i in range(len(dataset[0])):
|
|
2864
|
+
ok = True
|
|
2865
|
+
for col in column_indexes:
|
|
2866
|
+
if dataset[col][i] is None or dataset[col][i] == '':
|
|
2867
|
+
ok = False
|
|
2868
|
+
if ok:
|
|
2869
|
+
for col in column_indexes:
|
|
2870
|
+
clean_data[col].append(dataset[col][i])
|
|
2871
|
+
dataset = None
|
|
2872
|
+
# Convert quantitative data types
|
|
2873
|
+
if plot_type != "Category counts":
|
|
2874
|
+
x_data_type = [cs[1] for cs in self.column_specs if cs[0] == self.x_var.get()][0]
|
|
2875
|
+
cast_fn = data_type_cast_fn(x_data_type)
|
|
2876
|
+
for i in range(len(clean_data[0])):
|
|
2877
|
+
clean_data[0][i] = cast_fn(clean_data[0][i])
|
|
2878
|
+
if self.y_sel["state"] != "disabled" and self.y_var.get() != "" and len(clean_data) > 1:
|
|
2879
|
+
y_data_type = [cs[1] for cs in self.column_specs if cs[0] == self.y_var.get()][0]
|
|
2880
|
+
cast_fn = data_type_cast_fn(y_data_type)
|
|
2881
|
+
for i in range(len(clean_data[1])):
|
|
2882
|
+
clean_data[1][i] = cast_fn(clean_data[1][i])
|
|
2883
|
+
# Log-transform data if specified.
|
|
2884
|
+
if (self.xlog_ck["state"] != "disabled" and self.xlog_var.get() == "1") or (self.ylog_ck["state"] != "disabled" and self.ylog_var.get() == "1" and len(clean_data) > 1):
|
|
2885
|
+
log_data = [[] for _ in clean_data]
|
|
2886
|
+
log_error = False
|
|
2887
|
+
if self.xlog_ck["state"] != "disabled" and self.xlog_var.get() == "1":
|
|
2888
|
+
for i in range(len(clean_data[0])):
|
|
2889
|
+
try:
|
|
2890
|
+
log_data[0].append(math.log10(clean_data[0][i]))
|
|
2891
|
+
except:
|
|
2892
|
+
log_error = True
|
|
2893
|
+
break
|
|
2894
|
+
else:
|
|
2895
|
+
log_data[0] = clean_data[0]
|
|
2896
|
+
if not log_error and self.ylog_ck["state"] != "disabled" and self.ylog_var.get() == "1" and len(clean_data) > 1:
|
|
2897
|
+
for i in range(len(clean_data[1])):
|
|
2898
|
+
try:
|
|
2899
|
+
log_data[1].append(math.log10(clean_data[1][i]))
|
|
2900
|
+
except:
|
|
2901
|
+
log_error = True
|
|
2902
|
+
break
|
|
2903
|
+
else:
|
|
2904
|
+
if len(clean_data) > 1:
|
|
2905
|
+
log_data[1] = clean_data[1]
|
|
2906
|
+
if not log_error:
|
|
2907
|
+
clean_data = log_data
|
|
2908
|
+
log_data = None
|
|
2909
|
+
self.dataset = clean_data
|
|
2910
|
+
if self.y_var.get() != '':
|
|
2911
|
+
self.data_labels = [self.x_var.get(), self.y_var.get()]
|
|
2912
|
+
else:
|
|
2913
|
+
self.data_labels = [self.x_var.get()]
|
|
2914
|
+
self.data_btn["state"] = "normal"
|
|
2915
|
+
# Summarize and sort the data as needed for each type of plot.
|
|
2916
|
+
if plot_type == "Category counts":
|
|
2917
|
+
# Count of values for each X, ordered by X
|
|
2918
|
+
counter = collections.Counter(self.dataset[0])
|
|
2919
|
+
x_vals = list(counter.keys())
|
|
2920
|
+
x_vals.sort()
|
|
2921
|
+
x_counts = [counter[k] for k in x_vals]
|
|
2922
|
+
self.plot_data = [x_vals, x_counts]
|
|
2923
|
+
self.plot_data_labels = [self.x_var.get(), "Count"]
|
|
2924
|
+
elif plot_type == "Box plot":
|
|
2925
|
+
# A list of Y values for each X value
|
|
2926
|
+
x_vals = list(set(self.dataset[0]))
|
|
2927
|
+
ds = list(zip(self.dataset[0], self.dataset[1]))
|
|
2928
|
+
plot_data = []
|
|
2929
|
+
for x in x_vals:
|
|
2930
|
+
plot_data.append([d[1] for d in ds if d[0] == x])
|
|
2931
|
+
self.plot_data = plot_data
|
|
2932
|
+
self.plot_data_labels = x_vals
|
|
2933
|
+
elif plot_type == "Y range plot":
|
|
2934
|
+
# Min and max Y for each X
|
|
2935
|
+
x_vals = list(set(self.dataset[0]))
|
|
2936
|
+
x_vals.sort()
|
|
2937
|
+
y_vals = [[None, None]] * len(x_vals)
|
|
2938
|
+
plotdata = dict(zip(x_vals, y_vals))
|
|
2939
|
+
for i in range(len(self.dataset[0])):
|
|
2940
|
+
x = self.dataset[0][i]
|
|
2941
|
+
y = self.dataset[1][i]
|
|
2942
|
+
y_vals = plotdata[x]
|
|
2943
|
+
if y_vals[0] is None or y < y_vals[0]:
|
|
2944
|
+
plotdata[x][0] = y
|
|
2945
|
+
if y_vals[1] is None or y > y_vals[1]:
|
|
2946
|
+
plotdata[x][1] = y
|
|
2947
|
+
y1 = [plotdata[x][0] for x in x_vals]
|
|
2948
|
+
y2 = [plotdata[x][1] for x in x_vals]
|
|
2949
|
+
self.plot_data = [x_vals, y1, y2]
|
|
2950
|
+
self.plot_data_labels = [self.x_var.get(), self.y_var.get() + " min", self.y_var.get() + " max"]
|
|
2951
|
+
elif plot_type == "Line plot":
|
|
2952
|
+
# Sort by X
|
|
2953
|
+
ds = list(zip(self.dataset[0], self.dataset[1]))
|
|
2954
|
+
ds.sort()
|
|
2955
|
+
ds2 = list(zip(*ds))
|
|
2956
|
+
self.plot_data = [list(ds2[0]), list(ds2[1])]
|
|
2957
|
+
self.plot_data_labels = self.data_labels
|
|
2958
|
+
elif plot_type in ("Value counts", "Scatter plot", "Y range plot"):
|
|
2959
|
+
# No special preparation
|
|
2960
|
+
self.plot_data = self.dataset
|
|
2961
|
+
self.plot_data_labels = self.data_labels
|
|
2962
|
+
self.plot_data_btn["state"] = "normal"
|
|
2963
|
+
|
|
2964
|
+
def redraw(self):
|
|
2965
|
+
#self.plotfig.clear()
|
|
2966
|
+
#self.plot_axes = self.plotfig.add_subplot(111)
|
|
2967
|
+
#self.plotfig_canvas.draw()
|
|
2968
|
+
plot_type = self.type_var.get()
|
|
2969
|
+
if self.plot_data is not None and len(self.plot_data[0]) > 0:
|
|
2970
|
+
if plot_type == "Category counts":
|
|
2971
|
+
self.plot_axes.bar(self.plot_data[0], self.plot_data[1])
|
|
2972
|
+
self.plot_axes.set_xlabel(self.x_var.get())
|
|
2973
|
+
self.plot_axes.set_ylabel("Counts")
|
|
2974
|
+
elif plot_type == "Value counts":
|
|
2975
|
+
self.plot_axes.hist(self.plot_data[0])
|
|
2976
|
+
self.plot_axes.set_xlabel(self.x_var.get())
|
|
2977
|
+
self.plot_axes.set_ylabel("Counts")
|
|
2978
|
+
elif plot_type == "Scatter plot":
|
|
2979
|
+
self.plot_axes.scatter(self.plot_data[0], self.plot_data[1])
|
|
2980
|
+
self.plot_axes.set_xlabel(self.x_var.get())
|
|
2981
|
+
self.plot_axes.set_ylabel(self.y_var.get())
|
|
2982
|
+
elif plot_type == "Line plot":
|
|
2983
|
+
self.plot_axes.plot(self.plot_data[0], self.plot_data[1])
|
|
2984
|
+
self.plot_axes.set_xlabel(self.x_var.get())
|
|
2985
|
+
self.plot_axes.set_ylabel(self.y_var.get())
|
|
2986
|
+
elif plot_type == "Y range plot":
|
|
2987
|
+
self.plot_axes.fill_between(self.plot_data[0], self.plot_data[1], self.plot_data[2])
|
|
2988
|
+
self.plot_axes.set_xlabel(self.x_var.get())
|
|
2989
|
+
self.plot_axes.set_ylabel(self.y_var.get())
|
|
2990
|
+
elif plot_type == "Box plot":
|
|
2991
|
+
self.plot_axes.boxplot(self.plot_data, labels=self.plot_data_labels)
|
|
2992
|
+
self.plot_axes.set_xlabel(self.x_var.get())
|
|
2993
|
+
self.plot_axes.set_ylabel(self.y_var.get())
|
|
2994
|
+
self.plotfig_canvas.draw()
|
|
2995
|
+
self.plot_nav.update()
|
|
2996
|
+
|
|
2997
|
+
def do_close(self, *args):
|
|
2998
|
+
self.parent.remove_plot(self)
|
|
2999
|
+
self.dlg.destroy()
|
|
3000
|
+
def show(self):
|
|
3001
|
+
self.dlg.update_idle_tasks()
|
|
3002
|
+
self.dlg.minsize(width=500, height=500)
|
|
3003
|
+
self.dlg.wait_window(self.dlg)
|
|
3004
|
+
|
|
3005
|
+
|
|
2531
3006
|
|
|
2532
3007
|
class MsgDialog(object):
|
|
2533
3008
|
#def __init__(self, title, message, width=400, height=400):
|
|
@@ -2538,9 +3013,11 @@ class MsgDialog(object):
|
|
|
2538
3013
|
prompt_frame.grid(row=0, column=0, sticky=tk.NSEW, pady=(3,3))
|
|
2539
3014
|
msg_lbl = ttk.Label(prompt_frame, text=message)
|
|
2540
3015
|
msg_lbl.grid(row=0, column=0, padx=(6,6), pady=(3,3))
|
|
3016
|
+
self.content_frame = tk.Frame(self.dlg)
|
|
3017
|
+
self.content_frame.grid(row=1, column=0, sticky=tk.NSEW)
|
|
2541
3018
|
btn_frame = tk.Frame(self.dlg, borderwidth=3, relief=tk.RIDGE)
|
|
2542
3019
|
btn_frame.columnconfigure(0, weight=1)
|
|
2543
|
-
btn_frame.grid(row=
|
|
3020
|
+
btn_frame.grid(row=2, column=0, sticky=tk.EW, pady=(3,3))
|
|
2544
3021
|
btn_frame.columnconfigure(0, weight=1)
|
|
2545
3022
|
# Buttons
|
|
2546
3023
|
self.canceled = False
|
|
@@ -2553,7 +3030,7 @@ class MsgDialog(object):
|
|
|
2553
3030
|
def show(self):
|
|
2554
3031
|
self.dlg.grab_set()
|
|
2555
3032
|
center_window(self.dlg)
|
|
2556
|
-
|
|
3033
|
+
raise_window(self.dlg)
|
|
2557
3034
|
self.dlg.resizable(False, False)
|
|
2558
3035
|
self.dlg.wait_window(self.dlg)
|
|
2559
3036
|
|
|
@@ -2619,7 +3096,7 @@ class SelDataSrcDialog(object):
|
|
|
2619
3096
|
def select(self):
|
|
2620
3097
|
self.dlg.grab_set()
|
|
2621
3098
|
center_window(self.dlg)
|
|
2622
|
-
|
|
3099
|
+
raise_window(self.dlg)
|
|
2623
3100
|
self.dlg.resizable(False, False)
|
|
2624
3101
|
self.dlg.wait_window(self.dlg)
|
|
2625
3102
|
return self.rv
|
|
@@ -3025,7 +3502,7 @@ class XlsFile(object):
|
|
|
3025
3502
|
self.wbk = xlrd.open_workbook(filename, encoding_override=self.encoding)
|
|
3026
3503
|
self.datemode = self.wbk.datemode
|
|
3027
3504
|
def sheetnames(self):
|
|
3028
|
-
return self.wbk.
|
|
3505
|
+
return self.wbk.sheet_names()
|
|
3029
3506
|
def sheet_named(self, sheetname):
|
|
3030
3507
|
# Return the sheet with the matching name. If the name is actually an integer,
|
|
3031
3508
|
# return that sheet number.
|
|
@@ -3185,7 +3662,8 @@ class XlsxFile(object):
|
|
|
3185
3662
|
def xls_data(filename, sheetname, junk_header_rows, encoding=None):
|
|
3186
3663
|
# Returns the data from the specified worksheet as a list of headers and a list of lists of rows.
|
|
3187
3664
|
root, ext = os.path.splitext(filename)
|
|
3188
|
-
|
|
3665
|
+
ext = ext.lower()
|
|
3666
|
+
if ext == ".xls":
|
|
3189
3667
|
wbk = XlsFile()
|
|
3190
3668
|
else:
|
|
3191
3669
|
wbk = XlsxFile()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: mapdata
|
|
3
|
-
Version:
|
|
3
|
+
Version: 2.0.2
|
|
4
4
|
Summary: An interactive map and table explorer for geographic coordinates in a CSV file
|
|
5
5
|
Home-page: https://osdn.net/project/mapdata/
|
|
6
6
|
Author: Dreas Nielsen
|
|
@@ -27,6 +27,7 @@ Requires: pyproj
|
|
|
27
27
|
Requires: odfpy
|
|
28
28
|
Requires: openpyxl
|
|
29
29
|
Requires: xlrd
|
|
30
|
+
Requires: matplotlib
|
|
30
31
|
Requires-Python: >=3.8
|
|
31
32
|
Description-Content-Type: text/markdown
|
|
32
33
|
License-File: LICENSE.txt
|
|
@@ -63,6 +64,12 @@ export the map to an image file, and quit.
|
|
|
63
64
|
|
|
64
65
|
Selected rows in the data table can be exported to a CSV or spreadsheet file.
|
|
65
66
|
|
|
67
|
+
Data can also be displayed in several different types of plots: box plots, scatter
|
|
68
|
+
plots, line charts, and counts of categorical and quantitative variables. Plots
|
|
69
|
+
can use either all data or only data values that are selected in the map and
|
|
70
|
+
table. Plots have a live connection to the data table, so when selections are
|
|
71
|
+
changed the plots are automatically updated.
|
|
72
|
+
|
|
66
73
|
|
|
67
74
|
Complete documentation is at [https://mapdata.osdn.io](https://mapdata.osdn.io).
|
|
68
75
|
|
|
@@ -5,14 +5,14 @@ with io.open('README.md', encoding='utf-8') as f:
|
|
|
5
5
|
long_description = f.read()
|
|
6
6
|
|
|
7
7
|
setuptools.setup(name='mapdata',
|
|
8
|
-
version='
|
|
8
|
+
version='2.0.2',
|
|
9
9
|
description="An interactive map and table explorer for geographic coordinates in a CSV file",
|
|
10
10
|
author='Dreas Nielsen',
|
|
11
11
|
author_email='dreas.nielsen@gmail.com',
|
|
12
12
|
url='https://osdn.net/project/mapdata/',
|
|
13
13
|
scripts=['mapdata/mapdata.py'],
|
|
14
14
|
license='GPL',
|
|
15
|
-
requires=['tkintermapview', 'pyproj', 'odfpy', 'openpyxl', 'xlrd'],
|
|
15
|
+
requires=['tkintermapview', 'pyproj', 'odfpy', 'openpyxl', 'xlrd', 'matplotlib'],
|
|
16
16
|
python_requires = '>=3.8',
|
|
17
17
|
classifiers=[
|
|
18
18
|
'Development Status :: 5 - Production/Stable',
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|