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



Refer to TIBCO Javascript best apractices when implementing these scripts
If you find this site useful and you want to support, buy me a coffee   to keep this site alive and without ads.

JavaScript Radial Icon Menu



itemsPerLayer = 1



itemsPerLayer = 6







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

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"

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

Crosshair guides for visualizations

Pair of perpendicular lines that moves when the mouse moves. 




HTML

 <div id="crossHairX" style="cursor:default;width:100%;height:0;position:fixed;top:0;right:0;border-top:1px dashed green;"></div> 
 <div id="crossHairY" style="cursor:default;width:0;height:100%;position:fixed;top:0;right:0;border-left:1px dashed olive;"></div>

JavaScript

var cX = document.getElementById("crossHairX");
var cY = document.getElementById("crossHairY");

cX.style.zIndex=10
cY.style.zIndex=10

document.body.addEventListener("mousemove",e=>{
  cX.style.top=(e.clientY-2)+"px"
  cY.style.left=(e.clientX-2)+"px" 

}) 

eCharts on TextAreas

 Here is a way to create charts from JavaScript libraries such as echarts directly on Text Areas

This example shows a Combination Chart with Stacked bars




  1. Create a Text Area and add the following HTML code and save the text area

    <pre id="observed" hidden >
      <span class="xaxis" >Jan, Feb, Mar, Apr, May, Jun</span>
      <span  class="yaxis" style="name:Finance;type:bar;stack:x;yAxisIndex:0">
        Apr:33, Feb:132, Jan:85, Jun:58, Mar:71, May:75
      </span>
      <span class="yaxis" style="name:Legal;type:bar;stack:x;yAxisIndex:0" >
        Apr:8, Feb:29, Jan:56, Jun:16, Mar:19, May:19
      </span>
      <span class="yaxis" style="name:Marketing;type:line;stack:y;yAxisIndex:1" >
        Apr:248, Jan:312, Jun:302, Mar:245, May:225
      </span>
    </pre>
    <div id="container" style="height: 100%"></div>


  2. Copy the code from echarts.5.3.3.min.js and paste it onto a new Spotifre JavaScript

  3. Add this other JavaScript code as a new eChartTest.js file:

    //script parameters
    dataDiv = "observed"
    chartDiv = "container"

    //chart options
    options = {
     tooltip: {trigger: 'axis',axisPointer: {type: 'cross'}},
     legend: {},
     xAxis: [{type: 'category'}],
     yAxis: [{
         type: 'value',
         position:'right',
         name:"scale 1"
       },{
         type: 'value',
         position:'left',
         name:'scale 2'
       }]
    };

    var dom = document.getElementById(chartDiv);
    var myChart = echarts.init(dom, null, {
      renderer: 'svg',
      useDirtyRect: false
    });

    var app = {};
    var option;

    if (options && typeof options === 'object') {
      myChart.setOption(options);
    }

    //monitor input changes (connect data with chart dirven by calculated values)
    var observed = document.getElementById(dataDiv)
    var trigger = function(){
      xData = observed.querySelector(".xaxis").innerText.split(", ") 
      yAttr = {} 
      series=[]
      yAxis = [...observed.querySelectorAll(".yaxis")].forEach((aYAxis) => {
       yDatum={}
       aYAxis.innerText.split(", ").forEach(p=>{
          y=p.split(":");
          yDatum[y[0].trim()]=y[1].trim()
       })

       yData = xData.map((c,i)=>{return yDatum[xData[i]]})
       aYAxis.getAttribute("style").split(";").forEach(p=>{
           y=p.split(":");
           yAttr[y[0].trim()]=y[1].trim()
       })
       yAttr.data = yData
       series.push({...yAttr})
      })  

      let newOptions = {xAxis:[{data:xData}],series: series}
      myChart.setOption(newOptions)
    }
     
    mutationObserver = new MutationObserver(trigger)
    mutationObserver.observe( observed , {attributes:!true, subtree: true, childList: true} );

  4. To make the chart respond to marking from other visuals or filters, replace the hard coded values from step 1 with a Calculated Values to produce the same output. Example of calculated value:

    for x axis: 
      UniqueConcatenate([Month])
    for y axis:
      UniqueConcatenate([Month]&":"&count() over  (Intersect([Department],[Month])))



  5. Data comes from the Sales and Marketing sample analysis

Checkbox and Radio Buttons for TIBCO Spotfire






Checkbox Property Control
  1. Create Boolean document property "cb"
  2. Create a script that triggers when "cb" document property changes:

      #myVis is the target visualization script parameter
      from Spotfire.Dxp.Application.Visuals import Visualization
      myVis = myVis.As[Visualization]()
      myVis.XAxis.Scale.ShowGridlines = Document.Properties["cb"]
      myVis.YAxis.Scale.ShowGridlines = Document.Properties["cb"]


  3. Create a calculated value with custom expression and script:

    Custom expression:
    if(${cb},"[X]",[ ])

    Script:
    Document.Properties["cb"] = not Document.Properties["cb"]


Radio Button Property Control

  1. Create multiple Boolean document properties. For example:  "op1, op2, op3,..,opN"

  2. Create a script that triggers when each document property changes.
       if Document.Properties["op1"]: myVis.LabelVisibility = LabelVisibility.All
       if Document.Properties["op2"]: myVis.LabelVisibility = LabelVisibility.Marked 
       if Document.Properties["op3"]: myVis.LabelVisibility = LabelVisibility.None

  3. Create a calculated value with custom expression and assign the same script for each option (op1,op2,..,opN)

    Custom expression:
       if(${op1},"[X]",[ ])

    Script:
    #dp is the document property to set to true
      Document.Properties["op1"] = False
      Document.Properties["op2"] = False
      Document.Properties["op3"] = False
      Document.Properties[dp] = True

JavaScript Loader Indicator

 Sometimes is not obvious for the new user when something is running on the background. This shows a bigger progress indicator. It shows when the bottom right loader is spinning and hides otherwise.


You can customize your message with anything inside the outer div:

html

<div id="javascriptLoader">

  <h1>Loading</h1>
  <h4>Default theme</h4>
  <p>This loader overrides the default Spotfire loader
    <img height=86 src = "https://c.tenor.com/On7kvXhzml4AAAAj/loading-gif.gif">
  </p>

</div>     


js
//script parameters

customLoaderId = "javascriptLoader"

//Where is that spinning icon on the bottom right corner of the screen?
target = document.querySelector("[class^='sf-svg-loader']")

loader = document.getElementById(customLoaderId)
loader.hidden=true;

//wrap contents to box for theme
loader.innerHTML = `<div class="box">${loader.innerHTML}</div>`

//Show or hide the loader depending on the target style display state
trigger = function(x){
loader.hidden = target.style.display=="none"
}

//Run the trigger function if the target attribute changes 
mutationObserver = new MutationObserver(trigger)
mutationObserver.observe( target , {attributes:true} );

//hide progress overlay
loader.onclick  = function(){
  loader.hidden = true
  console.log("loader was clicked")
}

//add css
css = `
<style>
/* Absolute Center Loader */
#${customLoaderId} {
  position: fixed;
  z-index: 999;
  height: 2em;
  width: 2em;
  overflow: visible;
  margin: auto;
  top: 0;
  left: 0;
  bottom: 0;
  right: 0;
}

/* Transparent Overlay */
#${customLoaderId}:before {
  content: '';
  display: block;
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background-color: rgba(0,0,0,0.3);
}

/*default theme*/
#${customLoaderId} .box{
         color:gray;
    background: whitesmoke;
    text-align: center;
    transform: translate(-50%,-50%);
    position: absolute;
    cursor:pointer;
         width:333px;
}
</style>`

loader.insertAdjacentHTML( 'beforeend', css );

Create, Read and Delete Document Properties

from Spotfire.Dxp.Data import DataPropertyRegistry, DataPropertyClass, DataProperty, DataType, DataPropertyAttributes

# Create a property blueprint
propertyPrototype = DataProperty.CreateCustomPrototype("NewProperty", DataType.Integer, DataPropertyAttributes.IsVisible | DataPropertyAttributes.IsEditable | DataPropertyAttributes.IsPersistent | DataPropertyAttributes.IsPropagated)

# Instantiate the prototype
Document.Data.Properties.AddProperty(DataPropertyClass.Document, propertyPrototype)

# Set property
Document.Properties["NewProperty"] = 10

# Read property
print "NewPrperty value is: " + Document.Properties["NewProperty"]  

# Check the property's attributes
documentProperty = DataPropertyRegistry.GetProperty(Document.Data.Properties, DataPropertyClass.Document, "NewProperty")
print documentProperty.Attributes

# Delete property
DataPropertyRegistry.RemoveProperty(Document.Data.Properties, DataPropertyClass.Document, "NewProperty")
try:
print Document.Properties["NewProperty"] 
except:
print "NewProperty not found!"


# Delete all properties (cleanup)
# I use this script when using a dxp as a template that has many doc properties that I do not need anymore.
# WARNING: IT WILL REMOVE ALL PROPERTIES
from Spotfire.Dxp.Data import DataPropertyRegistry, DataPropertyClass
for p in Document.Properties:
try:
DataPropertyRegistry.RemoveProperty(Document.Data.Properties, DataPropertyClass.Document, p.Name)
except Exception as e :
print str(e)

tigger javascript on page load / scroll to the bottom of the page when page loads

1. add a text area at the bottom of the page with a div placeholder to determine the target location:

html 

<div hidden id="bottom"></div>

2. add a javascript to that text area to run script. wait a bit for page to load. (dont mess with window.onload)

js

setTimeout(()=>{

   //your script goes here. Example that scrolls into #bottom:
   document.getElementById("bottom").scrollIntoView({
     behavior: 'smooth'
   })


},1000) //adjust accordingly in ms 

star rating widget




html

<div  id='rating1'></div>
<div  id="dp1"><SpotfireControl id="3611e2f370ae4c2ab6786da2afd8c5e0" />
</div>

js

//script parameters
dp="dp1"
id="rating1"
options="1,2,3,4,5"
icon="★"


let inputs=[]
options.split(",").reverse().forEach((el,i)=>{
let anInput = `
    <input type="radio" name="rate_${options.split(",").join("")}" id="star${i}" value="${el}" />
<label for="star${i}" title="${el} rating">${el} rating</label>
`
inputs.push(anInput)
})


document.getElementById(id).innerHTML  = `
  <div class="rate_${id}">
${inputs.join("")}
  </div> 
  <span style="clear:both"> </span>
  
  <style>  
.rate_${id} {
float: left;
height: 50px;
padding: 0 0 0 54px;
}
.rate_${id}:not(:checked) > input {
position:absolute;
top:-9999px;
}
.rate_${id}:not(:checked) > label {
float:right;
width:1em;
overflow:hidden;
white-space:nowrap;
cursor:pointer;
font-size:30px; 
color:#ccc;
}
.rate_${id}:not(:checked) > label:before {
content: '${icon}';
padding:8px;
}
.rate_${id} > input:checked ~ label {
color: #ffc700;    
}
.rate_${id}:not(:checked) > label:hover,
.rate_${id}:not(:checked) > label:hover ~ label {
color: #deb217;  
}
.rate_${id} > input:checked + label:hover,
.rate_${id} > input:checked + label:hover ~ label,
.rate_${id} > input:checked ~ label:hover,
.rate_${id} > input:checked ~ label:hover ~ label,
.rate_${id} > label:hover ~ input:checked ~ label {
color: #c59b08;
}

  </style>`;

document.querySelectorAll(`.rate_${id} input`).forEach(e => {
e.addEventListener('click',(ev) => {
sfInput = document.querySelector("#"+dp+" input")
sfInput.value=ev.srcElement.value
sfInput.focus()
sfInput.blur()
});
})

Use Filters as custom dropdowns

html

<div id="main" style="display:flex;"> 

 <div class="nowrap">
  <SPAN class="dropdown-el icon" >
   <LABEL><SpotfireControl id="CALC_VALUE" /></LABEL> 
  </SPAN>
  <BR>
  <SPAN class=pop>
    <SpotfireControl id="FILTER" />
  </SPAN>
 </div>
:
:
:
 <div class="nowrap">
  <SPAN class="dropdown-el icon" >
   <LABEL><SpotfireControl id="CALC_VALUE" /></LABEL> 
  </SPAN>
  <BR>
  <SPAN class=pop>
    <SpotfireControl id="FILTER" />
  </SPAN>
 </div>

</div>  

js


//hides all
function hideAll(el){ [...document.getElementsByClassName("pop")] .map((e,i)=>{ if(el!=e){ e.style.visibility="hidden" } }) }

//bind events
var x=[...document.querySelectorAll(".dropdown-el.icon")]
x.forEach(el=>{
 el.addEventListener('click',(ev)=>{
   ev.stopPropagation()
   hideAll()
   let pop = el.parentNode.lastElementChild;
   pop.style.visibility=pop.style.visibility=="hidden"?"visible":"hidden"
 })
})

hideAll()

//add style
var css=`<style>
.dropdown-el {
  text-align: center;
  background: #ebf4fb;
  min-height: 95vh;
  margin: 0;
  padding: 0;
  border-bottom: 5vh solid #3694d7;
  font-family: "Myriad Pro", "Arial", sans;
  font-size: 12px;
}
.dropdown-el {
  max-width: 18em;
  position: relative;
  display: inline-block;
  margin-right: 1em;
  min-height: 2em;
  max-height: 2em;
  overflow: hidden;
  top: 0.5em;
  cursor: pointer;
  text-align: left;
  white-space: nowrap;
  color: #444;
  outline: none;
  border: 0.06em solid transparent;
  border-radius: 1em;
  background-color: #cde4f5;
  transition: 0.3s all ease-in-out;
}
.dropdown-el input:focus + label {
  background: #def;
}
.dropdown-el input {
  width: 1px;
  height: 1px;
  display: inline-block;
  position: absolute;
  opacity: 0.01;
}
.dropdown-el label {
  border-top: 0.06em solid #d9d9d9;
  display: block;
  height: 2em;
  line-height: 2em;
  padding-left: 1em;
  padding-right: 3em;
  cursor: pointer;
  position: relative;
  transition: 0.3s color ease-in-out;
}
.dropdown-el label:nth-child(2) {
  margin-top: 2em;
  border-top: 0.06em solid #d9d9d9;
}
.dropdown-el input:checked + label {
  display: block;
  border-top: none;
  position: absolute;
  top: 0;
  width: 100%;
}
.dropdown-el input:checked + label:nth-child(2) {
  margin-top: 0;
  position: relative;
}
.icon::after {
  content: "";
  position: absolute;
  right: 0.8em;
  top: 0.9em;
  border: 0.3em solid #3694d7;
  border-color: #3694d7 transparent transparent transparent;
  transition: 0.4s all ease-in-out;
}
.pop{
  border: 0.06em solid #3694d7;
  background: #fff;
  border-radius: 0.25em;
  padding: 0;
  box-shadow: rgba(0, 0, 0, 0.1) 3px 3px 5px 0px;
  min-height: 9em;
  overflow-y:auto;
  position:fixed;
  padding:2px;
  transition: 0.4s all ease-in-out;
 z-index:1;

</style>`

document.getElementById("main").insertAdjacentHTML('beforeend', css)

Reference: https://community.tibco.com/wiki/convert-filter-controls-custom-dropdowns


Add calculated value tooltips to a calculated value

Calculated values lacks of showing additional information other than the expression it was used (unless the display name changes). A little javascript helps to pass a hidden calculated value to it.

html

<div id="myCalcValue">
   Selected letters: <SpotfireControl id="6cdd0461700b42c98f246b8d61ffb55b" />
</div>

<div id="myCalcValueTootlips" hidden>
Calculated value tooltips:
  <SpotfireControl id="f245f0a3515d476783fbb36063e6669c" />
</div>

js

tooltip_source = "myCalcValueTootlips"
tooltip_target = "myCalcValue"

window.clearInterval(window.sectorTooltipInterval)
window.sectorTooltipInterval=setInterval(function(){
   document.querySelector("#"+tooltip_target).title = document.querySelector("#"+tooltip_source).innerText
}, 500);

output 








bypass html sanitation

To bypass html sanitation, just wrap your html with a <code> tag and add a single line of code in a js file. The file will replace the <code> tag with its contents

html

<input value="inputs not supported:"/>
<button onclick=msg('spotfire developer')>not supported</button>
<a title="not supported! really?" href="#">test</a>
<script>
  function msg(name){alert(`hello ${name}`)}
</script>

<hr>

<code>
<input value="inputs now supported:"/>
<button onclick="msg('spotfire developer')">now supported</button>
<a title="titles are now supported" href="#">mouse over to see tooltip (title attribute)</a>
<script>
function msg(name){alert(`good job ${name}`)}
</script>
</code>


js

//parses all code tags, but 
//does not parses properly the script tag
document.querySelectorAll("code").forEach(el=>{
el.innerHTML = el.innerText
})

//use jQuery if you need to parse the <script> tag
//var $ = window.CustomJQuery;
//$("code").html($($("code")[0].innerText))  //parse first code tag

//another way

   let cont = $("#"+id).contents().wrap('<span></span>').detach();
   $(`#${id}`).html(html)
   $(`#${id}_contents`).html(cont)








Hidden visualization properties

Sometimes latest API is not published or some functionality is not exposed. 

script

from Spotfire.Dxp.Application.Visuals.Maps import MapChart
#map is a script parameter pointing to your map
m = map.As[MapChart]()

import inspect
for i in inspect.getmembers(m):
    # Ignores anything starting with underscore 
    # (that is, private and protected attributes)
    if not i[0].startswith('_'):
        # Ignores methods
        if not inspect.ismethod(i[1]):
            print(i)


output

('AddAutoTitleTrigger', )
('ApplyColumnNamesOnAxis', )
:
('SerializeReadOnlyProperty', )
('ShowDescription', False)
('ShowInteractionMode', True)
('ShowLayerHandler', True)
('ShowNavigationControls', True)
('ShowScale', True)
('ShowSearchField', True)
('SupportsChangeDataTableByCheckingAxesAndExpressionColumns', )
('SupportsTransparency', True)
('SupportsTryChangeDataTable', )
('Title', 'Map chart')
('Transactions', )
('Transform', )
('Transparency', 0.0)
('Trellis', )
('TrellisLayerReference', )
('TryGetFilterRules', )
('TypeId', )
('UseSeparateColorForMarkedItems', False)
('ValidateAttached', )
('ViewExtent', )
('Visual', )

use case

from Spotfire.Dxp.Application.Visuals.Maps import MapChart
#map is a script parameter pointing to your map
m = map.As[MapChart]()

m.ShowLayerHandler= not m.ShowLayerHandler
m.ShowDescription= not m.ShowDescription
m.ShowSearchField= not m.ShowSearchField
m.ShowScale= not m.ShowScale
m.ShowInteractionMode= not m.ShowInteractionMode
m.ShowNavigationControls = not m.ShowNavigationControls #hide
m.ShowNavigationControls = True #show