Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added content filter control to event notification HTML page. #324

Merged
merged 3 commits into from
Jul 3, 2024
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading