logdetective 0.4.0__py3-none-any.whl → 2.11.0__py3-none-any.whl
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.
- logdetective/constants.py +33 -12
- logdetective/extractors.py +137 -68
- logdetective/logdetective.py +102 -33
- logdetective/models.py +99 -0
- logdetective/prompts-summary-first.yml +20 -0
- logdetective/prompts-summary-only.yml +13 -0
- logdetective/prompts.yml +90 -0
- logdetective/remote_log.py +67 -0
- logdetective/server/compressors.py +186 -0
- logdetective/server/config.py +78 -0
- logdetective/server/database/base.py +34 -26
- logdetective/server/database/models/__init__.py +33 -0
- logdetective/server/database/models/exceptions.py +17 -0
- logdetective/server/database/models/koji.py +143 -0
- logdetective/server/database/models/merge_request_jobs.py +623 -0
- logdetective/server/database/models/metrics.py +427 -0
- logdetective/server/emoji.py +148 -0
- logdetective/server/exceptions.py +37 -0
- logdetective/server/gitlab.py +451 -0
- logdetective/server/koji.py +159 -0
- logdetective/server/llm.py +309 -0
- logdetective/server/metric.py +75 -30
- logdetective/server/models.py +426 -23
- logdetective/server/plot.py +432 -0
- logdetective/server/server.py +580 -468
- logdetective/server/templates/base_response.html.j2 +59 -0
- logdetective/server/templates/gitlab_full_comment.md.j2 +73 -0
- logdetective/server/templates/gitlab_short_comment.md.j2 +62 -0
- logdetective/server/utils.py +98 -32
- logdetective/skip_snippets.yml +12 -0
- logdetective/utils.py +187 -73
- logdetective-2.11.0.dist-info/METADATA +568 -0
- logdetective-2.11.0.dist-info/RECORD +40 -0
- {logdetective-0.4.0.dist-info → logdetective-2.11.0.dist-info}/WHEEL +1 -1
- logdetective/server/database/models.py +0 -88
- logdetective-0.4.0.dist-info/METADATA +0 -333
- logdetective-0.4.0.dist-info/RECORD +0 -19
- {logdetective-0.4.0.dist-info → logdetective-2.11.0.dist-info}/entry_points.txt +0 -0
- {logdetective-0.4.0.dist-info → logdetective-2.11.0.dist-info/licenses}/LICENSE +0 -0
|
@@ -0,0 +1,432 @@
|
|
|
1
|
+
import datetime
|
|
2
|
+
from typing import Optional, Union, Dict
|
|
3
|
+
|
|
4
|
+
import numpy
|
|
5
|
+
from numpy.typing import ArrayLike
|
|
6
|
+
from matplotlib import dates, colormaps, axes, pyplot, figure
|
|
7
|
+
|
|
8
|
+
from logdetective.server.models import TimePeriod
|
|
9
|
+
from logdetective.server.database.models import (
|
|
10
|
+
AnalyzeRequestMetrics,
|
|
11
|
+
EndpointType,
|
|
12
|
+
Reactions,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Definition:
|
|
17
|
+
"""Define plot details, given a time period."""
|
|
18
|
+
|
|
19
|
+
def __init__(self, time_period: TimePeriod):
|
|
20
|
+
self.time_period = time_period
|
|
21
|
+
self.days_diff = time_period.get_time_period().days
|
|
22
|
+
if self.time_period.hours:
|
|
23
|
+
self._freq = "H"
|
|
24
|
+
self._time_format = "%Y-%m-%d %H"
|
|
25
|
+
self._locator = dates.HourLocator(interval=2)
|
|
26
|
+
self._time_unit = "hour"
|
|
27
|
+
self._time_delta = datetime.timedelta(hours=1)
|
|
28
|
+
elif self.time_period.days:
|
|
29
|
+
self._freq = "D"
|
|
30
|
+
self._time_format = "%Y-%m-%d"
|
|
31
|
+
self._locator = dates.DayLocator(interval=1)
|
|
32
|
+
self._time_unit = "day"
|
|
33
|
+
self._time_delta = datetime.timedelta(days=1)
|
|
34
|
+
elif self.time_period.weeks:
|
|
35
|
+
self._freq = "W"
|
|
36
|
+
self._time_format = "%Y-%m-%d"
|
|
37
|
+
self._locator = dates.WeekdayLocator(interval=1)
|
|
38
|
+
self._time_unit = "week"
|
|
39
|
+
self._time_delta = datetime.timedelta(weeks=1)
|
|
40
|
+
|
|
41
|
+
@property
|
|
42
|
+
def freq(self):
|
|
43
|
+
# pylint: disable=missing-function-docstring
|
|
44
|
+
return self._freq
|
|
45
|
+
|
|
46
|
+
@property
|
|
47
|
+
def time_format(self):
|
|
48
|
+
# pylint: disable=missing-function-docstring
|
|
49
|
+
return self._time_format
|
|
50
|
+
|
|
51
|
+
@property
|
|
52
|
+
def locator(self):
|
|
53
|
+
# pylint: disable=missing-function-docstring
|
|
54
|
+
return self._locator
|
|
55
|
+
|
|
56
|
+
@property
|
|
57
|
+
def time_unit(self):
|
|
58
|
+
# pylint: disable=missing-function-docstring
|
|
59
|
+
return self._time_unit
|
|
60
|
+
|
|
61
|
+
@property
|
|
62
|
+
def time_delta(self):
|
|
63
|
+
# pylint: disable=missing-function-docstring
|
|
64
|
+
return self._time_delta
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def create_time_series_arrays(
|
|
68
|
+
values_dict: dict[datetime.datetime, int],
|
|
69
|
+
plot_def: Definition,
|
|
70
|
+
start_time: datetime.datetime,
|
|
71
|
+
end_time: datetime.datetime,
|
|
72
|
+
value_type: Optional[Union[type[int], type[float]]] = int,
|
|
73
|
+
) -> tuple[numpy.ndarray, numpy.ndarray]:
|
|
74
|
+
"""Create time series arrays from a dictionary of values.
|
|
75
|
+
|
|
76
|
+
This function generates two aligned numpy arrays:
|
|
77
|
+
1. An array of timestamps from start_time to end_time
|
|
78
|
+
2. A corresponding array of valuesfor each timestamp
|
|
79
|
+
|
|
80
|
+
The timestamps are truncated to the precision specified by time_format.
|
|
81
|
+
If a timestamp in values_dict matches a generated timestamp, its values is used;
|
|
82
|
+
otherwise, the value defaults to zero.
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
values_dict: Dictionary mapping timestamps to their respective values
|
|
86
|
+
start_time: The starting timestamp of the time series
|
|
87
|
+
end_time: The ending timestamp of the time series
|
|
88
|
+
time_delta: The time interval between consecutive timestamps
|
|
89
|
+
time_format: String format for datetime truncation (e.g., '%Y-%m-%d %H:%M')
|
|
90
|
+
|
|
91
|
+
Returns:
|
|
92
|
+
A tuple containing:
|
|
93
|
+
- numpy.ndarray: Array of timestamps
|
|
94
|
+
- numpy.ndarray: Array of corresponding values
|
|
95
|
+
"""
|
|
96
|
+
num_intervals = int((end_time - start_time) / plot_def.time_delta) + 1
|
|
97
|
+
|
|
98
|
+
timestamps = numpy.array(
|
|
99
|
+
[
|
|
100
|
+
datetime.datetime.strptime(
|
|
101
|
+
(start_time + i * plot_def.time_delta).strftime(
|
|
102
|
+
format=plot_def.time_format
|
|
103
|
+
),
|
|
104
|
+
plot_def.time_format,
|
|
105
|
+
)
|
|
106
|
+
for i in range(num_intervals)
|
|
107
|
+
]
|
|
108
|
+
)
|
|
109
|
+
values = numpy.zeros(num_intervals, dtype=value_type)
|
|
110
|
+
|
|
111
|
+
timestamp_to_index = {timestamp: i for i, timestamp in enumerate(timestamps)}
|
|
112
|
+
|
|
113
|
+
for timestamp, count in values_dict.items():
|
|
114
|
+
if timestamp in timestamp_to_index:
|
|
115
|
+
values[timestamp_to_index[timestamp]] = count
|
|
116
|
+
|
|
117
|
+
return timestamps, values
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _add_bar_chart(
|
|
121
|
+
ax: axes.Axes,
|
|
122
|
+
plot_def: Definition,
|
|
123
|
+
timestamps: ArrayLike,
|
|
124
|
+
values: ArrayLike,
|
|
125
|
+
label: str,
|
|
126
|
+
) -> None:
|
|
127
|
+
"""Add a blue bar chart"""
|
|
128
|
+
bar_width = (
|
|
129
|
+
0.8 * plot_def.time_delta.total_seconds() / 86400
|
|
130
|
+
) # Convert to days for matplotlib
|
|
131
|
+
ax.bar(
|
|
132
|
+
timestamps,
|
|
133
|
+
values,
|
|
134
|
+
width=bar_width,
|
|
135
|
+
alpha=0.7,
|
|
136
|
+
color="skyblue",
|
|
137
|
+
label=label,
|
|
138
|
+
)
|
|
139
|
+
ax.set_xlabel("Time")
|
|
140
|
+
ax.set_ylabel(label, color="blue")
|
|
141
|
+
ax.tick_params(axis="y", labelcolor="blue")
|
|
142
|
+
|
|
143
|
+
ax.xaxis.set_major_formatter(dates.DateFormatter(plot_def.time_format))
|
|
144
|
+
ax.xaxis.set_major_locator(plot_def.locator)
|
|
145
|
+
|
|
146
|
+
pyplot.xticks(rotation=45)
|
|
147
|
+
|
|
148
|
+
ax.grid(True, alpha=0.3)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def _add_line_chart( # pylint: disable=too-many-arguments disable=too-many-positional-arguments
|
|
152
|
+
ax: axes.Axes,
|
|
153
|
+
timestamps: ArrayLike,
|
|
154
|
+
values: ArrayLike,
|
|
155
|
+
label: str,
|
|
156
|
+
color: str = "red",
|
|
157
|
+
set_label: bool = True,
|
|
158
|
+
):
|
|
159
|
+
"""Add a red line chart"""
|
|
160
|
+
ax.plot(timestamps, values, color=color, linestyle="-", linewidth=2, label=label)
|
|
161
|
+
if set_label:
|
|
162
|
+
ax.set_ylabel(label, color=color)
|
|
163
|
+
ax.tick_params(axis="y", labelcolor=color)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
async def requests_per_time(
|
|
167
|
+
period_of_time: TimePeriod,
|
|
168
|
+
endpoint: EndpointType = EndpointType.ANALYZE,
|
|
169
|
+
end_time: Optional[datetime.datetime] = None,
|
|
170
|
+
) -> figure.Figure:
|
|
171
|
+
"""
|
|
172
|
+
Generate a visualization of request counts over a specified time period.
|
|
173
|
+
|
|
174
|
+
This function creates a dual-axis plot showing:
|
|
175
|
+
1. A bar chart of request counts per time interval
|
|
176
|
+
2. A line chart showing the cumulative request count
|
|
177
|
+
|
|
178
|
+
The time intervals are determined by the provided TimePeriod object, which defines
|
|
179
|
+
the granularity and formatting of the time axis.
|
|
180
|
+
|
|
181
|
+
Args:
|
|
182
|
+
period_of_time: A TimePeriod object that defines the time period and interval
|
|
183
|
+
for the analysis (e.g., hourly, daily, weekly)
|
|
184
|
+
endpoint: One of the API endpoints
|
|
185
|
+
end_time: The end time for the analysis period. If None, defaults to the current
|
|
186
|
+
UTC time
|
|
187
|
+
|
|
188
|
+
Returns:
|
|
189
|
+
A matplotlib Figure object containing the generated visualization
|
|
190
|
+
"""
|
|
191
|
+
end_time = end_time or datetime.datetime.now(datetime.timezone.utc)
|
|
192
|
+
start_time = period_of_time.get_period_start_time(end_time)
|
|
193
|
+
plot_def = Definition(period_of_time)
|
|
194
|
+
requests_counts = await AnalyzeRequestMetrics.get_requests_in_period(
|
|
195
|
+
start_time, end_time, plot_def.time_format, endpoint
|
|
196
|
+
)
|
|
197
|
+
timestamps, counts = create_time_series_arrays(
|
|
198
|
+
requests_counts, plot_def, start_time, end_time
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
fig, ax1 = pyplot.subplots(figsize=(12, 6))
|
|
202
|
+
_add_bar_chart(ax1, plot_def, timestamps, counts, "Requests")
|
|
203
|
+
|
|
204
|
+
ax2 = ax1.twinx()
|
|
205
|
+
_add_line_chart(ax2, timestamps, numpy.cumsum(counts), "Cumulative Requests")
|
|
206
|
+
|
|
207
|
+
pyplot.title(
|
|
208
|
+
f"Requests received for API {endpoint} ({start_time.strftime(plot_def.time_format)} "
|
|
209
|
+
f"to {end_time.strftime(plot_def.time_format)})"
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
lines1, labels1 = ax1.get_legend_handles_labels()
|
|
213
|
+
lines2, labels2 = ax2.get_legend_handles_labels()
|
|
214
|
+
ax1.legend(lines1 + lines2, labels1 + labels2, loc="center")
|
|
215
|
+
|
|
216
|
+
pyplot.tight_layout()
|
|
217
|
+
|
|
218
|
+
return fig
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
async def average_time_per_responses( # pylint: disable=too-many-locals
|
|
222
|
+
period_of_time: TimePeriod,
|
|
223
|
+
endpoint: EndpointType = EndpointType.ANALYZE,
|
|
224
|
+
end_time: Optional[datetime.datetime] = None,
|
|
225
|
+
) -> figure.Figure:
|
|
226
|
+
"""
|
|
227
|
+
Generate a visualization of average response time and length over a specified time period.
|
|
228
|
+
|
|
229
|
+
This function creates a dual-axis plot showing:
|
|
230
|
+
1. A bar chart of average response time per time interval
|
|
231
|
+
1. A line chart of average response length per time interval
|
|
232
|
+
|
|
233
|
+
The time intervals are determined by the provided TimePeriod object, which defines
|
|
234
|
+
the granularity and formatting of the time axis.
|
|
235
|
+
|
|
236
|
+
Args:
|
|
237
|
+
period_of_time: A TimePeriod object that defines the time period and interval
|
|
238
|
+
for the analysis (e.g., hourly, daily, weekly)
|
|
239
|
+
endpoint: One of the API endpoints
|
|
240
|
+
end_time: The end time for the analysis period. If None, defaults to the current
|
|
241
|
+
UTC time
|
|
242
|
+
|
|
243
|
+
Returns:
|
|
244
|
+
A matplotlib Figure object containing the generated visualization
|
|
245
|
+
"""
|
|
246
|
+
end_time = end_time or datetime.datetime.now(datetime.timezone.utc)
|
|
247
|
+
start_time = period_of_time.get_period_start_time(end_time)
|
|
248
|
+
plot_def = Definition(period_of_time)
|
|
249
|
+
responses_average_time = (
|
|
250
|
+
await AnalyzeRequestMetrics.get_responses_average_time_in_period(
|
|
251
|
+
start_time, end_time, plot_def.time_format, endpoint
|
|
252
|
+
)
|
|
253
|
+
)
|
|
254
|
+
timestamps, average_time = create_time_series_arrays(
|
|
255
|
+
responses_average_time,
|
|
256
|
+
plot_def,
|
|
257
|
+
start_time,
|
|
258
|
+
end_time,
|
|
259
|
+
float,
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
fig, ax1 = pyplot.subplots(figsize=(12, 6))
|
|
263
|
+
_add_bar_chart(
|
|
264
|
+
ax1, plot_def, timestamps, average_time, "average response time (seconds)"
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
responses_average_length = (
|
|
268
|
+
await AnalyzeRequestMetrics.get_responses_average_length_in_period(
|
|
269
|
+
start_time, end_time, plot_def.time_format, endpoint
|
|
270
|
+
)
|
|
271
|
+
)
|
|
272
|
+
timestamps, average_length = create_time_series_arrays(
|
|
273
|
+
responses_average_length,
|
|
274
|
+
plot_def,
|
|
275
|
+
start_time,
|
|
276
|
+
end_time,
|
|
277
|
+
float,
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
ax2 = ax1.twinx()
|
|
281
|
+
_add_line_chart(ax2, timestamps, average_length, "average response length (chars)")
|
|
282
|
+
|
|
283
|
+
pyplot.title(
|
|
284
|
+
f"average response time for API {endpoint} ({start_time.strftime(plot_def.time_format)} "
|
|
285
|
+
f"to {end_time.strftime(plot_def.time_format)})"
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
lines1, labels1 = ax1.get_legend_handles_labels()
|
|
289
|
+
lines2, labels2 = ax2.get_legend_handles_labels()
|
|
290
|
+
ax1.legend(lines1 + lines2, labels1 + labels2, loc="center")
|
|
291
|
+
|
|
292
|
+
pyplot.tight_layout()
|
|
293
|
+
|
|
294
|
+
return fig
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
async def _collect_emoji_data(
|
|
298
|
+
start_time: datetime.datetime, plot_def: Definition
|
|
299
|
+
) -> Dict[str, Dict[datetime.datetime, int]]:
|
|
300
|
+
"""Collect and organize emoji feedback data
|
|
301
|
+
|
|
302
|
+
Counts all emojis given to logdetective comments created since start_time.
|
|
303
|
+
Collect counts in time accordingly to the plot definition.
|
|
304
|
+
"""
|
|
305
|
+
reactions = await Reactions.get_since(start_time)
|
|
306
|
+
reactions_values_dict: Dict[str, Dict] = {}
|
|
307
|
+
for comment_created_at, reaction in reactions:
|
|
308
|
+
comment_created_at_formatted = comment_created_at.strptime(
|
|
309
|
+
comment_created_at.strftime(plot_def.time_format), plot_def.time_format
|
|
310
|
+
)
|
|
311
|
+
if reaction.reaction_type in reactions_values_dict:
|
|
312
|
+
reaction_values_dict = reactions_values_dict[reaction.reaction_type]
|
|
313
|
+
if comment_created_at_formatted in reaction_values_dict:
|
|
314
|
+
reaction_values_dict[comment_created_at_formatted] += reaction.count
|
|
315
|
+
else:
|
|
316
|
+
reaction_values_dict[comment_created_at_formatted] = reaction.count
|
|
317
|
+
else:
|
|
318
|
+
reaction_values_dict = {comment_created_at_formatted: reaction.count}
|
|
319
|
+
reactions_values_dict.update({reaction.reaction_type: reaction_values_dict})
|
|
320
|
+
|
|
321
|
+
return reactions_values_dict
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def _plot_emoji_data( # pylint: disable=too-many-locals
|
|
325
|
+
ax: axes.Axes,
|
|
326
|
+
reactions_values_dict: Dict[str, Dict[datetime.datetime, int]],
|
|
327
|
+
plot_def: Definition,
|
|
328
|
+
start_time: datetime.datetime,
|
|
329
|
+
end_time: datetime.datetime,
|
|
330
|
+
):
|
|
331
|
+
"""Plot each emoji's data on its own axis."""
|
|
332
|
+
emoji_lines = {}
|
|
333
|
+
emoji_labels = {}
|
|
334
|
+
|
|
335
|
+
# Find global min and max y values to set consistent scale
|
|
336
|
+
all_counts = []
|
|
337
|
+
for emoji, dict_counts in reactions_values_dict.items():
|
|
338
|
+
timestamps, counts = create_time_series_arrays(
|
|
339
|
+
dict_counts, plot_def, start_time, end_time
|
|
340
|
+
)
|
|
341
|
+
all_counts.extend(counts)
|
|
342
|
+
|
|
343
|
+
colors = [
|
|
344
|
+
colormaps["viridis"](i)
|
|
345
|
+
for i in numpy.linspace(0, 1, len(reactions_values_dict))
|
|
346
|
+
]
|
|
347
|
+
|
|
348
|
+
first_emoji = True
|
|
349
|
+
for i, (emoji, dict_counts) in enumerate(reactions_values_dict.items()):
|
|
350
|
+
timestamps, counts = create_time_series_arrays(
|
|
351
|
+
dict_counts, plot_def, start_time, end_time
|
|
352
|
+
)
|
|
353
|
+
|
|
354
|
+
if first_emoji:
|
|
355
|
+
current_ax = ax
|
|
356
|
+
first_emoji = False
|
|
357
|
+
else:
|
|
358
|
+
current_ax = ax.twinx()
|
|
359
|
+
current_ax.spines["right"].set_position(("outward", 60 * (i - 1)))
|
|
360
|
+
|
|
361
|
+
_add_line_chart(current_ax, timestamps, counts, f"{emoji}", colors[i], False)
|
|
362
|
+
emoji_lines[emoji], emoji_labels[emoji] = current_ax.get_legend_handles_labels()
|
|
363
|
+
|
|
364
|
+
# Set the same y-limits for all axes
|
|
365
|
+
current_ax.set_ylim(0, max(all_counts) * 1.1)
|
|
366
|
+
|
|
367
|
+
# Only show y-ticks on the first axis to avoid clutter
|
|
368
|
+
if 0 < i < len(reactions_values_dict):
|
|
369
|
+
current_ax.set_yticks([])
|
|
370
|
+
|
|
371
|
+
return emoji_lines, emoji_labels
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
async def emojis_per_time(
|
|
375
|
+
period_of_time: TimePeriod,
|
|
376
|
+
end_time: Optional[datetime.datetime] = None,
|
|
377
|
+
) -> figure.Figure:
|
|
378
|
+
"""
|
|
379
|
+
Generate a visualization of overall emoji feedback
|
|
380
|
+
over a specified time period.
|
|
381
|
+
|
|
382
|
+
This function creates a multiple-axis plot showing
|
|
383
|
+
a line chart for every found emoji
|
|
384
|
+
|
|
385
|
+
The time intervals are determined by the provided TimePeriod object, which defines
|
|
386
|
+
the granularity and formatting of the time axis.
|
|
387
|
+
|
|
388
|
+
Args:
|
|
389
|
+
period_of_time: A TimePeriod object that defines the time period and interval
|
|
390
|
+
for the analysis (e.g., hourly, daily, weekly)
|
|
391
|
+
end_time: The end time for the analysis period. If None, defaults to the current
|
|
392
|
+
UTC time
|
|
393
|
+
|
|
394
|
+
Returns:
|
|
395
|
+
A matplotlib Figure object containing the generated visualization
|
|
396
|
+
"""
|
|
397
|
+
plot_def = Definition(period_of_time)
|
|
398
|
+
end_time = end_time or datetime.datetime.now(datetime.timezone.utc)
|
|
399
|
+
start_time = period_of_time.get_period_start_time(end_time)
|
|
400
|
+
reactions_values_dict = await _collect_emoji_data(start_time, plot_def)
|
|
401
|
+
|
|
402
|
+
fig, ax = pyplot.subplots(figsize=(12, 6))
|
|
403
|
+
|
|
404
|
+
emoji_lines, emoji_labels = _plot_emoji_data(
|
|
405
|
+
ax, reactions_values_dict, plot_def, start_time, end_time
|
|
406
|
+
)
|
|
407
|
+
|
|
408
|
+
pyplot.title(
|
|
409
|
+
f"Emoji feedback ({start_time.strftime(plot_def.time_format)} "
|
|
410
|
+
f"to {end_time.strftime(plot_def.time_format)})"
|
|
411
|
+
)
|
|
412
|
+
|
|
413
|
+
all_lines = []
|
|
414
|
+
for lines in emoji_lines.values():
|
|
415
|
+
all_lines.extend(lines)
|
|
416
|
+
all_labels = []
|
|
417
|
+
for labels in emoji_labels.values():
|
|
418
|
+
all_labels.extend(labels)
|
|
419
|
+
|
|
420
|
+
ax.legend(all_lines, all_labels, loc="upper left")
|
|
421
|
+
ax.set_xlabel("Time")
|
|
422
|
+
ax.set_ylabel("Count")
|
|
423
|
+
|
|
424
|
+
# Format x-axis
|
|
425
|
+
ax.xaxis.set_major_formatter(dates.DateFormatter(plot_def.time_format))
|
|
426
|
+
ax.xaxis.set_major_locator(plot_def.locator)
|
|
427
|
+
ax.tick_params(axis="x", labelrotation=45)
|
|
428
|
+
ax.grid(True, alpha=0.3)
|
|
429
|
+
|
|
430
|
+
pyplot.tight_layout()
|
|
431
|
+
|
|
432
|
+
return fig
|