<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
<title>Modify the color scheme of a continuous variable | Sample | ArcGIS Maps SDK for JavaScript</title>
<!-- Load the ArcGIS Maps SDK for JavaScript from CDN -->
<script type="module" src="https://js.arcgis.com/5.0/"></script>
max-height: calc(100vh - 100px);
color: var(--calcite-color-text-1);
font-size: var(--calcite-font-size--1);
border: 1px solid var(--calcite-color-border-1);
<arcgis-scene item-id="3a922ed0c7a8489ea4fbe8747ac560ba">
<arcgis-zoom slot="top-left"> </arcgis-zoom>
<arcgis-navigation-toggle slot="top-left"></arcgis-navigation-toggle>
<arcgis-compass slot="top-left"> </arcgis-compass>
<calcite-panel id="panelVoxelControl" style="visibility: hidden" slot="top-right">
<calcite-block label="Current Variable" expanded>
<calcite-select hidden id="selectVariable" label="Current Variable"> </calcite-select>
<div id="variableColorScheme"></div>
<calcite-block label="Color scheme settings" expanded>
<calcite-button id="buttonAddColorStop">Add Color Stop</calcite-button>
<div class="colorRowTitle">
<div style="float: left; width: 175px">Position(%):</div>
<div class="colorRow" id="containerColorStops"></div>
style="visibility: hidden"
<arcgis-expand slot="bottom-left">
<arcgis-legend id="expandLegend"></arcgis-legend>
const VoxelColorStop = await $arcgis.import("@arcgis/core/layers/voxel/VoxelColorStop.js");
const viewElement = document.querySelector("arcgis-scene");
const selectVariable = document.getElementById("selectVariable");
const panelVoxelControl = document.getElementById("panelVoxelControl");
const variableColorScheme = document.getElementById("variableColorScheme");
const containerColorStops = document.getElementById("containerColorStops");
const buttonAddColorStop = document.getElementById("buttonAddColorStop");
const colorPicker = document.getElementById("colorPicker");
await viewElement.viewOnReady();
const voxelLayer = viewElement.map.layers.find((layer) => layer.type === "voxel");
await viewElement.whenLayerView(voxelLayer);
// Show the control after the voxel layer is in the component's view
panelVoxelControl.style.visibility = "visible";
let currentVariableStyle = null;
let selectedIndexColor = 0;
voxelLayer.popupEnabled = true;
voxelLayer.renderMode = "volume";
voxelLayer.enableIsosurfaces = false;
for (let i = 0; i < voxelLayer.variables.length; ++i) {
const vxlVariable = voxelLayer.variables.getItemAt(i);
const item = document.createElement("calcite-option");
item.setAttribute("label", voxelLayer.variableStyles.getItemAt(i).label);
item.setAttribute("value", vxlVariable.id);
if (vxlVariable.id === 1) {
voxelLayer.currentVariableId = vxlVariable.id;
selectVariable.hidden = false;
selectVariable.appendChild(item);
currentVariableStyle = voxelLayer.getVariableStyle(null);
let transferFunction = currentVariableStyle.transferFunction;
// This function is to either reset the list and the color scheme color gradient or
// To refresh the color scheme color gradient
function changeColorScheme(toReset) {
while (containerColorStops.hasChildNodes()) {
containerColorStops.removeChild(containerColorStops.firstChild);
colorGradient = "linear-gradient(to right";
for (let i = 0; i < transferFunction.colorStops.length; i++) {
// colorStop's position is a normalized (i.e. 0 to 1) value.
// It can be converted to percentage for display or use it for styling a color gradient background.
let colorPercentage = (transferFunction.colorStops.getItemAt(i).position * 100).toFixed(
colorGradient += `, ${transferFunction.colorStops.getItemAt(i).color.toHex()} ${colorPercentage}%`;
createContinuousVariableItem(i, transferFunction.colorStops.getItemAt(i), false);
variableColorScheme.style.backgroundImage = colorGradient;
function createContinuousVariableItem(continuousIndex, colorStop, addNew) {
const divItemRow = document.createElement("div");
const divItemCol = document.createElement("div");
const itemPositionLabel = document.createElement("calcite-label");
const itemPositionInput = document.createElement("calcite-input");
const itemColorButton = document.createElement("button");
const divItemDeleteAction = document.createElement("div");
const itemIcon = document.createElement("calcite-icon");
divItemRow.classList.add("colorRow");
itemPositionLabel.textContent = "Position";
itemPositionLabel.layout = "inline";
itemPositionInput.id = `item${continuousIndex}_${colorStop.position.toFixed(3)}_Input`;
itemPositionInput.type = "number";
// It is required to have position 0 as the first colorStop and 1 as the last colorStop.
if (colorStop.position === 0 || colorStop.position === 1) {
// Position 0 and 1 as a non-editable position. Colors for position 0 and 1 are editable.
itemPositionInput.readOnly = true;
divItemDeleteAction.style = "float:right; width:15px; padding:2px 10px;";
divItemDeleteAction.style = "float:right; width:15px; padding:2px 10px; cursor: pointer;";
divItemDeleteAction.onclick = function () {
const index = colorStopsList
.map((obj) => obj.itemPositionInput)
.indexOf(itemPositionInput.id);
transferFunction.colorStops.removeAt(index);
colorStopsList.splice(index, 1);
changeColorScheme(false);
/* Listen to changes on the colorStop's position
* only allow updates on the position when the position is not overlapping the previous and next colorStop
itemPositionInput.addEventListener("calciteInputChange", function (e) {
// A continuous variable can have multiple colorStops.
// If a colorStop needs to be modified or removed, it is important to always check the index because it can change.
// Having a separate array variable that stores all the colorStops can help to specify which index to use.
const index = colorStopsList.map((obj) => obj.itemPositionInput).indexOf(this.id);
index === 0 ? null : transferFunction.colorStops.getItemAt(index - 1);
index === transferFunction.colorStops.length - 1
: transferFunction.colorStops.getItemAt(index + 1);
const thisColorStop = transferFunction.colorStops.getItemAt(index);
// Prevent a colorStop from overlapping the previous or next colorStop.
this.min = (prevColorStop.position * 100).toFixed(1) + 0.1;
this.max = (nextColorStop.position * 100).toFixed(1) - 0.1;
parseFloat(this.value) <= parseFloat((prevColorStop.position * 100).toFixed(1)) ||
parseFloat(this.value) >= parseFloat((nextColorStop.position * 100).toFixed(1))
this.value = (thisColorStop.position * 100).toFixed(1);
// Updating the position of a colorStop
thisColorStop.position = (this.value / 100).toFixed(3);
colorStopsList[index].position = (this.value / 100).toFixed(3);
changeColorScheme(false);
itemPositionInput.value = (colorStop.position * 100).toFixed(1).toString();
divItemCol.style = "float: left; width: 150px;";
itemColorButton.classList.add("btnColorPicker");
itemColorButton.id = `item${continuousIndex}_${colorStop.position.toFixed(3)}_Button`;
itemColorButton.style.background = colorStop.color;
// Adding the new colorStop before the last index.
transferFunction.colorStops.add(colorStop, continuousIndex - 1);
containerColorStops.insertBefore(divItemRow, containerColorStops.lastElementChild);
// Updating the array variable to include the new color stop.
// Remember to keep the index the same as in the colorStops collection
colorStopsList.splice(continuousIndex - 1, 0, {
itemPositionInput: itemPositionInput.id,
itemColorButton: itemColorButton.id,
position: colorStop.position,
containerColorStops.appendChild(divItemRow);
itemPositionInput: itemPositionInput.id,
itemColorButton: itemColorButton.id,
position: colorStop.position,
divItemRow.appendChild(divItemCol);
divItemCol.appendChild(itemPositionInput);
divItemRow.appendChild(itemColorButton);
divItemRow.appendChild(divItemDeleteAction);
divItemDeleteAction.appendChild(itemIcon);
itemColorButton.addEventListener("click", function () {
const rect = this.getBoundingClientRect();
let topValue = rect.bottom;
if (rect.bottom + colorPicker.clientHeight >= document.documentElement.clientHeight) {
topValue = rect.top - colorPicker.clientHeight;
selectedIndexColor = colorStopsList.map((obj) => obj.itemColorButton).indexOf(this.id);
colorPicker.style = `position:absolute;top:${topValue}px;right:${window.innerWidth + 20 - rect.right}px;z-index:999`;
colorPicker.value = colorStop.color.toHex();
/* Listen to the changes on the selected variable
* update the color scheme and the list of colorStops
selectVariable.addEventListener("calciteSelectChange", function () {
voxelLayer.currentVariableId = selectVariable.selectedOption.value;
currentVariableStyle = voxelLayer.getVariableStyle(null);
transferFunction = currentVariableStyle.transferFunction;
// Add a click event to add a colorStop on a variable
buttonAddColorStop.addEventListener("click", function (e) {
const prevColorStop = transferFunction.colorStops.getItemAt(
transferFunction.colorStops.length - 2,
const nextColorStop = transferFunction.colorStops.getItemAt(
transferFunction.colorStops.length - 1,
(nextColorStop.position - prevColorStop.position) / 2 + prevColorStop.position;
// Prevents adding a new colorStop with the same position as the previous colorStop or the next colorStop
prevColorStop.position.toFixed(3) === thisPosition.toFixed(3) ||
thisPosition.toFixed(3) === nextColorStop.position.toFixed(3)
const colorstop = new VoxelColorStop({
createContinuousVariableItem(transferFunction.colorStops.length, colorstop, true);
changeColorScheme(false);
// Update the color of a colorStop
colorPicker.addEventListener("calciteColorPickerChange", function () {
const colorStop = transferFunction.colorStops.getItemAt(selectedIndexColor);
const btnColor = document.getElementById(
colorStopsList[selectedIndexColor].itemColorButton,
btnColor.style.background = colorPicker.value;
colorStop.color = colorPicker.value;
changeColorScheme(false);
/* Listen to a click outside the colorPicker element
* close the color picker window
document.addEventListener("click", (e) => {
if (e.target !== colorPicker && !e.target.classList.contains("btnColorPicker")) {
colorPicker.style = "visibility:hidden";