Sisense Community logo
    • Community Feedback
    • Chapters
    • Events
    • Forums
      • Help and How To
      • Product Feedback Forum
      • Strategy & Use Cases
    • Blogs
    • KB Docs
      • KB Docs
      • Add-Ons & Plug-Ins
      • APIs
      • Best Practices
      • Blox
      • CDT
      • Cloud Managed Service
      • Data Models
      • Data Sources
      • Embedding Analytics
      • How-Tos & FAQs
      • Onboarding
      • PySisense
      • Security
      • Sisense Administration
      • Sisense Intelligence & AI
      • Troubleshooting
      • Widget & Dashboard Scripts
    • Support
    • Learning
      • Sisense Academy: Free Courses and Certifications
      • Official Developer Documentation
      • Official Product Documentation
      • Official Sisense Youtube Channel
      • Sisense Compose SDK Playground
    • Use Case Gallery
    All PostsDiscussionsBlogsIdeasQuestions
    Leaderboards
    •                    
    •                    
    •                    
    •                    
    •                    
    •                    
    •                    
    •                    
    •                    
    •                    
    •                    
    •                    
    •                    
    •                    
    •                    
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   
    Discussions
    • HomeChevronRightIcon
    • All postsChevronRightIcon
    • Knowledge Base DocsChevronRightIcon
    Widget & Dashboard Scripts

    Created Apr 15, 2026

    11 members

    224 discussions

    Widget & Dashboard Scripts
                       
                                       
    • Blog banner
      • Widget & Dashboard ScriptsChevronRightIcon

      [Linux] Accessibility in Sisense Fusion with Scripts

                       

      Accessibility (often abbreviated as a11y ) is about building applications that everyone can use, including people who rely on assistive technologies such as screen readers, keyboard navigation, voice control, or high contrast themes. If you're new to accessibility or would like to learn more about how it applies to Sisense and Compose SDK applications, check out our previous article: Accessibility in Compose SDK This article focuses specifically on Fusion Highcharts widgets and shows how to enhance the accessibility features already available in Sisense using Widget Scripts, Dashboard Scripts, or Plugins. Accessibility in Fusion Widgets Some Fusion widgets uses Highcharts to render. Highcharts includes an extensive Accessibility Module that helps charts become usable for users with disabilities by providing: Keyboard navigation Screen reader support Automatic chart descriptions High contrast compatibility Sisense do not enable the Highcharts accessibility module by default, so you will need an additional configuration options. Which approach should I use? There are three different ways to apply this configuration depending on your needs. Widget Script, rely on one widget. Dashboard Script, rely on all widgets in a dashboard. Plugins Entire Fusion, or in selected dashboards ids. All three approaches can use exactly the same Highcharts configuration. The only difference is where the code is executed. Option 1: Widget Script If you only need to improve accessibility for a single visualization, open the widget editor and navigate to: Edit Widget → Script Then paste the following script. (function () { widget.on("beforeviewloaded", function (se, ev) { var options = ev.options; // This configuration can be reused for the others scripts options.accessibility = { enabled: true, landmarkVerbosity: "all", highContrastTheme: true, keyboardNavigation: { enabled: true, seriesNavigation: { mode: "normal", }, }, point: { describeNull: true, }, series: { describeSingleSeries: true, pointDescriptionEnabledThreshold: 200, }, screenReaderSection: { beforeChartFormat: "<{headingTagName}>{chartTitle}</{headingTagName}>" + "<div>{typeDescription}</div>" + "<div>{chartSubtitle}</div>" + "<div>{chartLongdesc}</div>" + "<div>{viewTableButton}</div>", }, }; if (options.title && !options.title.text && ev.widget && ev.widget.title) { options.title.text = ev.widget.title; } if (options.exporting) { options.exporting.accessibility = { enabled: true, }; } }); })(); Option 2: Dashboard Script To apply the same accessibility improvements to every widget in a dashboard, move the same logic into a Dashboard Script. /** * Chart Accessibility (Highcharts) for All Widgets * This script turns on the Highcharts accessibility module with a fuller set of * options (keyboard navigation, screen-reader descriptions, live-data * announcements) for every chart widget on the dashboard, instead of requiring * the script to be added widget-by-widget. * * Features: * - Applies to all chart-type widgets currently on the dashboard * - Automatically wires up widgets added to the dashboard afterwards * - Ensures a screen-reader-visible chart title even if the widget title is hidden * - Enables accessible exporting menu * * Before Implementation: * - Only widgets whose type starts with "chart/" are targeted (Highcharts-based * widgets). Pivot, indicator, and map widgets are skipped since the Highcharts * accessibility module does not apply to them. */ (function () { // Applies the accessibility options to a single widget's Highcharts config function applyAccessibility(widget) { // Only Highcharts-based widgets support the accessibility module if (!widget.type || !widget.type.startsWith("chart/")) return; widget.on("beforeviewloaded", function (se, ev) { var options = ev.options; options.accessibility = { enabled: true, // How many ARIA landmarks are exposed to screen readers ("all" | "one" | "disabled"). landmarkVerbosity: "all", // Respect the OS/browser high-contrast setting instead of the widget's own colors. highContrastTheme: true, keyboardNavigation: { enabled: true, seriesNavigation: { // Lets arrow keys move between individual points, not just series. mode: "normal", }, }, point: { // Fallback wording for null/empty data points instead of skipping them silently. describeNull: true, }, series: { describeSingleSeries: true, // Avoid announcing every single point on very large series (perf + noise). pointDescriptionEnabledThreshold: 200, }, screenReaderSection: { // Surface a "View as data table" link for screen-reader users. beforeChartFormat: "<{headingTagName}>{chartTitle}</{headingTagName}>" + "<div>{typeDescription}</div>" + "<div>{chartSubtitle}</div>" + "<div>{chartLongdesc}</div>" + "<div>{viewTableButton}</div>", }, }; // Screen readers read the chart title first — make sure one exists even if // the widget's own title is visually hidden. if (options.title && !options.title.text && ev.widget && ev.widget.title) { options.title.text = ev.widget.title; } if (options.exporting) { options.exporting.accessibility = { enabled: true }; } }); } // Event Listeners // Wire up every widget already on the dashboard when it first loads dashboard.on("initialized", function () { for (const widget of dashboard.widgets.$$widgets) { applyAccessibility(widget); } }); // Wire up widgets added to the dashboard afterwards (e.g. via "Add Widget") dashboard.widgets.on("widgetadded", function (se, ev) { applyAccessibility(ev.widget); }); })(); This approach avoids duplicating Widget Scripts while keeping the changes limited to a single dashboard. Option 3: Plugin If accessibility improvements should be available throughout your Sisense environment, the same configuration can be added to a plugin. Once installed, you can configurate with specific dashboards that you want to apply and every compatible Highcharts widget automatically receives the enhanced accessibility configuration. You can download found the plugin here . Understanding the configuration Although the script is relatively small, it enables several useful Highcharts accessibility features. Keyboard Navigation keyboardNavigation: { enabled: true, seriesNavigation: { mode: "normal" } } Allows users to navigate through individual chart points using only the keyboard, improving accessibility for users who cannot use a mouse. High Contrast Support highContrastTheme: true Automatically adapts the chart colors when users enable High Contrast Mode in their operating system. Screen Reader Improvements screenReaderSection: { beforeChartFormat: ... } Creates a richer description for screen readers by including: Chart Title Chart Description Subtitle Long Description View as Data Table button This gives users additional context before interacting with the visualization. Better Handling of Missing Data point: { describeNull: true } Instead of silently skipping empty values, screen readers describe null data points. Large Dataset Optimization pointDescriptionEnabledThreshold: 200 Reading every point in a chart containing hundreds or thousands of values quickly becomes overwhelming. This threshold limits detailed point descriptions for large datasets while maintaining overall chart accessibility. Single-Series Improvements describeSingleSeries: true Provides more meaningful descriptions for charts containing only one series. Automatic Widget Titles if (options.title && !options.title.text && ev.widget && ev.widget.title) { options.title.text = ev.widget.title; } Screen readers announce the chart title before reading its contents. If the Highcharts title is empty but the Sisense widget has a title, this code copies the widget title into the Highcharts configuration, ensuring users always hear a meaningful title. Export Accessibility options.exporting.accessibility = { enabled: true }; When exporting is enabled, exported charts retain accessibility metadata whenever supported. Testing Your Changes After applying the script, it's a good idea to verify the accessibility improvements. Some useful testing methods include: Navigating the chart using only the keyboard Running Lighthouse accessibility audits Testing with screen readers such as: NVDA VoiceOver Windows/Mac Narrator Accessibility testing should always include real keyboard navigation and screen reader testing whenever possible. Limitations This solution applies only to Highcharts-based widgets available in Sisense Linux. It does not affect: Pivot tables BloX widgets Custom React visualizations Compose SDK custom widgets Those components require their own accessibility implementation. Conclusion Accessibility is an ongoing process rather than a one-time configuration. While Sisense provides a option through Highcharts, taking advantage of the additional accessibility options available can help create dashboards that are more inclusive and easier to use. We encourage you to experiment with these settings, adapt them to your organization's needs, and share any additional accessibility improvements. References/Related Content  Compose SDK Accessibility Scripting in Sisense Plugins in Sisense Highcharts Accessibility Module Plugin Repository Disclaimer: This post outlines a potential custom workaround for a specific use case or provides instructions regarding a specific task. The solution may not work in all scenarios or Sisense versions, so we strongly recommend testing it in your environment before deployment. If you need further assistance with this, please let us know.

      Micael Santana
      Micael SantanaPosted 1 week ago
      0
               
      • Widget & Dashboard ScriptsChevronRightIcon

      Copy a Pivot Cell Value to the Clipboard on Click [Linux]

                               

      Overview The Pivot 2.0 API exposes a cellClick event that fires whenever a user clicks a cell, handing to script the full cell context. This short guide uses that event to add a small convenience: click any cell and its value is copied straight to the clipboard, with a brief toast to confirm. The implementation is intentionally minimal. One event handler plus two small helpers. Availability: The Pivot 2.0 API (including cellClick) is available in Sisense L8.2.1 and later , on Linux versions, and only for Pivot 2.0 widgets.

      Step-by-step guide Open your Pivot 2.0 widget and click Edit Script (the pencil / script icon in the widget menu). Paste the script below into the editor. Click Apply , save the dashboard, and refresh. Clicking any cell now copies its value and shows a confirmation toast. widget.on("cellClick", function (widget, eventData) { const value = eventData?.metadata?.cellData?.content; if (value === undefined || value === null || value === "") { return; } if (typeof value !== "string") { console.error( "[pivot2-copy] Expected cell content to be a string, got " + typeof value + ":", value ); return; } copyToClipboard(value).then(function (ok) { if (ok) { showToast("Copied: " + value); } else { console.error("[pivot2-copy] Failed to copy cell value to clipboard"); } }); }); // --- helpers --------------------------------------------------------------- // Resolves to true only when the text actually reached the clipboard. // Prefer the async Clipboard API (requires HTTPS and, in iframe embeds, // allow="clipboard-write"); fall back to a hidden textarea otherwise. function copyToClipboard(text) { if (navigator.clipboard && navigator.clipboard.writeText) { return navigator.clipboard.writeText(text).then( function () { return true; }, function () { return legacyCopy(text); } ); } return Promise.resolve(legacyCopy(text)); } function legacyCopy(text) { const ta = document.createElement("textarea"); ta.value = text; ta.style.position = "fixed"; ta.style.opacity = "0"; document.body.appendChild(ta); ta.select(); let ok = false; try { ok = document.execCommand("copy"); } catch (e) { ok = false; } document.body.removeChild(ta); return ok; } function showToast(msg) { const id = "pivot2-copy-toast"; const existing = document.getElementById(id); if (existing) { existing.parentNode.removeChild(existing); } const el = document.createElement("div"); el.id = id; el.textContent = msg; el.style.cssText = [ "position:fixed", "bottom:24px", "left:50%", "transform:translateX(-50%)", "background:#2b2b2b", "color:#fff", "padding:8px 14px", "border-radius:6px", "font:13px/1.3 Arial, sans-serif", "box-shadow:0 2px 8px rgba(0,0,0,.3)", "z-index:99999", "opacity:0", "transition:opacity .15s ease", ].join(";"); document.body.appendChild(el); requestAnimationFrame(function () { el.style.opacity = "1"; }); setTimeout(function () { el.style.opacity = "0"; setTimeout(function () { if (el.parentNode) { el.parentNode.removeChild(el); } }, 200); }, 1400); } How it works Widget.on("cellClick", ...) fires on every cell click and passes eventData. The displayed text lives at eventData.metadata.cellData.content — that's what we copy, so it works for both value cells (the number) and member cells (the label). Empty cells (blank grand-total corners, etc.) are ignored, and a quick type check makes sure cell content is a string, as it should be. Toast is a self-removing element fixed to the bottom of the screen confirms the copy, then fades out after ~1.4s (can be adjusted). Re-clicking replaces any existing toast so they don't stack up. A note on the Clipboard API The copy uses the modern asynchronous Clipboard API (navigator.clipboard.writeText), which is the reliable path, but it has two requirements : Secure context (HTTPS) - on plain HTTP the Clipboard API is unavailable. Embedded dashboards (iframes) need allow="clipboard-write" on the iframe, or the write is blocked by permissions policy. When the Clipboard API is missing or blocked, the script falls back to the legacy approach — a hidden <textarea> plus document.execCommand("copy"). This works in more contexts (including some older embedded browsers) but is deprecated and can fail silently in others. Because both paths return their real success/failure, the toast only shows "Copied:" when the value genuinely reached the clipboard. If both paths fail, nothing is copied and a message is logged to the browser console ([pivot2-copy] Failed to copy...) rather than misleading the user with a false confirmation. Notes and limitations Pivot 2.0 / Linux / L8.2.1+ only, as noted above. The field path metadata.cellData.content reflects the current event payload; if a future version changes the shape, it should be adjusted. This copies the displayed content (formatted text), not the underlying raw numeric value. To copy the raw value target eventData.metadata.cellData.value Resources Pivot 2.0 API — cellClick event and the cell context payload: https://developer.sisense.com/guides/customJs/jsApiRef/widgetClass/pivot2.html#cellclick Widget class: https://developer.sisense.com/guides/customJs/jsApiRef/widgetClass/#widget-class

      Ivan Amoshyi
      Ivan AmoshyiPosted 3 weeks ago
      0
               
      • Widget & Dashboard ScriptsChevronRightIcon

      Freezing the first row and first columns of a Pivot 2.0 widget [Linux]

      Introduction We have received a couple of requests from our community for the ability to freeze columns and rows in Pivot widgets. To address this need while we still don't have it as a built-in feature, I have created a script and accompanying article to help you achieve this functionality. Here you will learn how to freeze (pin) the header row and the first columns of a Sisense Pivot 2.0 widget so they stay visible while scrolling large tables. Covers both the native Sisense freeze option and a custom widget script for cases the native feature doesn't cover. Applies to both Cloud and on-prem. Tested on version 2026.2.2-d2. Step-by-Step Guide Option 1: Native Sisense pivot freezing (recommended when it applies) Before using any custom script, check whether Sisense's built-in freezing covers your need - it is the most stable and maintainable option since it's a native feature. Sisense Pivot 2.0 does not have a dedicated "freeze" toggle in the widget design. Instead, freezing happens automatically as part of the pivot's own rendering and scrolling logic, under two conditions: Native vertical freezing (header row): The header row stays pinned automatically when the pivot has to scroll its content vertically. The key is that the widget must be constrained to a height smaller than the total content, so the table scrolls internally rather than expanding to fit everything. Native horizontal freezing (Rows fields): During horizontal scroll, Sisense automatically keeps the fields you added to the pivot's Rows panel pinned on the left, while the fields in the Values panel scroll horizontally. In other words, your row dimensions (for example, Brand and Category) stay in place and only the value columns move. This native horizontal freezing is only useful when your pivot has few Rows fields and many Values - the row dimensions stay visible while you scroll through the many value columns. How to enable native freezing (via auto height + widget height): Open the dashboard and enter edit mode. Open the Pivot 2.0 widget. Go to the widget's Design panel. Disable the Auto Height option. When auto height is on, the widget grows to fit all its content, so there is no internal scroll - and therefore nothing to freeze against. Set the widget height to a value smaller than the amount of content you can see in your screen . This forces the pivot to scroll internally. Apply and save. With auto height off and a constrained height, the header row remains pinned during vertical scroll, and the Rows fields remain pinned during horizontal scroll. Conditions and limitations of native freezing: This behavior relies on the widget having a height smaller than its content so it scrolls internally. If auto height is enabled, the header will not stay pinned. Vertical: the header row is pinned during vertical scroll. Horizontal: only the fields in the Rows panel are pinned during horizontal scroll - the Values columns scroll. This is only helpful when you have few Rows fields and many Values. If your requirement falls outside this (for example, freezing a specific number of leftmost visual columns regardless of whether they are Rows or Values, or a layout the native logic doesn't handle), use the custom script in Option 2.

      > [ GIF Description: native Sisense pivot freezing in action - show the header row staying pinned during vertical scroll, and the Rows fields staying pinned while the Values columns scroll horizontally.] Option 2: Custom widget script (when native freezing doesn't cover the use case) Use this approach only when the native freezing option does not achieve what you need - for example, when you need to freeze a specific number of leftmost visual columns rather than only the Rows fields, or when your pivot has many rows and the native Rows-based horizontal freezing does not fit your layout. When a pivot table has many rows and columns, scrolling causes the header row and the leftmost identifying columns (for example, Brand and Category) to scroll out of view, making it hard to read the data. This solution uses a widget script that mirrors the same 4-pane model Sisense itself uses internally. It clones the pivot table into fixed overlay panes, each cropped to show only the frozen region: Top pane - shows only the header row (cropped to header height) Left pane - shows only the first N columns (cropped to their width) Corner pane - shows the top-left intersection of both Each pane is a full clone of the table, so the cells keep their content and layout. As the user scrolls, the clones are shifted in the opposite direction to stay aligned with the real table underneath. Why a MutationObserver is used Pivot 2.0 does not re-fire the ready event when the user paginates - it swaps the table body in place. This means a one-time clone would become stale (it would keep showing page 1 values while the user is on page 2). To handle this, the script rebuilds the panes whenever the underlying table changes, using a MutationObserver , and also re-aligns them on scroll. How to apply the script Open the dashboard and enter edit mode. Open the Pivot 2.0 widget you want to freeze. In the widget's Design panel, disable the Auto Height option and set the widget height to a value smaller than its total content. You can set the height in one of two ways: by entering a number in the manual row height field in the widget Design panel, or by manually resizing the widget - clicking and dragging its edge - as long as the size you set is not bigger than the amount of content you can see on your screen. As explained in Option 1, this forces the pivot to scroll internally, which is what keeps the header row pinned. The script relies on this internal scroll to align the frozen panes. Click the widget menu (three dots) and select Edit Script . Paste the script below into the widget script editor. Adjust the FROZEN_COLS value at the top to match how many leftmost columns you want to freeze (the example freezes 2 columns). Save the script and refresh the widget. The script /** * Freeze first row + first column(s) of a Pivot 2.0 widget — cropped-clone panes. * * APPROACH: mirror Sisense's own 4-pane model. Clone the ENTIRE pivot table * into fixed overlay panes, each cropped with overflow:hidden so it shows only * the frozen region: * - TOP pane -> header row only (cropped to header height) * - LEFT pane -> first N columns only (cropped to their width) * - CORNER pane -> top-left intersection * Each pane is a full <table> clone, so cells keep content + layout. Panes are * pinned; the inner clone is translated opposite to scroll to stay aligned. * * IMPORTANT: Pivot 2.0 does NOT re-fire `ready` on pagination — it swaps the * table body in place. So a one-time clone goes stale (shows page 1 values on * page 2). We therefore REBUILD the panes whenever the real table mutates, * via a MutationObserver, and also on scroll for alignment. * * CONFIG */ var FROZEN_COLS = 2; // Brand + Category (col-0, col-1) var FREEZE_BORDER = '1px solid #c6cbd4'; // frozen-edge divider (match theme gridline) widget.on('ready', function () { var $el = $(element); var observer = null; var rebuildTimer = null; function getView() { var $v = $el.find('.pivot-scroller__view').first(); return $v.length ? $v : null; } function buildPanes() { var $view = getView(); if (!$view) return; var view = $view[0]; var $realTable = $view.find('table.table-grid__table').first(); if (!$realTable.length) return; // Remove previous panes before rebuilding (prevents stacking / staleness) var $scroller = $el.find('.pivot-scroller').first(); $scroller.find('.freeze-pane').remove(); $scroller.css('position', 'relative'); // Measure header height + frozen-column width from the CURRENT table var $headerRow = $realTable.find('tr.table-grid__row-0').first(); var headerH = $headerRow.length ? $headerRow[0].getBoundingClientRect().height : 0; var frozenW = 0; for (var c = 0; c < FROZEN_COLS; c++) { var $cell = $realTable.find('td[class*="table-grid__cell--col-' + c + '"]').first(); if ($cell.length) frozenW += $cell[0].getBoundingClientRect().width; } if (!headerH || !frozenW) return; function makePane(kind, cropW, cropH, zIndex) { var $pane = $('<div class="freeze-pane freeze-pane--' + kind + '"></div>').css({ position: 'absolute', top: 0, left: 0, width: (cropW ? cropW + 'px' : '100%'), height: (cropH ? cropH + 'px' : '100%'), overflow: 'hidden', 'pointer-events': 'none', background: '#fff', 'box-sizing': 'content-box', // border sits OUTSIDE cropW; no column pixels clipped 'z-index': zIndex }); // Fresh clone of the CURRENT table state (current page's rows) var $clone = $realTable.clone(); $clone.css({ position: 'absolute', top: 0, left: 0, margin: 0 }); $pane.append($clone); $pane.data('clone', $clone); return $pane; } var $left = makePane('left', frozenW, null, 4); var $top = makePane('top', null, headerH, 5); var $corner = makePane('corner', frozenW, headerH, 6); // Frozen-edge dividers. overflow:hidden on each pane clips the // clone's own border, so we draw the edge on the PANE itself: // - right edge -> frozen COLUMN boundary (left + corner panes) // - bottom edge -> frozen HEADER boundary (top + corner panes) $left.css('border-right', FREEZE_BORDER); $top.css('border-bottom', FREEZE_BORDER); $corner.css({ 'border-right': FREEZE_BORDER, 'border-bottom': FREEZE_BORDER }); $scroller.append($left).append($top).append($corner); function sync() { var x = view.scrollLeft; var y = view.scrollTop; $left.data('clone').css('transform', 'translate(0px,' + (-y) + 'px)'); // pin X, follow Y $top.data('clone').css('transform', 'translate(' + (-x) + 'px,0px)'); // pin Y, follow X $corner.data('clone').css('transform', 'translate(0px,0px)'); // pin both } $view.off('scroll.freeze').on('scroll.freeze', sync); sync(); } // Debounced rebuild so rapid DOM mutations (pagination render) collapse to one pass function scheduleRebuild() { clearTimeout(rebuildTimer); rebuildTimer = setTimeout(buildPanes, 30); } // Watch a STABLE ancestor for content swaps (pagination, sort, filter re-render). // The inner <table> inside .pivot-scroller__view is REPLACED on pagination, so // observing the table (or its immediate parent) stops firing once it's swapped. // .sisense-pivot / .multi-grid persist across re-renders, so observe there. function attachObserver() { var target = $el.find('.sisense-pivot').first()[0] || $el.find('.multi-grid').first()[0] || $el.find('.pivot-container').first()[0]; if (!target) return; if (observer) observer.disconnect(); observer = new MutationObserver(function (mutations) { // Ignore mutations that are only our own overlay panes, to avoid an // observe -> rebuild -> mutate -> observe feedback loop. function isPane(n) { return n && n.nodeType === 1 && n.className && String(n.className).indexOf('freeze-pane') !== -1; } function nodesArePanes(list) { if (!list || !list.length) return true; // nothing added/removed for (var i = 0; i < list.length; i++) { if (!isPane(list[i])) return false; } return true; } var relevant = mutations.some(function (m) { if (isPane(m.target)) return false; // change inside a pane if (m.target && isPane(m.target.parentNode)) return false; // If this mutation only added/removed panes, ignore it return !(nodesArePanes(m.addedNodes) && nodesArePanes(m.removedNodes)); }); if (relevant) scheduleRebuild(); }); observer.observe(target, { childList: true, subtree: true, characterData: true }); } buildPanes(); attachObserver(); }); Customizing the script FROZEN_COLS - Set this to the number of leftmost columns you want to keep frozen. For example, set it to 1 to freeze only the first column, or 3 to freeze the first three. FREEZE_BORDER - Controls the divider line on the frozen-column edge. Adjust the color to match your dashboard theme's gridlines.
      > [GIF Description: custom widget script in action - show the header row and first columns staying frozen while scrolling both vertically and horizontally, and remaining correct after pagination.] Conclusion Sisense Pivot 2.0 has its own internal rendering and scrolling logic - it decides how the table is drawn and scrolled, and under certain conditions it will freeze rows and columns automatically as part of that logic while we do not have the option to change it natively. Specifically, when the widget has a fixed height smaller than its content, the header row stays pinned during vertical scroll; and during horizontal scroll, the fields in the Rows panel stay pinned while the Values columns scroll (which is most useful when you have few Rows fields and many Values). This is why the native behavior should always be your first choice when it covers your use case: it works with the pivot's own engine, making it the most stable and maintainable approach. When the native behavior does not fit your specific requirement (for example, freezing a specific number of leftmost visual columns rather than only the Rows fields), the custom widget script replicates the behavior by cloning the table into cropped, pinned overlay panes for the header row, first columns, and their intersection. Because Pivot 2.0 swaps the table body in place during pagination, sorting, and filtering, the script uses a MutationObserver to rebuild the frozen panes automatically, keeping them accurate as the data changes. Adjust FROZEN_COLS to control how many columns stay pinned. Please note that this method may not cover every possible scenario or combination of features as it was developed thinking in simple scenarios, so experiencing issues in certain situations is a possibility. References/Related Content Customizing Sisense using JavaScript Pivot 2.0 API Widget class API reference Sisense Pivot documentation Disclaimer: This post outlines a potential custom workaround for a specific use case or provides instructions regarding a specific task. The solution may not work in all scenarios or Sisense versions, so we strongly recommend testing it in your environment before deployment. If you need further assistance with this, please let us know.

      Thalita Santos
      Thalita SantosPosted 3 weeks ago
      0
               
    • Blog banner
      • Widget & Dashboard ScriptsChevronRightIcon

      How to Hide a Column in Pivot 2.0

                       

      This article shows a solution for hiding a column in Pivot 2.0 . There are several ways to hide a column in a Sisense Pivot, and you can find a few of them here in the Sisense Community. This article presents another approach that can have advantages and limitations. Before getting started, it's important to know that Sisense has two different versions of the Pivot widget. This only affects users running Windows or Linux versions earlier than L8.2.1 . The newer version of the Pivot introduces additional capabilities, which also changes the way columns can be hidden. If you're looking for a solution that works with Windows/Pivot 1.0 , try this approach instead. The transformPivot method runs between the data fetching and DOM rendering phases. Instead of directly modifying the CSS or the DOM, it changes the Pivot properties so the widget renders differently. Another important detail is that this method works on individual cells, as shown in the following script: // SETUP: Add this script to a Pivot widget via Widget Script (Edit Widget > Script tab). // It hides specific measure columns from the pivot table without removing them from the data. widget.transformPivot({}, function (metadata, cell) { // SETUP: List the exact column titles you want to hide. // These must match the measure titles shown in the widget header (case-sensitive). // Example: ['Total Revenue', 'Cost', 'Margin %'] const columnsToHide = ["Cost"]; const columnTitle = (metadata.measure && metadata.measure.title) || (metadata.column && metadata.column.title) || null; if (columnTitle && columnsToHide.includes(columnTitle)) { cell.style.width = "0px"; cell.style.minWidth = "0px"; cell.style.maxWidth = "0px"; cell.style.padding = "0px"; cell.style.margin = "0px"; cell.style.border = "none"; cell.style.color = "transparent"; cell.style.fontSize = "0px"; cell.content = ""; } }); The script walks through each cell using transformPivot and retrieves the title of the measure or column. This makes it possible to provide a simple configuration that identifies the specific column title to hide. Since Pivot in general works with individual cells, every cell that belongs to the target column must be hidden. The style properties applied in the script work together to fully hide the column. While some of these properties alone can hide content with CSS, using all of them ensures the entire column is hidden correctly. Why not use display: none ? The Pivot is a smart widget that automatically calculates the layout sizes based on the cells size. When a cell is resized, the Pivot recalculates the layout to keep it responsive. Using display: none removes the element from the layout, which interferes with these calculations. As a result, columns may no longer be positioned correctly, especially when working with nested columns. For this reason, reducing the cell dimensions (such as width) is preferred over removing the element from the layout.

      Example of Pivot using display none to hide
      Using jQuerry You may have seen solutions that manipulate the Pivot using CSS programmatically. While those approaches can work, they are generally not the best practice. // This script hide the first column programmatically widget.on("ready", () => { $(".table-grid__cell--col-0", element).width(0); $(".table-grid__cell--col-0", element).hide(); // Equivalent to display: none }); As explained earlier, the Pivot is a smart widget. Although it is ultimately built with HTML and CSS, it is rendered based on internal properties. Hiding a cell directly in the DOM without updating those properties can lead to unexpected sizing issues because the Pivot may still treat the hidden cell as part of the layout.
      Example of a Pivot using DOM hide option
      Using transformPivot avoids this problem because the changes are applied before the DOM is rendered rather than after. Another potential issue with CSS-based approaches is relying on CSS classes. Class names within the application can change over time, causing the script to break. Whenever possible, avoid depending on CSS class selectors if a more reliable approach is available. References/Related Content  Sisense: Pivot 2.0 API Documentation Previous version of Script Pivot 2.0: Manipulating a Pivot Chart Customizing a Pivot 1.0 Widget (Legacy) Disclaimer: This post outlines a potential custom workaround for a specific use case or provides instructions regarding a specific task. The solution may not work in all scenarios or Sisense versions, so we strongly recommend testing it in your environment before deployment. If you need further assistance with this, please let us know.

      Micael Santana
      Micael SantanaPosted 1 month ago
      0
               
    • Blog banner
      • Widget & Dashboard ScriptsChevronRightIcon

      Setting Maximum Panel Items on Widget Panels via Scripting

                                                                                       

      Setting Maximum Items on Widget Panels via Script Sisense widgets set max panel item per type (values, rows, etc) limits that control how many dimensions, measures, and filters can be added to each panel. When these limits are reached, the Add Panel button is hidden, preventing users from adding additional fields. A widget script that sets the maxitems property on each panel is commonly used to raise (or occasionally lower) this limit. This very commonly used script may be experiencing issues on some new releases of Sisense (L2026.2.2-c). Dashboard designers and developers who have this script in place and are not experiencing any issues may leave the script unchanged. Dashboard designers who are experiencing problems can replace the script with the version shared below. For use cases that specifically use this script for pivot widgets and want it applied automatically across many widgets or configured per user group, the PivotMaxPanelItems plugin is a more scalable alternative. See Extending Pivot Widget Panel Limits in Sisense Using User Groups for details. Script That May Not Work The following script is commonly used as a widget script to raise the panel item limit: widget.manifest.data.panels.forEach(function(p) { p.metadata.maxitems = 40; // replace with the value needed }); This script accesses the panel metadata maxitems parameter directly through widget.manifest.data.panels . Based on current behavior, this path appears to be broken in some environments on specific Sisense versions.

      Without the script applied, if a panel type includes too many items, the Add Panel Button is hidden. Replacement Script The following script produces the same result and can be more reliable. widget.on('initialized', function() { widget.metadata.panels.forEach(function(p) { p.$$manifest.metadata.maxitems = 60; // replace with the value needed }); }); This version waits for the initialized event before accessing panel data, and references the panel's $$manifest.metadata path rather than going through widget.manifest.data . Dashboard designers and developers should update the maxitems value to match the limit needed for their use case. Note that setting a very high limit and adding a large number of fields to a widget may result in extended load times.
      With the script applied, the Add Panel Button does not disappear when below the new limit, and as many dimensions and fields as needed can be added to the widget. Applying This to More Complex Scripts and Plugins This potential fix applies to any plugin or script that sets maxitems on widget panels and uses this exact path. If a plugin or script accesses panel metadata through widget.manifest.data.panels and is functioning correctly, no changes are necessary. If it appears to no longer work, updating it to use widget.metadata.panels and reference p.$$manifest.metadata is likely to fix the issue. The scripts above are simply examples. More dynamic implementations, such as those that read current values, apply conditional logic, or use different values based on user type or user group, follow the same principle, if the older method is not working, replacing the maxitems path is a likely fix.

      Jeremy Friedel
      Jeremy FriedelPosted 1 month ago
      0
               
      • Widget & Dashboard ScriptsChevronRightIcon

      How to apply relationship filters in a dashboard programmatically [Linux]

      Introduction This article demonstrates how to programmatically apply filter relationships (e.g., OR logic between filters) in a Sisense dashboard using a dashboard script and the REST API. The solution is tested on Sisense L2026.2.1-c and works for both cloud and on-premises deployments. By following this guide, you can automate the application of filter relationship logic without manual UI configuration according to your needs. Step-by-step guide 1. Understand the filter relationship editor You can replicate the Relationship Filters behavior applied via UI the following way: in any dashboard, go to the filters panel, click the three dots menu → Filter relationship editor , apply the desired relationship to your existing filters, and save. You will see a PUT request is made to your dashboard adding the desired filter relations.

      Tip: The payload structure will vary depending on the relationship logic you need (OR, AND, nested expressions, etc.). The easiest approach is to first configure it manually in the UI, inspect the PUT request in your browser's network tab, copy the payload, and adapt it in your script. 2. Prepare your environment Create a new dashboard or go to an existing dashboard. Make sure you have at least two filters already added to the dashboard that you want to relate (e.g., Brand and Category). 3. Apply the code Go to your dashboard, click on the three dots menu at the dashboard level and select Edit Script , then paste the following code: dashboard.on("initialized", function (scope, args) { var dash = args.dashboard; var dashboardId = dash.oid; var $internalHttp = prism.$injector.get("base.factories.internalHttp"); // GET current dashboard filterRelations to check if OR is already applied $internalHttp( { url: "/api/dashboards/" + dashboardId + "?fields=filterRelations", method: "GET", dataType: "json", }, false, ) .then(function (response) { var existingRelations = response.data && response.data.filterRelations; // Check if OR logic is already present var alreadyApplied = existingRelations && existingRelations.length > 0 && existingRelations[0].filterRelations && existingRelations[0].filterRelations.value && existingRelations[0].filterRelations.value.operator === "OR"; if (alreadyApplied) { console.log("filterRelations OR already set. No action needed."); return; } // OR logic not present - apply it and reload console.log("filterRelations OR not set. Applying now..."); applyFilterRelationsAndReload(dash, dashboardId, $internalHttp); }) .catch(function (err) { console.error("Error checking current filterRelations:", err); }); }); function applyFilterRelationsAndReload(dash, dashboardId, $internalHttp) { var filters = dash.filters.$$items; var firstFilter = filters.find(function (f) { return f.jaql && f.jaql.dim === "[Brand.Brand]"; // Change to your required filter }); var secondFilter = filters.find(function (f) { return f.jaql && f.jaql.dim === "[Category.Category]"; // Change to your required filter }); if (!firstFilter || !secondFilter) { console.warn( "Could not find Brand or Category filter on dashboard. Aborting.", // Both filters should be present in the dashboard so we can apply the OR relation ); return; } var firstFilterInstanceId = firstFilter.instanceid; var secondFilterInstanceId = secondFilter.instanceid; var payload = { filterRelations: [ { datasource: "Sample ECommerce", // Change to your datasource accordingly, and your desired filter relations structure. filterRelations: { type: "ParenthesizedLogicalExpression", value: { type: "LogicalExpression", operator: "OR", left: { type: "Identifier", instanceId: firstFilterInstanceId }, right: { type: "Identifier", instanceId: secondFilterInstanceId }, }, }, }, ], filters: [ { jaql: firstFilter.jaql, instanceid: firstFilterInstanceId, isCascading: firstFilter.isCascading || false, }, { jaql: secondFilter.jaql, instanceid: secondFilterInstanceId, isCascading: secondFilter.isCascading || false, }, ], }; $internalHttp( { url: "/api/dashboards/" + dashboardId, method: "PUT", data: JSON.stringify(payload), contentType: "application/json", dataType: "json", }, false, ) .then(function () { console.log( "filterRelations OR logic applied successfully. Reloading...", ); location.reload(); }) .catch(function (err) { console.error("Failed to apply filterRelations:", err); }); }
      4. Code explanation This is a dashboard script that runs on the initialized event. When the dashboard loads, the script: Checks existing relations — It performs a GET request to the dashboard API to verify if the OR filter relationship is already applied. If it is, no action is taken. Finds the target filters — It locates the two filters on the dashboard by their dimension ( [Brand.Brand] and [Category.Category] in this example) and retrieves their instance IDs. Builds the payload — It constructs the filterRelations payload with the OR operator linking both filters via their instance IDs. Applies via PUT — It sends a PUT request to the dashboard API with the payload, then reloads the page to reflect the changes. The check-before-apply logic ensures the script doesn't unnecessarily re-apply the relationship or trigger repeated reloads on every dashboard load. Conclusion By leveraging the initialized dashboard event and the Sisense REST API, you can programmatically apply filter relationship logic that would otherwise require manual configuration through the UI. This is especially useful when you need to enforce specific filter relationships across multiple dashboards or ensure the logic persists regardless of user changes. Remember to adjust the dimension names, datasource, and payload structure to match your specific environment and requirements. References/Related content Sisense JavaScript API Reference Disclaimer: This post outlines a potential custom workaround for a specific use case or provides instructions regarding a specific task. The solution may not work in all scenarios or Sisense versions, so we strongly recommend testing it in your environment before deployment. If you need further assistance with this, please let us know.

      Thalita Santos
      Thalita SantosPosted 1 month ago
      0
               
      • Widget & Dashboard ScriptsChevronRightIcon

      How to add/remove a filter based on another filter's selection [Linux]

      Introduction This article demonstrates how to programmatically add or remove a dashboard filter in Sisense when another specific filter is added or removed. The solution is tested on Sisense L2026.2.1-c and works for both cloud and on-premises deployments. By following this guide, you can create dependent filter logic where one filter automatically controls the presence of another. Step-by-step guide 1. Prepare your environment Create a new dashboard or go to an existing dashboard. Make sure you have at least one filter already added to the dashboard (this will be your "trigger" filter — Filter A). 2. Apply the code Go to your dashboard, click on the three dots menu at the dashboard level and select Edit Script , then paste the following code: var isUpdatingFilterB = false; var filterADim = "[Brand.Brand]" var filterBDim = "[Brand.Brand ID]" dashboard.on("filterschanged", function (args) { if (isUpdatingFilterB) return; // Find Filter A: [Brand.Brand] var filterA = dashboard.filters.$$items.find(function (f) { return f.jaql && f.jaql.dim === filterADim; }); var filterOptions = { save: false, refresh: true, doNotTriggerChange: true, }; isUpdatingFilterB = true; try { if ( filterA && filterA.jaql.filter && filterA.jaql.filter.members && filterA.jaql.filter.members.length > 0 ) { // Filter A is active with selected members — apply Filter B var filterBJaql = { jaql: { dim: filterBDim, datatype: "text", title: "Brand ID", filter: { explicit: true, multiSelection: true, members: ["12"], // Apply as 12 - adjust if you need specific members }, }, }; dashboard.filters.update(filterBJaql, filterOptions); } else { // Filter A is removed or set to Include All — remove Filter B var existingFilterB = dashboard.filters.$$items.find(function (f) { return f.jaql && f.jaql.dim === filterBDim; }); if (existingFilterB) { dashboard.filters.remove(existingFilterB, filterOptions); } } } finally { // Reset the flag after a short delay to allow the update to complete setTimeout(function () { isUpdatingFilterB = false; }, 300); } });

      3. Code explanation In this example we are listening for the filterschanged event at the dashboard level. When any filter changes, the script checks whether Filter A ( [Brand.Brand] ) is active with selected members. If it is, Filter B ( [Brand.Brand ID] ) is automatically applied with the specified members. If Filter A is removed or set to "Include All," Filter B is automatically removed from the dashboard. The isUpdatingFilterB flag prevents an infinite loop — since adding/removing Filter B would itself trigger filterschanged , the flag ensures the script ignores its own changes. The flag is reset after a 300ms delay using setTimeout to allow the filter update to complete before the script becomes responsive again. This is a dashboard script (not a widget script), so it applies globally to the entire dashboard regardless of which widget is selected. Conclusion By leveraging the filterschanged dashboard event, you can create dependent filter relationships where one filter automatically controls the presence of another. This approach is useful when you need to enforce business rules — for example, always applying a secondary filter whenever a user selects a specific dimension value. The provided script is a flexible starting point — feel free to adjust the dimension names, member values, and logic to match your specific use case. References/Related content Sisense JavaScript API Reference Disclaimer: This post outlines a potential custom workaround for a specific use case or provides instructions regarding a specific task. The solution may not work in all scenarios or Sisense versions, so we strongly recommend testing it in your environment before deployment. If you need further assistance with this, please let us know.

      Thalita Santos
      Thalita SantosPosted 1 month ago
      0
               
      • Widget & Dashboard ScriptsChevronRightIcon

      How to apply a custom fixed sort order in a pivot table [Linux]

      Introduction This article demonstrates how to apply a custom fixed sort order to row values in a Sisense Pivot table using a widget script. The solution is tested on Sisense L2026.2.1-c and works for both cloud and on-premises deployments. By following this guide, you can pin specific items to the top of your pivot in a defined sequence, regardless of the default alphabetical or data-driven sort. Step-by-step guide 1. Prepare your environment Create a new dashboard or go to an existing dashboard, and create a Pivot widget (or use an existing one). 2. Apply the code Edit the Pivot widget, go to the three dots menu and Edit Script , and paste the following code: var classificationOrder = [ "Emdimax", "Dopglibin", "Ciptanefar", "Addimar", "ABC", ]; var refreshed = false; // Reset the flag on every genuine data fetch (filter change, sort change, etc.) // beforequery does NOT fire on the script-triggered sender.refresh() // when rawQueryResult is already present — only on real new queries. // You can remove/comment this beforequery event if you want the logic to be // applied only in the first dashboard-load. widget.on("beforequery", function (sender, ev) { refreshed = false; }); widget.on("domready", function (sender, ev) { if (sender.rawQueryResult && !refreshed) { refreshed = true; var rawData = sender.rawQueryResult; if (rawData && rawData.data && rawData.data.data) { var groups = rawData.data.data; var pinned = []; classificationOrder.forEach(function (brand) { for (var g = 0; g < groups.length; g++) { var group = groups[g]; if (!group.data || !Array.isArray(group.data)) continue; for (var i = 0; i < group.data.length; i++) { var val = group.data[i].value ? group.data[i].value.trim() : ""; if (val === brand) { pinned.push(group.data.splice(i, 1)[0]); break; } } } }); if (groups[0] && groups[0].data) { Array.prototype.unshift.apply(groups[0].data, pinned); } sender.refresh(); } } });

      3. Code explanation In this example we are defining a classificationOrder array with the exact sequence of row values we want pinned at the top of the pivot. The script listens for the domready event, accesses the raw query result data, extracts matching items from the data groups, and re-inserts them at the top in the defined order. The refreshed flag ensures sender.refresh() runs only once per data cycle, preventing an infinite loop. This refresh is necessary because Pivot 2.0 splits its render into HTML and raw data — a single pass won't visually apply the reorder. Additionally, the beforequery event listener resets the refreshed flag whenever a genuine new query is executed (e.g., when the user changes a filter or sort). This is important because beforequery does not fire on the script-triggered sender.refresh() when rawQueryResult is already present — it only fires on real new queries. This means the custom sort is correctly re-applied after every user interaction that fetches fresh data, while still avoiding the infinite refresh loop. Conclusion By leveraging the Pivot widget's scripting capabilities and the domready event, you can override the default sort behavior and enforce a fixed custom order for specific row values. This is particularly useful when business logic requires certain items (e.g., key products or categories) to always appear first, regardless of how the underlying data is sorted. The use of beforequery to reset the flag ensures the sort persists through filter and sort changes without manual intervention. The provided script is a flexible starting point — feel free to expand the classificationOrder array with your own values as needed for your use case. References/Related content Sisense JavaScript API Reference Disclaimer: This post outlines a potential custom workaround for a specific use case or provides instructions regarding a specific task. The solution may not work in all scenarios or Sisense versions, so we strongly recommend testing it in your environment before deployment. If you need further assistance with this, please let us know.

      Thalita Santos
      Thalita SantosPosted 1 month ago
      0
               
    • Blog banner
      • Widget & Dashboard ScriptsChevronRightIcon

      Hiding Widgets if a Widget Has No Results [Linux]

                                                                                                               

      Hiding Widgets if a Widget Has No Results [Linux] Introduction A common dashboard design requirement is hiding widgets that have no data. This article includes a widget script that conditionally hides an indicator widget when its primary value is empty or represents an N/A like value. This behavior is commonly requested when dashboard filters or formulas result in no meaningful value and the widget should be visually hidden rather than showing an empty or zero indicator. The article also includes an alternative dashboard-level approach that hides widgets based on filter selections, without inspecting widget query results. This alternative is derived from the linked external blog post . This is applicable to both on-cloud and on-prem Sisense in all recent Sisense versions. Use Case Customers often want indicator widgets to disappear when their calculated value is not meaningful. Common examples include: Filters resulting in no matching data Calculations returning N/A, null, or empty values Conditional metrics that only apply to certain filter selections Rather than showing an empty indicator, this approach hides the widget entirely and restores it automatically when the value becomes valid again. Layout Considerations For best visual results, it is ideal to use one of these two layouts for the indicator widgets that may be hidden: Place the indicator widget in its own dashboard row Place it at the end of a row Hiding a widget via a script does not automatically resize or reflow other widgets on the same row. Step by Step Guide Widget Script to Hide Widget Based on Indicator Value This widget script is applied directly to the indicator widget. It evaluates the widget’s primary value after each render and hides or shows the widget accordingly. Behavior If the primary value is null, empty, or an N/A like string, the widget container is set to display: none. When the value becomes valid and not null again, the widget is restored to visibility and redrawn to ensure correct indicator rendering. A guard variable prevents infinite redraw loops, since a redraw triggers the widget ready event. A debug flag allows optional console logging when needed. Widget Script /** * Hide widget when its primary indicator value is empty or N/A-like. * * Behavior: * - If the value is empty, set the widget container to display "none". * - If the returned value becomes a number or valid value, restore display style to original and redraw * - A redraw triggers "ready" again, so a guard variable prevents a redraw loop. * * Debug: * - Set debug variable = true to enable console logging to track script status. * * For best results use on indicator in own row or at end of row, if other widgets exist on row, empty space will appear in dashboard */ widget.on("ready", function () { function run() { var debug; var suppressNextReady; var hideValues; debug = false; suppressNextReady = false; hideValues = [ "n/a", "#n/a", "na", "none", "null", "undefined" ]; // Function to turn console logging on or off function log(message, data) { if (!debug) { return; } if (data === undefined) { console.log("[hide-empty-indicator] " + message); return; } console.log("[hide-empty-indicator] " + message, data); } // Widget CSS selector function getWidgetElement() { return document.querySelector('widget[widgetid="' + widget.oid + '"]'); } // Get value of primary indicator value function getPrimaryValue() { if ( widget.queryResult && widget.queryResult.value && widget.queryResult.value.data !== undefined ) { return widget.queryResult.value.data; } try { if ( widget.queryResult && widget.queryResult.data && widget.queryResult.data.length ) { return widget.queryResult.data[0][0]; } } catch (e) { log("Value read failed for data[0][0].", e); } if (widget.queryResult && Array.isArray(widget.queryResult)) { if (widget.queryResult.length && widget.queryResult[0].length) { if (widget.queryResult[0][0]) { return widget.queryResult[0][0].Value; } } } return null; } function shouldHide(value) { var text; var normalized; if (value === null || value === undefined) { return true; } text = String(value).trim(); if (!text) { return true; } normalized = text.replace(/\\/g, "/").toLowerCase(); return hideValues.indexOf(normalized) !== -1; } function isElementHidden(element) { if (!element) { return false; } return element.style.display === "none"; } function hideWidget(element) { if (!element) { return; } if (element.style.display === "none") { return; } element.style.display = "none"; log("Hid widget due to empty/N/A-like value."); } function showWidgetAndRedrawIfNeeded(element) { var wasHidden; if (!element) { return; } wasHidden = isElementHidden(element); element.style.display = ""; if (!wasHidden) { log("Widget already visible."); return; } if (typeof widget.redraw !== "function") { log("Widget restored, redraw not available."); return; } suppressNextReady = true; log("Widget restored, triggering redraw."); widget.redraw(); } function applyRule() { var element; var primaryValue; element = getWidgetElement(); if (!element) { log("Widget container element not found."); return; } if (widget.queryResult === undefined) { log("queryResult is not available yet, keeping widget visible."); element.style.display = ""; return; } primaryValue = getPrimaryValue(); log("Primary value evaluated.", primaryValue); if (shouldHide(primaryValue)) { hideWidget(element); return; } showWidgetAndRedrawIfNeeded(element); } function onReady() { if (suppressNextReady) { suppressNextReady = false; log("Ready fired after redraw, applying rule without redraw."); applyRule(); return; } applyRule(); } onReady(); } run(); }); Notes The script uses display: none instead of jQuery hide or show to avoid layout and rendering issues with indicator widgets. Redraw is triggered only when restoring visibility, not when hiding. The script relies only on the widget ready event, which fires again after redraw and filter changes. Dashboard Script to Hide Widgets Based on Filter Selections As an alternative, widgets can be hidden purely based on filter selections, without checking whether the widget returns data. This approach is useful when visibility rules are deterministic based on filters. This method uses a dashboard script and CSS classes to hide widget containers. Example Dashboard Script dashboard.on('filterschanged', function (se, ev) { let filterName = 'Region' //mapping of filter items and widgets to be hidden. //if selected filter item is not available in the list, widgets in 'default' key will be hidden let itemWidgetMapping = { 'Midwest':['6390b5a285a029002e9e2ad6'], 'South': ['6238887ba77683002ea4425b'], 'West':['6390b5a285a029002e9e2ad6', '6238887ba77683002ea4425b'], 'default':[] } selectedFilter = ev.items.find(el=>el.jaql.title == filterName) let selectedItem = 'default' if(selectedFilter && selectedFilter.jaql.filter.members) selectedItem = selectedFilter.jaql.filter.members[0] //unhide all widgets first and then hide widgets based on selected filter $(`widget`).closest('.dashboard-layout-subcell-host').removeClass('dontshowme-parent') if(selectedItem in itemWidgetMapping){ for (const [key, value] of Object.entries(itemWidgetMapping)) { if(key == selectedItem){ itemWidgetMapping[key].forEach(function (item, index) { $(`widget[widgetid="${item}"]`).closest('.dashboard-layout-subcell-host').addClass('dontshowme-parent') }); } } } else{ itemWidgetMapping['default'].forEach(function (item, index) { $(`widget[widgetid="${item}"]`).closest('.dashboard-layout-subcell-host').addClass('dontshowme-parent') }); } }); Choosing the Right Approach Generally the widget script is best suited when: Visibility depends on whether data is returned The indicator value can be empty due to calculations or filters Generally the dashboard script is best suited when: Visibility depends only on filter selections Centralized control over multiple widgets is required These approaches are alternatives and should not be used simultaneously for the same widgets. Conclusion Hiding indicator widgets based on their returned value can potentially improve dashboard clarity and user experience. The widget script approach provides result aware behavior, while the dashboard script approach offers deterministic, filter based control. Both methods are powerful tools for customizing dashboard widget visibility. Two Full Row Indicator Widgets, both visible First Row Indicator Widget is now hidden, by script, due to no data Two Indicators in one row, both visible Second Indicator is now hidden by script, due to no data Disclaimer: This post outlines a potential custom workaround for a specific use case or provides instructions regarding a specific task. The solution may not work in all scenarios or Sisense versions, so we strongly recommend testing it in your environment before deployment. If you need further assistance with this, please let us know.

      Jeremy Friedel
      Jeremy FriedelPosted 2 months ago
      0
               
      • Widget & Dashboard ScriptsChevronRightIcon

      Customizing an Indicator Widget

               

      Introduction The following document provides code snippets for customizing an indicator widget. Table of Contents Table of Contents Introduction Table of Contents How to Use This Page? Widget Customizations The “Simple” Numerical Widget The “Ticker” Numerical Widget The Gauge Widget The “Ticker” Gauge Widget Manipulating the Widget The “Simple” Numerical Widget Reading the Widget Type & Subtypes Reading and Modifying the “Primary Title” Object Reading and Modifying the “Primary Value” Object Reading and Modifying the “Secondary Title” Object Reading and Modifying the “Secondary Value” Object Modifying the Separator Object The “Bar” Numerical Widget Reading the Widget Type & Subtypes Reading and Modifying the “Primary Title” Object Reading and Modifying the “Primary Value” Object Reading and Modifying the “Secondary Title” Object Reading and Modifying the “Secondary Value” Object Modifying the Widget Brackets The “Ticker” Numerical Widget Reading the Widget Type & Subtypes Reading and Modifying the Text-Generic Properties Reading and Modifying the “Primary Title” Object Reading and Modifying the “Primary Value” Object Reading and Modifying the “Secondary Title” Object Reading and Modifying the “Secondary Value” Object Modifying the Divider The Gauge Widget Reading the Widget Type, Subtype, and Skin Reading and Modifying the Text-Generic Properties Reading and Modifying the “Primary Title” Object Reading and Modifying the “Primary Value” Object Reading and Modifying the “Secondary Title” Object Reading and Modifying the “Secondary Value” Object Reading and Modifying the “Min Value” and “Max Value” Objects Modifying the Widget Brackets Modifying the Gauge, Needle, and Ticks The “Ticker” Gague Widget Reading the Widget Type & Subtypes Reading and Modifying the Text-Generic Properties Reading and Modifying the “Primary Title” Object Reading and Modifying the “Primary Value” Object Reading and Modifying the “Secondary Title” Object Reading and Modifying the “Secondary Value” Object Modifying the Dividers Modifying the Gauge Bar How to Use This Page? To implement the customizations you’ll have to get familiar with the following: Basic JavaScript and JQuery Widget Lifecycle How to add a widget script Widget Customizations The indicator widget breaks down into a few widget types: Numerical Indicator - Simple / Bar / Ticker Gauge Indicator - Simple / Ticker The following document breaks down the different attributes for every widget type. Note that the indicator widget is different than other widgets - The widget is rendered to a picture (canvas) and presented in the widget. Once drawn, you can’t modify it. Instead, all modifications must be made while processing the widget’s query. The “Simple” Numerical Widget   Element Value Widget Type “indicator” Widget Sub-Type “indicator/numeric” Indicator Sub-Type “numericSimple” Background Color “transparent“ Primary Title Text = “Current Value” Color = #5B6372 Size = [12,15,18,22] Primary Value Value = 5,161,800 Text = “5.16M” Size = [23,32,46,66] Color = Conditional Formatting Secondary Title Text = “Estimated Forecast” Color = #9EA2AB Size = [10,10,14,20] Secondary Value Value = 5,407,600 Text = “5.4M” Color = #9EA2AB Size = [10,10,14,20] Font Weight = 800 Value Seperator Color = Gray Width = 1   The “Bar” Numerical Widget   Element Value Widget Type "indicator" Widget Sub-Type “indicator/numeric” Indicator Sub-Type “numericBar” Background Color “transparent“ Widget-Sourounding Bracket Color = #C6C6C6 Primary Title Text = “Current Value” Color = #5B6372 Size = [12,15,18,22] Primary Value Value = 5,161,800 Text = “5.16M” Size = [15,22,31,45] Color = #FFFFFF (White) Secondary Title Text = “Estimated Forecast” Color = #9EA2AB Size = [10,10,14,20] Secondary Value Value = 5,407,600 Text = “5.4M” Color = #9EA2AB Size = [10,10,14,20] Font Weight = 800 The “Ticker” Numerical Widget   Element Value Widget Type “indicator” Widget Sub-Type “indicator/numeric” Indicator Sub-Type “ticker” Background Color “transparent“ Primary Title Text = “Current Value” Color = #5B6372 Size = 15 Primary Value Value = 5,161,800 Text = “5.16M” Size = 15 Color = Conditional Formatting Font Weight = 800 Divider Color = #272A34 Height = 13 Width = 1 Secondary Title Text = “Estimated Forecast” Color = #9EA2AB Size = 15 Secondary Value Value = 5,407,600 Text = “5.4M” Color = #9EA2AB Size = 15  The Gauge Widget Skin #1 Skin #2   Element Value Widget Type “indicator” Widget Sub-Type “indicator/gauge” Indicator Sub-Type “gauge” Skin “1” / “2” Background Color “transparent“ Surrounding Bracket Color = #C6C6C6 Primary Title Text = “Current Value” Color = #5B6372 Size = [12,15,18,22] Primary Value Value = 5,161,800 Text = “5.16M” Size = [15,22,31,45] Color = Conditional Formatting Gague Gauge Opacity = 50% (Skin #1) Tick Range Start = 20° Tick Range End = 160° Tick Frequency = 10° Over Degrees = 5° Needle Color = #2B3342 Tick Color = #FFFFFF Min Value Value = 0 Text = “0” Color = #5B6372 Max Value Value = 6145000 Text = “6.15M” Color = #5B6372 Secondary Title Text = “Estimated Forecast” Color = #9EA2AB Size = [10,10,14,20] Secondary Value Value = 5,407,600 Text = “5.4M” Color = #9EA2AB Size = [10,10,14,20] Font Weight = 800 The “Ticker” Gauge Widget   Element Value Widget Type “indicator” Widget Sub-Type “indicator/gauge” Indicator Sub-Type “ticker” Background Color “transparent“ Generic Text Data Font Family = “Open Sans“ Font Size = 15 Text Padding = 6 Primary Title Text = “Current Value” Color = #5B6372 Primary Value Value = 5,161,800 Text = “5.16M” Color = Conditional Formatting Font Weight = 800 Divider Color = #272A34 Height = 13 Width = 1 Secondary Title Text = “Estimated Forecast” Color = #9EA2AB Secondary Value Value = 5,407,600 Text = “5.4M” Color = #9EA2AB Gauge Bar Height = 11 Opacity = 50% Width = 100 Handle Color = #2B3342 Manipulating the Widget The “Simple” Numerical Widget Reading the Widget Type & Subtypes To read the widget type and sub-types, use the following code: widget.on('processresult', function(widget, query) { // Read Widget Type console.log('Widget Type: ' + widget.type); // Returns "indicator" // Read Widget Sub-Type console.log('Widget Sub-type: ' + widget.subtype); // Returns "indicator/numeric" }); widget.on('ready', function(widget) { // Read Indicator Widget Sub-Type console.log('Widget SubType: ' + widget.indicatorInstance.type); // Returns "numericSimple" }); Reading and Modifying the “Primary Title” Object To read and modify the “Primary Title” use the following code: widget.on('processresult', function(widget, query) { opMap = widget.indicatorInstance._optionsMap["numericSimple"]; // Read Primary Title Text console.log('Primary Title Caption: ' + query.result.title.text); // Override Primary Title Text query.result.title.text = "Current Value" // Override Primary Title Color opMap.title.color='green' // Override Primary Title Font Size (see font section) opMap.title.fontSizes["big"] = 20 // Override Primary Title Font Style opMap.title.fontStyle = 'italic' opMap.title.fontWeight = 'bold' });  Reading and Modifying the “Primary Value” Object The “Primary Value” is represented in two formats: The numerical format - Represents the query’s result The textual form - Represents the string displayed in the widget To read and modify the “Primary Value” use the following code: widget.on('processresult', function(widget, query) { opMap = widget.indicatorInstance._optionsMap["numericSimple"]; // Read "Primary Value" Numerical & Text Values console.log('Primary Value Data: ' + query.result.value.data); console.log('Primary Value Text: ' + query.result.value.text); // Override "Primary Value" Text Value query.result.value.text = "5.16M" // Read the "Primary Value" Color colorSetting = widget.metadata.panels[0].items[0].format.color if (colorSetting.hasOwnProperty('color')) { // Value has a Fixed Color console.log('The "Primary Value" color: ' + colorSetting.color); } else { // Value has Conditional Formatting console.log('The "Primary Value" has conditional formatting (' + colorSetting.conditions.length + ' rules):'); colorSetting.conditions.forEach(function(condition) { console.log(' - Color: ' + condition.color + ' if value ' + condition.operator + ' ' + condition.expressionValue); }) } // Override the Primary Value's Color opMap.value.color='green' // Override the "Primary Value" Font Size (see font section) opMap.value.fontSizes["big"] = 20 // Override the "Primary Value" Font Style opMap.value.fontStyle = 'italic' opMap.value.fontWeight = 'bold' }); Reading and Modifying the “Secondary Title” Object To read and modify the “Secondary Title” use the following code: widget.on('processresult', function(widget, query) { opMap = widget.indicatorInstance._optionsMap["numericSimple"]; // Read “Secondary Title” Text console.log('Primary Title Caption: ' + query.result.secondaryTitle.text); // Override “Secondary Title” Text query.result.secondaryTitle.text = "Forecast Value" // Override “Secondary Title” Color opMap.secondaryTitle.color='green' // Override “Secondary Title” Font Size (see font section) opMap.secondaryTitle.fontSizes["big"] = 20 // Override “Secondary Title” Font Style opMap.secondaryTitle.fontStyle = 'italic' opMap.secondaryTitle.fontWeight = 'bold' }); Reading and Modifying the “Secondary Value” Object The “Secondary Value” is represented in two formats: The numerical format - Represents the query’s result The textual form - Represents the string displayed in the widget To read and modify the “Secondary Value” use the following code: widget.on('processresult', function(widget, query) { opMap = widget.indicatorInstance._optionsMap["numericSimple"]; // Read "Secondary Value" Numerical & Text Values console.log('Secondary Value Data: ' + query.result.secondary.data); console.log('Secondary Value Text: ' + query.result.secondary.text); // Override "Secondary Value" Display Text Value query.result.secondary.text = "5.16M" // Override "Secondary Value" Color opMap.secondaryValue.color='green' // Override "Secondary Value" Font Size (see font section) opMap.secondaryValue.fontSizes["big"] = 20 // Override "Secondary Value" Font Style opMap.secondaryValue.fontStyle = 'italic' opMap.secondaryValue.fontWeight = 'bold' }); Modifying the Separator Object To read and modify the “Separator” use the following code: widget.on('processresult', function(widget, query) { opMap = widget.indicatorInstance._optionsMap["numericSimple"]; // Read "Separator" color console.log('Value Separator Color = ' + opMap.borderColor); // Override "Separator" color opMap.borderColor = 'red' }); The “Bar” Numerical Widget Reading the Widget Type & Subtypes To read the widget type and sub-types, use the following code: widget.on('processresult', function(widget, query) { // Read Widget Type console.log('Widget Type: ' + widget.type); // Returns "indicator" // Read Widget Sub-Type console.log('Widget Sub-type: ' + widget.subtype); // Returns "indicator/numeric" }); widget.on('ready', function(widget) { // Read Indicator Widget Sub-Type console.log('Widget SubType: ' + widget.indicatorInstance.type); // Returns "numericBar" }); Reading and Modifying the “Primary Title” Object To read and modify the “Primary Title” use the following code: widget.on('processresult', function(widget, query) { opMap = widget.indicatorInstance._optionsMap["numericBar"]; // Read Primary Title Text console.log('Primary Title Caption: ' + query.result.title.text); // Override Primary Title Text query.result.title.text = "Current Value" // Override Primary Title Color opMap.title.color='green' // Override Primary Title Font Size (see font section) opMap.title.fontSizes["big"] = 20 // Override Primary Title Font Style opMap.title.fontStyle = 'italic' opMap.title.fontWeight = 'bold' }); Reading and Modifying the “Primary Value” Object The “Primary Value” is represented in two formats: The numerical format - Represents the query’s result The textual form - Represents the string displayed in the widget To read and modify the “Primary Value” use the following code: widget.on('processresult', function(widget, query) { opMap = widget.indicatorInstance._optionsMap["numericBar"]; // Read "Primary Value" Numerical & Text Values console.log('Primary Value Data: ' + query.result.value.data); console.log('Primary Value Text: ' + query.result.value.text); // Override "Primary Value" Text Value query.result.value.text = "5.16M" // Read the "Primary Value" Background Color colorSetting = widget.metadata.panels[0].items[0].format.color if (colorSetting.hasOwnProperty('color')) { // Value has a Fixed Color console.log('The "Primary Value" background color: ' + colorSetting.color); } else { // Value has Conditional Formatting console.log('The "Primary Value" has conditional formatting (' + colorSetting.conditions.length + ' rules):'); colorSetting.conditions.forEach(function(condition) { console.log(' - Background Color: ' + condition.color + ' if value ' + condition.operator + ' ' + condition.expressionValue); }) } // Read the "Primary Value" Foreground Color console.log('The "Primary Value" foreground color: ' + opMap.value.color); // Override the Primary Value's foreground Color opMap.value.color='green' // Override the "Primary Value" Font Size (see font section) opMap.value.fontSizes["big"] = 20 // Override the "Primary Value" Font Style opMap.value.fontStyle = 'italic' opMap.value.fontWeight = 'bold' }); Reading and Modifying the “Secondary Title” Object To read and modify the “Secondary Title” use the following code: widget.on('processresult', function(widget, query) { opMap = widget.indicatorInstance._optionsMap["numericBar"]; // Read “Secondary Title” Text console.log('Primary Title Caption: ' + query.result.secondaryTitle.text); // Override “Secondary Title” Text query.result.secondaryTitle.text = "Forecast Value" // Override “Secondary Title” Color opMap.secondaryTitle.color='green' // Override “Secondary Title” Font Size (see font section) opMap.secondaryTitle.fontSizes["big"] = 20 // Override “Secondary Title” Font Style opMap.secondaryTitle.fontStyle = 'italic' opMap.secondaryTitle.fontWeight = 'bold' }); Reading and Modifying the “Secondary Value” Object The “Secondary Value” is represented in two formats: The numerical format - Represents the query’s result The textual form - Represents the string displayed in the widget To read and modify the “Secondary Value” use the following code: widget.on('processresult', function(widget, query) { opMap = widget.indicatorInstance._optionsMap["numericBar"]; // Read "Secondary Value" Numerical & Text Values console.log('Secondary Value Data: ' + query.result.secondary.data); console.log('Secondary Value Text: ' + query.result.secondary.text); // Override "Secondary Value" Display Text Value query.result.secondary.text = "5.16M" // Override "Secondary Value" Color opMap.secondaryValue.color='green' // Override "Secondary Value" Font Size (see font section) opMap.secondaryValue.fontSizes["big"] = 20 // Override "Secondary Value" Font Style opMap.secondaryValue.fontStyle = 'italic' opMap.secondaryValue.fontWeight = 'bold' }); Modifying the Widget Brackets To read and modify the “Brackets” use the following code: widget.on('processresult', function(widget, query) { opMap = widget.indicatorInstance._optionsMap["numericBar"]; // Read "Brackets" color console.log('Value Separator Color = ' + opMap.bracketColor); // Override "Brackets" color opMap.bracketColor = 'red' }); The “Ticker” Numerical Widget Reading the Widget Type & Subtypes To read the widget type and sub-types, use the following code: widget.on('processresult', function(widget, query) { // Read Widget Type console.log('Widget Type: ' + widget.type); // Returns "indicator" // Read Widget Sub-Type console.log('Widget Sub-type: ' + widget.subtype); // Returns "indicator/numeric" }); widget.on('ready', function(widget) { // Read Indicator Widget Sub-Type console.log('Widget SubType: ' + widget.indicatorInstance.type); // Returns "ticker" }); Reading and Modifying the Text-Generic Properties To read and modify the general text styling use the following code: widget.on('processresult', function(widget, query) { opMap = widget.indicatorInstance._optionsMap["ticker"]; // Read Generic Text Properties console.log('Generic Text Font Family: ' + opMap.fontFamily); console.log('Generic Text Font Size: ' + opMap.fontSize); console.log('Generic Text Padding: ' + opMap.textPadding); // Override Generic Text Font Family opMap.fontFamily = "David" // Override Text Font Size opMap.fontSize = 30 // Override Text Padding (space between title and value) opMap.textPadding = 100 }); Reading and Modifying the “Primary Title” Object To read and modify the “Primary Title” use the following code: widget.on('processresult', function(widget, query) { opMap = widget.indicatorInstance._optionsMap["ticker"]; // Read Primary Title Text console.log('Primary Title Caption: ' + query.result.title.text); // Override Primary Title Text query.result.title.text = "Current Value" // Override Primary Title Color opMap.title.color='green' // Override Primary Title Font Size (overrides the "Generic" setting) opMap.title.fontSize = 40 // Override Primary Title Font Style opMap.title.fontStyle = 'italic' opMap.title.fontWeight = 'bold' }); Reading and Modifying the “Primary Value” Object The “Primary Value” is represented in two formats: The numerical format - Represents the query’s result The textual form - Represents the string displayed in the widget To read and modify the “Primary Value” use the following code: widget.on('processresult', function(widget, query) { opMap = widget.indicatorInstance._optionsMap["ticker"]; // Read "Primary Value" Numerical & Text Values console.log('Primary Value Data: ' + query.result.value.data); console.log('Primary Value Text: ' + query.result.value.text); // Override "Primary Value" Text Value query.result.value.text = "5.16M" // Read the "Primary Value" Color colorSetting = widget.metadata.panels[0].items[0].format.color if (colorSetting.hasOwnProperty('color')) { // Value has a Fixed Color console.log('The "Primary Value" background color: ' + colorSetting.color); } else { // Value has Conditional Formatting console.log('The "Primary Value" has conditional formatting (' + colorSetting.conditions.length + ' rules):'); colorSetting.conditions.forEach(function(condition) { console.log(' - Background Color: ' + condition.color + ' if value ' + condition.operator + ' ' + condition.expressionValue); }) } // Override the Primary Value's Color opMap.value.color='green' // Override the "Primary Value" Font Size (overrides the "Generic" setting) opMap.value.fontSize = 20 // Override the "Primary Value" Font Style opMap.value.fontStyle = 'italic' opMap.value.fontWeight = 'bold' }); Reading and Modifying the “Secondary Title” Object To read and modify the “Secondary Title” use the following code: widget.on('processresult', function(widget, query) { opMap = widget.indicatorInstance._optionsMap["ticker"]; // Read “Secondary Title” Text console.log('Primary Title Caption: ' + query.result.secondaryTitle.text); // Override “Secondary Title” Text query.result.secondaryTitle.text = "Forecast Value" // Override “Secondary Title” Color opMap.secondaryTitle.color='green' // Override “Secondary Title” Font Size (overrides the "Generic" setting) opMap.secondaryTitle.fontSize = 20 // Override “Secondary Title” Font Style opMap.secondaryTitle.fontStyle = 'italic' opMap.secondaryTitle.fontWeight = 'bold' }); Reading and Modifying the “Secondary Value” Object The “Secondary Value” is represented in two formats: The numerical format - Represents the query’s result The textual form - Represents the string displayed in the widget To read and modify the “Secondary Value” use the following code: widget.on('processresult', function(widget, query) { opMap = widget.indicatorInstance._optionsMap["ticker"]; // Read "Secondary Value" Numerical & Text Values console.log('Secondary Value Data: ' + query.result.secondary.data); console.log('Secondary Value Text: ' + query.result.secondary.text); // Override "Secondary Value" Display Text Value query.result.secondary.text = "5.16M" // Override "Secondary Value" Color opMap.secondaryValue.color='green' // Override "Secondary Value" Font Size (overrides the "Generic" setting) opMap.secondaryValue.fontSize = 20 // Override "Secondary Value" Font Style opMap.secondaryValue.fontStyle = 'italic' opMap.secondaryValue.fontWeight = 'bold' }); Modifying the Divider To read and modify the “Dividers” use the following code: widget.on('processresult', function(widget, query) { opMap = widget.indicatorInstance._optionsMap["ticker"]; // Read "Divider" Color console.log('Divider Color is = ' + opMap.dividerColor); // Read "Divider" Width console.log('Divider Color is = ' + opMap.dividerWidth); // Read "Divider" Height console.log('Divider Color is = ' + opMap.dividerHeight); // Override "Divider" Properties opMap.dividerColor = 'red' opMap.dividerWidth = 30 opMap.dividerHeight = 10 // Match the divider height to the widget's height opMap.dividerHeight = opMap.height }); The Gauge Widget Reading the Widget Type, Subtype, and Skin To read the widget type, sub-types, and skin use the following code: widget.on('processresult', function(widget, query) { // Read Widget Type console.log('Widget Type: ' + widget.type); // Returns "indicator" // Read Widget Sub-Type console.log('Widget Sub-type: ' + widget.subtype); // Returns "indicator/gauge" }); widget.on('ready', function(widget) { // Read Indicator Widget Sub-Type and Skin console.log('Widget SubType: ' + widget.indicatorInstance.type); // Returns "gauge" console.log('Widget Skin: ' + widget.indicatorInstance._dataMap.gauge.skin); // Returns "gauge" }); Reading and Modifying the Text-Generic Properties To read and modify the general text styling use the following code: widget.on('processresult', function(widget, query) { opMap = widget.indicatorInstance._optionsMap["gauge"]; // Read Generic Text Properties console.log('Generic Text Font Family: ' + opMap.fontFamily); // Override Generic Text Font Family opMap.fontFamily = "David" }); Reading and Modifying the “Primary Title” Object To read and modify the “Primary Title” use the following code: widget.on('processresult', function(widget, query) { opMap = widget.indicatorInstance._optionsMap["gauge"]; // Read Primary Title Text console.log('Primary Title Caption: ' + query.result.title.text); // Override Primary Title Text query.result.title.text = "Current Value" // Override Primary Title Color opMap.title.color='green' // Override Primary Title Font Size opMap.title.fontSizes["medium"] = 40 // Override Primary Title Font Style opMap.title.fontStyle = 'italic' opMap.title.fontWeight = 'bold' }); Reading and Modifying the “Primary Value” Object The “Primary Value” is represented in two formats: The numerical format - Represents the query’s result The textual form - Represents the string displayed in the widget To read and modify the “Primary Value” use the following code: widget.on('processresult', function(widget, query) { opMap = widget.indicatorInstance._optionsMap["gauge"]; // Read "Primary Value" Numerical & Text Values console.log('Primary Value Data: ' + query.result.value.data); console.log('Primary Value Text: ' + query.result.value.text); // Override "Primary Value" Text Value query.result.value.text = "5.16M" // Read the "Primary Value" Color colorSetting = widget.metadata.panels[0].items[0].format.color if (colorSetting.hasOwnProperty('color')) { // Value has a Fixed Color console.log('The "Primary Value" background color: ' + colorSetting.color); } else { // Value has Conditional Formatting console.log('The "Primary Value" has conditional formatting (' + colorSetting.conditions.length + ' rules):'); colorSetting.conditions.forEach(function(condition) { console.log(' - Background Color: ' + condition.color + ' if value ' + condition.operator + ' ' + condition.expressionValue); }) } // Override the Primary Value's Color opMap.value.color='green' // Override the "Primary Value" Font Size opMap.value.fontSizes["medium"] = 40 // Override the "Primary Value" Font Style opMap.value.fontStyle = 'italic' opMap.value.fontWeight = 'bold' }); Reading and Modifying the “Secondary Title” Object To read and modify the “Secondary Title” use the following code: widget.on('processresult', function(widget, query) { opMap = widget.indicatorInstance._optionsMap["gauge"]; // Read “Secondary Title” Text console.log('Primary Title Caption: ' + query.result.secondaryTitle.text); // Override “Secondary Title” Text query.result.secondaryTitle.text = "Forecast Value" // Override “Secondary Title” Color opMap.secondaryTitle.color='green' // Override “Secondary Title” Font Size (overrides the "Generic" setting) opMap.secondaryTitle.fontSizes["medium"] = 40 // Override “Secondary Title” Font Style opMap.secondaryTitle.fontStyle = 'italic' opMap.secondaryTitle.fontWeight = 'bold' }); Reading and Modifying the “Secondary Value” Object The “Secondary Value” is represented in two formats: The numerical format - Represents the query’s result The textual form - Represents the string displayed in the widget To read and modify the “Secondary Value” use the following code: widget.on('processresult', function(widget, query) { opMap = widget.indicatorInstance._optionsMap["gauge"]; // Read "Secondary Value" Numerical & Text Values console.log('Secondary Value Data: ' + query.result.secondary.data); console.log('Secondary Value Text: ' + query.result.secondary.text); // Override "Secondary Value" Display Text Value query.result.secondary.text = "5.16M" // Override "Secondary Value" Color opMap.secondaryValue.color='blue' // Override "Secondary Value" Font Size (overrides the "Generic" setting) opMap.secondaryValue.fontSizes["medium"] = 40 // Override "Secondary Value" Font Style opMap.secondaryValue.fontStyle = 'italic' opMap.secondaryValue.fontWeight = 'bold' }); Reading and Modifying the “Min Value” and “Max Value” Objects The “Min Value” and “Max Value” are represented in two formats: The numerical format - Represents the query’s result The textual form - Represents the string displayed in the widget To read and modify the values use the following code: widget.on('processresult', function(widget, query) { opMap = widget.indicatorInstance._optionsMap["gauge"]; // Read "Min Value" Numerical & Text Values console.log('Min Value Data: ' + query.result.min.data); console.log('Min Value Text: ' + query.result.min.text); // Override "Min Value" Display Text Value query.result.min.text = "MIN" console.log('Max Value Data: ' + query.result.max.data); console.log('Max Value Text: ' + query.result.max.text); // Override "Max Value" Display Text Value query.result.max.text = "MAX" // Read Min/Max label properties console.log('Min/Max Label Color: ' + opMap.label.color); // Override Min and Max Font Color opMap.label.color = 'green' // Override Min and Max Font Size opMap.label.fontSizes["medium"] = 40 // Override Min and Max Font Style opMap.label.fontStyle = 'italic' opMap.label.fontWeight = 'bold' }); Modifying the Widget Brackets To read and modify the “Brackets” use the following code: widget.on('processresult', function(widget, query) { opMap = widget.indicatorInstance._optionsMap["gauge"]; // Read "Brackets" color console.log('Value Separator Color = ' + opMap.bracketColor); // Override "Brackets" color opMap.bracketColor = 'red' }); Modifying the Gauge, Needle, and Ticks To read and modify the “Gauge”, “Needle”, and “Ticks” use the following code: widget.on('processresult', function(widget, query) { opMap = widget.indicatorInstance._optionsMap["gauge"]; // Read "Gauge" information - Relevant for "Skin 1" console.log('Gauge opacity is = ' + opMap.gaugeOpacity * 100 + '%'); // Bright area of the gauge // Read "Needle" information console.log('Needle range (degrees) = ' + opMap.startAngle + '-' + opMap.endAngle); console.log('Needle "out of" min/max range (degrees) = ' + opMap.overDegrees); console.log('Needle color = ' + opMap.needleColor); // Read "Ticks" information console.log('Ticks show every (degrees) = ' + opMap.tickDegreesIncrement); console.log('Ticks color = ' + opMap.tickColor); // Override "Gauge" properties opMap.gaugeOpacity = 0.2 // Override "Needle" properties opMap.startAngle = 5 opMap.endAngle = 175 opMap.needleColor = 'red' // Override "Ticks" properties opMap.tickDegreesIncrement = 5 opMap.tickColor = 'blue' }); The “Ticker” Gague Widget Reading the Widget Type & Subtypes To read the widget type and sub-types, use the following code: widget.on('processresult', function(widget, query) { // Read Widget Type console.log('Widget Type: ' + widget.type); // Returns "indicator" // Read Widget Sub-Type console.log('Widget Sub-type: ' + widget.subtype); // Returns "indicator/gauge" }); widget.on('ready', function(widget) { // Read Indicator Widget Sub-Type console.log('Widget SubType: ' + widget.indicatorInstance.type); // Returns "ticker" }); Reading and Modifying the Text-Generic Properties To read and modify the general text styling use the following code: widget.on('processresult', function(widget, query) { opMap = widget.indicatorInstance._optionsMap["ticker"]; // Read Generic Text Properties console.log('Generic Text Font Family: ' + opMap.fontFamily); console.log('Generic Text Font Size: ' + opMap.fontSize); console.log('Generic Text Padding: ' + opMap.textPadding); // Override Generic Text Font Family opMap.fontFamily = "David" // Override Text Font Size opMap.fontSize = 30 // Override Text Padding (space between title and value) opMap.textPadding = 100 }); Reading and Modifying the “Primary Title” Object To read and modify the “Primary Title” use the following code: widget.on('processresult', function(widget, query) { opMap = widget.indicatorInstance._optionsMap["ticker"]; // Read Primary Title Text console.log('Primary Title Caption: ' + query.result.title.text); // Override Primary Title Text query.result.title.text = "Current Value" // Override Primary Title Color opMap.title.color='green' // Override Primary Title Font Size (overrides the "Generic" setting) opMap.title.fontSize = 40 // Override Primary Title Font Style opMap.title.fontStyle = 'italic' opMap.title.fontWeight = 'bold' }); Reading and Modifying the “Primary Value” Object The “Primary Value” is represented in two formats: The numerical format - Represents the query’s result The textual form - Represents the string displayed in the widget To read and modify the “Primary Value” use the following code: widget.on('processresult', function(widget, query) { opMap = widget.indicatorInstance._optionsMap["ticker"]; // Read "Primary Value" Numerical & Text Values console.log('Primary Value Data: ' + query.result.value.data); console.log('Primary Value Text: ' + query.result.value.text); // Override "Primary Value" Text Value query.result.value.text = "5.16M" // Read the "Primary Value" Color colorSetting = widget.metadata.panels[0].items[0].format.color if (colorSetting.hasOwnProperty('color')) { // Value has a Fixed Color console.log('The "Primary Value" background color: ' + colorSetting.color); } else { // Value has Conditional Formatting console.log('The "Primary Value" has conditional formatting (' + colorSetting.conditions.length + ' rules):'); colorSetting.conditions.forEach(function(condition) { console.log(' - Background Color: ' + condition.color + ' if value ' + condition.operator + ' ' + condition.expressionValue); }) } // Override the Primary Value's Color opMap.value.color='green' // Override the "Primary Value" Font Size (overrides the "Generic" setting) opMap.value.fontSize = 20 // Override the "Primary Value" Font Style opMap.value.fontStyle = 'italic' opMap.value.fontWeight = 'bold' }); Reading and Modifying the “Secondary Title” Object To read and modify the “Secondary Title” use the following code: widget.on('processresult', function(widget, query) { opMap = widget.indicatorInstance._optionsMap["ticker"]; // Read “Secondary Title” Text console.log('Primary Title Caption: ' + query.result.secondaryTitle.text); // Override “Secondary Title” Text query.result.secondaryTitle.text = "Forecast Value" // Override “Secondary Title” Color opMap.secondaryTitle.color='green' // Override “Secondary Title” Font Size (overrides the "Generic" setting) opMap.secondaryTitle.fontSize = 20 // Override “Secondary Title” Font Style opMap.secondaryTitle.fontStyle = 'italic' opMap.secondaryTitle.fontWeight = 'bold' }); Reading and Modifying the “Secondary Value” Object The “Secondary Value” is represented in two formats: The numerical format - Represents the query’s result The textual form - Represents the string displayed in the widget To read and modify the “Secondary Value” use the following code: widget.on('processresult', function(widget, query) { opMap = widget.indicatorInstance._optionsMap["ticker"]; // Read "Secondary Value" Numerical & Text Values console.log('Secondary Value Data: ' + query.result.secondary.data); console.log('Secondary Value Text: ' + query.result.secondary.text); // Override "Secondary Value" Display Text Value query.result.secondary.text = "5.16M" // Override "Secondary Value" Color opMap.secondaryValue.color='green' // Override "Secondary Value" Font Size (overrides the "Generic" setting) opMap.secondaryValue.fontSize = 20 // Override "Secondary Value" Font Style opMap.secondaryValue.fontStyle = 'italic' opMap.secondaryValue.fontWeight = 'bold' }); Modifying the Dividers To read and modify the “Dividers” use the following code: widget.on('processresult', function(widget, query) { opMap = widget.indicatorInstance._optionsMap["ticker"]; // Read "Divider" Color console.log('Divider Color is = ' + opMap.dividerColor); // Read "Divider" Width console.log('Divider Color is = ' + opMap.dividerWidth); // Read "Divider" Height console.log('Divider Color is = ' + opMap.dividerHeight); // Override "Divider" Properties opMap.dividerColor = 'red' opMap.dividerWidth = 30 opMap.dividerHeight = 10 // Match the divider height to the widget's height opMap.dividerHeight = opMap.height }); Modifying the Gauge Bar To read and modify the “Gauge Bar” use the following code: widget.on('processresult', function(widget, query) { opMap = widget.indicatorInstance._optionsMap["ticker"]; // Read "Gague Bar" Properties console.log('Bar Height is = ' + opMap.barHeight); console.log('Bar Width is = ' + opMap.barWidth); console.log('Bar opacity is = ' + opMap.barOpacity * 100 + '%'); // Bright area of the bar // Read "Gague Bar Handle" Properties (indicator in the bar) console.log('Bar Handle color is = ' + opMap.barHandleColor); console.log('Bar Handle height is = ' + opMap.tickerBarHeight); console.log('Bar Handle widthis = ' + opMap.tickerBarWidth); // Override "Gague Bar" Properties opMap.barHeight = 20 opMap.barWidth = 150 opMap.barOpacity = 0.2 // Override "Gague Bar" Handle Properties opMap.barHandleColor = 'red' opMap.tickerBarHeight = opMap.barHeight opMap.tickerBarWidth = 5 });

      Ophir_Buchman
      Ophir_BuchmanPosted 4 years ago • Last reply 2 months ago
      1
               
      • Widget & Dashboard ScriptsChevronRightIcon

      How to change pivot cells font-size and style [Linux]

      Introduction This article explains how to change the font size and style of cells in a Sisense Pivot Table widget using the official Pivot 2.0 API (widget.transformPivot). A common approach found in community posts relies on jQuery class selectors such as .p-head-content and .p-value to apply CSS changes directly to the DOM. While this may work in older versions, it is not officially supported and is prone to breaking across Sisense updates, since class names and UI structure can change between releases. The recommended and supported approach is to use the widget.transformPivot API, which provides a stable, version-resilient way to style pivot cells programmatically. The solution is tested on Sisense L2026.1.2 and works for both cloud and on-premises deployments. Step-by-Step Guide 1. Open the widget script editor Click the three-dot menu on your Pivot Table widget → Edit Script . 2. Identify the cell types you want to style The transformPivot API allows you to target specific cell types within the pivot: You can target one, several, or all types in a single call. 3. Paste the script widget.transformPivot( { type: ['value', 'member', 'subtotal', 'grandtotal'] }, function(metadata, cell) { cell.style = cell.style || {}; cell.style.fontSize = '8px'; } ); 4. Customize the style At the core of the script, the cell.style object accepts standard CSS properties in camelCase format. You can extend it to apply additional styles beyond font size. For example: widget.transformPivot( { type: ['value', 'member', 'subtotal', 'grandtotal'] }, function(metadata, cell) { cell.style = cell.style || {}; cell.style.fontSize = '8px'; cell.style.fontWeight = 'bold'; cell.style.color = '#333333'; cell.style.fontFamily = 'Arial, sans-serif'; } ); 5. Click Apply and test Save the script and reload the widget. All targeted cell types will now reflect the styles you defined. Important notes Why not use jQuery class selectors? Scripts that rely on class names like .p-head-content or .p-value are tied to the internal DOM structure of the widget, which Sisense does not guarantee to remain stable across versions. These scripts may stop working after an upgrade without any warning. Pivot 2.0 API only: The widget.transformPivot method is part of the Pivot 2.0 API. Ensure your widget is using Pivot 2.0 (not the legacy Pivot widget) for this script to work. Cell types: If you only want to style specific cell types (e.g. only data values), simply remove the unwanted types from the array. For example, { type: ['value'] } will only affect data cells. Conclusion By using the widget.transformPivot API instead of direct DOM manipulation, you get a supported, upgrade-safe way to control the visual appearance of pivot cells. The cell.style object accepts any standard CSS property in camelCase, making it straightforward to extend this pattern to font family, weight, colour, and more. References / Related content Pivot 2.0 API - transformPivot Disclaimer:This post outlines a potential custom workaround for a specific use case or provides instructions regarding a specific task. The solution may not work in all scenarios or Sisense versions, so we strongly recommend testing it in your environment before deployment. If you need further assistance with this, please let us know.

      Thalita Santos
      Thalita SantosPosted 2 months ago
      0
               
      • Widget & Dashboard ScriptsChevronRightIcon

      Date Proximity Row Highlighting in Sisense Pivot Table Widgets [Linux]

      Introduction This article demonstrates how to automatically highlight rows in a Sisense Pivot Table widget based on how close a date column is to today's date. The script developed here adds visual urgency to your pivots by colour-coding each row based on the number of days remaining until the date in a specified column: Light green for rows where the deadline has already passed (expired). Light red for rows where the deadline is critically close (urgent). Light yellow for rows where the deadline is approaching within a configurable warning window. No highlight for rows where the deadline is comfortably in the future. This is particularly useful for dashboards tracking contract renewals, project deadlines, task due dates, or any time-sensitive data where users need to spot at-risk rows at a glance. The solution is tested on Sisense L2026.1.2 and works for both cloud and on-premises deployments. Step-by-Step Guide 1. Prepare your environment Open an existing dashboard that contains a Pivot Table widget, or create a new one. Make sure your table includes at least one column that contains date values. The script works best with dates formatted as mm/dd/yyyy or yyyy-mm-dd, which are the standard Sisense table output formats. You can use attached .dash and .sdata for testing. 2. Identify your date column index The script uses a 0-based column index to locate the date column. This means the first column is index 0, the second is 1, the third is 2, and so on. Count the columns in your table from left to right and note the position of your date column. You will use this number in the configuration section of the script. 3. Open the widget script editor Click the three-dot menu on the Pivot Table widget → Edit Script . 4. Paste the full script /** * Date Proximity Row Highlighting * * Compares a specific date column against today's date and paints the row * yellow (if the deadline is approaching) or red (if past due). */ // --- CONFIGURATION --- const DATE_COLUMN_INDEX = 2; // 0-based index of your date column (e.g., 2 is the 3rd column) const DAYS_WARNING = 14; // Highlight yellow if the deadline is within this many days const DAYS_URGENT = 5; // Highlight red if the deadline is urgent (within this many days) const EXPIRED_COLOR = '#d4edda'; // Light Green for past-due/expired deadlines const URGENT_COLOR = '#f8d7da'; // Light Red for urgent deadlines const WARNING_COLOR = '#fff3cd'; // Light Yellow for approaching deadlines widget.on('domready', function () { // Scope our DOM search to this specific widget only const $widget = $(element); // Find all data rows in the table body $widget.find('table tbody tr').each(function () { const $row = $(this); const $cells = $row.find('td'); // Safety check: ensure the row actually has our date column if ($cells.length <= DATE_COLUMN_INDEX) return; // Extract the text of the date column const dateText = $cells.eq(DATE_COLUMN_INDEX).text().trim(); // Skip empty rows or standard 'Grand Total' style texts if (!dateText || dateText.toLowerCase().includes('total')) return; // Parse the date (Works best with mm/dd/yyyy or yyyy-mm-dd Sisense formats) const rowDate = new Date(dateText); // Only proceed if it is a valid date if (!isNaN(rowDate.getTime())) { const today = new Date(); // Strip the time to compare pure calendar dates today.setHours(0, 0, 0, 0); rowDate.setHours(0, 0, 0, 0); // Calculate the difference in milliseconds and convert to days const diffTimeMilli = rowDate.getTime() - today.getTime(); const diffDays = Math.ceil(diffTimeMilli / (1000 * 60 * 60 * 24)); // Apply color to all cells in the row to guarantee the background isn't obscured if (diffDays < 0) { // Deadline is in the past! (Expired) -> Light Green $cells.css('background-color', EXPIRED_COLOR); } else if (diffDays <= DAYS_URGENT) { // Deadline is extremely close! (Urgent) -> Light Red $cells.css('background-color', URGENT_COLOR); } else if (diffDays <= DAYS_WARNING) { // Deadline is approaching within our warning threshold! -> Light Yellow $cells.css('background-color', WARNING_COLOR); } else { // Clear out the color if it's safe $cells.css('background-color', ''); } } }); }); 5. Configure the script constants At the top of the script, update the configuration constants to match your table and your business rules: const DATE_COLUMN_INDEX = 2;     // 0-based index of your date column (e.g., 2 = 3rd column) const DAYS_WARNING = 14;          // Highlight yellow if deadline is within this many days const DAYS_URGENT = 5;            // Highlight red if deadline is within this many days const EXPIRED_COLOR = '#d4edda'; // Light Green for past-due/expired rows const URGENT_COLOR = '#f8d7da';  // Light Red for urgent rows const WARNING_COLOR = '#fff3cd'; // Light Yellow for approaching rows DATE_COLUMN_INDEX – Set this to the 0-based position of your date column. DAYS_WARNING – Any row whose date is within this many days from today will be highlighted yellow. The default is 14 days. DAYS_URGENT – Any row whose date is within this many days from today will be highlighted red. The default is 5 days. This threshold takes priority over the warning threshold. Colour constants – The default colours follow a standard traffic-light convention. You can replace any hex value with your organisation's brand colours. 6. Click Apply and test Save the script and reload the widget. Rows containing dates will now be colour-coded automatically based on their proximity to today. Rows with dates safely in the future will have no background colour change. Important notes Date format compatibility: The script relies on JavaScript's native new Date() parser. This works reliably with mm/dd/yyyy and yyyy-mm-dd formats. If your Sisense instance is configured to display dates in a different format (e.g. dd/mm/yyyy), the parser may misinterpret the values. In that case, you will need to add a custom date parsing step before the new Date(dateText) call. Pagination: The script runs on domready, which fires each time the table re-renders – including after a page change. This means the highlighting is automatically reapplied when the user navigates to a different page of results. Grand Total rows: The script automatically skips any row whose date cell contains the word "total", preventing the Grand Total row from being incorrectly highlighted. Conclusion By hooking into the domready event and reading the rendered cell values directly from the DOM, this script adds a real-time, date-aware traffic-light system to any Sisense Pivot Table widget – with no changes required to the underlying data model. The configuration constants at the top of the script make it straightforward to adapt the column position, day thresholds, and colours to any use case. The same pattern can be extended to highlight rows based on other conditions, such as numeric thresholds, status text values, or combinations of multiple columns and conditions. References / Related content Sisense JavaScript API Reference – Widget Events Disclaimer:This post outlines a potential custom workaround for a specific use case or provides instructions regarding a specific task. The solution may not work in all scenarios or Sisense versions, so we strongly recommend testing it in your environment before deployment. If you need further assistance with this, please let us know.

      Thalita Santos
      Thalita SantosPosted 2 months ago
      0
               
    …