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

Add aviary metadata to the dashboard and add ability to copy cell values #131

Merged
merged 16 commits into from
Feb 14, 2024
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
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
17 changes: 17 additions & 0 deletions aviary/visualization/assets/aviary_vars/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,28 @@
<title>Aviary Dashboard</title>
<link rel='stylesheet' href='https://unpkg.com/[email protected]/dist/css/tabulator.min.css'>

<style>
.tabulator-tooltip {
background-color: rgb(164, 227, 215);
white-space: pre; /* This will respect the line breaks and white space in the text */
max-width:Min(1200px,100%);
}

.copy-button {
margin-right: 5px;
padding: 1px;
font-size: x-small;
}
</style>


</head>

<!-- this page is accessed with http://localhost:5000/assets/aviary_vars/index.html -->

<body>
<input type="checkbox" id="aviary-vars-filter"> Show Only Aviary Variables

<!-- partial:index.partial.html -->
<div id="example-table"></div>
<!-- partial -->
Expand Down
335 changes: 274 additions & 61 deletions aviary/visualization/assets/aviary_vars/script.js
Original file line number Diff line number Diff line change
@@ -1,71 +1,284 @@
$(function () {

// Read in the json file that the dashboard.py script generated
// fetch('/home/aviary_vars.json')
fetch('./aviary_vars.json')
var table;

var MAX_LINE_LENGTH = 80; // make sure the description field in the tooltip
// does not exceed this length

// Read in the json file that the dashboard.py script generated
// fetch('/home/aviary_vars.json')
hschilling marked this conversation as resolved.
Show resolved Hide resolved
fetch('./aviary_vars.json')
.then((response) => response.json())
.then((json) => createTabulator(json));

function displayValueInToolTip(e, cell, onRendered) {
//e - mouseover event
//cell - cell component
//onRendered - onRendered callback registration function

var el = document.createElement("div");
el.style.backgroundColor = "lightgreen";
// el.innerText = cell.getColumn().getField() + " - " + cell.getValue(); //return cells "field - value";
el.innerText = cell.getValue(); //return cells "field - value";

return el;
};

// Given the variable data as a JSON object, create the Tabulator displays on the Web page
function createTabulator(tableData)
{
var table = new Tabulator("#example-table", {
height:"100%",
data: tableData,
dataTree: true,
dataTreeFilter: false,
headerSort: true,
movableColumns: false,
dataTreeStartExpanded: false,
// fitData - not a big difference maybe because already gave fixed width to the columns
// fitDataFill - not a big difference maybe because already gave fixed width to the columns
// fitDataTable - not a big difference maybe because already gave fixed width to the columns
// fitColumns - not a big difference maybe because already gave fixed width to the columns
// fitDataStretch - uses the full width for the value, which is good
layout:"fitDataStretch",
columns: [
{
title: "Absolute Name",
field: "abs_name",
headerFilter: "input",
width: 550,
},
{
title: "Promoted Name",
field: "prom_name",
headerFilter: "input",
width: 300,
tooltip: true,
},
{
title: "Value",
field: "value",
width: 300,
tooltip: (e, cell, onRendered) => displayValueInToolTip(e, cell, onRendered),
},
{
title: "Units",
field: "units",
width: 200,
tooltip: (e, cell, onRendered) => displayValueInToolTip(e, cell, onRendered),
},
]
// Add event listener to the checkbox for showing only aviary variables
document.getElementById('aviary-vars-filter').addEventListener('change', function () {
if (this.checked) {
applyRegexFilter();
} else {
removeFilter();
}
});

}
// Function to apply the regex filter
function applyRegexFilter() {
var regex = new RegExp("^(aircraft:|mission:)"); // Replace with your regex
table.setFilter((data) => {
return regex.test(data.prom_name); // Assuming you are filtering the 'name' column
});
}

// Function to remove the filter
function removeFilter() {
table.clearFilter();
}

function isDictionary(obj) {
return typeof obj === 'object' && obj !== null && !Array.isArray(obj) && !(obj instanceof Function);
}

// function to count the initial spaces in a string
// format floating point numbers using decimal or scientific notation
// as needed
function formatNumberIntelligently(value, precision = 3) {
// Determine the absolute value to decide on the formatting
const absValue = Math.abs(value);

// Define thresholds for using scientific notation
const upperThreshold = 1e5;
const lowerThreshold = 1e-3;

// Use scientific notation for very small or very large numbers
if (absValue >= upperThreshold || (absValue > 0 && absValue < lowerThreshold)) {
return value.toExponential(precision);
}

// Use fixed-point notation for numbers within a 'normal' range, ensuring the precision is respected
// Adjust the maximumFractionDigits based on the integer part length to maintain overall precision
const integerPartLength = Math.floor(value) === 0 ? 1 : Math.floor(Math.log10(Math.abs(Math.floor(value)))) + 1;
const adjustedPrecision = Math.max(0, precision - integerPartLength);

return value.toLocaleString('en-US', {
minimumFractionDigits: adjustedPrecision,
maximumFractionDigits: precision,
useGrouping: false,
});
}

/**
* Convert an element to a string that is human readable.
* @param {Object} element The scalar item to convert.
* @returns {String} The string representation of the element.
*/
function elementToString(element) {
if (typeof element === 'number') {
if (Number.isInteger(element)) { return element.toString(); }
// return this.floatFormatter(element); /* float */
hschilling marked this conversation as resolved.
Show resolved Hide resolved
return formatNumberIntelligently(element); /* float */
}

if (element === 'nan') { return element; }
return JSON.stringify(element);
}

/**
* Convert a value to a string that can be used in Python code.
* @param {Object} val The value to convert.
* @returns {String} The string of the converted object.
*/
function valToCopyString(val, isTopLevel = true) {
if (!Array.isArray(val)) { return elementToString(val); }
let valStr;

if (isTopLevel) {
valStr = 'array([';
} else {
valStr = '[';
}
for (const element of val) {
valStr += valToCopyString(element, false) + ', ';
}

if (val.length > 0) {
if (isTopLevel) {
return valStr.replace(/^(.+)(, )$/, '$1])');
} else {
return valStr.replace(/^(.+)(, )$/, '$1]');
}
}
}

function countInitialSpaces(str) {
// This regex matches the initial spaces in the string
const match = str.match(/^\s*/);
// If there's a match, return its length; otherwise, return 0
return match ? match[0].length : 0;
}

// split a string so that it isn't too long for the tooltip
function splitStringIntoLines(str, maxLineLength = 80) {
// need to preserve the initial spaces of the string!
lengthOfInitialSpaces = countInitialSpaces(str);
initialWordOfSpaces = " ".repeat(lengthOfInitialSpaces);
const words = str.trim().split(' ');
words.unshift(initialWordOfSpaces);
const lines = [];
let currentLine = '';

words.forEach(word => {
// Truncate the word if it's longer than maxLineLength
if (word.length > maxLineLength) {
word = word.substring(0, maxLineLength);
}

if ((currentLine + word).length > maxLineLength) {
lines.push(currentLine);
currentLine = word;
} else {
currentLine += word + ' ';
}
});

if (currentLine) {
lines.push(currentLine);
}

return lines;
}

// format each individual string for metadata
function formatIndividualMetadataString(preText, item, maxLineLength = 80) {
indent = preText.length;
resultString = "";
s = JSON.stringify(item);
lines = splitStringIntoLines(preText + s, maxLineLength = MAX_LINE_LENGTH);
lines.forEach((line, index) => {
if (index == 0) {
resultString += line + "\n";
}
else {
resultString += " ".repeat(indent) + line + "\n";
}
})
return resultString;
}

// format the entire text for the metadata tooltip
function formatMetadataTooltip(cell) {
prom_name = cell.getValue();
metadata = cell.getData().metadata;

// Initialize a string to hold the resulting string
let resultString = "prom_name: " + prom_name + "\n";

dictObject = JSON.parse(metadata);

// Iterate over each key-value pair
for (let key in dictObject) {
if (dictObject.hasOwnProperty(key)) {
// Append key and value to the result string
if (isDictionary(dictObject[key])) {
resultString += key + ": " + "\n";
for (let hnkey in dictObject[key]) {
if (Array.isArray(dictObject[key][hnkey])) {
resultString += " " + hnkey + ": \n";
for (let item of dictObject[key][hnkey]) {
resultString += formatIndividualMetadataString(" ", item);
}
}
else {
resultString += formatIndividualMetadataString(" " + hnkey + ": ", dictObject[key][hnkey]);
}
}
}
else {
resultString += formatIndividualMetadataString(key + ": ", dictObject[key]);
}
}
}
return resultString;
}

// The Tabulator formatter function used to include a button to copy the
// value in the cell
function copyButtonFormatter(cell, formatterParams, onRendered) {
var cellContent = document.createElement("span");
var cellValue = cell.getValue();
if (cellValue.length === 0) {
return "";
}

var button = document.createElement("button");
button.textContent = "Copy";
button.classList.add("copy-button");

// Append the button to the cellContent container
cellContent.appendChild(button);

var text = document.createElement("value");
text.textContent = JSON.stringify(cellValue);
cellContent.appendChild(text);

onRendered(function () {
$(button).on('click', (e) => {
e.stopPropagation();
copiedCellValue = valToCopyString(cellValue);
navigator.clipboard.writeText(copiedCellValue);
});
}
);

return cellContent;
}

// Given the variable data as a JSON object, create the Tabulator displays on the Web page
function createTabulator(tableData) {
table = new Tabulator("#example-table", {
height: "100%",
data: tableData,
dataTree: true,
dataTreeFilter: false,
headerSort: true,
movableColumns: false,
dataTreeStartExpanded: false,
layout: "fitDataStretch",
columns: [
{
title: "Absolute Name",
field: "abs_name",
headerFilter: "input",
width: 550,
tooltip: function (e, cell) {
return formatMetadataTooltip(cell);
},
formatter: copyButtonFormatter
},
{
title: "Promoted Name",
field: "prom_name",
headerFilter: "input",
width: 300,
tooltip: function (e, cell) {
return formatMetadataTooltip(cell);
},
formatter: copyButtonFormatter
},
{
title: "Value",
field: "value",
width: 300,
tooltip: function (e, cell) {
// return formatValueTooltip(cell);
return cell.getValue();
},
formatter: copyButtonFormatter
},
{
title: "Units",
field: "units",
width: 120,
},
]
});
}
});
Loading
Loading