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.

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")




Preventing zoom when scrolling with the mouse wheel on visuals with zoom sliders

Here is how to prevent zoom on visualization that have zoom sliders on to zoom when scrolling the page down using the mouse wheel





preventMouseScrollFromZoom.js

divsWithZoomSliders = [...document.querySelectorAll(".sf-element.sf-element-visual")].filter(div => div.querySelector(".sf-element-zoom-slider"));
divsToDisable = divsWithZoomSliders.map(div => div.querySelector(".sf-element.sf-element-canvas-visualization"));
divsToDisable.forEach(div => {div.firstChild.addEventListener('wheel', function(event) {event.preventDefault()}, { passive: true })});


Highlight Visual based on Document Property

 When a document property changes, it highlights a visual. This can be useful for data analysis to pay close attention to visuals that require focus



html

<pre id=docPropValues xhidden>

 "Indexed price charter":"<SpotfireControl id="5858b9bd6d344a98ba87c742af3d9f05" />", 
 "Top holders by percent held":"<SpotfireControl id="96f46c37e0ab4731a43124c827f3956f" />",
 "Historical data":"<SpotfireControl id="5302059ba4724d1f8e45c6a1b95bcfe6" />",
 "Calendar quarter estimates":"<SpotfireControl id="21331969168d4e2fb600d4ed1e0004be" />"
}
</pre>


JavaScript

//finds visuals in which title contains visualTitle (use *= for contains, ^= starts with, $= ends with or = exact match)
elements = Array.from(document.querySelectorAll(".sf-element.sf-element-visual"));
function highlighVisual(visualTitle){

//set background for those visuals found
elementWithChild = elements.filter(element => element.querySelector(
[title*='"+visualTitle+"']") !== null); //<-- change here for search operator
elementWithChild.forEach(x=>x.style.background="red")
}


element = document.querySelector('#docPropValues'); 

observer = new MutationObserver(_ => {
json = JSON.parse(element.innerText);

//reset visual backgrounds
elements.forEach(x=>{x.style.background=""})
Object.entries(json)
.filter(([key, value]) => value === "True")
.map(([key, value]) => key)
.forEach(visualTitle => {highlighVisual(visualTitle)});
});

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

IronPython Show/Hide Items

from Spotfire.Dxp.Application.Visuals import BarChart
barChart = vis.As[BarChart]()

#get filter rules
gotFilerRules,filterRuleCollection  = barChart.TryGetFilterRules()

#delete all filter rules
if gotFilerRules:
for filterRule in filterRuleCollection:
filterRuleCollection.Remove(filterRule)
#print filterRule.DisplayName, filterRule.Expression

#add a filter rule collection to show top 5 axis values
#filterRuleCollection.AddTopNRule("THEN [Y.FinalValue]",5,True) 



Create trellised visualizations based on marking



from Spotfire.Dxp.Application.Visuals import BarChart, VisualContent, VisualTypeIdentifiers, LabelOrientation, BarChartOrientation  

from Spotfire.Dxp.Application.Layout import LayoutDefinition

#script params
dataTable = Document.Data.Tables["Best predictors"]

#delete all barchart visuals
page = Document.ActivePageReference
for vis in page.Visuals:
if vis.TypeId == VisualTypeIdentifiers.BarChart:
page.Visuals.Remove(vis)

#The last visual left should be the treemap
tm = next(iter(page.Visuals))

#create a barchart template
bc = Application.Document.ActivePageReference.Visuals.AddNew[BarChart]()
bc.Data.DataTableReference = dataTable
bc.Title = "${DataTable.DisplayName}"
bc.Legend.Visible= False
bc.YAxis.Expression = "Sum([p-value])"
bc.XAxis.Expression = "<[Case Name]>"
bc.SortedBars=True
bc.Orientation = BarChartOrientation.Horizontal

#duplicate as many barcharts as selected sites from marking

siteNames = Document.Properties["markedSites"]
sites = [s.strip() for s in siteNames.split(',')]

#setup first barchart
firstSite = sites.pop()
bc.Title = firstSite
siteVisuals = [bc]

bc.Data.WhereClauseExpression = "[Site_No] = '"+firstSite+"'"

#create visuals
for site in sites:
vis = page.Visuals.AddDuplicate(bc.Visual)
vis.Title =site
bc = vis.As[BarChart]()
bc.Data.WhereClauseExpression = "[Site_No] = '"+site+"'"
siteVisuals.append(vis.As[BarChart]())

#arrange visuals
#tm is the existing treemap and will take 10% of the screen
ld = LayoutDefinition()
ld.BeginSideBySideSection()
ld.Add(tm, 10)

# Begin a stacked section for the second column at 70% of the screen
ld.BeginStackedSection(70)

i = 0
for bc in siteVisuals:
    if i % 3 == 0:
        if i > 0: ld.EndSection()
        ld.BeginSideBySideSection()
    ld.Add(bc.Visual)
    i += 1
ld.EndSection()
ld.EndSection()
ld.EndSection()

page.ApplyLayout(ld)


To trigger this script when marking changes, create a bypass data function. The script definition is simply an 'x' and so is the input and output. Make sure it runs automatically. The script parameters for the 'x' input is "UniqueConcatenate([Explore_YieldData - Explore_YieldData].[Site])" limited by the blue Marking. The output is a document property called "markedSites" that must be setup to trigger the above script when its value changes.

ColorPicker



html

<span id="color">
   <SpotfireControl id="Input Filed goes here" />
</span>




ColorPicker.js

colorInput = document.querySelector("#color input")
colorInput.type="color"




Here is a use case on how to use it. 

changeColor.py
from Spotfire.Dxp.Application.Visuals import CombinationChart, CategoryKey
from System.Drawing import Color

# cast visual to combo chart
combinationChart = vis.As[CombinationChart]()

# get script parameters from doc props
category = CategoryKey("Banana") 

# get the color from the color picker doc prop
hcolor = Document.Properties["color"] #hexadecimal color
color = Color.FromArgb(int(hcolor[1:3], 16), int(hcolor[3:5], 16), int(hcolor[5:7], 16))


# change the color for the corresponding category
combinationChart.ColorAxis.Coloring.SetColorForCategory(category,color)


Change Series on Combination Chart






Each document property triggers the corresponding iron python script when the value changes



for the category doc prop: reads the combination chart color and series type from the selected category

from Spotfire.Dxp.Application.Visuals import CombinationChart, CategoryKey

#cast visual to combo chart
combinationChart = vis.As[CombinationChart]()
category = CategoryKey(Document.Properties["category"])

#get type
Document.Properties["type"]  = str(combinationChart.IndexedSeriesType[category])

#get color
import re
color = str(combinationChart.ColorAxis.Coloring.GetColorForCategory(category))
color = re.findall(r"\[([^\[\]]+)\]", color)[0]
Document.Properties["color"] = color.lower()


for the color doc prop: changes the color for the selected category

from Spotfire.Dxp.Application.Visuals import CombinationChart, CategoryKey
from System.Drawing import Color

#cast visual to combo chart
combinationChart = vis.As[CombinationChart]()

#get script parameters from doc props
category = CategoryKey(Document.Properties["category"]) #string representing a category from the series

color = Color.FromName(Document.Properties["color"]) #named color such as blue, green, magenta, beige...

#color = Color.FromArgb(255,0,0) #if you know the rgb values
#if hexadecimal color (hc) comes from a color picker (#FF0000)
#color = Color.FromArgb(int(hc[1:3], 16), int(hc[3:5], 16), int(hc[5:7], 16)) 




# change the color for the corresponding category
combinationChart.ColorAxis.Coloring.SetColorForCategory(category,color)



and for the type dropdown that changes the series type for the selected category:

from Spotfire.Dxp.Application.Visuals import CombinationChart, CombinationChartSeriesType, CategoryKey

#cast visual to combo chart
combinationChart = vis.As[CombinationChart]()

#get script parameters from doc props
category = CategoryKey(Document.Properties["category"])

#string representing a category from the series
type = CombinationChartSeriesType.Bar if Document.Properties["type"] == "Bar" else CombinationChartSeriesType.Line 

# change series type as Bar or line
combinationChart.IndexedSeriesType[category] = type




Reset Visible Filters

#1. Take the filter panel from the desired page in filter panel
#filterPanel = Document.Pages[1].FilterPanel
filterPanel = Document.ActivePageReference.FilterPanel

#1.1 Select the filtering scheme to use (optional)
#filterringScheme = Document.FilteringSchemes[Document.Data.Filterings["Filtering scheme"]]
#filterPanel.FilteringSchemeReference = filteringScheme 

#2. Reset only visible filters
for tableGroup in filterPanel.TableGroups:
   for filterHandle in tableGroup.FilterHandles:
      if filterHandle.Visible:
         filterHandle.FilterReference.Reset()


Sync zoom sliders

 sync the zoom sliders to the exact same range between different visuals.


The style attribute of the activeVisual tag hides the input visibility. It has an input property control that holds the visual title. This title is updated by the javascript that takes the last active visual on mouse over. This property control is then passed as a parameter for the sync.ip to take the last active visual as a reference.

html

<div style="position:fixed;left:-1000px" id="activeVisual">
   <SpotfireControl id="input button" />
</div>
<br>
<span id="syncBtn">
   <SpotfireControl id="replace with button to run sync.ip" />
</span>
 <SpotfireControl id="optional reset button that runs reset.ip" />

sync.ip

from Spotfire.Dxp.Application.Visuals import AxisRange, ScatterPlot

# We detect which visual is "active"
sourceVisualTitle = Document.Properties["activeVisualTitle"]

visX = None

# Iterate through all visuals on the active page
for visual in Document.ActivePageReference.Visuals:
    if visual.Title == sourceVisualTitle:
visX = visual

# We need to cast the visual script parameters visA, visB and visC to ScatterPlot object or whatever visual you are using in your analysis

scatterX = visX.As[ScatterPlot]()
scatterA = visA.As[ScatterPlot]()
scatterB = visB.As[ScatterPlot]()
scatterC = visC.As[ScatterPlot]()

# We create a reference to the Y axis ZoomRange from the first visual (A)
zoomXy = scatterX.YAxis.ZoomRange
 

# We need to create an AxisRange object based on visual X range settings for Y axis
axisRangeX = AxisRange(zoomXy.Low, zoomXy.High)

# Apply scatterA,B and C to the selected axisRange
scatterA.YAxis.ZoomRange = axisRangeX
scatterB.YAxis.ZoomRange = axisRangeX
scatterC.YAxis.ZoomRange = axisRangeX


reset.ip

from Spotfire.Dxp.Application.Visuals import AxisRange, ScatterPlot

# We need to cast the visual parameters visA, visB and visC to ScatterPlot object or whatever visual you are using in your analysis

scatterA = visA.As[ScatterPlot]()
scatterB = visB.As[ScatterPlot]()
scatterC = visC.As[ScatterPlot]()

#reset scatterA,B and C ranges
scatterA.YAxis.ZoomRange=AxisRange.DefaultRange
scatterB.YAxis.ZoomRange=AxisRange.DefaultRange
scatterC.YAxis.ZoomRange=AxisRange.DefaultRange


js

function getActiveVisual(){
 vis = document.querySelector(".sfpc-active .sf-element-visual-title").innerText.trim();
 inp = document.querySelector("#activeVisual input");
 inp.value = vis;
 inp.focus();
 inp.blur();
}

document.getElementById("syncBtn").onmouseover = getActiveVisual

Loop through pages



html

<span id="startButton" style="cursor:default">[Start]</span>
<span id="stopButton" style="cursor:pointer">[Stop]</span>

JavaScript

//parameters
var pages = [0, 1, 3];   //◄ Select the pages you want to cycle through. 0 is the first one
var timeout = 10000;      //◄ Select the time in ms to delay between pages. 10000 is 10 seconds

//maintain a registry of interval in case multiple are triggered to stop them all at once with the stop button

window["intervalIds"]??=[];
tmp=[...pages]

function startCycle() {
(function cycle(){
page = tmp.shift();
 
if(!tmp.length) tmp=[...pages]
goToPage(page);
window.intervalIds.push(setTimeout(cycle, timeout));
})();
}


function stopCycle() {
  console.log("Slideshow Ended")
  window.intervalIds.forEach(clearInterval);
}


function goToPage(x) {
  document.querySelectorAll(".sf-element-page-tab")[x].click();
}


// Hook html buttons to start and stop functions
document.getElementById("startButton").onclick = startCycle;
document.getElementById("stopButton").onclick = stopCycle;

List table columns

# Replace with the actual table name
table_name = "Data Table"  

# Get the table by name
table = Document.Data.Tables[table_name]

# Iterate through the columns and add their names to the list
for column in table.Columns:
print (column.Name)


# Iterate through all columns and add their column names and data types
for t in Document.Data.Tables:
   print t.Name
   columnCollection=t.Columns
   for col in columnCollection:
      print "\t", col.Name, "(",col.Properties.DataType,")"






Update a multiple select list box

The List box (multiple select) Property Control holds an array of values. Understanding this makes it easy to update its values




Filter Property Control

Create Searchable dropdowns / autocomplete with the help of Filters to act as Input Controls 


The map is not driven by the filter, but an expression that contains a document property. The filter drives the Property Control




This recipe allows you to leaverage the power of filters to drive property controls to enhance the user experience of Spotfire. 

Ingredients

  • New filtering scheme
  • A filter from the new filtering scheme
  • A calculated Value
  • 1 Dropdown or Input property control
  • 1 Input field property control
  • html and javaScript

html

<div id="myInput" >
  <span class="ddown">
    ◄spotfire 'selection' input property control goes here►
    <span class='srchBtn'>⋯</span> 
  </span>
  <span class="sfFltr"> ◄spotfire filter(s) goes here► 
    <span class='closeBtn'>✕</span> 
  </span>
  <div class="sfCalcVal">◄spotfire calculated value from new filtering scheme goes heere►</div>
  <div class="sfInput">◄spotfire 'selection' input fileld control goes here► </div>
</div>

javaScript

//script parameters
target = "myInput"

//node elements
container = document.getElementById(target);
dropdown = document.querySelector(".ddown");
filter = document.querySelector(".sfFltr");
searchButton= document.querySelector(".srchBtn");
closeButton = document.querySelector(".closeBtn");
selection = document.querySelector(".sfCalcVal");
sfInput = document.querySelector(".sfInput input");

//events
closeButton.addEventListener("click",()=>{
  filter.hidden=true;
  dropdown.hidden=false;
})

searchButton.addEventListener("click",()=>{
    dropdown.hidden = true;
    filter.hidden = false;
})

//monitor selection when its value changes
observer = new MutationObserver(()=>{
  filter.hidden=true;
  dropdown.hidden=false;
  sfInput.value = selection.innerText;
  sfInput.focus();
  sfInput.blur();
})

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

//apply styling and attributes 
filter.hidden = true;
selection.hidden = true;
css = `<style>
.closeBtn, .srchBtn{
  vertical-align:top;
  cursor:pointer; 
}

.sfFltr {position:fixed;z-index:1;}
.sfInput{position:fixed;z-Index:-1;}

</style>`

container.insertAdjacentHTML('afterend',css)

Preparation

  1. Open the Sales and Marketing analysis from the library/samples folder
    • Delete all but Sales performance and just keep the map
    • Limit the SalesAndMarketing map layer with expression:
      upper([Store Name]) ~= Upper("${selection}")

  2. Create a new filtering scheme called "filtersForPropertyControls" or something like that
    • you can do that by right clicking the filter panel and show the filtering scheme menu

  3. Change the [Store Name] filter type to use for the input property control as List Box Filter

  4. Make sure the "filtersForPropertyControls" filtering scheme IS NOT selected in the filters panel

  5. Add a Text area, edit in html and copy the html below 

  6. Create an input or dropdown property control linked to a new document property called "selection" from a Text area.  Place the control as the first child of the <span class="ddown"> element
    • if using a dropdown, make sure to select unique values from [Store Name] column

  7. Add the filters from the "filtersForPropertyControls" filtering scheme inside the <span class="sfFltr"> element

  8. Create a calculated value inside the <div class="sfCalcVal"> to get the first element from the filter and limit the data only from the "filtersForPropertyControls" filtering scheme
    • First([Store Name])
    • very important not to leave any spaces. For example:
      <div class="sfCalcVal"><SpotfireControl id="2d1..26cc" /></div>

  9. Insert an Input field property control inside the <div class="sfInput"> element and link it to the "selection" document property

  10. Save the text area and Insert the JavaScript below

IronPython Terminal

script to run ironPython scripts

import sys
from io import StringIO
code = Document.Properties["input"] 

capturer = StringIO()
sys.stdout = capturer

try:
exec(code)
sys.stdout = sys.__stdout__
output = capturer.getvalue()
except Exception as e:
output = str(e)

Document.Properties["output"]=output

script to run Python code via a data function

import sys
from io import StringIO
capturer = StringIO()
sys.stdout = capturer

try:
exec(code)
sys.stdout = sys.__stdout__
output = capturer.getvalue()
except Exception as e:
output = str(e)


html

<SpotfireControl id="input field multiple lines property control" />
<SpotfireControl id="action control that runs the above script" />

<pre style="height:50%;background:blue;color:yellow;overflow:auto;padding:10px;border:3px inset">
<SpotfireControl id="output document property (Label)" /></pre>



Example code to list python packages on TIBCO Cloud spotfire from this termnal

import pandas as pd
import pkg_resources

installed_packages = pkg_resources.working_set
installed_packages_list = sorted(["%s==%s" % (i.key, i.version)
    for i in installed_packages])
package_list = [[r.split('==')[0],r.split('==')[1]] for r in installed_packages_list]

packages = pd.DataFrame(columns=["Package Name", "Package Version"])

idx = 0
for pkg in package_list:
    packages.loc[idx] = [pkg[0], pkg[1]]
    idx += 1

print(packages.to_string())




Note: if you put this code on a data function and you want to return a data table with the list of packages, then remove the last line and set the output parameter to point to the packages variable

Note: Please be careful running this script in production environments.

With great power comes great responsibiliy

Checkbox List Property Control


 

On a text area, create a multiple select list box property control and call it "myStringList" and another string input field property control and call it "transfer". The transfer document property should trigger the following script when it changes:


#update multiSelect
Document.Properties["myStringList"] = Document.Properties["transfer"].split(",")
Edit the text area html and wrap the controls with a tag element with id "myCheckboxList"

html

<div id="myChecboxList">
   <SpotfireControl id="'myStringList' muiltiple selection list box" />
   <SpotfireControl id="'transfer' input control " />
</div>


JS 

id = "myChecboxList" 

//get items from multiSelect
ph = document.getElementById(id);
items = ph.innerText.split("\n").filter(x=>x!="..."&&x.trim());

//get checked values from csv
target = ph.querySelector("input");  
checked = target.value.split(",").filter(x=>x!=""); 

//create checkboxes
items.forEach((x,i)=>{
   val = items[i];
   cb = document.createElement("input");
   cb.type ="checkbox";
   cb.value = val;
   tn = document.createTextNode(" "+val);
   br = document.createElement("br");
   [br,cb,tn].forEach(e => {ph.appendChild(e)});
 

   //check if checked
   cb.checked = checked.includes(val)
   console.log(val,checked,val in checked) 

   //onclick 

  cb.onclick = () => {
    vals = [...document.querySelectorAll(`#${id} input[type='checkbox']`)]
    target.value = vals.filter(x => x.checked).map(x => x.checked ? x.value : null);
    target.focus();
    target.blur();
  };

   //hide input
   target.style.position="fixed"
   target.style.zIndex=-1"

   //hide multiselect
   ph.firstElementChild.style.display="none"  
})

JavaScript Radial Icon Menu



itemsPerLayer = 1



itemsPerLayer = 4







html

<div class="iconMenu">
 <SpotfireControl id="spotfire action control link 1" />
 <SpotfireControl id="spotfire action control link 2" />
 <SpotfireControl id="spotfire action control link 3" />
 <SpotfireControl id="spotfire action control link 4" />
 <SpotfireControl id="spotfire action control link 5" />
 <SpotfireControl id="spotfire action control link 6" />
</div>

<img class="icons fa-solid fa-bolt,fa-solid fa-bolt-lightning,icon-graph,fa-solid fa-arrow-trend-up,fa-solid fa-check,fa-solid fa-house"/>


js

/*
description:converts spotfire links to an iconic menu. Supports font awesome and simple-line-icons
usage:

<div class="iconMenu">
   <a>spotfire link 1</a>
   <a>spotfire link 2</a>
   <a>spotfire link 3</a>
</div>
<img class="icons icon-user,icon-fire,fa-solid fa-arrow-trend-up"

*/

//script parameters
let spaceBetweenLayers = 40;
let itemsPerLayer = 5; //◄ set to 1 for horizontal menu

style = `
<link  rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/simple-line-icons/2.4.1/css/simple-line-icons.css"></link>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.3.0/css/all.min.css"/>

<style>
.iconMenu {
  position: fixed;
  top:37px;left:53px;
  z-index:1;
  width: 50px;
  height: 50px;
}

.iconMenu a {
    position: fixed;
    width: 20px;
    height: 20px;
    background-color: #ccc;
    border-radius: 50%;
    justify-content: center;
    transition-timing-function: cubic-bezier(0,0,0,1);
    transition-duration: .5s; 
    display: flex;
    justify-content: center;
    align-items: center;
text-decoration:none !important;
 
    /*look and feel*/
    background: #1f83f2;
    box-shadow: 5px 5px 7px rgba(0, 0, 0, 0.25), inset 2px 2px 5px rgba(255, 255, 255, 0.5), inset -3px
3px 5px rgba(0, 0, 0, 0.5);
    color:white !important; 
    height:23px;
    width:23px;
}

.iconMenu a:last-child{opacity:1}
.iconMenu a:hover{
    background: #8f18f8;
}
</style>
`

document.querySelector(".iconMenu").insertAdjacentHTML("afterend",style);

//script
let timeOutPID=0;
let boxShadow = document.querySelector(".iconMenu a").style.boxShadow;

function hover() {
  let gap = spaceBetweenLayers;
  let elements = document.querySelectorAll(".iconMenu a");
  elements.forEach((e, i) => {
    if(i==elements.length-1) return;
    let angle = (100 / itemsPerLayer) * (i%itemsPerLayer);
    i%itemsPerLayer||(gap+=spaceBetweenLayers)
    e.style.transform = `rotate(${angle}deg) translate(${gap}px) rotate(-${angle}deg)`;
    e.style.boxShadow=boxShadow;
    e.onmouseover = resetDelayClose;
    e.onmouseout = delayClose;
  });
 
 resetDelayClose();
}

function close(){
  let elements = document.querySelectorAll(".iconMenu a");
  elements.forEach((e, i) => {
    if (i==elements.length-1) return;
    e.style.transform = `translate(0px)`;
    e.style.boxShadow="unset";
  });
}

function delayClose(){
  timeOutPID = setTimeout(close,1234)
}

function resetDelayClose(){
  timeOutPID && clearTimeout(timeOutPID);
}

document.querySelector(".iconMenu a:last-child").onmouseover = hover;
document.querySelector(".iconMenu a:last-child").onmouseout = delayClose; 

//setup icons on links
icons = document.querySelector(".icons").classList.value.split(",")
icons[0] = icons[0].replace("icons ","");
console.log(icons)

document.querySelectorAll(".iconMenu a").forEach((e,i)=>{
  e.className = icons[i];
  e.title = e.innerText;
  e.innerText=""
})  

hover();
delayClose();

Add Autocomplete to an existing Spotfire input control (only webplayer)




Step 1

. Create a calcualted colum with the input data. For example:

"<option value=""" & [Holders] & """>" as [options] 

Step 2. Edit the html of a Text Area and Create an Input Property Control and wrap it with an identified tag. Example:

<a id="myTickers"><SpotfireControl id="60e360db89924916ab4790b20e85d339" /></a>

Step 3. Create a Calculated Value that concatenates the unique values of the calculated column in step 1:

UniqueConcatenate([options])

Step 4. Wrap the Calcualted Value with an iddentified hidden tag. The id is the same as the id from step2 + "-data" sufix.  For example:

<a id="myTickers-data" hidden ><SpotfireControl Calculated Value Dynamic Item goes here /></a>

Step 5: Add the following javascript

//Note: only works on webplayer 
//html:
//<a id="autocompleteElement"><input /></a>
//<a id="autocompleteElement-data" hidden >uno,dos,tres,cuatro</a>   

function setupAutocomplete(id) {
  const autocomplete = document.querySelector("#"+id+" input");
  autocomplete.setAttribute("list",id+"-datalist");
  const datalist = document.createElement("datalist");
  datalist.id=id+"-datalist";
  document.body.appendChild(datalist);
  const data = document.getElementById(id+"-data");

  const setData = () => {
    datalist.innerHTML = '';
    data.innerText.split(',').forEach(item => {
      let option = document.createElement('option');
      option.value = item;
      datalist.appendChild(option);
    });
  }

  //run setData as soon as the calculated value changes
  const mutationObserver = new MutationObserver(setData);
  mutationObserver.observe(data, {subtree: true, childList: true} );
  setData(); // populate the datalist initially
}

setupAutocomplete("autocompleteElement");

Step 6: Save the analysis to the library and open it on web player because autocomplete does not work on the client.

Here is how everything looks together:


Spotfire Confirmation Dialogs

These funcitons are not officially supported by TIBCO and might change from one version to the other. Use of them at your own risk

js 

okClick = function(x){alert("mee too!")}
noClick = function(x){alert("too bad")} 
xClick = function(x){alert("why cancel?")}

Spotfire.ConfirmDialog.showYesNoCancelDialog("Hello","do you like this",okClick,noClick,xClick)//last two areguments are optional




myDialog=Spotfire.ConfirmDialog.showDialog("hello","there",[])

myDialog.close()





Spotifre.ConfirmDialog methods:

showDialog(title,message,emptyArray?)
showOkDialog(title,message,okCallbackFunction)
showOkCancelDialog(title,message,okFunction,CancelFunction)
showYesNoDialog(title,message,yesFunction,NoFunction,?,?)
showYesNoCancelDialog(title,message,yesFunction,NoFunction,CancelFunction,?)
? are optional unknown arguments

To explore other function, open the console on developer tools and type Spotfire


Here are some more Spotfire API snippets

progress = Spotfire.Progress.createProgressOverlay(); 
progress.setText("Loading, please wait"); 
  setTimeout(function(){ 
  progress.node().remove(); 
},3000);

This ones can be useful to detect the user agent:

Spotfire.isWebPlayer
Spotfire.isProfessional
Spotfire.isAuthorMode
Spotfire.isInitialized


Explore the JavaScript Spotfire object properties and methods by searching for keywords

function traversePropertiesAndFunctions(obj, keyword, path = []) {
  for (let prop in obj) {
    if (obj.hasOwnProperty(prop)) {
      const currentPath = [...path, prop];

      if (typeof obj[prop] === 'function') {
        if (prop.toLowerCase().includes(keyword.toLowerCase())) {
          console.log(`Found function: ${currentPath.join('.')}`);
        }
      } else if (typeof obj[prop] === 'object') {
        traversePropertiesAndFunctions(obj[prop], keyword, currentPath);
      } else if (typeof obj[prop] === 'string') {
        if (prop.toLowerCase().includes(keyword.toLowerCase())) {
          console.log(`Found property: ${currentPath.join('.')}`);
        }
      }
    }
  }
}


// Example usage
traversePropertiesAndFunctions(Spotfire, 'page');



Create a JavaScript programatically from IronPython

Create a Script

# Creates a JavasSript and adds it to the applicaiton document
from Spotfire.Dxp.Application.Scripting import ScriptDefinition, ScriptParameterCollection 
from Spotfire.Dxp.Application.Scripting.ScriptLanguage import JavaScript

#this is the JavaScript code we want to create
jsCode = '''
   function world(){
      alert("world!")
   }
'''

# 1. Creates the script
# 1.1 Define parameters (none in this example)
scriptParams = ScriptParameterCollection([])

# 1.2 Define the script with title, description, code, language, params and perform in transaction (not applies to JS)
scriptDef = ScriptDefinition.Create("myJavaScript","hello world",jsCode, JavaScript, scriptParams, False)

# 2. Adds the script to the application document
Application.Document.ScriptManager.AddScriptDefinition(scriptDef) 




List available scripts

# list available scripts and their coding language
scripts = Application.Document.ScriptManager.GetScripts()
for script in scripts:
   print script.Name, " ► ",script.Language.Language

# Get a specific script
script = Application.Document.ScriptManager.TryGetScript("myJavaScript" )
if(script[0]):
   print "script found!:", script[1].Name, script[1].Language.Language
else:
   print "script not found. Check the script name and try again"

Read the code from a script
script = Application.Document.ScriptManager.TryGetScript("renderImage")[1]
print (script.ScriptCode)


Attach a script form IronPyton to a text area

# Attach a script from IronPyton to a text area
# Attached and existing JavasSript from the Applicatin Document to a text area
from Spotfire.Dxp.Application.Scripting import HtmlTextAreaScript

# Get the script from the Document
script = Application.Document.ScriptManager.TryGetScript("myJavaScript" )[1]

# Prepare the script for the text area
htmlScript = HtmlTextAreaScript(script,{}) 

# Attach the script to the TextArea. ta is a TextArea Visualization script parameter
from Spotfire.Dxp.Application.Visuals import HtmlTextArea
vis = ta.As[HtmlTextArea]()
vis.Scripts.Add(htmlScript)