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();
                });