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.

Enhance Find Results

 


(function() {
    if (window.scriptRan) {
        return; // Exit if the script has already run
    }
    window.scriptRan = true; // Mark the script as run

    const styleId = "fx-category-style"; // Unique ID for the style tag
    // Remove existing style tag if it exists
    const existingStyle = document.getElementById(styleId);
    if (existingStyle) {
        existingStyle.remove();
    }

    // Create and append new style tag
    const style = document.createElement("style");
    style.id = styleId;
    style.textContent = `
        /* Category Title Styling */
        [class*="sfx_category-title"] {
            font: bold 18px 'Roboto';
            color: #caca;
            display: block;
            cursor: pointer;
            position: relative;
            padding: 10px;
        }
        /* Add expandable arrow using ::after use same class for the collapseAllButton */
        [class*="sfx_category-title"]::after, .collapseAllButton {
            content: " ▲"; /* Up arrow */
            position: absolute;
            right: 20px;
            font-size: 14px;
            color: #caca;
            transition: transform 0.3s ease-in-out;
        }
        /* Rotates arrow when collapsed */
        [class*="sfx_category-title"].collapsed::after {
            transform: rotate(180deg);
        }
        /* Hide results with the hidden class */
        .hidden {
            display: none !important;
        }
    `;  
    document.body.appendChild(style);

    // Function to toggle individual category results
    function addCollapseOnCategoryResults() {
        const categoryTitles = document.querySelectorAll('[class*="sfx_category-title"]');
        categoryTitles.forEach(title => {
            title.onclick = function () {
                this.classList.toggle('collapsed');

                let current = this.nextElementSibling;
                while (current && !current.className.includes('sfx_category-title')) {
                    if (current.className.includes('sfx_result-container')) {
                        current.classList.toggle('hidden');
                    }
                    current = current.nextElementSibling;
                }
            };
        });
    }

    // Function to toggle all categories
    let allCollapsed = false; // Track state

    function toggleAllCategories() {
        const categoryTitles = document.querySelectorAll('[class*="sfx_category-title"]');
        allCollapsed = !allCollapsed; // Toggle state

        categoryTitles.forEach(title => {
            if (allCollapsed) {
                title.classList.add('collapsed'); // Collapse all
            } else {
                title.classList.remove('collapsed'); // Expand all
            }

            let current = title.nextElementSibling;
            while (current && !current.className.includes('sfx_category-title')) {
                if (current.className.includes('sfx_result-container')) {
                    if (allCollapsed) {
                        current.classList.add('hidden'); // Hide all
                    } else {
                        current.classList.remove('hidden'); // Show all
                    }
                }
                current = current.nextElementSibling;
            }
        });

        // Update collapse button symbol
        collapseAllButton.innerHTML = allCollapsed ? "▼" : "▲";
    }
 
    // Adds a collapse/expand button
    let collapseAllButton = null;

    function addCollapseButton() {
        const targetElement = document.querySelector('[class*="sfx_searchBoxClass"] [class*="sfx_searching"]');

        if (targetElement && !collapseAllButton) { // Ensure we don't add multiple buttons
            collapseAllButton = document.createElement("div");
            collapseAllButton.classList.add('collapseAllButton');
            collapseAllButton.style.position = "absolute";
            //collapseButton.style.right = "20px";
            collapseAllButton.innerHTML = "▲"; // Default to expanded state

            collapseAllButton.onclick = toggleAllCategories; // Set toggle function

            targetElement.appendChild(collapseAllButton);

            setTimeout(addCollapseOnCategoryResults, 1000);
        }
    }

    function removeCollapseButton() {
        if (collapseAllButton && collapseAllButton.parentNode) {
            collapseAllButton.parentNode.removeChild(collapseAllButton);
            collapseAllButton = null; // Clear reference
        }
    }

    // Detects when to add collapser button
    const rootElement = document.querySelector('[class*="sfx_sf-stacking-root"]');

    if (rootElement) {
        const observer = new MutationObserver(() => {
            const targetChild = rootElement.querySelector('[class*="sfx_result-content"]');
            if (targetChild) {
                addCollapseButton();
            } else {
                removeCollapseButton();
            }
        });

        observer.observe(rootElement, { childList: true, subtree: true });
    }

    console.log("done", rootElement);
})();

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!",[])