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 UI features and algorithm to support label inference #762

Merged
merged 3 commits into from
Jul 1, 2021
Merged
Show file tree
Hide file tree
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
17 changes: 15 additions & 2 deletions www/css/main.diary.css
Original file line number Diff line number Diff line change
Expand Up @@ -110,19 +110,32 @@ a.item-content {
margin-left: 3px;
margin-top: 3px;
}
.btn-input-confirm-red,
.btn-input-confirm-red:hover,
.btn-input-confirm-red:active {
background-color: #ED2D3A;
color: white;
}
.btn-input-confirm-yellow,
.btn-input-confirm-yellow:hover,
.btn-input-confirm-yellow:active {
background-color: #FFC108;
color: white;
}
.btn-input-confirm-green,
.btn-input-confirm-green:hover,
.btn-input-confirm-green:active {
background-color: #30A64A;
color: white;
}
.btn-input-confirm-white,
/* White confirm buttons are currently unused */
/* .btn-input-confirm-white,
.btn-input-confirm-white:hover,
.btn-input-confirm-white:active {
background-color: #ddd;
color: #333;
font-size: 0.8em;
}
} */
.btn-input-confirm {
line-height: 30px;
min-height: 30px;
Expand Down
36 changes: 34 additions & 2 deletions www/css/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -874,6 +874,10 @@ button.button.back-button.buttons.button-clear.header-item {
.list-card-lg {
width: 95%;
}
.list-card .row {
padding-left: 5px;
padding-right: 5px;
}
.list-col-left-margin {
text-align: center; padding: 0.7em 0.8em 0.4em 0.8em; border-right-width: 0.5px; border-right-color: #ccc; border-right-style: solid; border-bottom-color: #ccc; border-bottom-width: 0.5px; border-bottom-style: solid;
}
Expand Down Expand Up @@ -958,9 +962,37 @@ button.button.back-button.buttons.button-clear.header-item {
display: inline-block;
width: calc(100% - 25px);
}
.diary-arrow-container i {
.diary-more-container i {
font-size: 32px;
float: right;
}
.diary-checkmark-container i {
font-size: 24px;
padding: 3px;
}

.diary-checkmark-container i.can-verify {
color: #30A64A;
background-color: #ddd;
border-radius: 5px;
}
.diary-checkmark-container i.cannot-verify {
color: #E6B8B8;
}
.diary-checkmark-container i.already-verified {
color: #B8E6C2;
}
/* .diary-checkmark-container i.already-verified, .diary-checkmark-container i.cannot-verify {
color: #BFBFBF;
} */

.center-vert {
display: flex;
align-items: center;
}

.center-horiz {
display: flex;
justify-content: center;
}

.side-menu-item {
Expand Down
144 changes: 131 additions & 13 deletions www/js/diary/infinite_scroll_list.js
Original file line number Diff line number Diff line change
Expand Up @@ -69,10 +69,13 @@ angular.module('emission.main.diary.infscrolllist',['ui-leaflet',
ctList.reverse();
ctList.forEach($scope.populateBasicClasses);
ctList.forEach((trip, tIndex) => {
// console.log("Inferred labels from server: "+JSON.stringify(trip.inferred_labels));
trip.userInput = {};
ConfirmHelper.INPUTS.forEach(function(item, index) {
$scope.populateManualInputs(trip, ctList[tIndex+1], item, $scope.data.manualResultMap[item]);
});
trip.finalInference = {};
$scope.inferFinalLabels(trip);
});
ctList.forEach(function(trip, index) {
fillPlacesForTripAsync(trip);
Expand Down Expand Up @@ -249,6 +252,29 @@ angular.module('emission.main.diary.infscrolllist',['ui-leaflet',
return ($scope.differentCommon(tripgj))? "stop-time-tag-lower" : "stop-time-tag";
}

/**
* MODE (becomes manual/mode_confirm) becomes mode_confirm
*/
$scope.inputType2retKey = function(inputType) {
return ConfirmHelper.inputDetails[inputType].key.split("/")[1];
}

/**
* Insert the given userInputLabel into the given inputType's slot in inputField
*/
$scope.populateInput = function(tripField, inputType, userInputLabel) {
if (angular.isDefined(userInputLabel)) {
var userInputEntry = $scope.inputParams[inputType].value2entry[userInputLabel];
if (!angular.isDefined(userInputEntry)) {
userInputEntry = ConfirmHelper.getFakeEntry(userInputLabel);
$scope.inputParams[inputType].options.push(userInputEntry);
$scope.inputParams[inputType].value2entry[userInputLabel] = userInputEntry;
}
// console.log("Mapped label "+userInputLabel+" to entry "+JSON.stringify(userInputEntry));
tripField[inputType] = userInputEntry;
}
}

/**
* Embed 'inputType' to the trip
*/
Expand All @@ -261,24 +287,92 @@ angular.module('emission.main.diary.infscrolllist',['ui-leaflet',
inputList);
var userInputLabel = unprocessedLabelEntry? unprocessedLabelEntry.data.label : undefined;
if (!angular.isDefined(userInputLabel)) {
// manual/mode_confirm becomes mode_confirm
const retKey = ConfirmHelper.inputDetails[inputType].key.split("/")[1];
userInputLabel = tripgj.user_input[retKey];
}
if (angular.isDefined(userInputLabel)) {
var userInputEntry = $scope.inputParams[inputType].value2entry[userInputLabel];
if (!angular.isDefined(userInputEntry)) {
userInputEntry = ConfirmHelper.getFakeEntry(userInputLabel);
$scope.inputParams[inputType].options.push(userInputEntry);
$scope.inputParams[inputType].value2entry[userInputLabel] = userInputEntry;
}
// console.log("Mapped label "+userInputLabel+" to entry "+JSON.stringify(userInputEntry));
tripgj.userInput[inputType] = userInputEntry;
userInputLabel = tripgj.user_input[$scope.inputType2retKey(inputType)];
}
$scope.populateInput(tripgj.userInput, inputType, userInputLabel);
// Logger.log("Set "+ inputType + " " + JSON.stringify(userInputEntry) + " for trip starting at " + JSON.stringify(tripgj.start_fmt_time));
$scope.editingTrip = angular.undefined;
}

/**
* Given the list of possible label tuples we've been sent and what the user has already input for the trip, choose the best labels to actually present to the user.
* The algorithm below operationalizes these principles:
* - Never consider label tuples that contradict a green label
* - Obey "conservation of uncertainty": the sum of probabilities after filtering by green labels must equal the sum of probabilities before
* - After filtering, predict the most likely choices at the level of individual labels, not label tuples
* - Never show user yellow labels that have a lower probability of being correct than confidenceThreshold
*/
$scope.inferFinalLabels = function(trip) {
// Display a label as red if its most probable inferred value has a probability of less than or equal to confidenceThreshold
// TODO: make this configurable
const confidenceThreshold = 0.5;

// Deep copy the possibility tuples
let labelsList = JSON.parse(JSON.stringify(trip.inferred_labels));

// Capture the level of certainty so we can reconstruct it later
const totalCertainty = labelsList.map(item => item.p).reduce(((item, rest) => item + rest), 0);

// Filter out the tuples that are inconsistent with existing green labels
for (const inputType of ConfirmHelper.INPUTS) {
const userInput = trip.userInput[inputType];
if (userInput) {
const retKey = $scope.inputType2retKey(inputType);
labelsList = labelsList.filter(item => item.labels[retKey] == userInput.value);
}
}

// Red labels if we have no possibilities left
if (labelsList.length == 0) {
for (const inputType of ConfirmHelper.INPUTS) $scope.populateInput(trip.finalInference, inputType, undefined);
return;
}

// Normalize probabilities to previous level of certainty
const certaintyScalar = totalCertainty/labelsList.map(item => item.p).reduce((item, rest) => item + rest);
labelsList.forEach(item => item.p*=certaintyScalar);

for (const inputType of ConfirmHelper.INPUTS) {
// For each label type, find the most probable value by binning by label value and summing
const retKey = $scope.inputType2retKey(inputType);
let valueProbs = new Map();
for (const tuple of labelsList) {
const labelValue = tuple.labels[retKey];
if (!valueProbs.has(labelValue)) valueProbs.set(labelValue, 0);
valueProbs.set(labelValue, valueProbs.get(labelValue) + tuple.p);
}
let max = {p: 0, labelValue: undefined};
for (const [thisLabelValue, thisP] of valueProbs) {
// In the case of a tie, keep the label with earlier first appearance in the labelsList (we used a Map to preserve this order)
if (thisP > max.p) max = {p: thisP, labelValue: thisLabelValue};
}

// Apply threshold
if (max.p <= confidenceThreshold) max.labelValue = undefined;

$scope.populateInput(trip.finalInference, inputType, max.labelValue);
}
$scope.updateVerifiability(trip);
}

/**
* For a given trip, compute how the "verify" button should behave.
* If the trip has at least one yellow label, the button should be clickable.
* If the trip has all green labels, the button should be disabled because everything has already been verified.
* If the trip has all red labels or a mix of red and green, the button should be disabled because we need more detailed user input.
*/
$scope.updateVerifiability = function(trip) {
var allGreen = true;
var someYellow = false;
for (const inputType of ConfirmHelper.INPUTS) {
const green = trip.userInput[inputType];
const yellow = trip.finalInference[inputType] && !green;
if (yellow) someYellow = true;
if (!green) allGreen = false;
}
trip.verifiability = someYellow ? "can-verify" : (allGreen ? "already-verified" : "cannot-verify");
}

$scope.getFormattedDistanceInMiles = function(input) {
return (0.621371 * $scope.getFormattedDistance(input)).toFixed(1);
}
Expand All @@ -292,6 +386,10 @@ angular.module('emission.main.diary.infscrolllist',['ui-leaflet',
tripgj.end_ts);
tripgj.background = "bg-light";
tripgj.listCardClass = $scope.listCardClass(tripgj);
tripgj.verifiability = "cannot-verify";
// Pre-populate start and end names with &nbsp; so they take up the same amount of vertical space in the UI before they are populated with real data
tripgj.start_display_name = "\xa0";
tripgj.end_display_name = "\xa0";
}

const fillPlacesForTripAsync = function(tripgj) {
Expand Down Expand Up @@ -528,6 +626,25 @@ angular.module('emission.main.diary.infscrolllist',['ui-leaflet',
$scope.popovers[inputType].hide();
};

/**
* verifyTrip turns all of a given trip's yellow labels green
*/
$scope.verifyTrip = function($event, trip) {
if (trip.verifiability != "can-verify") return;

$scope.draftInput = {
"start_ts": trip.start_ts,
"end_ts": trip.end_ts
};
$scope.editingTrip = trip;

for (const inputType of ConfirmHelper.INPUTS) {
const inferred = trip.finalInference[inputType];
// TODO: figure out what to do with "other". For now, do not verify.
if (inferred && !trip.userInput[inputType] && inferred != "other") $scope.store(inputType, inferred, false);
}
}

/**
* Store selected value for options
* $scope.selected is for display only
Expand Down Expand Up @@ -594,6 +711,7 @@ angular.module('emission.main.diary.infscrolllist',['ui-leaflet',
} else {
tripToUpdate.userInput[inputType] = $scope.inputParams[inputType].value2entry[input.value];
}
$scope.inferFinalLabels(tripToUpdate); // Recalculate our inferences based on this new information
});
});
if (isOther == true)
Expand Down
73 changes: 41 additions & 32 deletions www/templates/diary/infinite_scroll_list.html
Original file line number Diff line number Diff line change
Expand Up @@ -48,43 +48,52 @@ <h3 ng-if="!infScrollControl.reachedEnd">{{'diary.scroll-to-load-more' | transla

<ion-item id="diary-item" style="background-color: transparent;" class="list-item">
<div id="diary-card" ng-class="trip.listCardClass" ng-style="{height: '150px'}">
<div ng-click="showDetail($event, trip)" ng-class="listTextClass" style="font-size: 14px; padding-left: 30px; margin-bottom: 0;" translate=".date-distance-in-time" translate-value-date="{{ trip.display_date }}" translate-value-distance="{{ trip.display_distance }}" translate-value-time="{{ trip.display_time }}"></div>
<div class="row">
<div class="col-90" ng-click="showDetail($event, trip)">
<div ng-class="listLocationClass" id="no-border" href="#" style="background-color: transparent; font-size: 0.8em; padding-top: 5px; padding-bottom: 5px; padding-left: 30px; margin-top: 0; margin-bottom: 0;">
<i class="icon ion-ios-location" style="font-size: 16px; left: 0; color: #33e0bb;"></i>
{{trip.start_display_name}}

<div class="row">
<div ng-click="showDetail($event, trip)" class="col-90 center-vert" ng-class="listTextClass" style="font-size: 14px; padding-left: 30px; margin-bottom: 0; justify-self: flex-start;" translate=".date-distance-in-time" translate-value-date="{{ trip.display_date }}" translate-value-distance="{{ trip.display_distance }}" translate-value-time="{{ trip.display_time }}"></div>
<div ng-click="showDetail($event, trip)" class="col-10 diary-more-container center-vert center-horiz">
<i class="ion-more"></i>
</div>
</div>
<div ng-class="listLocationClass" id="no-border" href="#" style="background-color: transparent; font-size: 0.8em; padding-top: 5px; padding-bottom: 5px; padding-left: 30px; margin-top: 0; margin-bottom: 0;">
<i class="icon ion-ios-location" style="font-size: 16px; left: 0; color: #ff5251;"></i>
{{trip.end_display_name}}
<div class="row">
<div class="col-90" ng-click="showDetail($event, trip)">
<div ng-class="listLocationClass" id="no-border" href="#" style="background-color: transparent; font-size: 0.8em; padding-top: 5px; padding-bottom: 5px; padding-left: 30px; margin-top: 0; margin-bottom: 0;">
<i class="icon ion-ios-location" style="font-size: 16px; left: 0; color: #33e0bb;"></i>
{{trip.start_display_name}}
</div>
<div ng-class="listLocationClass" id="no-border" href="#" style="background-color: transparent; font-size: 0.8em; padding-top: 5px; padding-bottom: 5px; padding-left: 30px; margin-top: 0; margin-bottom: 0;">
<i class="icon ion-ios-location" style="font-size: 16px; left: 0; color: #ff5251;"></i>
{{trip.end_display_name}}
</div>
</div>
<div ng-click="verifyTrip($event, trip)" class="col-10 diary-checkmark-container center-vert center-horiz">
<i ng-class="trip.verifiability" class="ion-checkmark-round"></i>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FYI, please continue using properties instead of functions here. Using functions to retrieve the ng-class, although it works, plays havoc with the caching and the performance really suffers.

</div>
</div>
</div>
<div ng-click="showDetail($event, trip)" class="col-10 diary-arrow-container">
<i class="ion-more"></i>
</div>
</div>
<div class="row" style="padding-left: 5px;padding-right: 5px;margin-top: 0px">
<div ng-repeat="input in userInputDetails" class={{input.width}} style="text-align: center;font-size: 14px;font-weight: 600;" ng-attr-id="{{ 'userinputlabel' + input.name" translate>
{{input.labeltext}}
</div>
</div>
<div class="row" style="padding-left: 5px;padding-right: 5px;">
<div ng-repeat="input in userInputDetails" class={{input.width}} style="text-align: center;" ng-attr-id="{{ 'userinput' + input.name">
<div ng-if="trip.userInput[input.name]" class="input-confirm-container">
<button ng-click ="openPopover($event, trip, input.name)" class="button btn-input-confirm btn-input-confirm-green">
{{trip.userInput[input.name].text}}
</button>
<div class="row" style="margin-top: 0px">
<div ng-repeat="input in userInputDetails" class={{input.width}} style="text-align: center;font-size: 14px;font-weight: 600;" ng-attr-id="{{ 'userinputlabel' + input.name" translate>
{{input.labeltext}}
</div>
<div ng-if="!trip.userInput[input.name]" class="input-confirm-container">
<button ng-click ="openPopover($event, trip, input.name)" class="button btn-input-confirm btn-input-confirm-white" translate>
{{input.choosetext}}
</button>
</div>
<div class="row">
<div ng-repeat="input in userInputDetails" class={{input.width}} style="text-align: center;" ng-attr-id="{{ 'userinput' + input.name">
<div ng-if="trip.userInput[input.name]" class="input-confirm-container">
<button ng-click ="openPopover($event, trip, input.name)" class="button btn-input-confirm btn-input-confirm-green">
{{trip.userInput[input.name].text}}
</button>
</div>
<div ng-if="!trip.userInput[input.name] && trip.finalInference[input.name]" class="input-confirm-container">
<button ng-click ="openPopover($event, trip, input.name)" class="button btn-input-confirm btn-input-confirm-yellow" translate>
{{trip.finalInference[input.name].text}}
</button>
</div>
<div ng-if="!trip.userInput[input.name] && !trip.finalInference[input.name]" class="input-confirm-container">
<button ng-click ="openPopover($event, trip, input.name)" class="button btn-input-confirm btn-input-confirm-red" translate>
{{input.choosetext}}
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</ion-item>
</div>
<div class="start-time-tag-inf-scroll">{{trip.display_start_time}}</div>
Expand Down