Skip to content

Commit

Permalink
Added content filter control to event notification HTML page. (#324)
Browse files Browse the repository at this point in the history
# New Features
- Added control for filtering events based on type name or content
  • Loading branch information
adamshapiro0 authored Jul 3, 2024
2 parents a0a08fd + c5c9923 commit 47cb284
Showing 1 changed file with 170 additions and 14 deletions.
184 changes: 170 additions & 14 deletions python/fusion_engine_client/analysis/analyzer.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,15 +49,15 @@
}


def _data_to_table(col_titles: List[str], values: List[List[Any]], row_major: bool = False):
def _data_to_table(col_titles: List[str], values: List[List[Any]], row_major: bool = False, id='table'):
if row_major:
# If values is row major (outer index is the table rows), transpose it.
col_values = list(map(list, zip(*values)))
else:
col_values = values

table_html = '''\
<table>
table_html = f'''\
<table id={id}>
<tbody style="vertical-align: top">
<tr style="background-color: #a2c4fa">
'''
Expand Down Expand Up @@ -909,21 +909,21 @@ def _plot_data(name, idx, source_id, marker_style=None):
if marker_style is not None:
style['marker'].update(marker_style)

# Only put default source ID on map by default.
legendgroup = None if len(self.source_ids) == 1 else source_id
visible = None if source_id == min(self.source_ids) else 'legendonly'

if np.any(idx):
text = ["Time: %.3f sec (%.3f sec)<br>Std (ENU): (%.2f, %.2f, %.2f) m" %
(t, t + float(self.t0), std[0], std[1], std[2])
for t, std in zip(time[idx], std_enu_m[:, idx].T)]
# Only put default source ID on map by default.
if source_id == min(self.source_ids):
map_data.append(go.Scattermapbox(lat=lla_deg[0, idx], lon=lla_deg[1, idx], name=name, text=text,
legendgroup=source_id, **style))
else:
map_data.append(go.Scattermapbox(lat=lla_deg[0, idx], lon=lla_deg[1, idx], name=name, text=text,
legendgroup=source_id, visible='legendonly', **style))
map_data.append(go.Scattermapbox(lat=lla_deg[0, idx], lon=lla_deg[1, idx], name=name, text=text,
legendgroup=legendgroup, visible=visible, **style))

else:
# If there's no data, draw a dummy trace so it shows up in the legend anyway.
map_data.append(go.Scattermapbox(lat=[np.nan], lon=[np.nan], name=name, visible='legendonly', **style))
map_data.append(go.Scattermapbox(lat=[np.nan], lon=[np.nan], name=name, legendgroup=legendgroup,
visible='legendonly', **style))

# Read the pose data.
for source_id in self.source_ids:
Expand Down Expand Up @@ -2213,10 +2213,166 @@ def plot_events(self):
if system_time_ns in times_before_resets:
rows[-1][2] = f'{(times_before_resets[system_time_ns]):.3f}'

table_html = _data_to_table(table_columns, rows, row_major=True)
body_html = f"""\
table_data = ',\n '.join([repr(row) for row in rows])
body_html = """\
<script>
// Reference: https://jsfiddle.net/ej7z5kdc/
class FilteredTable {
static next_checkbox_id = 0;
constructor(columns, data, filter_col_indices, filter_placeholder) {
this.columns = columns;
this.input_data = data;
this.filter_col_indices = filter_col_indices;
this.filter_placeholder = filter_placeholder;
}
/** @returns {{match: boolean, $node: Element}[]} */
search(filter, invert) {
if (!this.$tbody) {
return;
}
let count = 0;
let results = this.data.map(entry => {
const searchable_data = entry.searchable_data;
const $node = entry.$node;
let matches = false;
if (filter === "") {
matches = true;
}
else {
for (let i = 0; i < searchable_data.length; ++i) {
if (searchable_data[i].indexOf(filter) >= 0) {
matches = true;
break;
}
}
if (invert) {
matches = !matches;
}
}
if (matches) {
++count;
}
return {
match: matches,
$node,
};
});
return {count: count, results: results};
}
getControls() {
this._createTable();
return this.$controls;
}
getElement() {
this._createTable();
return this.$container;
}
_createTable() {
if (!this.$container) {
const $controls = document.createElement("div");
this.$controls = $controls;
let checkbox_id = "__checkbox_" + FilteredTable.next_checkbox_id++;
$controls.innerHTML = `
<div><input type="text" class="filter" style="width: 100%;" placeholder="${this.filter_placeholder}"></div>
<div>
<input type="checkbox" class="invert" id="${checkbox_id}">
<label for="${checkbox_id}"> Invert Selection</label>
</div>
<div>Displaying <div class="count" style="display: inline;"></div>/<div class="total" style="display: inline;"></div> elements.</div>`;
this.$filter = $controls.querySelector(".filter");
this.$invert = $controls.querySelector(".invert");
this.$count = $controls.querySelector(".count");
this.$total = $controls.querySelector(".total");
const $container = document.createElement("div");
this.$container = $container;
$container.innerHTML = `
<table><tbody style="vertical-align: top"></tbody></table>`;
this.$tbody = $container.querySelector("tbody");
// Bind a filter function to the controls.
const filterData = () => {
const filter = this.$filter.value.toLowerCase();
const invert = this.$invert.checked;
let results = this.search(filter, invert);
results.results.forEach(entry => entry.$node.style.display = entry.match ? "" : "none");
this.$count.textContent = results.count;
};
this.$filter.addEventListener("blur", filterData);
var typing_timer;
this.$filter.addEventListener("keydown", () => {
clearTimeout(typing_timer);
});
this.$filter.addEventListener("keyup", (event) => {
clearTimeout(typing_timer);
if (event.key === "Enter") {
filterData();
}
else {
typing_timer = setTimeout(filterData, 250);
}
});
this.$invert.addEventListener("change", filterData);
// Populate the table header.
const $header_tr = document.createElement("tr");
$header_tr.style = "background-color: #a2c4fa";
this.columns.map(text => {
const $td = document.createElement("th");
$td.innerHTML = text;
$header_tr.appendChild($td);
});
this.$tbody.appendChild($header_tr);
// Populate the table contents, and save a reference to the DOM row nodes with our data.
this.data = this.input_data.map(entry => {
const $tr = document.createElement("tr");
let searchable_data = [];
for (let col = 0; col < entry.length; ++col) {
const $td = document.createElement("td");
$td.innerHTML = entry[col];
$tr.appendChild($td);
if (this.filter_col_indices.indexOf(col) >= 0) {
searchable_data.push(entry[col].toLowerCase());
}
}
this.$tbody.appendChild($tr);
return {
$node: $tr,
searchable_data: searchable_data,
};
});
this.$count.textContent = this.data.length;
this.$total.textContent = this.data.length;
}
}
}
</script>
""" + f"""\
<h2>Device Event Log</h2>
<pre>{table_html}</pre>
<div class="controls"></div>
<pre><div class="table"></div></pre>
<script>
const column_headers = {repr(table_columns)};
const table_data = [
{table_data}
];
const filtered_table = new FilteredTable(column_headers, table_data, [3, 5], "Filter by event type or description...");
document.body.querySelector(".controls").appendChild(filtered_table.getControls());
document.body.querySelector(".table").appendChild(filtered_table.getElement());
</script>
"""

self._add_page(name='event_log', html_body=body_html, title="Event Log")
Expand Down

0 comments on commit 47cb284

Please sign in to comment.