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
    • TagsChevronRightIcon
    widget script
      • 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
               
    • 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
               
    • Blog banner
      • Use Case GalleryChevronRightIcon

      Disabling navigation hover UI for viewer users in Sisense

                               

      What the Solution Does The  RemoveNavigationHoverAndMenu  plugin simplifies the Sisense navigation for viewer users by: Hiding the three-dots “more” menu in the left navigation. Hiding the dashboard metadata tooltip that appears on hover. Preventing hover-triggered UI behavior, so menus and tooltips do not activate. Leaving the default navigation fully intact for admins and authors. The plugin automatically detects the user’s base role (prism.user.baseRoleName) and applies these changes only for viewers. It uses scoped JavaScript and CSS to remove the unwanted hover interactions without modifying Sisense core files or affecting navigation performance. How it works: Viewer-only condition:  Runs only for viewer users (where prism.user.baseRoleName === "consumer"). Hover interception: Capture-phase event listeners block hover tooltip appearance  Scoped CSS:  Injects a short style block to hide hover UI elements and remove tooltip styling. Installation: Download  RemoveNavigationHoverAndMenu.zip . Extract the folder  RemoveNavigationHoverAndMenu  into your Sisense plugins directory:/opt/sisense/storage/plugins/Alternatively, upload it through  Admin > System Management > File Management  to the plugins folder. Refresh dashboards or restart Sisense to activate the plugin. Verification: Log in as a viewer user. Hover over dashboards or folders in the left navigation. Confirm the three-dots menu and metadata tooltip no longer appear. Log in as an admin and confirm the navigation behaves normally. Files included: RemoveNavigationHoverAndMenu/plugin.json RemoveNavigationHoverAndMenu/main.6.js RemoveNavigationHoverAndMenu/README.md Why It’s Useful Simplifying the Sisense interface for viewer users creates a cleaner, more focused environment that emphasizes content rather than controls. By removing hover-based menus and tooltips for viewers while preserving them for admins, this plugin improves usability without compromising functionality. This approach also supports governance and user-experience goals: Governance:  Viewers no longer see or interact with features they do not need. Consistency:  Admins and authors retain their full toolset for management tasks. Stability:  The plugin modifies only the UI layer and requires no changes to data models or access permissions. With this small enhancement, organizations can deliver a more streamlined viewing experience while maintaining full control for those managing dashboards and content. Outcome After installation, viewer users experience a simplified left navigation that shows only essential content. The three-dots menu and dashboard metadata tooltip are removed, and hover-based interactions no longer trigger any UI overlays. Admins and authors retain the complete navigation behavior, ensuring full functionality for management and editing tasks. The result is a cleaner, more predictable interface for viewers and a consistent, role appropriate experience across the Sisense environment. Hover Before Change (for viewers): Hover After Plugin (for viewers): Three Dot Menu Before Change (for viewers): Three Dot After Plugin (Is not visible, for viewers):   Side-by-Side Comparison Before and After Comparison:

      Jeremy Friedel
      Jeremy FriedelPosted 9 months ago • Last reply 3 months ago
      4
               
    • Blog banner
      • Use Case GalleryChevronRightIcon

      Extending pivot widget panel limits in Sisense using user groups

                                                       

      Overview Pivot widgets in Sisense are often used to explore and visualize complex datasets with multiple dimensions and measures. In some scenarios, users need to build very large pivot tables with many rows, columns, values, or filters. However, pivot widgets enforce internal panel item limits that can restrict how many fields and dimensions can be added to each panel. While these limits are useful for protecting performance, load time, and usability in general, they can become a constraint for advanced users working with large datasets or detailed analytical models. At the same time, organizations may want to apply different limits for different groups of users rather than increasing limits globally for everyone. This use case describes a Sisense plugin that automatically increases pivot panel limits as widgets load, with support for both a global default and optional overrides based on user group membership. The challenge By default, pivot widgets in Sisense can reach panel item limits that prevent users from adding additional dimensions or measures. This can affect: Analysts building large exploratory pivot tables Power users working with wide schemas or detailed hierarchies Dashboards that rely on complex pivots with many fields Manually adjusting widget configuration is not scalable, especially when dashboards contain many pivot widgets or when widgets are opened independently outside a dashboard. In addition, organizations often want different limits for different user groups, rather than applying a single global setting. It is important to note that if a very large number of dimensions are used in an individual pivot widget, that widget may have an extended load time. What the solution does The  PivotMaxPanelItems  plugin automatically sets the panel.metadata.maxitems value for every panel in pivot-type widgets as they load. At a high level, the plugin: Applies only to pivot widgets Updates all pivot panels (rows, columns, values, filters) Works for widgets inside dashboards and for widgets opened directly Supports a configurable default limit for all users Supports optional overrides based on Sisense user group membership The plugin runs on dashboard and widget load events, ensuring that pivot panel limits are applied consistently without requiring manual changes to individual widgets. Role and group-based configuration The plugin can apply different panel limits depending on the user’s Sisense group membership. This allows organizations to: Grant higher limits to advanced users or analysts Keep more conservative limits for general users Control behavior centrally through configuration If a user belongs to multiple configured groups, the plugin applies the first matching group based on the order defined in the configuration file. If no group matches, the default limit is used. This approach provides flexibility while keeping behavior predictable and easy to manage. How it is used Configuration is handled through a simple configuration file included with the plugin. Administrators can define: A default maximum number of items per pivot panel Optional overrides for specific Sisense user groups Once configured and installed, the plugin will likely require minimal ongoing maintenance in most circumstances. It applies automatically whenever pivot widgets are initialized. The full plugin is attached as a Zip file to this article and is available to download. The code is not compressed or obfuscated, and can be modified as needed, or used as example code for similar plugins. The plugin can be installed as a standard plugin by placing the decompressed folder into the plugin folder. The plugin includes a Readme file with further information. Why it’s useful This approach allows organizations to remove artificial constraints on pivot widget design while still maintaining control over performance and usability. Key benefits include: Enabling larger and more flexible pivot tables Reducing manual widget configuration and rework Applying consistent behavior across dashboards and standalone widgets Supporting different usage patterns across user groups Centralized control through a single configuration file The solution is particularly valuable in environments where advanced users need more flexibility without changing defaults for all users. Outcome With the  PivotMaxPanelItems  plugin in place, pivot widgets can support more dimensions without manually adding widget scripts. Advanced users gain the flexibility they need, while administrators retain control over limits at the group level through simple configuration. By applying limits automatically and consistently at load time, the plugin ensures predictable behavior across dashboards and widgets, supporting scalable group and role-aware analytics and visualization in Sisense. Screenshots Without a plugin, if a panel type includes too many items, the Add Panel Button is hidden With the plugin, the Add Button does not disappear when below the new limit, as many dimensions and fields as needed can be added to the widget.

      Jeremy Friedel
      Jeremy FriedelPosted 7 months ago • Last reply 4 months ago
      3
               
    • Discussion
      Rafael Ferreira
      • Help and How-To
               
      Rafael Ferreira
      How to add Scientific Units to a pivot table
                                       

      Hello Sisense world, Sometimes you are working with scientific units or any units for that matter that are not available in the number formatting within Sisense. i.e. Currency values, number or percentage. With the widget edit script provided below you can add a scientific unit or any type of symbol that lets your audience know what the measure/value is being calculated by.   const myTarget = { type: ['value'], values: [ { title: 'Total Quantity' // desired column } ] }; widget.transformPivot(myTarget, function(metadata, cell) { if (cell.value !== null && cell.value !== undefined) { cell.content = cell.value + " J⋅s"; } }); Here at Cause and Effect we provide a lot of useful solutions to enhance your analytics with Sisense, feel free to reach out to us to enhance your Models, Dashboard, Embedded Dashboards, etc. We have helped out hundreds of clients to produce dashboards that are easily digestable to their audience and bring their KPI's to life.   Rafael Ferreira Cause + Effect Strategy rferreira@cestrategy.us www.causeandeffectstrategy.com

      5 months ago
      0
               
    • Blog banner
      • Use Case GalleryChevronRightIcon

      Views bookmarking: Use case of a financial technology company

                                                                       

      Introduction FlexTrade is a global provider of multi-asset execution and order management systems, supporting trading workflows across asset classes, venues, and strategies. Their platforms generate large volumes of highly detailed data that users rely on for day-to-day analysis and decision-making. Companies like FlexTrade operate in an environment where users need deep, flexible analysis across a wide range of dimensions: asset class, venue, strategy, region, client, trader, time, and more. Pivot tables are a natural fit for this kind of detailed, highly dimensional analysis. However, as the number of dimensions grows, teams quickly hit a trade-off: Putting all dimensions into a single widget becomes expensive to query and difficult to interpret. Creating separate widgets for every dimension (or combination of dimensions) leads to bloated dashboards, slower load times, and a poor user experience. This use case focuses on how BloX was used to solve this problem by introducing view bookmarking, a flexible way for users to switch between different slicing configurations (in this example, a set of four dimensions) within a single widget. It also highlights how BloX can be used not just for custom visualizations, but also for building small, purpose-driven mini apps directly inside a dashboard . What the solution does This solution uses  BloX to manage view bookmarks for a pivot table. Instead of permanently adding all dimensions to the widget, BloX acts as a control layer that lets users select dimensions to include in the pivot at a time. Each selected combination can be saved as a view bookmark , representing a specific slicing configuration of the same underlying pivot. With this solution, users can: Select up to four dimensions to apply to the pivot table Save the selected combination as a personal bookmark Load and reuse previously saved bookmarks Delete bookmarks that are no longer needed Up to 20 bookmarks are supported out of the box, and all bookmarks are user-specific, allowing each user to maintain their own set of preferred analytical views. The solution also includes basic validation and error handling, such as preventing empty and duplicate bookmark names. From a technical perspective, BloX dynamically updates the pivot’s metadata. From a user perspective, it feels like switching views within a single widget. This keeps the analysis flexible while the dashboard structure remains simple and performant. Why it’s useful Scales to 10+ dimensions without UI overload Multi-asset trading analysis often requires exploring many dimensions, but not all at the same time. This solution allows FlexTrade users to work with 10+ dimensions while only surfacing the few that matter for the current question, resulting in less visual noise , lower cognitive load , and faster insights . Maintains dashboard performance and keeps dashboards clean and maintainable By avoiding massive pivots with every dimension enabled or dozens of near-duplicate widgets, the solution keeps queries efficient and dashboards responsive, even as analytical depth increases. One widget with dynamic views replaces an entire grid of narrowly focused widgets, resulting in dashboards that are easier to navigate , faster to load , and easier to maintain . Attachments BloX-ViewDimensionBookmarks.dash (example dashboard using the Sample ECommerce cube) BloXActionsForBookmarks.zip (BloX actions' scripts) ViewsBookmarkV2-2025-12-29.json (BloX template for the view bookmark widget, also included in the .dash file above). Note: The BloX widget also includes a script that automatically populates the dropdown menus with the available dimension names and existing bookmarks based on the widget’s metadata. Here is the script: // Dropdown classes used in the BloX code const dropdownClasses = [ "dimensionDropdown", //dropdowns for selecting the four dimensions "bookmarkDropdown" // dropdown for selecting existing bookmarks ]; const valueToDisable = "Select"; // placeholder value to disable widget.on('ready', function() { dimensions = widget.metadata.panels[0].items; dimensionTitles = dimensions .map(i => i.jaql.title); // Add each dimension title to the dimension dropdowns dimensionTitles.forEach(function(title, index) { $('.dimensionDropdown', element).append( '<option value="' + (index + 1) + '">' + title + '</option>' ); }); bookmarks = widget.metadata.panels[1].items; bookmarkTitles = bookmarks .filter(i => !i.disabled) // keep only not disabled .map(i => i.jaql.title); // extract title // Add each existing bookmark title to the bookmark dropdown bookmarkTitles.forEach(function(title) { $('#bookmarkDropdown', element).append( '<option value="' + title + '">' + title + '</option>' ); }); // Disable placeholder values from selection dropdownClasses.forEach(cls => { $(`.${cls}`).each(function () { let $select = $(this); if (!$select.is("select")) { $select = $select.find("select"); } if ($select.length === 0) return; $select.find("option").first().prop("disabled", true); }); }); });

      Tri Anthony
      Tri AnthonyPosted 7 months ago • Last reply 7 months ago
      1
               
    • Question
      • Help and How-ToChevronRightIcon
                       
      Querying Model in Text Widgets?
      Rafael Ferreira
      Rafael Ferreira
      Posted 9 months ago • Last reply 9 months ago
      5
               
    • Blog banner
      • Use Case GalleryChevronRightIcon

      Comprehensive framework for menu item removal in Sisense

               

      Overview Companies and organizations often wish to control which Sisense menu options are visible to different role types of users. Sisense administrators or software developers may require menu items such as  Edit Script  or  Embed Code , while business users should potentially see a more streamlined interface with only the essential options visible in the menu. Customizing these menus can be an important part of both governance and usability, ensuring that users are not viewing nonrelevent menu items. Code modifying menu's in Sisense can be present in both widget and dashboard scripts , and plugins . Custom menu items, added by plugins and scripts, can also be removed for the relevant user types. Challenge Removing menu items in Sisense requires intercepting the  beforemenu  event and filtering items out before the menu is displayed. This can be straightforward for most options, but there are several complexities to account for: Menu items can be identified by different properties (caption, command.title, description, or tooltip). Simply reassigning menuArgs.settings.items to a new array does not always work, since Sisense components and other listeners may hold references to the original array. In some cases, items may be injected into the menu after it is already open. The dashboard co-authoring  Hide  option is an example of this edge case, requiring a second filtering pass to remove consistently. The requirement was to create a robust framework that could handle these variations, while also supporting conditional filtering based on user type. Sisense exposes the property  prism.user .baseRoleName, which identifies the base role of the logged-in user. This allows developers to apply conditional logic when customizing menus. For example, users with the role "super" are administrators who may need access to advanced actions such as  Export  or  Embed Code , while users with the role "consumer" are viewers who should only see a simplified set of options. By checking this property, menu filtering can be adapted to match governance rules and usability needs for each audience. For example, administrators (prism.user.baseRoleName = "super") may retain advanced options, while viewers (prism.user.baseRoleName = "consumer") see a simplified set of actions. Solution A comprehensive framework was developed with the following key techniques: Configuration-driven filtering: Administrators can specify which object properties to inspect for matching strings, and which menu items to hide, making the script flexible and maintainable. In-place mutation of the menu array: Instead of replacing the array, the script updates it in place with splice. This ensures all references across the Sisense application reflect the updated menu. Two-phase filtering: The filter runs immediately during the beforemenu event to catch standard items, and again with setTimeout to handle menu items injected later, such as the  Hide option, which is an unusual case. Conditional visibility by user role:  By checking prism.user.baseRoleName, the script can apply different filtering rules for administrators and consumers. This ensures the right balance of power and simplicity depending on the user type. Full Implementation   /* * Hide specified Sisense menu items at both the dashboard and widget level. * All configuration is defined inside the beforemenu function for clarity */ prism.on("beforemenu", function (event, menuArgs) { // no menu items to process if (!menuArgs?.settings?.items) { return; } /** * Configuration * * fieldsToCheck: choose which object parameters to inspect on each menu item. * Set any field to false to skip it. * * itemsToHide: case-insensitive list of labels to remove from the menu. * Provide readable strings, such as "embed code" or "get url for embedding". */ const configuration = { fieldsToCheck: { caption: true, commandTitle: true, description: true, tooltip: true }, itemsToHide: [ "delete", "duplicate", "export", "edit script", "embed code", "get url for embedding", "simply ask (nlq)", "exploration paths", "widget filters indication", "widget affects dashboard filters", "dashboard settings", "hide widget header", "hide dashboard for non-owners", "featured on mobile app", "show dashboard in the featured dashboards list in the mobile app.", "navver.dashboard_menu_options.restore_dashboard", "switch to administrator view", "share the assistant", "change data source", "hide widget header", "hide" ] }; /** * Prepare a fast lookup set from the configured hide list. * Comparison is case-insensitive and ignores extra whitespace. */ const hiddenLabelSet = new Set( (configuration.itemsToHide || []) .map(formatLabelForComparison) .filter(Boolean) ); /** * Standardize a label for comparison: * - Trim whitespace * - Convert to lowercase * Returns undefined for non-strings or empty values after trimming. */ function formatLabelForComparison(rawValue) { if (typeof rawValue !== "string") { return undefined; } const trimmedLowercase = rawValue.trim().toLowerCase(); return trimmedLowercase.length ? trimmedLowercase : undefined; } /** * Gather all label values from the menu item according to the configuration. * Returns an array of standardized, non-empty strings. */ function getLabelsForMenuItem(menuItem) { const labels = []; if (configuration.fieldsToCheck.caption) { const captionLabel = formatLabelForComparison(menuItem.caption); if (captionLabel) { labels.push(captionLabel); } } if (configuration.fieldsToCheck.commandTitle) { const commandTitleLabel = formatLabelForComparison(menuItem.command?.title); if (commandTitleLabel) { labels.push(commandTitleLabel); } } if (configuration.fieldsToCheck.description) { const descriptionLabel = formatLabelForComparison(menuItem.desc); if (descriptionLabel) { labels.push(descriptionLabel); } } if (configuration.fieldsToCheck.tooltip) { const tooltipLabel = formatLabelForComparison(menuItem.tooltip); if (tooltipLabel) { labels.push(tooltipLabel); } } return labels; } /** * Decide whether a menu item should be hidden. * If any menu item parameter matches an entry in the hidden label set, the item is removed. */ function shouldHideMenuItem(menuItem) { const labels = getLabelsForMenuItem(menuItem); return labels.some(function (label) { return hiddenLabelSet.has(label); }); } /** * Recursively filter menu structures: * - Remove items that match the hide list. * - If an item has children (sub-menu), filter those sub-menus as well. */ function filterMenuItemsRecursively(menuItems) { return (menuItems || []) .filter(function (menuItem) { return !shouldHideMenuItem(menuItem); }) .map(function (menuItem) { if (Array.isArray(menuItem.items)) { menuItem.items = filterMenuItemsRecursively(menuItem.items); } return menuItem; }); } // Apply the filtering to the base array of menu items (function runFilter() { const filteredItems = filterMenuItemsRecursively(menuArgs.settings.items); // replace the contents of the original array so any existing references see the filtered result menuArgs.settings.items.splice(0, menuArgs.settings.items.length, ...filteredItems); })(); // Run the filter again after other beforemenu listeners finish setTimeout(function () { const filteredItems = filterMenuItemsRecursively(menuArgs.settings.items); menuArgs.settings.items.splice(0, menuArgs.settings.items.length, ...filteredItems); }, 0); });   Outcome With this framework, companies and organizations can reliably hide sensitive or unnecessary menu items while keeping the interface clear and consistent for end users. Because it mutates the menu array in place and applies filtering twice, it covers both standard and rare late injected menu items. By using prism.user.baseRoleName as a condition, administrator users can still view all menu items where needed, while viewer or designer level users  see only the options relevant to them. This makes the solution comprehensive, flexible, and aligned with the exact organization requirements for menu items.      

      Jeremy Friedel
      Jeremy FriedelPosted 10 months ago • Last reply 9 months ago
      1
               
    • Blog banner
      • Use Case GalleryChevronRightIcon

      Add presets to a Blox date filter widget

                                       

      We have some dashboards that have widgets as filters. One of these is a Blox widget that functions as a date filter, which I created with help from the community here. I recently added presets to the date filter to make it easier and faster to apply date filtering. Create a Blox Widget Paste the script below in the script editor section. { "style": ".blox-slides button:hover{background-color:#014E66 !important;} .date-input-container { position: relative; } .date-input-container input[type='date'] { cursor: pointer; } .date-input-container::before { content: ''; position: absolute; top: 0; left: 0; right: 0; bottom: 0; z-index: 1; cursor: pointer; } .date-input-container input::-webkit-calendar-picker-indicator { opacity: 0; position: absolute; right: 10px; width: 20px; height: 20px; cursor: pointer; z-index: 2; }", "title": "", "showCarousel": true, "carouselAnimation": { "showButtons": false }, "script": "setTimeout(function() { const fromInput = document.getElementById('SelectVal_from'); const toInput = document.getElementById('SelectVal_to'); function formatDate(date) { const year = date.getFullYear(); const month = String(date.getMonth() + 1).padStart(2, '0'); const day = String(date.getDate()).padStart(2, '0'); return year + '-' + month + '-' + day; } function setDates(fromDate, toDate) { if (fromInput) fromInput.value = formatDate(fromDate); if (toInput) toInput.value = formatDate(toDate); } function getDateRanges() { const today = new Date(); const currentYear = today.getFullYear(); const currentMonth = today.getMonth(); const currentQuarter = Math.floor(currentMonth / 3); return { last30days: { from: new Date(today.getTime() - 30 * 24 * 60 * 60 * 1000), to: today }, quarter: { from: new Date(currentYear, currentQuarter * 3, 1), to: new Date(currentYear, (currentQuarter + 1) * 3, 0) }, ytd: { from: new Date(currentYear, 0, 1), to: today }, lastyear: { from: new Date(currentYear - 1, 0, 1), to: new Date(currentYear - 1, 11, 31) } }; } const ranges = getDateRanges(); document.querySelectorAll('[data-filter-type]').forEach(function(btn) { btn.addEventListener('click', function() { const filterType = this.getAttribute('data-filter-type'); if (ranges[filterType]) { setDates(ranges[filterType].from, ranges[filterType].to); } }); }); if (fromInput) { fromInput.addEventListener('click', function(e) { if (e.target.tagName === 'INPUT') { e.target.showPicker ? e.target.showPicker() : e.target.click(); } }); fromInput.style.cursor = 'pointer'; } if (toInput) { toInput.addEventListener('click', function(e) { if (e.target.tagName === 'INPUT') { e.target.showPicker ? e.target.showPicker() : e.target.click(); } }); toInput.style.cursor = 'pointer'; } }, 1000);", "body": [ { "type": "Container", "width": "90%", "style": { "margin": "0 auto" }, "items": [ { "type": "ActionSet", "actions": [ { "type": "date-preset", "title": "Last 30 Days", "style": { "color": "white", "background-color": "#007FAA" }, "data": { "FilterType": "last30days", "FilterFields": [ "[Dm_dates.date_data (Calendar)]" ] } }, { "type": "date-preset", "title": "This Quarter", "style": { "color": "white", "background-color": "#007FAA" }, "data": { "FilterType": "quarter", "FilterFields": [ "[Dm_dates.date_data (Calendar)]" ] } }, { "type": "date-preset", "title": "Year to Date", "style": { "color": "white", "background-color": "#007FAA" }, "data": { "FilterType": "ytd", "FilterFields": [ "[Dm_dates.date_data (Calendar)]" ] } }, { "type": "date-preset", "title": "Last Year", "style": { "color": "white", "background-color": "#007FAA" }, "data": { "FilterType": "lastyear", "FilterFields": [ "[Dm_dates.date_data (Calendar)]" ] } } ] }, { "type": "Container", "style": { "display": "flex", "flexDirection": "row", "justifyContent": "space-between", "marginTop": "20px", "gap": "10px" }, "items": [ { "type": "Container", "style": { "width": "48%" }, "items": [ { "type": "TextBlock", "text": "From", "weight": "lighter", "color": "black" }, { "type": "Container", "style": { "position": "relative" }, "items": [ { "type": "Input.Date", "id": "SelectVal_from", "placeholder": "mm/dd/yyyy", "calendar": true, "style": { "width": "100%", "padding": "14px", "background-color": "#F4F4F8", "border-radius": "8px", "border": "1px solid #ccc", "font-size": "16px", "cursor": "pointer" } } ] } ] }, { "type": "Container", "style": { "width": "48%" }, "items": [ { "type": "TextBlock", "text": "To", "weight": "lighter", "color": "black" }, { "type": "Container", "style": { "position": "relative" }, "items": [ { "type": "Input.Date", "id": "SelectVal_to", "placeholder": "mm/dd/yyyy", "calendar": true, "style": { "width": "100%", "padding": "14px", "background-color": "#F4F4F8", "border-radius": "8px", "border": "1px solid #ccc", "font-size": "16px", "cursor": "pointer" } } ] } ] } ] }, { "type": "ActionSet", "style": { "marginTop": "20px", "text-align": "center" }, "actions": [ { "type": "DateX", "id": "submit_btn", "title": "Apply", "style": { "color": "white", "background-color": "#007FAA" }, "data": { "FilterFields": [ "[Dm_dates.date_data (Calendar)]" ] } }, { "type": "filter-date-clear", "title": "Clear", "style": { "color": "white", "background-color": "#007FAA" }, "data": { "FilterFields": [ "[Dm_dates.date_data (Calendar)]" ] } } ] } ] } ] }   Create the necessary actions for the buttons to work: date-preset const filterType = payload.data.FilterType; const filterDims = payload.data.FilterFields; const dash = payload.widget.dashboard; const now = new Date(); const yyyy = now.getFullYear(); const mm = String(now.getMonth() + 1).padStart(2, '0'); const dd = String(now.getDate()).padStart(2, '0'); const today = `${yyyy}-${mm}-${dd}`; let fromDate = ''; let toDate = today; //Year to date if (filterType === 'ytd') { fromDate = `${yyyy}-01-01`; //Quarter } else if (filterType === 'quarter') { const q = Math.floor(now.getMonth() / 3); const startMonth = q * 3 + 1; fromDate = `${yyyy}-${String(startMonth).padStart(2, '0')}-01`; //Last Year } else if (filterType === 'lastyear') { fromDate = `${yyyy - 1}-01-01`; toDate = `${yyyy - 1}-12-31`; // Last 30 days: from 30 days ago to today } else if (filterType === 'last30days') { const pastDate = new Date(now); pastDate.setDate(pastDate.getDate() - 30); const pastY = pastDate.getFullYear(); const pastM = String(pastDate.getMonth() + 1).padStart(2, '0'); const pastD = String(pastDate.getDate()).padStart(2, '0'); fromDate = `${pastY}-${pastM}-${pastD}`; } else { console.log('Unknown FilterType:', filterType); if (typeof sendResponse === 'function') sendResponse(false); return; } let newFilter = {}; $('#SelectVal_from').val(fromDate); $('#SelectVal_to').val(toDate); newFilter = { jaql: { dim: "", filter: { from: fromDate, to: toDate } } }; filterDims.forEach(function(dim) { newFilter.jaql.dim = dim; dash.filters.update(newFilter, { refresh: true, save: true }); });   Datex -- Apply button  var today = new Date(); var dd = String(today.getDate()).padStart(2, '0'); var mm = String(today.getMonth() + 1).padStart(2, '0'); //January is 0! var yyyy = today.getFullYear(); today = yyyy + '-' + mm + '-' + dd; const filVal_from = payload.data.SelectVal_from == '' ? '1800-01-01' : payload.data.SelectVal_from; const filVal_to = payload.data.SelectVal_to == '' ? '2100-01-01' : payload.data.SelectVal_to; const filterDims = payload.data.FilterFields; const dash = payload.widget.dashboard; let newFilter = {}; console.log(filVal_from); console.log(filVal_to); newFilter = { jaql: { dim: "", filter: { from: filVal_from, to: filVal_to } } }; filterDims.forEach(function (dim) { newFilter.jaql.dim = dim; dash.filters.update(newFilter, { refresh: true, save: true }) })   Clear dates var today = new Date(); var dd = String(today.getDate()).padStart(2, '0'); var mm = String(today.getMonth() + 1).padStart(2, '0'); //January is 0! var yyyy = today.getFullYear(); today = yyyy + '-' + mm + '-' + dd; const filVal_from = "1800-01-01" const filVal_to = '2100-01-01' const filterDims = payload.data.FilterFields; const dash = payload.widget.dashboard; let newFilter = {}; newFilter = { jaql: { dim: "", filter: { from: filVal_from, to: filVal_to } } }; $('#SelectVal_from').val(''); $('#SelectVal_to').val(''); filterDims.forEach(function (dim) { newFilter.jaql.dim = dim; dash.filters.update(newFilter, { refresh: true, save: true }) })   Save everything and try it out. 

      Zach Williams
      Zach WilliamsPosted 11 months ago
      0
               
    • Blog banner
      • Use Case GalleryChevronRightIcon

      A Guide to Creating Bullet Charts

               

       Instructions: Create a Bar Chart Add your main Category ( ex. 'Company') Add your three required measures:   Actual Value:   This is the primary metric you are measuring. It's the " what happened " value. ( ex. YTD Revenue, Actual Spend, Units Sold) Target Value: This is the goal or benchmark you are comparing against. It's the " what should have happened " value. ( ex.    Sales Quota, Budget, Last Year's Revenue ) Qualitative Value (Forecast) : This is a secondary comparative point, often used to show a projection. It's the " what we think will happen " value. ( ex. Forecasted Spend, Projected Sales, Pipeline Value )  Navigate to the widget's script editor:     Paste the script into the widget's script editor and follow instructions: */ widget.on('processresult', function(widget, args) { // ======================================================== // CONFIGURATION // ======================================================== // --- Step 1: Field Names --- // These names MUST EXACTLY MATCH the names in your widget's Values panel. const actualValue_FieldName = 'Actual Value'; const targetValue_FieldName = 'Target'; const qualitative_FieldName = 'Qualitative'; // --- Step 2: Chart Labels --- // These are the clean names that will show up in the legend and tooltips. const actualValue_DisplayName = 'Actual Value'; const targetValue_DisplayName = 'Target'; const qualitative_DisplayName = 'Qualitative'; // Used for the marker line // --- Step 3: Color Palette --- // Define the colors for your chart elements. const color_ActualValue = '#3682ff'; // Blue for the main 'Actual Value' bar. const color_TargetValue_Fill = '#d3d3d3'; // Light gray for the background 'Target' bar. const color_TargetValue_Border = '#a9a9a9'; // Darker gray for the border. // --- EDIT MARKER COLORS HERE --- // Control the colors for the forecast marker based on your goal. // By default, OVER target is red and UNDER is green (good for tracking costs). // To make OVER target green (good for tracking revenue), just swap the color hex codes below. const color_Marker_Over = '#e45c5c'; // Color for when Forecast > Target const color_Marker_Under = '#4dbd33'; // Color for when Forecast < Target   Add this directly underneath the script you just customized: if (!args.result || !args.result.series || args.result.series.length === 0) { return; } const result = args.result; const categories = result.categories; // Helper function to read data from your widget. const getDataBySeriesName = (name) => { for (let i = 0; i < result.series.length; i++) { if (result.series[i].name === name) { return result.series[i].data.map(point => typeof point === 'object' ? point.y : point); } } console.warn('Field not found in widget data:', name); return new Array(categories.length).fill(0); }; // --- DATA RETRIEVAL --- const actualValueData = getDataBySeriesName(actualValue_FieldName); const targetValueData = getDataBySeriesName(targetValue_FieldName); const qualitativeData = getDataBySeriesName(qualitative_FieldName); // Clear the default chart series to build our custom one. result.series = []; // --- PLOT OPTIONS --- result.plotOptions = { bar: { stacking: null, grouping: false, pointPadding: 0, groupPadding: 0.25, // Space between each bullet chart. borderWidth: 1, borderRadius: 3, // Rounded corners for the bars. dataLabels: { enabled: false } } }; // --- SERIES CONFIGURATION --- // 1. A hidden series to hold the raw qualitative data for the tooltip. result.series.push({ name: qualitative_FieldName, type: 'bar', data: qualitativeData.map((val, index) => ({ x: index, y: val })), visible: false, showInLegend: false }); // 2. Legend item for "Over Target". result.series.push({ name: 'Over Target', type: 'line', color: color_Marker_Over, marker: { symbol: 'line', lineWidth: 4, radius: 5 }, data: [], showInLegend: true }); // 3. Legend item for "Under Target". result.series.push({ name: 'Under Target', type: 'line', color: color_Marker_Under, marker: { symbol: 'line', lineWidth: 4, radius: 5 }, data: [], showInLegend: true }); // 4. The wide, background bar representing the Target value. result.series.push({ name: targetValue_DisplayName, type: 'bar', data: targetValueData.map((val, index) => ({ x: index, y: val })), color: color_TargetValue_Fill, borderColor: color_TargetValue_Border, pointWidth: 30, // Thickness of the background target bar. zIndex: 0, states: { hover: { enabled: false } }, showInLegend: true }); // 5. The narrower, foreground bar representing the Actual value. result.series.push({ name: actualValue_DisplayName, type: 'bar', data: actualValueData.map((val, index) => ({ x: index, y: val })), color: color_ActualValue, pointWidth: 15, // Thickness of the main actual value bar. zIndex: 1, showInLegend: true }); // 6. The thin vertical line that marks the Qualitative/Forecast value. result.series.push({ name: 'Qualitative Marker', type: 'bar', data: qualitativeData.map((val, i) => ({ y: val, color: (val - targetValueData[i]) >= 0 ? color_Marker_Over : color_Marker_Under })), pointWidth: 3, // Thickness of the marker line. zIndex: 2, showInLegend: false, enableMouseTracking: false }); // --- CHART & AXIS CONFIGURATION --- if (!result.chart) result.chart = {}; result.chart.inverted = true; // Flips chart to be horizontal. if (result.yAxis.title) result.yAxis.title.text = ''; // Hides Y-axis title. if (result.xAxis.title) result.xAxis.title.text = ''; // Hides X-axis title. // --- LEGEND & TOOLTIP --- result.legend = { ...result.legend, enabled: true, reversed: true }; result.tooltip = { enabled: true, shared: true, useHTML: true, backgroundColor: 'rgba(255, 255, 255, 1)', borderWidth: 1, borderColor: '#E0E0E0', formatter: function() { try { if (!this.points || this.points.length === 0) return false; const pointIndex = this.points[0].point.index; const categoryName = this.x; const chart = this.points[0].series.chart; const actualVal = chart.series.find(s => s.name === actualValue_DisplayName).data[pointIndex].y; const targetVal = chart.series.find(s => s.name === targetValue_DisplayName).data[pointIndex].y; const qualitativeVal = chart.series.find(s => s.name === qualitative_FieldName).data[pointIndex].y; const percentOfTarget = (targetVal === 0) ? 0 : (actualVal / targetVal); const varianceVal = qualitativeVal - targetVal; const varianceColor = varianceVal >= 0 ? color_Marker_Over : color_Marker_Under; let s = `<div style="padding: 10px; font-family: 'lato', sans-serif; font-size: 13px;">`; s += `<div style="font-size: 14px; margin-bottom: 10px; font-weight: 700;">${categoryName}</div>`; s += `<table style="width: 100%;">`; s += `<tr><td style="padding: 4px 2px;"><span style="background-color:${color_ActualValue}; width: 12px; height: 12px; border-radius: 2px; display: inline-block; margin-right: 8px;"></span>${actualValue_DisplayName}</td><td style="text-align: right; font-weight: 700;">${Highcharts.numberFormat(actualVal, 0, '.', ',')}</td></tr>`; s += `<tr><td style="padding: 4px 2px;"><span style="background-color:${color_TargetValue_Fill}; border: 1px solid ${color_TargetValue_Border}; width: 12px; height: 12px; border-radius: 2px; display: inline-block; margin-right: 8px; box-sizing: border-box;"></span>${targetValue_DisplayName}</td><td style="text-align: right; font-weight: 700;">${Highcharts.numberFormat(targetVal, 0, '.', ',')}</td></tr>`; s += `<tr><td style="padding: 4px 2px; padding-left: 24px;">% of Target</td><td style="text-align: right; font-weight: 700;">${Highcharts.numberFormat(percentOfTarget * 100, 0)}%</td></tr>`; s += `<tr><td style="padding: 4px 2px;"><span style="background-color:${varianceColor}; width: 3px; height: 12px; border-radius: 2px; display: inline-block; margin-right: 8px; margin-left: 4px;"></span>${qualitative_DisplayName}</td><td style="text-align: right; font-weight: 700;">${Highcharts.numberFormat(qualitativeVal, 0, '.', ',')}</td></tr>`; s += `<tr><td style="padding: 4px 2px; padding-left: 24px;">Variance</td><td style="text-align: right; font-weight: 700; color: ${varianceColor};">${Highcharts.numberFormat(varianceVal, 0, '.', ',')}</td></tr>`; s += `</table></div>`; return s; } catch (e) { return 'Error creating tooltip.'; } } }; }); Save script and refresh the widget. You're done!  

      Patrick Morris
      Patrick MorrisPosted 1 year ago • Last reply 1 year ago
      1