diff --git a/bin/lib/chats_viewer.py b/bin/lib/chats_viewer.py
index 34ab6157..0654d9e9 100755
--- a/bin/lib/chats_viewer.py
+++ b/bin/lib/chats_viewer.py
@@ -465,6 +465,22 @@ def get_user_account_nb_all_week_messages(user_id, chats, subchannels):
nb_day += 1
return stats
+def get_user_account_usernames_timeline(subtype, user_id):
+ user_account = UsersAccount.UserAccount(user_id, subtype)
+ usernames = user_account.get_usernames_history()
+ if usernames:
+ for row in usernames:
+ row['obj'] = row['obj'].rsplit(':', 1)[1]
+ if row['start'] > row['end']:
+ t = row['start']
+ row['start'] = row['end']
+ row['end'] = t
+ if row['start'] == row['end']:
+ row['end'] = row['end'] + 1
+ row['start'] = row['start'] * 1000
+ row['end'] = row['end'] * 1000
+ return usernames
+
def get_user_account_chats_chord(subtype, user_id):
nb = {}
user_account = UsersAccount.UserAccount(user_id, subtype)
@@ -733,7 +749,7 @@ def api_get_user_account(user_id, instance_uuid, translation_target=None):
user_account = UsersAccount.UserAccount(user_id, instance_uuid)
if not user_account.exists():
return {"status": "error", "reason": "Unknown user-account"}, 404
- meta = user_account.get_meta({'chats', 'icon', 'info', 'subchannels', 'threads', 'translation', 'username', 'username_meta'}, translation_target=translation_target)
+ meta = user_account.get_meta({'chats', 'icon', 'info', 'subchannels', 'threads', 'translation', 'username', 'usernames', 'username_meta'}, translation_target=translation_target)
if meta['chats']:
meta['chats'] = get_user_account_chats_meta(user_id, meta['chats'], meta['subchannels'])
return meta, 200
diff --git a/bin/lib/objects/UsersAccount.py b/bin/lib/objects/UsersAccount.py
index 950b2c1c..dea1b7a2 100755
--- a/bin/lib/objects/UsersAccount.py
+++ b/bin/lib/objects/UsersAccount.py
@@ -132,7 +132,15 @@ class UserAccount(AbstractSubtypeObject):
return self._get_timeline_username().get_last_obj_id()
def get_usernames(self):
- return self._get_timeline_username().get_objs_ids()
+ usernames = []
+ names = self._get_timeline_username().get_objs_ids()
+ for name in names:
+ _, subtype, obj_id = name.split(':', 2)
+ usernames.append({'type': 'username', 'subtype': subtype, 'id': obj_id})
+ return usernames
+
+ def get_usernames_history(self):
+ return self._get_timeline_username().get_objs()
def update_username_timeline(self, username_global_id, timestamp):
self._get_timeline_username().add_timestamp(timestamp, username_global_id)
diff --git a/bin/lib/timeline_engine.py b/bin/lib/timeline_engine.py
index 58c222f6..5d6ad81e 100755
--- a/bin/lib/timeline_engine.py
+++ b/bin/lib/timeline_engine.py
@@ -112,9 +112,29 @@ class Timeline:
for block in r_meta.zrange(f'line:{self.id}:{self.name}', 0, -1):
if block:
if block.startswith('start:'):
+ print(self._get_block_obj_global_id(block[6:]))
objs.add(self._get_block_obj_global_id(block[6:]))
return objs
+ def get_objs(self):
+ objs = []
+ blocks = r_meta.zrange(f'line:{self.id}:{self.name}', 0, -1, withscores=True)
+ for i in range(0, len(blocks), 2):
+ block1, score1 = blocks[i]
+ block2, score2 =blocks[i + 1]
+ score1 = int(score1)
+ score2 = int(score2)
+ if block1.startswith('start:'):
+ start = score1
+ end = score2
+ obj = self._get_block_obj_global_id(block1[6:])
+ else:
+ start = score2
+ end = score1
+ obj = self._get_block_obj_global_id(block2[6:])
+ objs.append({'obj': obj, 'start': start, 'end': end})
+ return objs
+
# def get_objs_ids(self):
# objs = {}
# last_obj_id = None
diff --git a/var/www/blueprints/chats_explorer.py b/var/www/blueprints/chats_explorer.py
index fd2f8b34..83126b1e 100644
--- a/var/www/blueprints/chats_explorer.py
+++ b/var/www/blueprints/chats_explorer.py
@@ -288,6 +288,9 @@ def objects_user_account():
if target == "Don't Translate":
target = None
user_account = chats_viewer.api_get_user_account(user_id, instance_uuid, translation_target=target)
+ # print()
+ # print(user_account[0]['usernames'])
+ # print()
if user_account[1] != 200:
return create_json_response(user_account[0], user_account[1])
else:
@@ -297,6 +300,15 @@ def objects_user_account():
ail_tags=Tag.get_modal_add_tags(user_account['id'], user_account['type'], user_account['subtype']),
translation_languages=languages, translation_target=target)
+@chats_explorer.route("/objects/user-account_usernames_timeline_json", methods=['GET']) # TODO API
+@login_required
+@login_read_only
+def objects_user_account_usernames_timeline_json():
+ subtype = request.args.get('subtype')
+ user_id = request.args.get('id')
+ json_graph = chats_viewer.get_user_account_usernames_timeline(subtype, user_id)
+ return jsonify(json_graph)
+
@chats_explorer.route("/objects/user-account_chats_chord_json", methods=['GET']) # TODO API
@login_required
@login_read_only
diff --git a/var/www/static/js/d3/chord_directed_diagram.js b/var/www/static/js/d3/chord_directed_diagram.js
index 5be3f294..9ffa6a49 100644
--- a/var/www/static/js/d3/chord_directed_diagram.js
+++ b/var/www/static/js/d3/chord_directed_diagram.js
@@ -8,6 +8,10 @@
const create_directed_chord_diagram = (container_id, data, min_value= 0, max_value = -1, fct_mouseover, fct_mouseout, options) => {
+ if(!Object.keys(data.data).length){
+ return;
+ }
+
// Filter data by value between target and source
if (min_value > 0) {
data.data = data.data.filter(function(value) {
diff --git a/var/www/static/js/d3/timeline_basic.js b/var/www/static/js/d3/timeline_basic.js
new file mode 100644
index 00000000..ce3f2415
--- /dev/null
+++ b/var/www/static/js/d3/timeline_basic.js
@@ -0,0 +1,119 @@
+//Requirement: - D3v7
+// - jquery
+
+// container_id = #container_id "data": [{"obj": username, "start": 1111111100000, "end": 2222222200000}, ...]
+// tooltip = d3 tooltip object
+
+const create_timeline_basic = (container_id, data) => {
+
+ if(!Object.keys(data).length){
+ return;
+ }
+
+ const width = 800;
+ const height = 100;
+ const margin = { top: 10, right: 10, bottom: 40, left: 40 };
+
+ const colorScale = d3.scaleOrdinal(d3.schemeCategory10)
+ .domain(data.map(d => d.obj));
+
+ const xScale = d3.scaleTime()
+ .domain([d3.min(data, d => d.start), d3.max(data, d => d.end)])
+ .range([margin.left, width - margin.right]);
+
+ const svg = d3.select(container_id)
+ .append("svg")
+ .attr("width", width + margin.left + margin.right)
+ .attr("height", height + margin.top + margin.bottom)
+
+ const tooltip = d3.select("body")
+ .append("div")
+ .attr("class", "tooltip_basic_timeline")
+ .style("opacity", 0)
+ //d3.select(".tooltip")
+ .style("position", "absolute")
+ .style("text-align", "center")
+ .style("padding", " 8px")
+ .style("font", "sans-serif")
+ .style("font-size", "12px")
+ .style("background", "whitesmoke")
+ .style("border", "0px")
+ .style("border-radius", "8px")
+ .style("pointer-events", "none");
+
+ // date-time format
+ const dateTimeFormat = d3.timeFormat("%Y-%m-%d %H:%M");
+
+ svg.selectAll("rect")
+ .data(data)
+ .enter()
+ .append("rect")
+ .attr("x", d => xScale(d.start))
+ .attr("y", 20)
+ .attr("width", d => xScale(d.end) - xScale(d.start))
+ .attr("height", 20)
+ .attr("fill", d => colorScale(d.obj))
+ .on("mouseover", function(event, d) {
+ tooltip.transition()
+ .duration(200)
+ .style("opacity", .9);
+ tooltip.html(`${d.obj}
⏳ ${dateTimeFormat(new Date(d.start))}
⌛ ${dateTimeFormat(new Date(d.end))}`)
+ .style("left", (event.pageX + 5) + "px")
+ .style("top", (event.pageY - 28) + "px");
+ })
+ .on("mousemove", function(event) {
+ tooltip.style("left", (event.pageX + 5) + "px")
+ .style("top", (event.pageY - 28) + "px");
+ })
+ .on("mouseout", function() {
+ tooltip.transition()
+ .duration(500)
+ .style("opacity", 0);
+ });
+
+ svg.selectAll("text")
+ .data(data)
+ .enter()
+ .append("text")
+ .attr("x", d => xScale(d.start) + 5)
+ .attr("y", 35)
+ .text(d => d.obj)
+ .attr("fill", "white")
+ .style("pointer-events", "none");
+
+ // Date format
+ const dateFormat = d3.timeFormat("%Y-%m-%d");
+
+ // x-axis
+ const xAxis = d3.axisBottom(xScale)
+ .ticks(d3.timeMonth.every(1))
+ .tickFormat(dateFormat);
+
+ svg.append("g")
+ .attr("transform", `translate(0,${height - margin.bottom})`)
+ .call(xAxis)
+ .selectAll("text")
+ .attr("transform", "rotate(-45)")
+ .style("text-anchor", "end");
+
+ const startDate = new Date(d3.min(data, d => d.start));
+ const endDate = new Date(d3.max(data, d => d.end));
+
+ svg.append("text")
+ .attr("x", margin.left)
+ .attr("y", height - margin.bottom + 14)
+ .style("text-anchor", "end")
+ .style("font-size", "10px")
+ .attr("transform", `rotate(-90, ${margin.left}, ${height - margin.bottom + 10})`)
+ .text(dateFormat(startDate));
+
+ svg.append("text")
+ .attr("x", width - margin.right)
+ .attr("y", height - margin.bottom + 14)
+ .style("text-anchor", "end")
+ .style("font-size", "10px")
+ .attr("transform", `rotate(-90, ${width - margin.right}, ${height - margin.bottom + 10})`)
+ .text(dateFormat(endDate));
+
+ //return svg.node();
+}
diff --git a/var/www/templates/chats_explorer/card_user_account.html b/var/www/templates/chats_explorer/card_user_account.html
index a892c3ca..ee3fa8b4 100644
--- a/var/www/templates/chats_explorer/card_user_account.html
+++ b/var/www/templates/chats_explorer/card_user_account.html
@@ -19,7 +19,7 @@
username | +usernames | ID | First Seen | Last Seen | @@ -28,7 +28,17 @@
---|---|---|---|---|
{{ meta['username']['id'] }} | +
+ {% if 'usernames' in meta %}
+
|
{{ meta['id'] }} |
{% if meta['first_seen'] %}
diff --git a/var/www/templates/chats_explorer/user_account.html b/var/www/templates/chats_explorer/user_account.html
index a1832af3..627c4b87 100644
--- a/var/www/templates/chats_explorer/user_account.html
+++ b/var/www/templates/chats_explorer/user_account.html
@@ -20,6 +20,7 @@
+
@@ -47,6 +48,9 @@
User All Messages:+Usernames:+ +Numbers of Messages Posted by Chat:@@ -85,6 +89,13 @@ {# {% endif %}#} }); + +let url_t = "{{ url_for('chats_explorer.objects_user_account_usernames_timeline_json') }}?subtype={{ meta["subtype"] }}&id={{ meta["id"] }}" +d3.json(url_t) + .then(function(data) { + create_timeline_basic('#timeline_user_usernames', data); +}); + d3.json("{{ url_for('chats_explorer.user_account_messages_stats_week_all') }}?subtype={{ meta['subtype'] }}&id={{ meta['id'] }}") .then(function(data) { create_heatmap_week_hour('#heatmapweekhourall', data); |