Customer Banners (Ads) - SpiceUp. AX and SpotfireX Disclaimer



If you find this site useful and you want to support, buy me a coffee   to keep this site alive and without ads.

Search Bookmarks

 This JavaScript adds a search bar to the bookmark panel. 



(()=>{
function addSearchBookmarks() {
    let original = document.querySelector(".sf-element.sf-element-bookmark-capture");

    if (!original || document.querySelector(".sf-element-clone")) return; // Exit if no original or clone exists

    // Clone and modify the original element
    let clone = original.cloneNode(true);
    clone.classList.add("sf-element-clone");

    let originalInput = original.querySelector(".sf-element-input");
    let clonedInput = clone.querySelector(".sf-element-input");

    if (clonedInput) clonedInput.placeholder = "Search Bookmarks";

    let button = clone.querySelector(".sf-element-button");
    if (button) {
        button.setAttribute("title", "Search Bookmarks");

        let svg = button.querySelector("svg");
        if (svg) {
            svg.removeAttribute("style");
            svg.innerHTML = `
                <circle cx="2.5" cy="2.5" r="2" stroke="currentColor" stroke-width="1" fill="none"></circle>
                <line x1="4" y1="4" x2="7" y2="7" stroke="currentColor" stroke-width="1"></line>
            `;
        }
    }

    // Bookmark filtering function
    function filterBookmarks() {
        let searchInput = clonedInput?.value.toLowerCase();
        document.querySelectorAll(".sf-element-bookmark-item").forEach(bookmark => {
            bookmark.style.display = bookmark.innerText.toLowerCase().includes(searchInput) ? "block" : "none";
        });
    }

    // Attach event listeners for filtering
    if (button) button.addEventListener("click", filterBookmarks);
    if (clonedInput) {
        clonedInput.addEventListener("keyup", filterBookmarks);
        clonedInput.addEventListener("keydown", (event) => {
            if (event.key === "Enter") filterBookmarks();
        });
    }

    // Insert the modified clone after the original
    original.parentNode.insertBefore(clone, original.nextSibling);

    // Adjust scrollbar position
    let scrollbar = original.parentElement.querySelector(".VerticalScrollbarContainer");
    if (scrollbar) {
        let extraHeight = clone.getBoundingClientRect().height;
        scrollbar.style.top = `${parseFloat(getComputedStyle(scrollbar).top) + extraHeight}px`;
        scrollbar.style.height = `${parseFloat(getComputedStyle(scrollbar).height) - extraHeight}px`;
    }

    // Observe width changes of the original input
    if (originalInput && clonedInput) {
        let resizeObserver = new ResizeObserver(() => {
            clonedInput.style.width = originalInput.style.width;
            clone.style.width = original.style.width;
        });

        resizeObserver.observe(originalInput);
    }
}

// Check and initialize when the original element is found
function checkForOriginalElement() {
    if (document.querySelector(".sf-element.sf-element-bookmark-capture")) addSearchBookmarks();
}

// Observe DOM changes to detect new elements
const observer = new MutationObserver(checkForOriginalElement);
observer.observe(document.body, { childList: true, subtree: true });

// Initial check in case the element is already present
checkForOriginalElement();

})()


Warning / Notice Popup or Banner

This script is useful for announcements. A floating button is visible to bring the popup again if needed. Includes a checkbox to turn it off next time the script is triggered. 



JavaScript

(function popup({ position = "center" } = {}) {
    const popupKey = "showPopup";
    const existingPopup = document.getElementById("popupContainer");
    if (existingPopup) return;
    
    const shouldShowPopup = localStorage.getItem(popupKey) !== "false";

    const popupContainer = document.createElement("div");
    popupContainer.id = "popupContainer";
    popupContainer.style.position = "fixed";
    popupContainer.style.textWrapStyle = "balance";
    popupContainer.style.padding = "0px 0px 5px 0px";
    popupContainer.style.fontFamily = "verdana";
    popupContainer.style.background = "navy";
    popupContainer.style.color = "yellow";
    popupContainer.style.boxShadow = "0px 4px 6px rgba(0, 0, 0, 0.1)";
    popupContainer.style.borderRadius = "8px";
    popupContainer.style.textAlign = "center";
    popupContainer.style.zIndex = "1000";
  

    switch (position) {
        case "top":
            popupContainer.style.top = "10px";
            popupContainer.style.left = "50%";
            popupContainer.style.transform = "translateX(-50%)";
            break;
        case "bottom":
            popupContainer.style.bottom = "10px";
            popupContainer.style.left = "50%";
            popupContainer.style.transform = "translateX(-50%)";
            break;
        case "left":
            popupContainer.style.top = "50%";
            popupContainer.style.left = "10px";
            popupContainer.style.transform = "translateY(-50%)";
            break;
        case "right":
            popupContainer.style.top = "50%";
            popupContainer.style.right = "10px";
            popupContainer.style.transform = "translateY(-50%)";
            break;
        case "top-left":
            popupContainer.style.top = "10px";
            popupContainer.style.left = "10px";
            break;
        case "top-right":
            popupContainer.style.top = "10px";
            popupContainer.style.right = "10px";
            break;
        case "bottom-left":
            popupContainer.style.bottom = "10px";
            popupContainer.style.left = "10px";
            break;
        case "bottom-right":
            popupContainer.style.bottom = "10px";
            popupContainer.style.right = "10px";
            break;
        case "banner":
            popupContainer.style.top = "0";
            popupContainer.style.left = "0";
            popupContainer.style.width = "100%";
            popupContainer.style.borderRadius = "0";
            break;
        default:
            popupContainer.style.top = "50%";
            popupContainer.style.left = "50%";
            popupContainer.style.transform = "translate(-50%, -50%)";
    }

    popupContainer.innerHTML = `
        <p>This popup can be shown at the top, center, right or left. Just change the parameter at the end of the script. Currently is set as ${position}. Feel free to change the style within the code</p>
        <label>
            <input type="checkbox" id="dontShowAgain" ${shouldShowPopup ? "" : "checked"}> Don't show this again
        </label>
        <br>
        <button id="closePopup">Close</button>
    `;

    document.body.appendChild(popupContainer);

    document.getElementById("closePopup").onclick = () => {
        if (document.getElementById("dontShowAgain").checked) {
            localStorage.setItem(popupKey, "false");
        } else {
            localStorage.setItem(popupKey, "true");
        }
        popupContainer.remove();
    };

    let restoreButton = document.getElementById("restorePopupButton");
    if (!restoreButton) {
        restoreButton = document.createElement("button");
        restoreButton.id = "restorePopupButton";
        restoreButton.textContent = "Show Popup Again";
        restoreButton.style.position = "fixed";
        restoreButton.style.top = "42px";
        restoreButton.style.right = "10px";
        restoreButton.style.padding = "10px";
        restoreButton.style.background = "#007bff";
        restoreButton.style.color = "white";
        restoreButton.style.border = "none";
        restoreButton.style.borderRadius = "5px";
        restoreButton.style.cursor = "pointer";
        restoreButton.onclick = () => {
            localStorage.setItem(popupKey, "true");
            popup({ position });
        };
        document.body.appendChild(restoreButton);
    }
    
    if (!shouldShowPopup) {
        popupContainer.remove();
    }
})({ position: "banner" }); 

Incrementally add new rows without reloading the entire dataset

Suppose that you have a main table with all your data and you want to bring new records. You will need to setup a temporary table pointing to the same data source but just brings new records that are not yet on your main table.  The temporary table has to be setup on-demand to bring new records that do not exists on the main table. Something like:  >= Max([MainTable].[id]) + 1 id is a sequence. 




step by step setup procedure:

  1. Add two data tables in memory on your analysis
    1. destination Table contains all the data. Could be millions of records
    2. source Table comes from the same source. Set on-demand parameters:
      • Set a min range expression input to be : Max([destinationTable].[id]) + 1
      • That will take new records
  2. At this point nothing is shown, unless you refresh sourceTable with new records
  3. The iron python script programmatically reloads only the sourceTable and appends those rows to the main table. 
  4. To keep a fair amount of records and prevent the data canvas to have unlimited number of transformations, you can delete old records.


from Spotfire.Dxp.Data import AddRowsSettings
from Spotfire.Dxp.Data.Import import DataTableDataSource

# Parameters
source_table = sourceTable  # Temporary table for new records
dest_table = destTable      # Main table holding all data
max_ops = 4                 # Max transformations in the data canvas

# 1. Reload Source Table to Fetch New Records
source_table.ReloadAllData()

# 2. Append Rows to Destination Table
if source_table.RowCount > 0:
    # 2.1 Define the data source from Source Table
    data_source = DataTableDataSource(source_table)
    
    # 2.2 Append rows to the Destination Table
    row_settings = AddRowsSettings(dest_table, data_source)
    dest_table.AddRows(data_source, row_settings)

# 3. Remove Old Transformations from Destination Table (optional)
source_view = dest_table.GenerateSourceView()
operations = source_view.OperationsSupportingTransformations
if operations.Count > max_ops:
    first_operation = operations[1].DataOperation
    source_view.RemoveOperation(first_operation)

Tooltips for column headers

 Enable html tooltips with this simple script and power up your cross table, graphical table or data tables



(() => {
    let i = 0;
    let tooltipTimeout = null;

    // Tooltips dictionary with lowercase keys
    const tooltips = {
      'city': 'Another big city!',
      'state': 'The inner text or textContent for this item is "state"',
      'population': 'Population in millions<br>Look, we can use images!<br><img style="background:white" src="https://img.icons8.com/ios-filled/100/000000/crowd.png">',
      'country': 'Tooltip for country',
      'latitude': 'Geographical latitude.',
      'longitude': 'Geographical longitude.',
      'average temperature (f)': 'Average temperature data.',
      'air quality index': 'Current air quality index.',
      'observation date': 'Date of observation.',
      'bullet graph':'This bullet graph represnts the air quality index min and max'
    };

    const tooltip = document.createElement('div');
    tooltip.id = 'tooltip';
    tooltip.style.position = 'absolute';
    tooltip.style.backgroundColor = 'black';
    tooltip.style.color = 'white';
    tooltip.style.padding = '5px';
    tooltip.style.borderRadius = '5px'; 
    tooltip.style.fontSize = '12px';
    tooltip.style.visibility = 'hidden'; 
    tooltip.style.opacity = '0';
    tooltip.style.transition = 'opacity 0.2s';
    tooltip.style.pointerEvents = 'none'; // Ensures the tooltip doesn't trigger any events

    document.body.appendChild(tooltip);

    const hasColumnHeaderClass = (element) => {
      while (element) {
        if ([...element.classList].some(cls => cls.includes('column-header'))) {
          return true;
        }
        element = element.parentElement;
      }
      return false;
    };

    // Function to handle mouseover event
    const handleMouseOver = (event) => {
      const target = event.target;
      const textContent = target.textContent.trim().toLowerCase();

      // Clear any existing timeout to avoid overlapping timeouts
      if (tooltipTimeout) {
        clearTimeout(tooltipTimeout);
      }

      tooltipTimeout = setTimeout(() => {
        if (tooltips[textContent] && hasColumnHeaderClass(target)) {
          tooltip.innerHTML = tooltips[textContent];
          tooltip.style.visibility = 'visible';
          tooltip.style.opacity = '1';
          i++;
          console.log(i);
        } else {
          tooltip.style.visibility = 'hidden';
          tooltip.style.opacity = '0';
        }
      }, 800); // 0.8 seconds delay
    };

    // Function to handle mousemove event
    const handleMouseMove = (event) => {
      tooltip.style.left = `${event.pageX + 10}px`;
      tooltip.style.top = `${event.pageY + 10}px`;
    };

    // Function to handle mouseout event
    const handleMouseOut = () => {
      // Clear any existing timeout when mouse leaves
      if (tooltipTimeout) {
        clearTimeout(tooltipTimeout);
      }
      tooltip.style.visibility = 'hidden';
      tooltip.style.opacity = '0';
    };

    // Attach event handlers using 'on' properties
    document.body.onmouseover = handleMouseOver;
    document.body.onmousemove = handleMouseMove;
    document.body.onmouseout = handleMouseOut;
  })();

Range Slider

Convert a regular Spotfire Input field Property Control as a Range Slider in Spotfire text area



html

<div id='slider'>

   <SpotfireControl id="Input Field Property Control" />   <SpotfireControl id="label Property Control" />

</div>

js

(()=>{

  const slider=document.querySelector('#slider input');
  slider.type="range";

  slider.min = 10;
  slider.max = 50;
  slider.step = 0.5;

  slider.oninput  = () => {
    slider.blur();
    slider.focus();
  }

})()

Confirmation Dialog

Prompts user before execution of an Action Control that runs a Data Function or script



html

<div id="confirmExecute">

   <SpotfireControl id="2fe15f7fd4174c08b40c0ee27c5591c1" />

</div>

<div id="actualExecute" style="position:fixed;top:-100px">

   <SpotfireControl id="7475a74b91ae474c80bf4f979bd1f56a" />

</div>


JavaScript

//setup the first spotfire sleep button (that does nothing) show a confirm dialog
document.querySelector("#confirmExecute input").onclick=function(){
   Spotfire.ConfirmDialog.showYesNoCancelDialog("Confirm Execution","Do you really want to execute?",okClick,noClick,null)
}

//programatically click on spotfire control when confirming

okClick = function(x){
   document.querySelector("#actualExecute input").click();
}

//display a friendly message if
noClick = function(x){
//alert("OK, no worries")
    Spotfire.ConfirmDialog.showDialog("OK","No worries!",[])



Get Image Layer dimensions on Map Visualization

 Just uncheck the marker layer or remove the Positioning markers (geocoding or coordinate columns) from the marker layer, go back to the image layer and hit the reset button




Autocomplete

 Add autocomplete to an existing Spotfire input control (webplayer and cliente )


html

<div id="autocomplete"> 
<SpotfireControl id="spotfire_Input" />
</div>

<div id="autocomplete-data" hidden>
John Doe,Jane Smith,Robert Johnson,Michael Brown,Emily Davis,Sarah Miller,James Wilson,Patricia Moore,Richard Taylor,Linda Anderson
</div>

note: replace the autocomplete-csv-values with a uniqueConcatenate([column]) calculated value, but you can use as is to test the script



javascript

function setupAutocomplete(id){
    let currentFocus = -1;
    let isScriptTriggered = false;

    const autoCompleteDiv = document.getElementById(id);
    autoCompleteDiv.position = "relative";
    const spotfireInput = autoCompleteDiv.firstElementChild;
    const autocompleteInput = document.createElement('input');
    autocompleteInput.id = id + "Input";
    autocompleteInput.style.display = 'none';

    // Copy style from SpotfireControl to input
    for (let style in spotfireInput.style) {
        if (spotfireInput.style.hasOwnProperty(style) && spotfireInput.style[style]) {
            autocompleteInput.style[style] = spotfireInput.style[style];
        }
    }
    autoCompleteDiv.appendChild(autocompleteInput);

    //popup for autocomplete items
    const popup = document.createElement('div');
    popup.style.width = `${spotfireInput.offsetWidth}px`;

    popup.style.position = 'absolute';
    popup.style.zIndex = '100';
    popup.className = 'autocomplete-items';
    popup.style.display = 'none';
    autoCompleteDiv.appendChild(popup);

    //observer for csv values as it comes from a calculated value
    const csvObserver = new MutationObserver(() => {
        csvValues = document.getElementById(id + '-data').innerText.split(',');
    });

    csvObserver.observe(document.getElementById(id + '-data'), {
        childList: true,
        characterData: true,
        subtree: true
    });

    let csvValues = document.getElementById(id + '-data').innerText.split(',');


    function toggleInputDisplay() {
        spotfireInput.style.display = '';
        autocompleteInput.style.display = 'none';
        spotfireInput.value = autocompleteInput.value;
        isScriptTriggered = true;
        spotfireInput.focus();
        spotfireInput.blur();
        isScriptTriggered = false;

    }

    autocompleteInput.addEventListener('click', () => {
        autocompleteInput.value = "";
    });

    autocompleteInput.addEventListener('input', () => {
        const inputValue = autocompleteInput.value;
        const filteredValues = csvValues.filter(name => name.toLowerCase().includes(inputValue.toLowerCase()));
        popup.innerHTML = '';

        filteredValues.forEach(value => {
            const div = document.createElement('div');
            div.className = "sf-element-dropdown-list-item";
            div.innerHTML = value.replace(new RegExp(inputValue, 'gi'), match => "<span class='highlight'>" + match + "</span>");

            // Copy style from src input to div
            for (var style in autocompleteInput.style) {
                if (autocompleteInput.style.hasOwnProperty(style) && autocompleteInput.style[style]) {
                    div.style[style] = autocompleteInput.style[style];
                }
            }

            div.addEventListener('click', () => {
                autocompleteInput.value = value;
                popup.style.display = 'none';
                spotfireInput.value = value;
                currentFocus = -1;

                toggleInputDisplay();


            });
            popup.appendChild(div);
        });
        popup.style.display = filteredValues.length ? 'block' : 'none';
    });




    autocompleteInput.addEventListener('keydown', (e) => {
        const divs = popup.getElementsByTagName('div');
        if (e.keyCode == 40) { // Down arrow
            currentFocus++;
            if (currentFocus >= divs.length) currentFocus = 0;
        } else if (e.keyCode == 38) { // Up arrow
            currentFocus--;
            if (currentFocus < 0) currentFocus = divs.length - 1;
        } else if (e.keyCode == 13) { // Enter
            if (currentFocus > -1) {
                divs[currentFocus].click();
            } else {
                spotfireInput.value = autocompleteInput.value;
                isScriptTriggered = true;
                toggleInputDisplay();
                isScriptTriggered = false;
            }
        } else if (e.keyCode == 27) { // Escape key
            isScriptTriggered = true;
            toggleInputDisplay();
            isScriptTriggered = false;

        }
        for (let i = 0; i < divs.length; i++) {
            divs[i].classList.remove('over');
        }
        if (currentFocus > -1) {
            divs[currentFocus].classList.add('over');
        }
    });

    spotfireInput.addEventListener('focus', () => {
        if (isScriptTriggered) return;
        // When spotfireInput gets focus, hide it and show input
        spotfireInput.style.display = 'none';
        autocompleteInput.style.display = '';
        autocompleteInput.value = "";
        autocompleteInput.focus();
    });


    autocompleteInput.addEventListener('blur', () => {
        setTimeout(() => {
            popup.style.display = 'none';
            toggleInputDisplay();
        }, 200);
    });



    //styles
    const styleTag = `
    <style>

    #autocompleteInput{
        outline: none; 
    }

        .autocomplete-items{
        background-color: #fff;
        border:1px solid #c6c8cc;
        }

        .autocomplete-items div:hover, 
        .autocomplete-items div.over {
        color: #FFFFFF !important;
        background-color: #7289F9;
        cursor:default;
        }

        .highlight {
        font-weight: bold;
        text-decoration: underline;
        }
    </style>`

    autoCompleteDiv.insertAdjacentHTML('afterEnd', styleTag)

}

setupAutocomplete("autocomplete")