Skip to content

Commit 9a388dc

Browse files
committed
chg: [chat viewer] add chat messages by current year heatmap
1 parent 55a35bf commit 9a388dc

File tree

4 files changed

+123
-103
lines changed

4 files changed

+123
-103
lines changed

bin/lib/chats_viewer.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -795,6 +795,19 @@ def api_get_nb_week_messages(chat_type, chat_instance_uuid, chat_id):
795795
week = chat.get_nb_week_messages()
796796
return week, 200
797797

798+
def api_get_nb_year_messages(chat_type, chat_instance_uuid, chat_id, year):
799+
chat = get_obj_chat(chat_type, chat_instance_uuid, chat_id)
800+
if not chat.exists():
801+
return {"status": "error", "reason": "Unknown chat"}, 404
802+
try:
803+
year = int(year)
804+
except (TypeError, ValueError):
805+
year = datetime.now().year
806+
nb_max, nb = chat.get_nb_year_messages(year)
807+
nb = [[date, value] for date, value in nb.items()]
808+
return {'max': nb_max, 'nb': nb}, 200
809+
810+
798811
def api_get_chat_participants(chat_type, chat_subtype, chat_id):
799812
if chat_type not in ['chat', 'chat-subchannel', 'chat-thread']:
800813
return {"status": "error", "reason": "Unknown chat type"}, 400

bin/lib/objects/abstract_chat_object.py

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
import time
1212
from abc import ABC
1313

14-
from datetime import datetime
14+
from datetime import datetime, timezone
1515
# from flask import url_for
1616

1717
sys.path.append(os.environ['AIL_BIN'])
@@ -160,10 +160,14 @@ def _get_messages(self, nb=-1, page=-1):
160160
return messages, {'nb': nb, 'page': page, 'nb_pages': nb_pages, 'total': total, 'nb_first': nb_first, 'nb_last': nb_last}
161161

162162
def get_timestamp_first_message(self):
163-
return r_object.zrange(f'messages:{self.type}:{self.subtype}:{self.id}', 0, 0, withscores=True)
163+
first = r_object.zrange(f'messages:{self.type}:{self.subtype}:{self.id}', 0, 0, withscores=True)
164+
if first:
165+
return int(first[0][1])
164166

165167
def get_timestamp_last_message(self):
166-
return r_object.zrevrange(f'messages:{self.type}:{self.subtype}:{self.id}', 0, 0, withscores=True)
168+
last = r_object.zrevrange(f'messages:{self.type}:{self.subtype}:{self.id}', 0, 0, withscores=True)
169+
if last:
170+
return int(last[0][1])
167171

168172
def get_first_message(self):
169173
return r_object.zrange(f'messages:{self.type}:{self.subtype}:{self.id}', 0, 0)
@@ -223,6 +227,38 @@ def get_nb_week_messages(self):
223227
nb_day += 1
224228
return stats
225229

230+
def get_message_years(self):
231+
timestamp = datetime.utcfromtimestamp(float(self.get_timestamp_first_message()))
232+
year_start = int(timestamp.strftime('%Y'))
233+
timestamp = datetime.utcfromtimestamp(float(self.get_timestamp_last_message()))
234+
year_end = int(timestamp.strftime('%Y'))
235+
return list(range(year_start, year_end + 1))
236+
237+
def get_nb_year_messages(self, year):
238+
nb_year = {}
239+
nb_max = 0
240+
start = int(datetime(year, 1, 1, 0, 0, 0, tzinfo=timezone.utc).timestamp())
241+
end = int(datetime(year, 12, 31, 23, 59, 59, tzinfo=timezone.utc).timestamp())
242+
243+
for mess_t in r_object.zrangebyscore(f'messages:{self.type}:{self.subtype}:{self.id}', start, end, withscores=True):
244+
timestamp = datetime.utcfromtimestamp(float(mess_t[1]))
245+
date = timestamp.strftime('%Y-%m-%d')
246+
if date not in nb_year:
247+
nb_year[date] = 0
248+
nb_year[date] += 1
249+
nb_max = max(nb_max, nb_year[date])
250+
251+
subchannels = self.get_subchannels()
252+
for gid in subchannels:
253+
for mess_t in r_object.zrangebyscore(f'messages:{gid}', start, end, withscores=True):
254+
timestamp = datetime.utcfromtimestamp(float(mess_t[1]))
255+
date = timestamp.strftime('%Y-%m-%d')
256+
if date not in nb_year:
257+
nb_year[date] = 0
258+
nb_year[date] += 1
259+
260+
return nb_max, nb_year
261+
226262
def get_message_meta(self, message, timestamp=None, translation_target='', options=None): # TODO handle file message
227263
message = Messages.Message(message[9:])
228264
if not options:

var/www/blueprints/chats_explorer.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,12 @@ def chats_explorer_instance():
7676
chat_instance = chat_instance[0]
7777
return render_template('chat_instance.html', chat_instance=chat_instance)
7878

79+
@chats_explorer.route("chats/explorer/chats/selector", methods=['GET'])
80+
@login_required
81+
@login_read_only
82+
def chats_explorer_chats_selector():
83+
return jsonify(chats_viewer.api_get_chats_selector())
84+
7985
@chats_explorer.route("chats/explorer/chat", methods=['GET'])
8086
@login_required
8187
@login_read_only
@@ -123,6 +129,20 @@ def chats_explorer_messages_stats_week_all():
123129
else:
124130
return jsonify(week[0])
125131

132+
@chats_explorer.route("chats/explorer/messages/stats/year", methods=['GET'])
133+
@login_required
134+
@login_read_only
135+
def chats_explorer_messages_stats_year():
136+
chat_type = request.args.get('type')
137+
instance_uuid = request.args.get('subtype')
138+
chat_id = request.args.get('id')
139+
year = request.args.get('year')
140+
stats = chats_viewer.api_get_nb_year_messages(chat_type, instance_uuid, chat_id, year)
141+
if stats[1] != 200:
142+
return create_json_response(stats[0], stats[1])
143+
else:
144+
return jsonify(stats[0])
145+
126146
@chats_explorer.route("/chats/explorer/subchannel", methods=['GET'])
127147
@login_required
128148
@login_read_only

var/www/templates/chats_explorer/chat_viewer.html

Lines changed: 51 additions & 100 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
<html>
33

44
<head>
5-
<title>Chats Protocols - AIL</title>
5+
<title>Chat - AIL</title>
66
<link rel="icon" href="{{ url_for('static', filename='image/ail-icon.png') }}">
77

88
<!-- Core CSS -->
@@ -18,6 +18,7 @@
1818
<script src="{{ url_for('static', filename='js/dataTables.bootstrap.min.js')}}"></script>
1919
<script src="{{ url_for('static', filename='js/d3.min.js')}}"></script>
2020
<script src="{{ url_for('static', filename='js/d3/heatmap_week_hour.js')}}"></script>
21+
<script src="{{ url_for('static', filename='js/echarts.min.js')}}"></script>
2122

2223
<style>
2324
.chat-message-left,
@@ -112,6 +113,9 @@ <h5 class="mx-5 text-secondary">This week:</h5>
112113
<div id="heatmapweekhour"></div>
113114
{% endif %}
114115

116+
<h5>Messages by year:</h5>
117+
<div id="heatmapyear" style="width: 100%;height: 300px;"></div>
118+
115119
{% with translate_url=url_for('chats_explorer.chats_explorer_chat', subtype=chat['subtype']), obj_id=chat['id'], pagination=chat['pagination'] %}
116120
{% include 'chats_explorer/block_translation.html' %}
117121
{% endwith %}
@@ -218,109 +222,56 @@ <h5 class="mx-5 text-secondary">This week:</h5>
218222
})
219223
{% endif %}
220224

221-
/*
222-
223-
// set the dimensions and margins of the graph
224-
const margin = {top: 30, right: 30, bottom: 30, left: 30},
225-
width = 450 - margin.left - margin.right,
226-
height = 450 - margin.top - margin.bottom;
227-
228-
// append the svg object to the body of the page
229-
const svg = d3.select("#my_dataviz")
230-
.append("svg")
231-
.attr("width", width + margin.left + margin.right)
232-
.attr("height", height + margin.top + margin.bottom)
233-
.append("g")
234-
.attr("transform", `translate(${margin.left},${margin.top})`);
235-
236-
// Labels of row and columns
237-
const myGroups = ["A", "B", "C", "D", "E", "F", "G", "H", "I", "J"]
238-
const myVars = ["v1", "v2", "v3", "v4", "v5", "v6", "v7", "v8", "v9", "v10"]
239-
240-
//Read the data
241-
d3.csv("").then( function(data) {
242-
243-
// Labels of row and columns -> unique identifier of the column called 'group' and 'variable'
244-
const myGroups = Array.from(new Set(data.map(d => d.group)))
245-
const myVars = Array.from(new Set(data.map(d => d.variable)))
246-
247-
// Build X scales and axis:
248-
const x = d3.scaleBand()
249-
.range([ 0, width ])
250-
.domain(myGroups)
251-
.padding(0.05);
252-
svg.append("g")
253-
.style("font-size", 15)
254-
.attr("transform", `translate(0, ${height})`)
255-
.call(d3.axisBottom(x).tickSize(0))
256-
.select(".domain").remove()
257-
258-
// Build Y scales and axis:
259-
const y = d3.scaleBand()
260-
.range([ height, 0 ])
261-
.domain(myVars)
262-
.padding(0.01);
263-
svg.append("g")
264-
.style("font-size", 15)
265-
.call(d3.axisLeft(y).tickSize(0))
266-
.select(".domain").remove()
267-
268-
// Build color scale
269-
const myColor = d3.scaleSequential()
270-
.interpolator(d3.interpolateInferno)
271-
.domain([1,100])
272-
273-
// create a tooltip
274-
const tooltip = d3.select("#my_dataviz")
275-
.append("div")
276-
.style("opacity", 0)
277-
.attr("class", "tooltip")
278-
.style("background-color", "white")
279-
.style("border", "solid")
280-
.style("border-width", "2px")
281-
.style("border-radius", "5px")
282-
.style("padding", "5px")
283-
284-
// Three function that change the tooltip when user hover / move / leave a cell
285-
const mouseover = function(event,d) {
286-
tooltip.style("opacity", 1)
287-
d3.select(this)
288-
.style("stroke", "black")
289-
.style("opacity", 1)
290-
}
291-
const mousemove = function(event,d) {
292-
tooltip.html("The exact value of<br>this cell is: " + d)
293-
.style("left", (event.x)/2 + "px")
294-
.style("top", (event.y)/2 + "px")
295-
}
296-
const mouseleave = function(d) {
297-
tooltip.style("opacity", 0)
298-
d3.select(this)
299-
.style("stroke", "none")
300-
.style("opacity", 0.8)
301-
}
302225

226+
var heatyearChart = echarts.init(document.getElementById('heatmapyear'));
227+
window.addEventListener('resize', function() {
228+
heatyearChart.resize();
229+
});
230+
var optionheatmap;
303231

304-
svg.selectAll()
305-
.data(data, function(d) {return d.group+':'+d.variable;})
306-
.join("rect")
307-
.attr("x", function(d) { return x(d.group) })
308-
.attr("y", function(d) { return y(d.variable) })
309-
.attr("rx", 4)
310-
.attr("ry", 4)
311-
.attr("width", x.bandwidth() )
312-
.attr("height", y.bandwidth() )
313-
.style("fill", function(d) { return myColor(d.value)} )
314-
.style("stroke-width", 4)
315-
.style("stroke", "none")
316-
.style("opacity", 0.8)
317-
.on("mouseover", mouseover)
318-
.on("mousemove", mousemove)
319-
.on("mouseleave", mouseleave)
232+
optionheatmap = {
233+
tooltip: {
234+
position: 'top',
235+
formatter: function (p) {
236+
//const format = echarts.time.format(p.data[0], '{yyyy}-{MM}-{dd}', false);
237+
return p.data[0] + ': ' + p.data[1];
238+
}
239+
},
240+
visualMap: {
241+
min: 0,
242+
max: 100,
243+
calculable: true,
244+
orient: 'horizontal',
245+
left: '500',
246+
top: '-10'
247+
},
248+
calendar: [
249+
{
250+
orient: 'horizontal',
251+
//range: new Date().getFullYear(),
252+
range: '2024',
253+
},
254+
],
255+
series: [
256+
{
257+
type: 'heatmap',
258+
coordinateSystem: 'calendar',
259+
data: []
260+
},
261+
262+
]
263+
};
264+
heatyearChart.setOption(optionheatmap);
265+
266+
$.getJSON("{{ url_for('chats_explorer.chats_explorer_messages_stats_year') }}?type=chat&subtype={{ chat['subtype'] }}&id={{ chat['id'] }}")
267+
.done(function(data) {
268+
optionheatmap['visualMap']['max'] = data['max']
269+
optionheatmap['series'][0]['data'] = data['nb']
270+
heatyearChart.setOption(optionheatmap)
320271

321-
})
272+
}
273+
);
322274

323-
*/
324275
</script>
325276

326277

0 commit comments

Comments
 (0)