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.

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)


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

//id can be a script parameter when using multiple autocompletes
id = "myTickers";
autocomplete = document.querySelector("#"+id+" input");
autocomplete.setAttribute("list",id+"-datalist");
datalist = document.createElement("datalist");
datalist.id=id+"-datalist";
autocomplete.parentNode.insertBefore(datalist,autocomplete);
data = document.getElementById(id+"-data");

setData = ()=>{datalist.innerHTML = data.innerText.replaceAll(",","")}

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

Step 6: Save the analysis to the library and open it on webplayer 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)




Keyboard shortcuts for custom expressions

You can use the following keyboard shortcuts in many expression or script editing fields in Spotfire:








Option

Description

Ctrl+C

Copy

Ctrl+X

Cut

Ctrl+V

Paste

Ctrl+Z

Undo

Ctrl+A

Select all

Ctrl+D

Duplicates the current line.

Ctrl+L

Deletes the current line.

Ctrl+T

Switch current line position with the previous line position.

Alt+Shift+Arrow

Column mode select.

Alt+Left Mouse Click

Column mode select.

Tab (selection of several lines)

Indent

Shift+Tab (selection of several lines)

Remove indent.

Ctrl+(Keypad-/Keypad+) or Ctrl+Mouse Wheel

Zoom in/zoom out.

Ctrl+Keypad division sign

Restore the original size from zoom.

Ctrl+BackSpace

Delete to start of word.

Ctrl+Shift+BackSpace

Delete to end of word.

Ctrl+Shift+Delete

Delete everything after the current cursor position on this line.

Ctrl+U

Convert to lower case.

Ctrl+Shift+U

Convert to upper case.

Ctrl+H

Opens a Find/Replace dialog.

F3 (in Find/Replace dialog)

Find next.

Shift+F3  (in Find/Replace dialog)

Find previous.

Ctrl+G

Opens a GoToLine dialog.


column custom sort order

 


html

 Select columns to sort in order: 

 <TABLE><TBODY><TR>
   <TD><SpotfireControl id="List box (multiple select) Property Control" /></TD>
   <TD><SpotfireControl id="Label Property Control" /></TD>
 </TR></TBODY></TABLE>
 <SpotfireControl id="IronPython Action Control" />


ironPython

# dt is a Data Table Script parameter
# values is a String List Document Property from a List box (multiple select) Property Control with Unique values in column
values = Document.Properties["values"]
dt.Columns["type"].Properties.SetCustomSortOrder(values)



hardcoded method

values = System.Collections.Generic.List[str](["A","B","AB","O"])
Document.Data.Tables["Table1"].Columns["type"].Properties.SetCustomSortOrder(values)

Duplicate or update a visual across all pages

 This script is useful when you want to update, for exampe, a text area that serves as a masthead across all tabs. The first time the visualization will be placed randomly, but once you adjust the size and position on every page, that's all you need to do


#vis is a script parameter indicating the visual to copy (or replace) across all pages
for p in Document.Pages:
	if not p.Visuals.Contains(vis):
		v2 = p.Visuals.AddDuplicate(vis)
		v2.Title = vis.Title+"!"
		for v in p.Visuals:
			if v.Title == vis.Title:
				p.Visuals.Remove(v)
		v2.Title = vis.Title

Add tooltips for columns on a summary table

 



js

//define tooltips for measurements columns. Use \n to add a space tooltip=[ "the sum of the values", "the average of the values for each column", "min stands for minimum", "maximum value", "median is the middle number in a \nsorted, ascending or descending list of numbers", "Standar deviation refers to the square root of its variance " ]; //adds tooltips to measurements and re-run every 5 seconds in case resize happens function setTooltips(){ [...document.querySelectorAll(".frozenRowsContainer div[row]")].map((e,i)=>{e.title = tooltip[i]}) setTimeout(setTooltips,5000) } setTooltips()



If you really want to change the column titles, then change the setTooltips function to this:

function setTooltips(){ [...document.querySelectorAll(".frozenRowsContainer div[row]")].map((e,i)=>{e.innerText = tooltip[i]}) setTimeout(setTooltips,5000) }

if you really want to add html to titles, then replace "innerText" with "innerHTML"

Change Summary Table Headings

You can change the "column" heading with a little javascript. Just add a text area and add this script:


js

//script parameter
const newColName = "something+ descriptive"
let i=0

function setColName(){
//set column name

// Select the node that will be observed for mutations
const colHeader= document.querySelectorAll(".sfc-summary-table .sfc-column-header");

//fisrt column is last colHeader ¯\_(ツ)_/¯  
const columnHeader = colHeader[colHeader.length-1]

//change fisrt col name
columnHeader.innerHTML = newColName

//if first col name changes, change it back
// Options for the observer (which mutations to observe)
const config = { attributes: !true, childList: true, subtree: true };

// Callback function to execute when mutations are observed
const callback = function(mutationList, observer) {
console.log(i,targetNode.innerText)
    // Use traditional 'for loops' for IE 11
//    for(const mutation of mutationList) {
// targetNode.innerText = newColName
//    }
};

// Create an observer instance linked to the callback function
const observer = new MutationObserver(callback);

// Start observing the target node
const targetNode = document.querySelectorAll(".sfc-summary-table .topLeftCornerCanvas")[0]
observer.observe(targetNode, config);

observer.disconnect();

//keep monitoring every 3 seconds in case it changes back to "column"
setTimeout(setColName,3000)
}

setColName()