Skip to content

Commit 1fad005

Browse files
committed
#5 Work in progress:
- Improve Matplotlib plot [ci skip]
1 parent 8bfc718 commit 1fad005

File tree

3 files changed

+116
-58
lines changed

3 files changed

+116
-58
lines changed

ezgpx/gpx/gpx.py

+92-56
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ def __init__(
6565
self.precisions: Dict = None
6666
self.time_data: bool = False
6767
self.time_format: str = None
68+
self.dataframe: pd.DataFrame = None
6869

6970
# GPX
7071
if file_path.endswith(".gpx"):
@@ -615,7 +616,8 @@ def to_dataframe(
615616
speed: bool = False,
616617
pace: bool = False,
617618
ascent_rate: bool = False,
618-
ascent_speed: bool = False) -> pd.DataFrame:
619+
ascent_speed: bool = False,
620+
distance_from_start: bool = False) -> pd.DataFrame:
619621
"""
620622
Convert GPX object to Pandas Dataframe.
621623
@@ -633,13 +635,25 @@ def to_dataframe(
633635
Toggle ascent rate, by default False
634636
ascent_speed : bool, optional
635637
Toggle ascent speed, by default False
638+
distance_from_start : bool, optional
639+
Toggle distance from start, by default False
636640
637641
Returns
638642
-------
639643
pd.DataFrame
640644
Dataframe containing data from GPX.
641645
"""
642-
return self.gpx.to_dataframe(projection, elevation, speed, pace, ascent_rate, ascent_speed)
646+
if not self.time_data:
647+
speed = False
648+
pace = False
649+
ascent_speed = False
650+
return self.gpx.to_dataframe(projection,
651+
elevation,
652+
speed,
653+
pace,
654+
ascent_rate,
655+
ascent_speed,
656+
distance_from_start)
643657

644658
def to_csv(
645659
self,
@@ -775,7 +789,7 @@ def _matplotlib_plot_text(
775789
plt.text(speed[0], speed[1],
776790
f"Speed:\n{self.avg_speed():.2f} km/h", **text_parameters)
777791

778-
def matplotlib_axes_plot(
792+
def matplotlib_map_plot(
779793
self,
780794
axes: Axes,
781795
projection: Optional[str] = None,
@@ -833,60 +847,45 @@ def matplotlib_axes_plot(
833847
column_y = "y"
834848
self.gpx.project(projection) # Project all track points
835849

836-
# Create dataframe containing data from the GPX file
837-
gpx_df = None
838-
if color == "elevation":
839-
gpx_df = self.to_dataframe(projection=True, elevation=True, speed=False, pace=False, ascent_rate=False, ascent_speed=False)
840-
elif color == "speed":
841-
gpx_df = self.to_dataframe(projection=True, elevation=False, speed=True, pace=False, ascent_rate=False, ascent_speed=False)
842-
elif color == "pace":
843-
gpx_df = self.to_dataframe(projection=True, elevation=False, speed=False, pace=True, ascent_rate=False, ascent_speed=False)
844-
elif color == "ascent_rate":
845-
gpx_df = self.to_dataframe(projection=True, elevation=False, speed=False, pace=False, ascent_rate=True, ascent_speed=False)
846-
elif color == "ascent_speed":
847-
gpx_df = self.to_dataframe(projection=True, elevation=False, speed=False, pace=False, ascent_rate=False, ascent_speed=True)
848-
else:
849-
gpx_df = self.to_dataframe(projection=True, elevation=False, speed=False, pace=False, ascent_rate=False, ascent_speed=False)
850-
851850
# Scatter all track points
852851
if color == "elevation":
853852
cmap = matplotlib.colors.LinearSegmentedColormap.from_list("", ["green","blue"])
854-
im = axes.scatter(gpx_df[column_x], gpx_df[column_y],
855-
c=gpx_df["ele"], cmap=cmap)
853+
im = axes.scatter(self.dataframe[column_x], self.dataframe[column_y],
854+
c=self.dataframe["ele"], cmap=cmap)
856855
elif color == "speed":
857856
# cmap = matplotlib.colors.LinearSegmentedColormap.from_list("", ["lightskyblue", "deepskyblue", "blue", "mediumblue", "midnightblue"])
858857
cmap = matplotlib.colors.LinearSegmentedColormap.from_list("", ["lightskyblue", "mediumblue", "midnightblue"])
859-
im = axes.scatter(gpx_df[column_x], gpx_df[column_y],
860-
c=gpx_df["speed"], cmap=cmap)
858+
im = axes.scatter(self.dataframe[column_x], self.dataframe[column_y],
859+
c=self.dataframe["speed"], cmap=cmap)
861860
elif color == "pace":
862861
cmap = matplotlib.colors.LinearSegmentedColormap.from_list("", ["lightskyblue", "midnightblue"])
863-
im = axes.scatter(gpx_df[column_x], gpx_df[column_y],
864-
c=gpx_df["pace"], cmap=cmap)
862+
im = axes.scatter(self.dataframe[column_x], self.dataframe[column_y],
863+
c=self.dataframe["pace"], cmap=cmap)
865864
elif color == "vertical_drop":
866865
cmap = matplotlib.colors.LinearSegmentedColormap.from_list("", ["yellow", "orange", "red", "purple", "black"])
867-
im = axes.scatter(gpx_df[column_x], gpx_df[column_y],
868-
c=abs(gpx_df["ascent_rate"]), cmap=cmap)
866+
im = axes.scatter(self.dataframe[column_x], self.dataframe[column_y],
867+
c=abs(self.dataframe["ascent_rate"]), cmap=cmap)
869868
elif color == "ascent_rate":
870869
cmap = matplotlib.colors.LinearSegmentedColormap.from_list("", ["darkgreen", "green", "yellow", "red", "black"])
871-
im = axes.scatter(gpx_df[column_x], gpx_df[column_y],
872-
c=gpx_df["ascent_rate"], cmap=cmap)
870+
im = axes.scatter(self.dataframe[column_x], self.dataframe[column_y],
871+
c=self.dataframe["ascent_rate"], cmap=cmap)
873872
elif color == "ascent_speed":
874873
cmap = matplotlib.colors.LinearSegmentedColormap.from_list("", ["deeppink", "lightpink", "lightcoral", "red", "darkred"])
875-
im = axes.scatter(gpx_df[column_x], gpx_df[column_y],
876-
c=gpx_df["ascent_speed"], cmap=cmap)
874+
im = axes.scatter(self.dataframe[column_x], self.dataframe[column_y],
875+
c=self.dataframe["ascent_speed"], cmap=cmap)
877876
else:
878-
im = axes.scatter(gpx_df[column_x], gpx_df[column_y], color=color)
877+
im = axes.scatter(self.dataframe[column_x], self.dataframe[column_y], color=color)
879878

880879
# Colorbar
881880
if colorbar:
882881
plt.colorbar(im)
883882

884883
# Scatter start and stop points with different color
885884
if start_stop_colors is not None:
886-
axes.scatter(gpx_df[column_x][0],
887-
gpx_df[column_y][0], color=start_stop_colors[0])
888-
axes.scatter(gpx_df[column_x][len(gpx_df[column_x])-1],
889-
gpx_df[column_y][len(gpx_df[column_x])-1], color=start_stop_colors[1])
885+
axes.scatter(self.dataframe[column_x][0],
886+
self.dataframe[column_y][0], color=start_stop_colors[0])
887+
axes.scatter(self.dataframe[column_x][len(self.dataframe[column_x])-1],
888+
self.dataframe[column_y][len(self.dataframe[column_x])-1], color=start_stop_colors[1])
890889

891890
# Scatter way points with different color
892891
if way_points_color:
@@ -905,18 +904,28 @@ def matplotlib_axes_plot(
905904
self._matplotlib_plot_text(axes.get_figure(), duration, distance, ascent, pace, speed)
906905

907906
# Add ticks
908-
axes.set_xticks([min(gpx_df[column_x]), max(gpx_df[column_x])])
909-
axes.set_yticks([min(gpx_df[column_y]), max(gpx_df[column_y])])
907+
axes.set_xticks([min(self.dataframe[column_x]), max(self.dataframe[column_x])])
908+
axes.set_yticks([min(self.dataframe[column_y]), max(self.dataframe[column_y])])
910909

911910
# Add axis limits (useless??)
912911
if projection is not None:
913-
axes.set_xlim(left=min(gpx_df[column_x]),
914-
right=max(gpx_df[column_x]))
915-
axes.set_ylim(bottom=min(gpx_df[column_y]),
916-
top=max(gpx_df[column_y]))
912+
axes.set_xlim(left=min(self.dataframe[column_x]),
913+
right=max(self.dataframe[column_x]))
914+
axes.set_ylim(bottom=min(self.dataframe[column_y]),
915+
top=max(self.dataframe[column_y]))
916+
917+
def matplotlib_elevation_profile_plot(
918+
self,
919+
axes: Axes):
920+
# Clear axes
921+
axes.clear()
922+
923+
# Plot
924+
im = axes.plot(self.dataframe["distance_from_start"].values, self.dataframe["ele"].values) # .values to avoid -> Multi-dimensional indexing (e.g. `obj[:, None]`) is no longer supported. Convert to a numpy array before indexing instead.
917925

918926
def matplotlib_plot(
919927
self,
928+
map: bool = True,
920929
projection: Optional[str] = None,
921930
color: str = "#101010",
922931
colorbar: bool = False,
@@ -928,6 +937,7 @@ def matplotlib_plot(
928937
ascent: Optional[Tuple[float, float]] = None,
929938
pace: Optional[Tuple[float, float]] = None,
930939
speed: Optional[Tuple[float, float]] = None,
940+
elevation_profile: bool = False,
931941
file_path: Optional[str] = None):
932942
"""
933943
Plot GPX using Matplotlib.
@@ -959,23 +969,49 @@ def matplotlib_plot(
959969
speed : Optional[Tuple[float, float]], optional
960970
Display speed, by default None
961971
"""
972+
# Create dataframe containing data from the GPX file
973+
self.dataframe = self.to_dataframe(projection=True,
974+
elevation=True,
975+
speed=True,
976+
pace=True,
977+
ascent_rate=True,
978+
ascent_speed=True,
979+
distance_from_start=True)
980+
962981
# Create figure with axes
963982
fig = plt.figure(figsize=(14, 8))
964-
fig.add_subplot(111)
965-
966-
# Plot on axes
967-
self.matplotlib_axes_plot(fig.axes[0],
968-
projection,
969-
color,
970-
colorbar,
971-
start_stop_colors,
972-
way_points_color,
973-
title,
974-
duration,
975-
distance,
976-
ascent,
977-
pace,
978-
speed)
983+
984+
# Plot map
985+
if map:
986+
if elevation_profile:
987+
fig.add_subplot(2, 1, 1)
988+
else:
989+
fig.add_subplot(1, 1, 1)
990+
self.matplotlib_map_plot(fig.axes[0],
991+
projection,
992+
color,
993+
colorbar,
994+
start_stop_colors,
995+
way_points_color,
996+
title,
997+
duration,
998+
distance,
999+
ascent,
1000+
pace,
1001+
speed)
1002+
1003+
# Plot elevation profile
1004+
if elevation_profile:
1005+
if map:
1006+
fig.add_subplot(2, 1, 2)
1007+
axes_idx = 1
1008+
if colorbar:
1009+
axes_idx = 2
1010+
else:
1011+
fig.add_subplot(1, 1, 1)
1012+
axes_idx = 0
1013+
self.matplotlib_elevation_profile_plot(fig.axes[axes_idx])
1014+
9791015

9801016
# Save or display plot
9811017
if file_path is not None:

ezgpx/gpx_elements/gpx.py

+23-2
Original file line numberDiff line numberDiff line change
@@ -270,7 +270,7 @@ def distance(self) -> float:
270270
float
271271
Distance (meters).
272272
"""
273-
dst = 0
273+
dst = 0.0
274274
previous_point = self.tracks[0].trkseg[0].trkpt[0]
275275
for track in self.tracks:
276276
for track_segment in track.trkseg:
@@ -279,6 +279,20 @@ def distance(self) -> float:
279279
previous_point = track_point
280280
return dst
281281

282+
def compute_points_distance_from_start(self):
283+
"""
284+
Compute distance from start at each point.
285+
"""
286+
dst = 0.0
287+
previous_point = self.tracks[0].trkseg[0].trkpt[0]
288+
previous_point.distance_from_start = dst
289+
for track in self.tracks:
290+
for track_segment in track.trkseg:
291+
for track_point in track_segment.trkpt:
292+
dst += haversine_distance(previous_point, track_point)
293+
track_point.distance_from_start = dst
294+
previous_point = track_point
295+
282296
def ascent(self) -> float:
283297
"""
284298
Compute the total ascent (meters) of the tracks contained in the Gpx element.
@@ -847,7 +861,8 @@ def to_dataframe(
847861
speed: bool = False,
848862
pace: bool = False,
849863
ascent_rate: bool = False,
850-
ascent_speed: bool = False) -> pd.DataFrame:
864+
ascent_speed: bool = False,
865+
distance_from_start: bool = False) -> pd.DataFrame:
851866
"""
852867
Convert GPX object to Pandas Dataframe.
853868
@@ -865,6 +880,8 @@ def to_dataframe(
865880
Toggle ascent rate, by default False
866881
ascent_speed : bool, optional
867882
Toggle ascent speed, by default False
883+
distance_from_start : bool, optional
884+
Toggle distance from start, by default False
868885
869886
Returns
870887
-------
@@ -882,6 +899,8 @@ def to_dataframe(
882899
self.compute_points_ascent_rate()
883900
if ascent_speed and test_point.ascent_speed is None:
884901
self.compute_points_ascent_speed()
902+
if distance_from_start and test_point.distance_from_start is None:
903+
self.compute_points_distance_from_start()
885904

886905
route_info = []
887906
for track in self.tracks:
@@ -907,6 +926,8 @@ def to_dataframe(
907926
track_point_dict["ascent_rate"] = track_point.ascent_rate
908927
if ascent_speed:
909928
track_point_dict["ascent_speed"] = track_point.ascent_speed
929+
if distance_from_start:
930+
track_point_dict["distance_from_start"] = track_point.distance_from_start
910931
route_info.append(track_point_dict)
911932
df = pd.DataFrame(route_info)
912933
return df

ezgpx/gpx_elements/way_point.py

+1
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,7 @@ def __init__(
115115
self.pace: float = None
116116
self.ascent_rate: float = None
117117
self.ascent_speed: float = None
118+
self.distance_from_start: float = None
118119

119120
# Projection
120121
self._x: int = None

0 commit comments

Comments
 (0)