Pivot2. How to set up styles with the scripts?
Currently, in Linux installation, you can face widget [Pivot2]. This is a new realization of the widget [Pivot]. This means that it has new styles, new classes, etc. Almost all the scripts to change styles of the resulting table used DOM selectors to find elements that will be changed. Since a structure of a new pivot differs from the previous realization, then scripts developed for the first implementation of the pivot are not applicable to the new one. Below, I will explain a common approach on how to change styles in Pivot2. First of all, now Pivot is not fully generated HTML code. Data are retrieved via WebSocket protocol and each pivot has three parts: Headers; Categories; Values. These parts are loaded independently but will be displayed as an entire pivot. The second - in a new pivot attribute [fidx=i] does not exist, so you cannot use this attribute as a selector to select some column. The third - values are displayed in HTML element <div> (not in <td> as it was before in the first version) New pivot structure: FORMATTING EXAMPLES 1. Highlight text in column 2 $("div[class*='--col-1'] div.table-grid__content__inner").css('backgroundColor','red’) $("td[class*='--col-1'] div.table-grid__content__inner") Two things are going on here: $("td[class*='--col-1’]”) // select any td cell with classname that contains --col-1 $(“x y”) // select x first, then within the x select the y. Specifically, find all cells with a classname that contains --col-1. Then, inside those, only select the div with the classname table-grid__content__inner 2. Highlight text + padding in column 2 $("div[class*='--col-1'] div.table-grid__content__inner").parent().css('backgroundColor','red') 3. Highlight all cells in column 2 $("div[class*='--col-1'] div.table-grid__content__inner").parent().parent().css('backgroundColor','red') < jQuery Tip > $("DIV[CLASS*='--COL-1'] DIV.TABLE-GRID__CONTENT__INNER") is the lowest level <div>, which contains the displayed text. .parent() – one level up – another <div>. .PARENT().PARENT() – (2 LEVELS UP) – THE CELL. 3 levels up, .parent().parent().parent() is the entire row. 4. Highlight entire rows of values $("div[class*='--col-1'] div.table-grid__content__inner").parent().parent().parent().css('backgroundColor','yellow') 5. Highlight the first dimension column $("td[class*='--col-0'] div.table-grid__content__inner").css('backgroundColor','yellow') 6. Highlight row 2 of the dimension (country) $("td[class*='--row-1']").css('backgroundColor','yellow') 7. Color Entire Rows Highlight the entire 7th row: $("[class*='--row-7']").css('backgroundColor','yellow') Color rows 2 and 3: $(".table-grid__row-1").css('backgroundColor','yellow'); $(".table-grid__row-2").css('backgroundColor','lightgray') 8. Highlighting Entire First Row Of Data If we try the same approach with the top row, we will end up grabbing the header and the data - two rows: $(".table-grid__row-0").css('backgroundColor','yellow') That is not what we want, so let's be more selective in our jQuery. After checking the DOM structure of the pivot, let’s only look for our rows under the div.multi-grid__bottom-part, which contains dims /categories (country names) $("div.multi-grid__bottom-part .table-grid__row-0").css('backgroundColor','yellow') < jQuery Tip > $("DIV.MULTI-GRID__BOTTOM-PART .TABLE-GRID__ROW-0") FIRST, FIND DIV.MULTI-GRID__BOTTOM-PART. THEN, WITHIN THAT, ELEMENTS OF CLASS .TABLE-GRID__ROW-0. GENERALLY: $(“X Y“) – FIND Y WITHIN X Plus also get the top row of the values: $("div.scroll-elem .table-grid__row-0").css('backgroundColor','yellow') Now, let's combine these two separate jQuery selectors: $("div.scroll-elem .table-grid__row-0, div.multi-grid__bottom-part .table-grid__row-0").css('backgroundColor','yellow') < jQuery Tip > $("DIV.SCROLL-ELEM .TABLE-GRID__ROW-0, DIV.MULTI-GRID__BOTTOM-PART .TABLE-GRID__ROW-0") TWO THINGS ARE GOING ON HERE: $(“A B, C D”) // SELECT $(“A B”), ALSO SELECT $(“C D”), COMBINE THE RESULT $(“X Y”) // SELECT EVERY X, THEN SELECT EVERY Y INSIDE THE X CONDITIONAL FORMATTING 9. Let's turn yellow every number greater than 1000 in column 2: $("div[class*='--col-1'] div.table-grid__content__inner").each(function(index, item) {var myItemNum = parseFloat($(item).text() .replace(',','').replace('$','')); if (myItemNum > 1000) {$(item).parent().css('backgroundColor','yellow') }; }); 10. Now, same, but let's color the entire cell (not just its text): $("div[class*='--col-1'] div.table-grid__content__inner") .each(function(index, item) { var myItemNum = parseFloat($(item).text().replace(',','').replace('$','')); if (myItemNum > 1000) {$(item).parent().parent().css('backgroundColor','yellow')}; }); 11. Now, for every item > 1000 in column 2, let's color the entire row of values: $("div[class*='--col-1'] div.table-grid__content__inner") .each(function(index, item) { var myItemNum = parseFloat($(item).text().replace(',','').replace('$','')); if (myItemNum > 1000) {$(item).parent().parent().parent().css('backgroundColor','yellow’)}; }); 12. Now, for each of the values > 1000, let's color the entire row, including values and the country dimension This is more difficult because there is no way to find the right text that's > 1000, then navigate from that cell to the corresponding row of the country (the way we used to do this in Pivot 1), because now they exist in different table structures. $("div[class*='--col-1'] div.table-grid__content__inner").each(function(index, item) { var myItemNum = parseFloat($(item).text().replace(',', '').replace('$', '')); var myRowNum; var myDimSelector = ""; if (myItemNum > 1000) { $(item).parent().parent().parent().css('backgroundColor', 'yellow'); // row of values myRowNum = parseInt($(item).parent().parent().parent().attr('class').slice(-1)) + 1; myDimSelector = "td.table-grid__cell--row-" + myRowNum; $(myDimSelector).css('backgroundColor', 'lightgreen'); //row of dimension headers }; }); Explanation: To better understand this, let’s do this step by step. $("div[class*='--col-1'] div.table-grid__content__inner").each(function(index, item) { var myItemNum = parseFloat($(item).text().replace(',','').replace('$','')); if (myItemNum > 1000) {$(item).parent().parent().parent().css('backgroundColor','yellow') }; }); This returns 3 rows that match, each row is like this: div.table-grid__row.table-grid_row-1 $("div[class*='--col-1'] div.table-grid__content__inner").each(function(index, item) { var myItemNum = parseFloat($(item).text().replace(',','').replace('$','')); if (myItemNum > 1000) {console.log($(item).parent().parent().parent() ) }; }); The jQuery we want to execute is this: $("td.table-grid__cell--row-1").css('backgroundColor’,’lightgreen’) , but where row-1 is replaced with the current value of the row Running: $("div[class*='--col-1'] div.table-grid__content__inner").each(function(index, item) { var myItemNum = parseFloat($(item).text().replace(',','').replace('$','')); if (myItemNum > 1000) {console.log($(item).parent().parent().parent().attr('class') ) }; }); Returns: table-grid__row table-grid__row-1 table-grid__row table-grid__row-3 table-grid__row table-grid__row-5 Let's grab 1 character from the right using slice(-1) (1 char from the right). FINAL SCRIPT: $("div[class*='--col-1'] div.table-grid__content__inner").each(function(index, item) { var myItemNum = parseFloat($(item).text().replace(',', '').replace('$', '')); var myRowNum; var myDimSelector = ""; if (myItemNum > 1000) { $(item).parent().parent().parent().css('backgroundColor', 'yellow'); // row of values myRowNum = parseInt($(item).parent().parent().parent().attr('class').slice(-1)) + 1; myDimSelector = "td.table-grid__cell--row-" + myRowNum; $(myDimSelector).css('backgroundColor', 'lightgreen'); //row of dimension headers }; });2.5KViews2likes1CommentWidget Script: Stream Data Into A Widget With Sub-Second Refresh
Challenge The utility of a sub-second refresh rate is limited to monitoring use-cases. This, of course, is not BI, but there may be cases where a BI use-case also incorporates live streaming data to trigger an action or decision. An example is if there is a power surge, a worker may want to immediately turn off the flow of electricity and then use BI to analyze what the cause of the surge was. The challenge is presented when we try to query large bodies of data and ingest them with a dashboard quickly. With data sources like memSQL, it is certainly possible to query and display 100K records every second, but this requires a specific set of tools and configurations. Solution As a general user with a generic MSQL database but perhaps with a REST endpoint attached to IOT devices, it is certainly easier to make a query for the latest point of data and reflect this in the dashboard. This is precicely what we will do. Creating The Base Widget: This widget will need base data to create a Sisense widget. Once the widget is created we can apply a script to feed in live values. There are three requirements to the base widget you should consider: How many values do you want to see in your chart The value the base widget will show before new values are passed in BASE VALUES It’s best to set the widget up with values that align with the data you will query. If you do not have data in your EC that corresponds with the live data you can feed in a hardcoded 0 into the value panel. NUMBER OF VALUES INSIDE THE LIVE CHART The number of values will stay consistent as more values are populated. As new values are populated, old values are dropped. Set the initial range of values by applying a filter to the X-axis and specify that you want to see the latest X units. In the example above, the chart will always display 30 units. Setting The Widget Script This script will do the following: Query a REST endpoint every X milliseconds Push the result into a line chart Update the Sisense Chart Copy and paste the widget script below into a widget script. Save the script, refresh the widget, and apply it. I recommend creating new functions for every widget you apply this on to avoid function conflicts. COMPLETE CODE WITH COMMENTS widget.on('domready', () => { asyncCallForDeviceA(); }) var getNewDataPointForDeviceA = function(){ // assign a value to a variable "quit" in the console to stop getting new values if (typeof quit == "undefined"){ var newValue; var newTime; setTimeout( () => { //Get data with REST API $.getJSON( 'https://api.exchangeratesapi.io/latest', function( json ) { //get value newValue = json.rates.AUD; //get time at which the data was retrieved newTime = new Date().toString().slice(4,24); //update the widget data widget.rawQueryResult.values.push( [ {data:String(newTime), text:String(newTime)}, {data:newValue, text:String(newValue)} ] ); //drop the first value widget.rawQueryResult.values.shift() //redraw the widget with the new value widget.redraw(); }); }, 800); //800 means this function will occur every 800 milliseconds } } var asyncCallForDeviceA = async function(){ await getNewDataPointForDeviceA(); } 6 Post actions1.1KViews1like0CommentsHow to create a Personalized Navigation Landing Page
Requirement In this article, we will look to create a personalized dashboard navigation landing page that shows: The logged user's most popular dashboards. Most popular dashboards on the company level. Consider further enhancing the logic presented in this article. Solution Steps: 1) To address the need, we will first need to learn how to implement a landing page - Follow this article to learn more on that. The reasoning behind it is we will use data-security where we will give a logged user permissions to see: His own records - Retrieving his popular dashboards The dummy user records - Retrieving the most popular dashboards on a company level. 4) Add data-security rules to the model that enables the user to see his own UserID/UserName from DimUsers table + UserId/UserName '-1'. 5) Find the attached BloX template that introduced action buttons to jump to a specific dashboard. LandingPage.json: { "card": { "style": "", "script": "", "title": "", "showCarousel": false, "body": [ { "spacing": "", "type": "Container", "items": [ { "type": "TextBlock", "text": "{panel:Dashboard Name}", "horizontalAlignment": "center", "size": "medium", "style": { "color": "#117899" } }, { "spacing": "small", "type": "TextBlock", "text": "{panel:Total Column} # of Actions", "color": "default", "horizontalAlignment": "center" } ] } ], "actions": [ { "type": "Action.OpenUrl", "title": "{panel:Dashboard Name}", "url": "http://10.50.57.72:30845/app/main#/dashboards/{panel:Dashboard ID}" } ] }, "config": { "fontFamily": "Open Sans", "fontSizes": { "default": 16, "small": 12, "medium": 22, "large": 32, "extraLarge": 50 }, "fontWeights": { "default": 500, "light": 100, "bold": 1000 }, "containerStyles": { "default": { "backgroundColor": "lightgrey", "foregroundColors": { "default": { "normal": "#3A4356" }, "white": { "normal": "#ffff" }, "grey": { "normal": "#5C6372" }, "orange": { "normal": "#f2B900" }, "yellow": { "normal": "#ffcb05" }, "black": { "normal": "#000000" }, "lightGreen": { "normal": "#3ADCCA" }, "green": { "normal": "#54a254" }, "red": { "normal": "#dd1111" }, "accent": { "normal": "#2E89FC" }, "good": { "normal": "#54a254" }, "warning": { "normal": "#e69500" }, "attention": { "normal": "#cc3300" } } } }, "imageSizes": { "default": 40, "small": 40, "medium": 80, "large": 160 }, "imageSet": { "imageSize": "medium", "maxImageHeight": 100 }, "actions": { "color": "", "backgroundColor": "#10a5F5", "maxActions": 5, "spacing": "small", "buttonSpacing": 20, "actionsOrientation": "horizontal", "actionAlignment": "center", "showCard": { "actionMode": "inline", "inlineTopMargin": 16, "style": "default" } }, "spacing": { "default": 5, "small": 20, "medium": 40, "large": 60, "extraLarge": 40, "padding": 25 }, "separator": { "lineThickness": 1, "lineColor": "#eeeeee" }, "factSet": { "title": { "size": "default", "color": "default", "weight": "bold", "warp": true }, "value": { "size": "default", "color": "default", "weight": "default", "warp": true }, "spacing": 20 }, "supportsInteractivity": true, "height": 842, "imageBaseUrl": "" } } 6) Import the attached dashboard example NavigationDashboard (1).dash. You can learn more on how to customize BloX in the following documentation page: https://www.sisense.com/marketplace/blox-widgets/ That's it!2.4KViews1like1CommentAdvanced Dynamic Filtering
Get quick, comparative insights from large datasets by creating flexible pivot tables or widgets. Whether comparing time periods, countries, or sales figures, you can leverage a single filter to handle multiple comparisons without clutter. This method uses custom fields, measured values, ranking filters, and the smart label plugin to streamline comparisons across various dimensions for clean, dynamic visualizations.3.6KViews1like1CommentFilter For Last Working Day
When a filter based on this custom column is active, the dashboard will reference the last working day. If the dashboard is checked on Monday or Sunday, Friday's data will be shown. If the dashboard is checked on Tuesday - Saturday, the prior day will be shown. Apply this SQL script to a custom column of a DateDim table and apply a filter based on this new column to the dashboard set to 1. ifint( [Date] = case when DayOfWeek(CurrentDate()) in (2,3,4,5,6) then adddays(createdate(getyear(now()), getmonth(now()), getday(now())), -1) -- if the day is between Tuesday and Saturday go back return yesterday when DayOfWeek(CurrentDate()) = 1 then adddays(createdate(getyear(now()), getmonth(now()), getday(now())), -3) -- if the day is a Monday go back 3 days for last friday when DayOfWeek(CurrentDate()) = 7 then adddays(createdate(getyear(now()), getmonth(now()), getday(now())), -2) -- if the day is a Sunday go back 2 days for last friday end , 1, 0)601Views1like0CommentsMaintain Percentage Contribution Using ALL Function
Analytical Need Even though this requirement sounds fairly simple, a lot of times, this use case gets confusing. If there are 10 stores for a Retail Chain, sometimes the Management would only want to compare 3 stores and split the entire contribution within the 3 retail stores. But there would be instances where the business would want to see the numbers of 3 selected stores but at the same time, would want to see the contribution of each store to the overall sales figure (sum of 10 stores). In the case of the latter, the selected three stores will be filtered, but the contribution percentage should not be altered only for the 3 stores, but it should be calculated with respect to all the stores of the Retail Chain. Mathematically, the denominator should be the same number (Total Sales across 10 stores) before and after selecting the 3 specific stores. Modeling Challenge The first case where the contribution percentage gets altered based on the filtered values, we could make use of the ‘Contribution’ function and achieve the same. Contribution function could be used from the Functions tab or by simply using the ‘Quick Functions’ of the Value menu. (or) But to achieve the latter use case, we cannot make use of the ‘CONTRIBUTION’ function but try working this out mathematically. Solution MATHEMATICAL SOLUTION: Irrespective of the filter applied on the Store, the percentage contribution should not change. So, Percentage Contribution of Store A = Total Sales $ of Store A / (Total Sales of All Stores), Percentage Contribution of Store B = Total Sales $ of Store B / (Total Sales of All Stores), and so on… To maintain the percentage contribution unchanged when filtering out few members, we need to have the denominator constant. SISENSE SOLUTION: We make use of the ALL function to create the denominator as mentioned in the documentation. This way, the widget filter to pick specific stores will not affect the percentage contribution value. So, depending on what the use case is, we could choose which formula needs to be utilized.4.7KViews1like0CommentsDivergent/Negative Bar Chart
Sometimes you may want to compare two values alongside each other to measure impact of one vs the other but without stacking them or having them side by side. This is often called a divergent bar chart. This can be done in Sisense with just a little bit of work. First take your left measure and multiply it by -1 to make it negative, you will want to sort ascending on that measure. This gives us almost what we want, but the tooltips and the axis show negative values which can be misleading! So let's take care of those! The axis label We can easily extend the usual sisense formatter to replace its output when it contains a "-" sign. widget.on('beforeviewloaded',function(se,ev){ old=ev.options.yAxis[0].labels.formatter ev.options.yAxis[0].labels.formatter= function () { return old.apply(this).replace("-","") } }) The tooltip Again here we can use the "beforedatapointtooltip" event to access the tooltip context each time and replace the "-" in the tooltip. widget.on("beforedatapointtooltip", function (se, args){ args.context.points[0].value=args.context.points[0].value.replace("-", "") })909Views1like0CommentsCalculate the Month Difference Between Min and Max Date Fields from the Dashboard (without scripting)
Analytical Need For a metric, I need to calculate the month difference between the min and max date of purchase for each brand in my dataset. I want the dates to be dynamic in respect to filtering, etc. I am using the attached .ecdata and .dash files for this solution. See this article on importing a .dash file, and this on importing an .sdata file. Challenge The MIN() and MX() dashboard functions do not work on date fields. And even if we create a Numeric Representation Of Date Fields, a simple subtraction of the results won't work because we can't just use the difference in days (think of the case where the dates are two days apart, but land in different months). How can we identify the earliest and latest date and calculate the month difference between them? Elasticube Prep(Step 1) Data Requirements In the Elasticube, create a custom field called [Date Numeric] to convert your date field to an integer using 10000*getyear(Date)+100*getmonth(Date)+getday(Date) Create this field in the fact table since it is a value we will be performing a calculation on. Dashboarding Solution (Step 2) We need to extract the year and month values from our [Date Numeric] field in order to calculate the month difference between to date values. To do this, we will reverse the logic used to convert the field to an integer. The formula syntax below achieves this for all three components of the earliest date. To generate the year, month and day corresponding to the latest date value, we can swap out MIN for MAX. Year of MIN Date Integer: FLOOR(min([Date Numeric])/10000) Return the Month of MIN Date: FLOOR((min([Date Numeric])-(10000*(FLOOR(min([Date Numeric])/10000))))/100) Return the Day of MIN Date: (min([Date Numeric])-(10000*(FLOOR(min([Date Numeric])/10000))))-(100*(FLOOR((min([Date Numeric])-(10000*FLOOR(min([Date Numeric])/10000)))/100))) For this specific use case, we want to get the month difference between the min and max: Final Solution - Month Difference Formula: ((FLOOR(max([Date Numeric])/10000) - FLOOR(min([Date Numeric])/10000) )*12) + (floor((max([Date Numeric])-(10000*(FLOOR(max([Date Numeric])/10000))))/100) - floor((min([Date Numeric])-(10000*(FLOOR(min([Date Numeric])/10000))))/100)) ((FLOOR(max([Date Numeric])/10000) - FLOOR(min([Date Numeric])/10000) )*12) + (floor((max([Date Numeric])-(10000*(FLOOR(max([Date Numeric])/10000))))/100) - floor((min([Date Numeric])-(10000*(FLOOR(min([Date Numeric])/10000))))/100))1.6KViews1like1CommentCreating 'From-To' Date using BloX
Analytical Need Attached below is a BloX template. A common request is to have a 'From To' input at the top of the dashboard to affect a calendar range selection of dates. To achieve this need we can utilize BloX's form capabilities. Picking a date range in a calendar of a 'Date' filter and looking into the jaql created by it shows the following: We can use input fields to change the date range values selected in the from:to keys. Solution We will use the skeleton that creates an action that affects the selected filters: Make sure to change the "filterName" to the right filter column name. Also, make sure to use the right jaql that needs to be populated. Example: { "type": "ActionSet", "actions": [ { "type": "Filters", "title": "Submit", "data": { "filters": [ { "filterName": "Days in Date", "filterJaql": { "from": "", "to": "", "custom": true } } ] } } ] } Then, make sure to create the required Input field to pass on the input value and populate the from/to: { "type": "Input.Date", "id": "data.filters[0].filterJaql.from", "placeholder": "yyyy-mm-dd", "defaultValue": "", "style": { "width": "100%" }, "borderRadius": "4px", "borderStyle": "none", "backgroundColor": "#F4F4F8" } The "id" should hold the entire path of the key that needs to be populated with the value. If you prefer you can use this JSON template file for the full script: json script { "card": { "style": "", "script": "", "title": "", "showCarousel": true, "body": [ { "type": "Container", "width": "90%", "style": { "margin": "0 auto" }, "items": [ { "spacing": "large", "type": "TextBlock", "text": "From Date", "weight": "light", "color": "black" }, { "type": "Input.Date", "id": "data.filters[0].filterJaql.from", "placeholder": "yyyy-mm-dd", "defaultValue": "", "style": { "width": "100%" }, "borderRadius": "4px", "borderStyle": "none", "backgroundColor": "#F4F4F8" }, { "spacing": "medium", "type": "TextBlock", "text": "To Date", "color": "black" }, { "type": "Input.Date", "id": "data.filters[0].filterJaql.to", "placeholder": "yyyy-mm-dd", "defaultValue": "", "style": { "width": "100%" }, "borderRadius": "4px", "borderStyle": "none", "backgroundColor": "#F4F4F8" }, { "type": "ActionSet", "actions": [ { "type": "Filters", "title": "Submit", "data": { "filters": [ { "filterName": "Days in Date", "filterJaql": { "from": "", "to": "", "custom": true } } ] } } ] } ] } ] }, "config": { "fontFamily": "Open Sans", "fontSizes": { "default": 16, "small": 14, "medium": 22, "large": 32, "extraLarge": 50 }, "fontWeights": { "default": 400, "light": 100, "bold": 800 }, "containerStyles": { "default": { "backgroundColor": "#ffffff", "foregroundColors": { "default": { "normal": "#3A4356" }, "white": { "normal": "#ffffff" }, "grey": { "normal": "#9EA2AB" }, "orange": { "normal": "#f2B900" }, "yellow": { "normal": "#ffcb05" }, "black": { "normal": "#000000" }, "lightGreen": { "normal": "#93c0c0" }, "green": { "normal": "#2BCC7F" }, "red": { "normal": "#FA5656" }, "accent": { "normal": "#2E89FC" }, "good": { "normal": "#54a254" }, "warning": { "normal": "#e69500" }, "attention": { "normal": "#cc3300" } } } }, "imageSizes": { "default": 40, "small": 40, "medium": 80, "large": 120 }, "imageSet": { "imageSize": "medium", "maxImageHeight": 100 }, "actions": { "color": "", "backgroundColor": "", "maxActions": 5, "spacing": "large", "buttonSpacing": 20, "actionsOrientation": "horizontal", "actionAlignment": "right", "margin": "0", "showCard": { "actionMode": "inline", "inlineTopMargin": 16, "style": "default" } }, "spacing": { "default": 5, "small": 5, "medium": 10, "large": 20, "extraLarge": 40, "padding": 0 }, "separator": { "lineThickness": 1, "lineColor": "#D8D8D8" }, "factSet": { "title": { "size": "default", "color": "default", "weight": "bold", "warp": true }, "value": { "size": "default", "color": "default", "weight": "default", "warp": true }, "spacing": 20 }, "supportsInteractivity": true, "imageBaseUrl": "", "height": 197 } }3.1KViews1like4CommentsViews 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.txt (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: Remove the .txt extension before importing the dashboard (.dash) file. 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); }); }); });64Views1like1Comment