diff --git a/pg_lupa/lupa.py b/pg_lupa/lupa.py index 56bf5c1..04625e0 100644 --- a/pg_lupa/lupa.py +++ b/pg_lupa/lupa.py @@ -15,7 +15,7 @@ import pkg_resources import pydantic -__version__ = "0.0.2" +__version__ = "0.0.3" def format_duration(d: datetime.timedelta) -> str: @@ -385,6 +385,7 @@ class ProcessVizData(pydantic.BaseModel): id: str y: int height: int + pid: int class StatementVizData(pydantic.BaseModel): @@ -419,6 +420,8 @@ class VizData(pydantic.BaseModel): total_duration_string: str start_time_string: str end_time_string: str + start_time_unix_seconds: float + end_time_unix_seconds: float def classify_sql(sql: str) -> str: @@ -640,6 +643,8 @@ def format_datetime(t: datetime.datetime) -> str: total_duration_string=format_duration(max_time_dt - min_time_dt), start_time_string=format_datetime(min_time_dt), end_time_string=format_datetime(max_time_dt), + start_time_unix_seconds=min_time_dt.timestamp(), + end_time_unix_seconds=max_time_dt.timestamp(), total_height=len(pids) * bar_height, ) @@ -649,6 +654,7 @@ def format_datetime(t: datetime.datetime) -> str: id=f"process_{pid}", y=index * bar_height, height=bar_height, + pid=pid, ) ) diff --git a/pg_lupa/resources/lupa.embed.js b/pg_lupa/resources/lupa.embed.js index 60376ee..693b8c6 100644 --- a/pg_lupa/resources/lupa.embed.js +++ b/pg_lupa/resources/lupa.embed.js @@ -10,7 +10,7 @@ function unfocus() { contentLockedToID = null; } -function setContent(id, content, lock, processes1, processes2) { +function setSidebarContent(id, content, lock, processes1, processes2) { if (contentLockedToID && !lock) return; const alreadyLockedToThis = contentLockedToID && contentLockedToID === id; @@ -21,10 +21,13 @@ function setContent(id, content, lock, processes1, processes2) { } $("#context-info").html(content); - contentLockedToID = lock ? id : null; + contentLockedToID = lock && id ? id : null; $(".context-highlight").removeClass("context-highlight"); - $("#" + id).addClass("context-highlight"); + + if (id) { + $("#" + id).addClass("context-highlight"); + } $(".process-highlight").removeClass( "process-primary-highlight process-secondary-highlight" @@ -45,8 +48,15 @@ function setContent(id, content, lock, processes1, processes2) { } } +function resetSidebarContent(force) { + setSidebarContent(null, "", force); +} + function draw() { - const width = $("#timeline").width() - 20; + const rightSpacing = 20; + const topLegendSpace = 30; + const leftLegendSpace = 50; + const width = $("#timeline").width() - rightSpacing - leftLegendSpace; const data = JSON.parse(document.getElementById("data").textContent); const div = d3 @@ -58,9 +68,25 @@ function draw() { const svg = d3 .select("#timeline") .append("svg") - .attr("width", width) + .attr("width", width + leftLegendSpace) .attr("height", data.total_height); + const scale = d3 + .scaleLinear() + .domain([data.start_time_unix_seconds, data.end_time_unix_seconds]) + .range([leftLegendSpace, width + leftLegendSpace]); + const axis = d3 + .axisBottom() + .scale(scale) + .tickFormat(function (x) { + const t = new Date(x * 1000); + const hs = t.getHours().toString().padStart(2, "0"); + const ms = t.getMinutes().toString().padStart(2, "0"); + const ss = t.getSeconds().toString().padStart(2, "0"); + return hs + ":" + ms + ":" + ss; + }); + svg.append("g").call(axis); + svg .selectAll() .data(data.processes) @@ -70,15 +96,32 @@ function draw() { return d.id; }) .attr("x", 0) - .attr("width", width) + .attr("width", width + leftLegendSpace) .attr("y", function (d) { - return d.y; + return d.y + topLegendSpace; }) .attr("height", function (d) { return d.height; }) .attr("class", "pg_process"); + svg + .selectAll() + .data(data.processes) + .enter() + .append("text") + .attr("x", 0) + .attr("y", function (d) { + return d.y + topLegendSpace; + }) + .attr("font-size", "10px") + .text(function (d) { + return "" + d.pid; + }) + .on("click", function (evt, d) { + evt.stopPropagation(); + }); + svg .selectAll() .data(data.statements) @@ -89,10 +132,12 @@ function draw() { }) .attr("class", "pg_stmt") .attr("x", function (d) { - return (d.t_offset / data.total_duration_seconds) * width; + return ( + leftLegendSpace + (d.t_offset / data.total_duration_seconds) * width + ); }) .attr("y", function (d) { - return d.y; + return d.y + topLegendSpace; }) .attr("width", function (d) { return (d.duration / data.total_duration_seconds) * width; @@ -104,13 +149,15 @@ function draw() { return d.colour; }) .on("click", function (evt, d) { - setContent(d.id, d.mouseover_content, true); + setSidebarContent(d.id, d.mouseover_content, true); + evt.stopPropagation(); }) .on("mouseover", function (evt, d) { - setContent(d.id, d.mouseover_content, false); + setSidebarContent(d.id, d.mouseover_content, false); + evt.stopPropagation(); }) .on("mouseout", function (d) { - setContent(null, "", false); + resetSidebarContent(false); }); svg @@ -122,39 +169,47 @@ function draw() { return d.id; }) .attr("cx", function (d) { - return (d.t_offset / data.total_duration_seconds) * width; + return ( + leftLegendSpace + (d.t_offset / data.total_duration_seconds) * width + ); }) .attr("fill", function (d) { return d.colour; }) .attr("cy", function (d) { - return d.cy; + return topLegendSpace + d.cy; }) .attr("r", function (d) { return d.size; }) .attr("class", "pg_event") .on("click", function (evt, d) { - setContent( + setSidebarContent( d.id, d.mouseover_content, true, d.primary_related_process_ids, d.secondary_related_process_ids ); + evt.stopPropagation(); }) .on("mouseover", function (evt, d) { - setContent( + setSidebarContent( d.id, d.mouseover_content, false, d.primary_related_process_ids, d.secondary_related_process_ids ); + evt.stopPropagation(); }) .on("mouseout", function (d) { - setContent(null, "", false); + resetSidebarContent(false); }); } +$("#timeline").on("click", function () { + resetSidebarContent(true); +}); + draw();