Adding Color Gradients
Summary
In this chapter, we will use the supplemental application data to display the range of data values visually with a colored gradient.
Concepts
Determining the color gradients
Setting the color gradients
Capturing the native default colors assigned to the model
We have seen how we can visually filter nodes that meet certain criteria by setting the rendering style for those nodes. We can also display our supplemental data in other way too. By using something like a color gradient, we can see all at once how part information compares to others, all at once. We will set up color gradients for both the price and stock ranges. By the end of this chapter, we should be able to see which parts are low or high in inventory and in price, just by looking at their assigned colors.
The first thing we will need to do is track the original face colors of the nodes. While there is an option to reset all node face colors at once, we are going to be working with individual nodes, so want to be able to just restore a particular nodes color to its original. In our DisplayFilter
class, we will make another Map
, linking node IDs to their original colors.
We will follow the same approach for assigning price and stock gradient colors, storing the nodeId
as the keys for our map, and the assigned color as the value.
In the constructor for DisplayFilter
, we will have three Map
objects for each of these tracked colors.
this._priceColorMap = new Map();
this._stockColorMap = new Map();
this._defaultColors = new Map();
As mentioned before, we need to capture the default colors of the model. Let’s write a member function in DisplayFilter
that performs this task. This is as simple as using HOOPS Communicator to query the face color of the model and storing the returned color in the map.
captureNativeColors(modelData) {
let valuesIterator = modelData.values();
for (let nodeValues of valuesIterator) {
this._viewer.model.getNodesEffectiveFaceColor([nodeValues.ID])
.then(([color]) => {
this._defaultColors.set(nodeValues.ID, color);
});
}
}
Determine the range of active values
The next bit of functionality is to assign colors to the filtered nodes (the ones that will be displayed at a given time). We could assign the colors to be static regardless of the current filter selections, but we have chose to make the colors change dynamically. In our DisplayFilter
class, write a function called updateColorGradients
that takes in the supplemental model data, determines which nodes are active, and assigns colors based on the range of price and stock of those active nodes. Of course, there are many algorithms you may choose to determine your colors, and our color choice functions are somewhat arbitrary. The important takeaway is that we are using the application data to determine a range, then assigning a color representative of where it falls in that range.
updateColorGradients(modelData) {
let minPrice = 250, maxPrice = 0, maxStock = 1000, minStock = 0;
this._filteredNodes.forEach((node) => {
let nodeValues = modelData.get(node);
if (nodeValues.Price < minPrice)
minPrice = nodeValues.Price;
if (nodeValues.Price > maxPrice)
maxPrice = nodeValues.Price;
if (nodeValues.Stock < minStock)
minStock = nodeValues.Stock;
if (nodeValues.Stock > maxStock)
maxStock = nodeValues.Stock;
});
let valuesIterator = modelData.values();
for (let nodeValues of valuesIterator) {
if (this._filteredNodes.indexOf(nodeValues.ID) == -1) {
this._priceColorMap.set(nodeValues.ID, this._defaultColors.get(nodeValues.ID));
this._stockColorMap.set(nodeValues.ID, this._defaultColors.get(nodeValues.ID));
}
else {
let pr = (nodeValues.Price - minPrice) / (maxPrice - minPrice) * 255;
let pb = (1 - (nodeValues.Price - minPrice) / (maxPrice - minPrice)) * 255;
let pg = (1 - Math.abs((nodeValues.Price - minPrice) / (maxPrice - minPrice) - (1 - ((nodeValues.Price - minPrice) / (maxPrice - minPrice))))) * 255;
let sr = (1 - (nodeValues.Stock - minStock) / (maxStock - minStock)) * 255;
let sb = (1 - (nodeValues.Stock) / (maxStock)) * 50;
let sg = (nodeValues.Stock - minStock) / (maxStock - minStock) * 160;
this._priceColorMap.set(nodeValues.ID, new Communicator.Color(pr, pg, pb));
this._stockColorMap.set(nodeValues.ID, new Communicator.Color(sr, sg, 0));
}
}
}
Of course, since we want to update the colors each time our filtered nodes change, we can place a call to updateColorGradients
within the gatherFilteredNodes
function.
let psNodesPassed = pNodesPassed.filter(value => -1 !== sNodesPassed.indexOf(value));
this._filteredNodes = psNodesPassed.filter(value => -1 !== mNodesPassed.indexOf(value));
this.updateColorGradients(modelData);
Include calls to the new color determinant functions
We now have two new functions to help set our colors. The best time to capture the native colors and determine the first gradient assignments is when loading our model and gathering the supplemental model information. In the loadModel
function in our app.js file, lets add a call to captureNativeColors
and updateColorGradients
.
fetch("/data/database/" + modelName + ".json")
.then((resp) => {
if (resp.ok) {
resp.json()
.then((data) => {
let nodeData = data.NodeData;
let numEntries = nodeData.length;
let clippedID;
let totalCost = 0;
this._modelData.clear();
for (let i = 0; i < numEntries; ++i) {
clippedID = nodeData[i].ID;
this._modelData.set(clippedID, nodeData[i]);
totalCost += nodeData[i].Price;
}
;
// Display the total cost of the assembly
document.getElementById("inv-total-cost").innerHTML = `$ ${totalCost.toFixed(2)}`;
this._displayFilter.captureNativeColors(this._modelData);
this._displayFilter.gatherFilteredNodes(this._modelData);
this._displayFilter.updateColorGradients(this._modelData);
this._displayFilter.setRenderingSelection();
});