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
    Add-Ons
    • Question
      • Help and How-ToChevronRightIcon
                       
      Dashboard exports breaking layout when sent as PDF or Excel? Report Studio is built to fix that, without leaving Sisense
      Ido Darnell
      Ido Darnell
      Posted 4 days ago
      0
               
    • Discussion
      Ido Darnell
      • Sisense Partners
               
      Ido Darnell
      Pixel-perfect design and reporting for Sisense, a review of available options
                                                                                                                                               

      If you manage Sisense, you've had some version of this conversation: someone asks for a report that actually looks like a finished document, not a dashboard screenshot with the edges cut off. There are more ways to solve this than most admins realize, and each one solves a different piece of the problem. A quick rundown: Native PDF export - zero setup, but the layout is whatever the dashboard happens to produce. No control over page breaks, no way to add narrative text, and pivot tables cut off at the edge of the screen. Sisense Report Manager - solid scheduling and delivery, but it's still exporting the dashboard as-is. No design layer, no multi-dashboard composition. Jasper Reports - genuinely pixel-perfect for table and pivot widgets specifically, if you have developer resource to build and maintain JRXML templates. Doesn't touch charts or KPI widgets. SAP Crystal Reports via ODBC - full design control, but it pulls your data out of Sisense entirely to do it, which is a real consideration if data residency matters to you. Where this leaves most teams: something that handles scheduling, or something that handles pixel-perfect tabular data, but not both, and rarely anything that lets you design a report as its own document rather than a dashboard export. That's the gap QBeeQ Report Studio is built for: a canvas inside Sisense where you design the report as a report - pulling from multiple dashboards if you need to, with full control over layout, brand, and every widget on the page. Report Studio Deliver , our scheduled distribution layer, is coming in beta this month. For the full breakdown with a side-by-side comparison table, here's the complete write-up .

      1 month ago
      0
               
    • Blog banner
      • Add-ons & Plug-InsChevronRightIcon

      Remove Whiskers from Box and Whisker Widgets

                                               

      Download Link: removeWhiskersFromBoxWhiskerPlot.zip Overview This customization removes the whiskers from Box and Whisker Plot widgets by hiding both the horizontal whisker caps and the vertical whisker lines. This can help simplify the visualization when the focus is only on the quartile ranges and median values. The solution is available in two formats: A widget script version for applying the customization to individual widgets A plugin/add-on version for applying the customization to all Box and Whisker widgets globally across dashboards In addition to removing the whiskers, the script/plugin also: Renames the tooltip labels from "Min" and "Max" to user-defined labels such as "Lower Quartile" and "Upper Quartile" Reorders the tooltip values so the median appears between the quartiles instead of at the end This can be useful for: Simplifying Box and Whisker visualizations Aligning terminology with statistical or business standards Making quartile-based visualizations easier for business users to interpret Standardizing tooltip terminology across dashboards Implementation / Installation Instructions 1. Widget Script Version The script removes the whiskers from Box and Whisker widgets. whiskerWidth controls the horizontal line at the end of the whiskers lineWidth controls the vertical lines above and below the boxes Both attributes are set to 0 to completely remove the whiskers from view. The tooltip labels can also be customized by updating the first three variables in the script. Script // Define the new names for the boxes' "Min", "Max", and "Median" labels const newBoxMaxName = "Upper Quartile"; const newBoxMinName = "Lower Quartile"; const newMedianName = "Median"; // Remove whiskers and lines widget.on('processresult', function(se, ev) { // remove horizontal lines at the end of whiskers ev.result.series[0].whiskerWidth = "0"; // remove vertical lines above and below boxes ev.result.series[0].lineWidth = "0"; }); // Remove whiskers information from tooltip and rename boxes' "Min", "Max", and "Median" to user-defined labels widget.on("beforedatapointtooltip", function(w, args){ args.template = "<div class=\"boxplot-tooltip\"><div class=\"boxplot-tooltip-data\"><div class=\"boxplot-tooltip-value-name\"> " + newBoxMaxName + " </div><div class=\"boxplot-tooltip-value\" data-ng-style=\"{color: model.memberColor}\"> {{model.boxMax}} </div></div><div class=\"boxplot-tooltip-data\"><div class=\"boxplot-tooltip-value-name\"> " + newMedianName + " </div><div class=\"boxplot-tooltip-value\" data-ng-style=\"{color: model.memberColor}\"> {{model.boxMedian}} </div></div><div class=\"boxplot-tooltip-data\"><div class=\"boxplot-tooltip-value-name\"> " + newBoxMinName + " </div><div class=\"boxplot-tooltip-value\" data-ng-style=\"{color: model.memberColor}\"> {{model.boxMin}} </div></div><div class=\"highcharts-tooltip-line-separator\"></div></div><div class=\"boxplot-tooltip-category\"><div class=\"boxplot-tooltip-value-category\"> {{model.xAxis}} </div></div></div>"; }); Steps Copy the script above. Open the widget’s script editor from the widget menu. Paste the script into the editor. Modify the quartile/median labels if needed. Save the script. Refresh the widget. 2. Plugin / Add-on Version This version applies the customization globally instead of configuring each widget individually. Installation Steps Download and unzip the plugin zip file . Extract the files directly. Do not create an additional folder level; the plugin files are already organized correctly within the zip file. Upload the removeWhiskersFromBoxWhiskerPlot folder to the Sisense plugins directory ( /opt/sisense/storage/plugins ). This folder can be accessed from Admin tab -> App Configuration -> File Management -> plugins . Check the Add-ons page in the Admin tab under Server & Hardware and confirm the plugin is listed and enabled. To modify the quartile and median labels, open the config.js file inside the plugin folder and update the configuration values (see more details in the Configurations section below), then save the file. Wait approximately 30–60 seconds for the plugin service to restart. Configurations The following configuration values can be customized to control the labels displayed in the tooltip to any preferred naming convention. newBoxMaxName Controls the label displayed for the top edge of the box (labeled "Max" in the default tooltip). Example: "Upper Quartile" newBoxMinName Controls the label displayed for the bottom edge of the box (labeled "Min" in the default tooltip). Example: "Lower Quartile" newMedianName Controls the label displayed for the median value shown inside the box. Example: "Middle Quartile" 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.

      Tri Anthony
      Tri AnthonyPosted 2 months ago
      0
               
    • Blog banner
      • PySisenseChevronRightIcon

      Managing Sisense Plugin States with PySisense

                                                                                                       

      Managing Sisense Plugin States with PySisense Overview Enabling and disabling Sisense plugins through the admin interface becomes time consuming when maintaining consistent plugin configurations across environments, testing against a clean instance, or iterating on plugin development. This article describes plugin_manager_pysisense.py , a Python command-line tool that manages Sisense plugin states using the PySisense library and a configuration file. The script is also a working example of how PySisense can be used for Sisense automation. This tool covers the same functionality as plugin_manager.py , documented in Managing Sisense Plugin States via the REST API . The difference is that this version delegates all API request code to PySisense rather than using the requests library directly. The two scripts share the same config.yaml format and produce identical output. The script enables and disables plugins, isolates a single plugin for faster development builds, and saves and restores named snapshots of plugin state. It does not install, uninstall, or update plugins, and does not roll back plugin versions. About PySisense PySisense is introduced in PySisense: Programmable Sisense Environment Management on the Sisense Community as a Python SDK for the Sisense REST API. It provides wrappers around common Sisense operations, including plugin management, dashboard access, and user administration. It is installed via pip and does not need to be placed alongside the script. This script provides a practical example of PySisense applied to a real automation task. Using PySisense rather than writing directly against the REST API reduces the amount of code required and removes several categories of boilerplate. Authentication headers, HTTP session management, SSL configuration, pagination across large result sets, and request logging are all handled by the library. The consistent SisenseClient and module pattern also makes it straightforward to extend a script to cover other Sisense operations, such as users, dashboards, or data models, without reworking connection and authentication code. Scripts built on PySisense are shorter and easier to read than equivalent scripts built on requests directly, and the library is maintained and tested independently of the scripts that use it. Note: The current release of PySisense on PyPI does not yet include the Plugins module used in this script. This functionality is planned for the next PySisense release. The development branch of the PySisense repository already contains it and can be installed directly from there. This script uses the Plugins class from PySisense: get_all_plugins() returns the full plugin list as a list of dicts, handling the pagination in the underlying API automatically. enable_plugins(names, bulk=True) enables one or more plugins by name. When bulk=True , all changes are sent in a single PATCH request. disable_plugins(names, bulk=True) disables one or more plugins by name, with the same bulk option. A SisenseClient instance is constructed from the config values and passed to the Plugins class. All HTTP calls are then handled by PySisense. from pysisense import Plugins, SisenseClient client = SisenseClient(domain="https://your-sisense-instance.com", token="YOUR_TOKEN") plugins = Plugins(api_client=client) # List all installed plugins all_plugins = plugins.get_all_plugins() # Enable plugins by short name, full name, or folderName result = plugins.enable_plugins(["SwitchDimension", "plugin-CustomPlugin"], bulk=True) # Disable plugins result = plugins.disable_plugins(["UnneededPlugin"], bulk=True) Both enable_plugins and disable_plugins return a summary dict with changed , already_enabled or already_disabled , not_found , and errors keys. Features that require knowing the current plugin state before making changes, such as strict mode, enable-one, and dry-run previews, call get_all_plugins() once at the start of the command and compute the set of changes at the script level. Plugin name matching Plugin names in the configuration file and on the command line accept either the short form ( SwitchDimension ) or the full form ( plugin-SwitchDimension ). Matching is case-insensitive. PySisense normalizes names internally, so both forms work with enable_plugins() and disable_plugins() as well. Prerequisites Python 3.10 or later Network access to the Sisense instance (the tool runs remotely and does not need to be installed on the server) A Sisense API token: Admin > REST API > v1.0 > GET /authentication/tokens/api Setup Install the required Python packages: pip install pysisense pyyaml Create a config.yaml and fill in the Sisense instance URL and API token: sisense: url: https://your-sisense-instance.com token: YOUR_API_TOKEN_HERE Add the plugins to enable to the enable_plugins list. See the Configuration reference section for all available settings. The script is then ready to run. Configuration reference sisense: url: https://your-sisense-instance.com token: YOUR_API_TOKEN_HERE # Admin → REST API → v1.0 → GET /authentication/tokens/api bulk: true # set false to update plugins one at a time instead of a single batch request # Plugins to enable. # Accepts short names (SwitchDimension) or full names (plugin-SwitchDimension). # Matching is case-insensitive. # # Default behavior: enable listed plugins; leave every other plugin unchanged. # With --strict: also disable any currently-enabled plugin NOT in this list, # making the instance exactly match this config. enable_plugins: - examplePlugin # Plugins to explicitly disable (optional). # Only needed in the default sync (no --strict), where unlisted plugins are # left alone. Listing a plugin here ensures it stays off and documents the # intentional exclusion. # disable_plugins: # - anotherExamplePlugin enable_plugins : the list of plugins to enable. By default, plugins not on this list are left in their current state. With --strict , any currently enabled plugin not on this list will be disabled. disable_plugins : optional. Plugins to explicitly disable in the default sync without --strict . Useful for documenting intentional exclusions when strict mode is not being used. bulk : controls whether plugin state changes are sent as a single batch API request or one at a time. Set to false for individual requests, which is slower but can be useful for diagnosing issues with specific plugins. Usage Run the script with no arguments to enable plugins in enable_plugins , disable plugins in disable_plugins , and leave everything else unchanged: python plugin_manager_pysisense.py Before making any changes, the script saves a timestamped snapshot of the current plugin state. To restore the most recent snapshot: python plugin_manager_pysisense.py --restore-snapshot To preview what would change without making any API calls: python plugin_manager_pysisense.py --dry-run Common use cases Maintaining a known good plugin baseline Organizations that need to ensure consistent plugin configuration across upgrades, support sessions, or environment changes can maintain an enable_plugins list in the configuration file. Running the script applies the defined configuration to the instance. To also disable any currently enabled plugin not in the list: python plugin_manager_pysisense.py --strict Testing on a stock Sisense instance Plugin interference is a common source of issues that are difficult to reproduce. To quickly disable all plugins and test against a clean instance, clear enable_plugins in the configuration file and run with --strict . The current state is saved automatically before any changes are made, and can be restored when testing is complete: # with enable_plugins cleared in config.yaml python plugin_manager_pysisense.py --strict # restore when done python plugin_manager_pysisense.py --restore-snapshot Developing and testing a single plugin When many plugins are installed, Sisense rebuilds all of them whenever any plugin changes. Using --enable-one disables all currently enabled plugins and enables only the specified plugin, which can significantly reduce rebuild time on instances with many plugins: python plugin_manager_pysisense.py --enable-one MyPlugin Snapshots The script saves snapshots as YAML files to a snapshots/ folder alongside the script. Snapshots capture the full enabled and disabled plugin state at a point in time. Snapshots are saved automatically before any run that changes state. Named snapshots can also be saved and restored explicitly: # save the current state with a specific name python plugin_manager_pysisense.py --save-snapshot before-upgrade # list all saved snapshots python plugin_manager_pysisense.py --list-snapshots # restore a specific snapshot python plugin_manager_pysisense.py --restore-snapshot before-upgrade Including --log-version when saving a snapshot records the version of each plugin at that point in time. Snapshots saved by plugin_manager_pysisense.py and plugin_manager.py are compatible. Both scripts read from the same snapshots/ directory, and each handles the other's snapshot format. Full option reference usage: plugin_manager_pysisense.py [-h] [--config FILE] [--dry-run] [--no-bulk] [--skip-snapshot] [--log-version] [--strict] [--enable-only | --disable-only] [--enable-one PLUGIN | --save-snapshot [NAME] | --list-snapshots | --restore-snapshot [NAME]] Manage Sisense plugin states via PySisense. options: -h, --help show this help message and exit --config FILE Path to config.yaml (default: config.yaml next to this script) --dry-run Preview what would change; make no API calls and save no snapshot --no-bulk Send one API call per plugin instead of a single batch PATCH. Slower but easier to diagnose partial failures. Overrides sisense.bulk in config. --skip-snapshot Do not automatically save a snapshot before this run --log-version Record each plugin's version field (from the API response) in any snapshot saved during this run --strict Also disable every currently enabled plugin that is not listed in enable_plugins. Without this flag, only plugins in disable_plugins are disabled; everything else is left alone. Always active for --restore-snapshot. --enable-only Run only the enable step; skip all disables --disable-only Run only the disable step; skip all enables --enable-one PLUGIN Enable exactly one plugin by name and disable all currently enabled others. Accepts the short or full plugin name. --save-snapshot [NAME] Save the current plugin state as a named snapshot. Omit NAME for an auto generated timestamp filename. Combine with --log-version to also record versions. --list-snapshots List all saved snapshots with creation date and enabled plugin count --restore-snapshot [NAME] Restore plugin state to exactly match a saved snapshot. Omit NAME to use the most recently created snapshot. Always strict (disables plugins not in snapshot) unless --enable-only. Plugin names accept the short form (SwitchDimension) or the full form (plugin-SwitchDimension) in both CLI args and config.yaml. Default behavior: enable plugins in enable_plugins, disable plugins in disable_plugins, leave everything else unchanged. Use --strict to also disable any enabled plugin not in enable_plugins. A snapshot is saved automatically before each run that modifies state. Use --skip-snapshot to suppress this. Use Case Managing plugins via PySisense and a configuration file offers several advantages over manual administration: Applies plugin active list consistently and repeatably without manual steps Provides automatic rollback through snapshots saved before each change Supports quickly switching between plugin states for testing or development Reduces the time required to restore a known working plugin list after an issue is resolved Demonstrates how PySisense can be used to build automation scripts for Sisense instances Summary With this tool in place, administrators and developers can manage Sisense plugin states quickly and reliably from the command line. The configuration file serves as a single source of truth for the intended plugin set, and the snapshot system ensures that any change can be reversed. The script also serves as a concrete example of how PySisense can be used in automation scripts: constructing a SisenseClient from config values, passing it to a feature class, and using the returned result dicts to drive output and error handling. The code is not compressed or obfuscated and can be modified as needed or used as a reference for similar tools. j""" plugin_manager_pysisense.py — Manage Sisense plugin states via pySisense. Same feature set as plugin_manager.py but delegates all API calls to the pySisense library instead of calling the Sisense REST API directly. Configuration is read from config.yaml. This script is also a working example of how pySisense can be used for Sisense automation. Install pySisense with: pip install pysisense Overview Manages Sisense plugin states via pySisense. Treats plugin configuration as code: define which plugins should be enabled in config.yaml, run the script, and the instance matches, quickly, from anywhere with network access. Before applying any changes, the current plugin state is automatically captured in a timestamped snapshot. If a change needs to be reverted, a single --restore-snapshot command rolls the instance back to exactly how it was. By default only the plugins explicitly listed in config.yaml are enabled or disabled, everything else is left as is. Use --strict to also disable any enabled plugin not in the list, making the instance enabled plugin list exactly match the config. Common use cases Maintain a known good plugin baseline Keep enable_plugins in config.yaml up to date. Run the script after upgrades, support sessions, or any time the instance may have drifted from the expected state. Test against a stock (no plugins) Sisense instance Clear enable_plugins in config.yaml, run --strict to disable everything, test, then restore: python plugin_manager_pysisense.py --strict python plugin_manager_pysisense.py --restore-snapshot Develop and iterate on a single plugin quickly python plugin_manager_pysisense.py --enable-one MyPlugin Roll back any change instantly python plugin_manager_pysisense.py --list-snapshots python plugin_manager_pysisense.py --restore-snapshot before-upgrade Usage # Enable listed plugins, disable listed disable_plugins, leave rest unchanged python plugin_manager_pysisense.py # Strict: also disables any enabled plugin not in enable_plugins python plugin_manager_pysisense.py --strict # Preview what would change (no API calls, no auto snapshot) python plugin_manager_pysisense.py --dry-run python plugin_manager_pysisense.py --strict --dry-run # Run only one half of the sync python plugin_manager_pysisense.py --enable-only # enable listed; skip all disables python plugin_manager_pysisense.py --disable-only # disable listed; skip all enables # Enable exactly one plugin and disable every other currently enabled plugin python plugin_manager_pysisense.py --enable-one SwitchDimension # Snapshots python plugin_manager_pysisense.py --save-snapshot [NAME] python plugin_manager_pysisense.py --save-snapshot [NAME] --log-version python plugin_manager_pysisense.py --list-snapshots python plugin_manager_pysisense.py --restore-snapshot [NAME] # omit NAME for most recent # Other flags (combinable with any of the above) --no-bulk one API call per plugin instead of a single batch request --skip-snapshot do not save an auto-snapshot before this run --log-version record each plugin's version in any snapshot saved this run --config FILE use a config file other than the default config.yaml Plugin names All plugin names accept either the short form (SwitchDimension) or the full form (plugin-SwitchDimension), both in CLI arguments and in config.yaml. Matching is case insensitive. API token Admin → REST API → v1.0 → GET /authentication/tokens/api Setup pip install pysisense pyyaml # Python 3.10+ required cp config.yaml.example config.yaml # Edit config.yaml: set sisense.url, sisense.token, and enable_plugins list """ import sys from datetime import datetime, timezone from pathlib import Path import yaml from pysisense import Plugins, SisenseClient SCRIPT_DIR = Path(__file__).parent DEFAULT_CONFIG = SCRIPT_DIR / "config.yaml" SNAPSHOTS_DIR = SCRIPT_DIR / "snapshots" # Config def load_config(path: Path) -> dict: if not path.exists(): print(f"Config file not found: {path}") print("Copy config.yaml.example to config.yaml and fill in your instance URL and API token.") sys.exit(1) with path.open() as f: return yaml.safe_load(f) def build_client(sisense_cfg: dict) -> SisenseClient: url = sisense_cfg.get("url", "") token = sisense_cfg.get("token", "") if not url or not token: print("config.yaml must include sisense.url and sisense.token.") sys.exit(1) is_ssl = not url.startswith("http://") return SisenseClient(domain=url, token=token, is_ssl=is_ssl) # Name matching — mirrors pySisense internals, used for dry-run previews and # strict-mode computation (pySisense handles matching internally for real calls) def _normalize(name: str) -> str: lower = name.lower() return lower[len("plugin-"):] if lower.startswith("plugin-") else lower def _build_lookup(names: list) -> set: lookup: set = set() for n in names: norm = _normalize(n) lookup.add(norm) lookup.add("plugin-" + norm) return lookup def _matches_lookup(plugin: dict, lookup: set) -> bool: return bool({plugin["folderName"].lower(), plugin["name"].lower()} & lookup) # Snapshot file I/O def write_snapshot_file(name: str, snapshot: dict) -> Path: SNAPSHOTS_DIR.mkdir(exist_ok=True) path = SNAPSHOTS_DIR / f"{name}.yaml" with path.open("w") as f: yaml.dump(snapshot, f, sort_keys=False, default_flow_style=False, allow_unicode=True) return path def read_snapshot_file(name: str) -> dict: path = SNAPSHOTS_DIR / f"{name}.yaml" if not path.exists(): print(f"Snapshot not found: {path}") sys.exit(1) with path.open() as f: data = yaml.safe_load(f) # Translate 'enable_plugins' key (plugin_manager.py snapshot format) to 'plugins' if "enable_plugins" in data and "plugins" not in data: data["plugins"] = data["enable_plugins"] return data def list_snapshot_files() -> list: if not SNAPSHOTS_DIR.exists(): return [] return sorted(SNAPSHOTS_DIR.glob("*.yaml")) def latest_snapshot_name() -> str: files = list_snapshot_files() if not files: print("No snapshots found.") sys.exit(1) def created_at(path: Path) -> str: with path.open() as f: return yaml.safe_load(f).get("created", "") return max(files, key=created_at).stem def _auto_save_snapshot(all_plugins: list, include_version: bool) -> None: name = datetime.now().strftime("auto_%Y%m%d_%H%M%S") print(f"\nSaving snapshot '{name}' before applying changes...") try: enabled = sorted(p["folderName"] for p in all_plugins if p.get("isEnabled")) disabled = sorted(p["folderName"] for p in all_plugins if not p.get("isEnabled")) snapshot: dict = { "plugins": enabled, "created": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"), } if include_version: versions: dict = {} for p in all_plugins: v = p.get("version") or p.get("pluginVersion") if v: versions[p["folderName"]] = str(v) if versions: snapshot["plugin_versions"] = versions path = write_snapshot_file(name, snapshot) print(f" Saved to {path} ({len(enabled)} enabled / {len(disabled)} disabled)") except Exception as exc: print(f" Warning: auto snapshot failed: {exc}") # Sync computation — determines which plugins need their state changed. # Used for dry-run previews, strict mode, and enable-one. def _compute_updates( all_plugins: list, enable_names: list, disable_names: list, *, strict: bool, enable_only: bool, disable_only: bool, ) -> tuple: enable_lookup = _build_lookup(enable_names) disable_lookup = _build_lookup(disable_names) matched_enable = [p for p in all_plugins if _matches_lookup(p, enable_lookup)] enable_folder_set = {p["folderName"] for p in matched_enable} to_enable: list = [] if disable_only else sorted( p["folderName"] for p in matched_enable if not p.get("isEnabled") ) if enable_only: to_disable: list = [] elif strict: to_disable = sorted( p["folderName"] for p in all_plugins if p.get("isEnabled") and p["folderName"] not in enable_folder_set ) else: to_disable = sorted( p["folderName"] for p in all_plugins if p.get("isEnabled") and _matches_lookup(p, disable_lookup) ) matched_norms = ( {_normalize(p["folderName"]) for p in matched_enable} | {_normalize(p["name"]) for p in matched_enable} ) unmatched = sorted(n for n in enable_names if _normalize(n) not in matched_norms) return to_enable, to_disable, unmatched # Output helpers def _print_update_plan( to_enable: list, to_disable: list, already_correct: int, untouched: int = 0, ) -> None: print(f" To enable: {len(to_enable)}") print(f" To disable: {len(to_disable)}") if already_correct > 0: print(f" Already set: {already_correct}") if untouched > 0: print(f" Untouched: {untouched} (not listed in config; left as-is)") print() def _print_dry_run_preview(to_enable: list, to_disable: list) -> None: if not to_enable and not to_disable: print("Nothing to change.") if to_enable: print("Would enable:") for f in to_enable: print(f" + {f}") if to_disable: print("Would disable:") for f in to_disable: print(f" - {f}") print("\n[DRY RUN] No changes made.") def _report_unmatched(unmatched: list) -> None: if unmatched: print(f"\nNot found in instance ({len(unmatched)}):") for n in unmatched: print(f" - {n}") def _apply_enable(plugins_obj: Plugins, to_enable: list, bulk: bool) -> int: if not to_enable: return 0 result = plugins_obj.enable_plugins(to_enable, bulk=bulk) if "error" in result: print(f"Failed to enable plugins: {result['error']}") sys.exit(1) for f in result["changed"]: print(f" [ENABLED] {f}") return len(result.get("errors", [])) def _apply_disable(plugins_obj: Plugins, to_disable: list, bulk: bool) -> int: if not to_disable: return 0 result = plugins_obj.disable_plugins(to_disable, bulk=bulk) if "error" in result: print(f"Failed to disable plugins: {result['error']}") sys.exit(1) for f in result["changed"]: print(f" [DISABLED] {f}") return len(result.get("errors", [])) # Commands def cmd_sync_to_config( plugins_obj: Plugins, enable_names: list, disable_names: list, *, dry_run: bool, bulk: bool, strict: bool, enable_only: bool, disable_only: bool, skip_snapshot: bool, include_version: bool, ) -> None: print("Fetching plugin list...") all_plugins = plugins_obj.get_all_plugins() if all_plugins and "error" in all_plugins[0]: print(f"Failed to fetch plugins: {all_plugins[0]['error']}") sys.exit(1) print(f" {len(all_plugins)} plugins total\n") to_enable, to_disable, unmatched = _compute_updates( all_plugins, enable_names, disable_names, strict=strict, enable_only=enable_only, disable_only=disable_only, ) enable_lookup = _build_lookup(enable_names) disable_lookup = _build_lookup(disable_names) matched = [p for p in all_plugins if _matches_lookup(p, enable_lookup)] already_correct = sum(1 for p in matched if p.get("isEnabled")) untouched = 0 if strict or enable_only else sum( 1 for p in all_plugins if not _matches_lookup(p, enable_lookup) and not _matches_lookup(p, disable_lookup) ) if matched: print(f"Matched {len(matched)} plugin(s) from enable_plugins:\n") print(f" {'Folder name':<45} {'API name':<35} {'Enabled'}") print(f" {'-'*45} {'-'*35} {'-'*7}") for p in sorted(matched, key=lambda x: x["folderName"]): print(f" {p['folderName']:<45} {p['name']:<35} {p['isEnabled']}") print() _print_update_plan(to_enable, to_disable, already_correct, untouched) if dry_run: _print_dry_run_preview(to_enable, to_disable) _report_unmatched(unmatched) return if not skip_snapshot and (to_enable or to_disable): _auto_save_snapshot(all_plugins, include_version) errors = _apply_disable(plugins_obj, to_disable, bulk) + _apply_enable(plugins_obj, to_enable, bulk) if not to_enable and not to_disable: print("Nothing to change.") else: print(f"\nDone. Enabled: {len(to_enable)} Disabled: {len(to_disable)} Errors: {errors}") _report_unmatched(unmatched) def cmd_enable_one( plugins_obj: Plugins, plugin_name: str, *, dry_run: bool, bulk: bool, skip_snapshot: bool, include_version: bool, ) -> None: target_lookup = _build_lookup([plugin_name]) print("Fetching plugin list...") all_plugins = plugins_obj.get_all_plugins() if all_plugins and "error" in all_plugins[0]: print(f"Failed to fetch plugins: {all_plugins[0]['error']}") sys.exit(1) print(f" {len(all_plugins)} plugins total\n") target = next((p for p in all_plugins if _matches_lookup(p, target_lookup)), None) if target is None: print(f"Plugin not found: {plugin_name!r}") print("Tip: check spelling against the Sisense admin panel, or run --save-snapshot to capture current names.") sys.exit(1) target_folder = target["folderName"] target_on = target.get("isEnabled", False) to_enable = [] if target_on else [target_folder] to_disable = sorted( p["folderName"] for p in all_plugins if p.get("isEnabled") and p["folderName"] != target_folder ) print(f"Target plugin: {target_folder}") print(f" Currently: {'enabled' if target_on else 'disabled'}\n") _print_update_plan(to_enable, to_disable, already_correct=1 if target_on else 0) if dry_run: _print_dry_run_preview(to_enable, to_disable) return if not skip_snapshot and (to_enable or to_disable): _auto_save_snapshot(all_plugins, include_version) errors = _apply_disable(plugins_obj, to_disable, bulk) + _apply_enable(plugins_obj, to_enable, bulk) if not to_enable and not to_disable: print("Nothing to change.") else: print(f"\nDone. Enabled: {len(to_enable)} Disabled: {len(to_disable)} Errors: {errors}") def cmd_save_named_snapshot(plugins_obj: Plugins, name: str, include_version: bool) -> None: print("Fetching plugin list...") all_plugins = plugins_obj.get_all_plugins() if all_plugins and "error" in all_plugins[0]: print(f"Failed to fetch plugins: {all_plugins[0]['error']}") sys.exit(1) print(f" {len(all_plugins)} plugins total\n") enabled = sorted(p["folderName"] for p in all_plugins if p.get("isEnabled")) disabled = sorted(p["folderName"] for p in all_plugins if not p.get("isEnabled")) snapshot: dict = { "plugins": enabled, "created": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"), } if include_version: versions: dict = {} for p in all_plugins: v = p.get("version") or p.get("pluginVersion") if v: versions[p["folderName"]] = str(v) if versions: snapshot["plugin_versions"] = versions path = write_snapshot_file(name, snapshot) print(f"Snapshot '{name}' saved to {path}") print(f" {len(enabled)} enabled / {len(disabled)} disabled plugin(s) captured.") if include_version and snapshot.get("plugin_versions"): print(f" Versions recorded for {len(snapshot['plugin_versions'])} plugin(s).") def cmd_list_snapshots() -> None: files = list_snapshot_files() if not files: print("No snapshots found.") return print(f"Snapshots ({len(files)}):\n") print(f" {'Name':<32} {'Created':<22} {'Enabled':<9} {'Versions'}") print(f" {'-'*32} {'-'*22} {'-'*9} {'-'*8}") for path in files: with path.open() as f: data = yaml.safe_load(f) enabled_count = len(data.get("plugins") or data.get("enable_plugins", [])) versions_logged = "yes" if data.get("plugin_versions") else "no" print( f" {path.stem:<32} " f"{data.get('created', 'unknown'):<22} " f"{enabled_count:<9} " f"{versions_logged}" ) def cmd_restore_snapshot( plugins_obj: Plugins, name: str, *, dry_run: bool, bulk: bool, enable_only: bool, disable_only: bool, skip_snapshot: bool, include_version: bool, ) -> None: snapshot = read_snapshot_file(name) snapshot_plugins = set(snapshot.get("plugins", [])) print(f"Snapshot '{name}' ({snapshot.get('created', 'unknown')})") print(f" {len(snapshot_plugins)} plugin(s) enabled in snapshot") if snapshot.get("plugin_versions"): print(f" Plugin versions recorded: {len(snapshot['plugin_versions'])}") print() print("Fetching plugin list...") all_plugins = plugins_obj.get_all_plugins() if all_plugins and "error" in all_plugins[0]: print(f"Failed to fetch plugins: {all_plugins[0]['error']}") sys.exit(1) print(f" {len(all_plugins)} plugins total\n") by_folder = {p["folderName"]: p for p in all_plugins} to_enable: list = [] if disable_only else sorted( f for f in snapshot_plugins if f in by_folder and not by_folder[f].get("isEnabled") ) to_disable: list = [] if enable_only else sorted( p["folderName"] for p in all_plugins if p.get("isEnabled") and p["folderName"] not in snapshot_plugins ) not_in_instance = sorted(f for f in snapshot_plugins if f not in by_folder) already_correct = max(0, len(snapshot_plugins) - len(to_enable) - len(not_in_instance)) _print_update_plan(to_enable, to_disable, already_correct) if dry_run: _print_dry_run_preview(to_enable, to_disable) if not_in_instance: print(f"\nNot found in instance ({len(not_in_instance)}):") for f in not_in_instance: print(f" - {f}") return if not skip_snapshot and (to_enable or to_disable): _auto_save_snapshot(all_plugins, include_version) errors = _apply_disable(plugins_obj, to_disable, bulk) + _apply_enable(plugins_obj, to_enable, bulk) if not to_enable and not to_disable: print("Nothing to change.") else: print(f"\nDone. Enabled: {len(to_enable)} Disabled: {len(to_disable)} Errors: {errors}") if not_in_instance: print(f"\nNot found in instance ({len(not_in_instance)}):") for f in not_in_instance: print(f" - {f}") # Main def main() -> None: import argparse parser = argparse.ArgumentParser( prog="plugin_manager_pysisense.py", description="Manage Sisense plugin states via pySisense.", formatter_class=argparse.RawDescriptionHelpFormatter, epilog=( "Plugin names accept the short form (SwitchDimension) or the full\n" "form (plugin-SwitchDimension) in both CLI args and config.yaml.\n" "\n" "Default behavior: enable plugins in enable_plugins, disable plugins\n" "in disable_plugins, leave everything else unchanged.\n" "Use --strict to also disable any enabled plugin not in enable_plugins.\n" "\n" "A snapshot is saved automatically before each run that\n" "modifies state. Use --skip-snapshot to suppress this." ), ) parser.add_argument( "--config", default=str(DEFAULT_CONFIG), metavar="FILE", help="Path to config.yaml (default: config.yaml next to this script)", ) parser.add_argument( "--dry-run", action="store_true", help="Preview what would change; make no API calls and save no snapshot", ) parser.add_argument( "--no-bulk", action="store_true", help=( "Send one API call per plugin instead of a single batch PATCH. " "Slower but easier to diagnose partial failures. " "Overrides sisense.bulk in config." ), ) parser.add_argument( "--skip-snapshot", action="store_true", help="Do not automatically save a snapshot before this run", ) parser.add_argument( "--log-version", action="store_true", help=( "Record each plugin's version field (from the API response) in any " "snapshot saved during this run (manual --save-snapshot or auto saved snapshot)" ), ) parser.add_argument( "--strict", action="store_true", help=( "Also disable every currently enabled plugin that is not listed in " "enable_plugins. Without this flag, only plugins in disable_plugins are " "disabled; everything else is left alone. " "Always active for --restore-snapshot." ), ) direction_group = parser.add_mutually_exclusive_group() direction_group.add_argument( "--enable-only", action="store_true", help="Run only the enable step; skip all disables", ) direction_group.add_argument( "--disable-only", action="store_true", help="Run only the disable step; skip all enables", ) mode_group = parser.add_mutually_exclusive_group() mode_group.add_argument( "--enable-one", metavar="PLUGIN", help=( "Enable exactly one plugin by name and disable all currently enabled " "others. Accepts the short or full plugin name." ), ) mode_group.add_argument( "--save-snapshot", metavar="NAME", nargs="?", const="__auto__", help=( "Save the current plugin state as a named snapshot. " "Omit NAME for an auto generated timestamp filename. " "Combine with --log-version to also record versions for each plugin." ), ) mode_group.add_argument( "--list-snapshots", action="store_true", help="List all saved snapshots with creation date and enabled plugin count", ) mode_group.add_argument( "--restore-snapshot", metavar="NAME", nargs="?", const="__latest__", help=( "Restore plugin state to exactly match a saved snapshot. " "Omit NAME to use the most recently created snapshot. " "Always strict (disables plugins not in snapshot) unless --enable-only." ), ) args = parser.parse_args() if args.list_snapshots: cmd_list_snapshots() return cfg = load_config(Path(args.config)) sisense_cfg = cfg.get("sisense", {}) bulk = sisense_cfg.get("bulk", True) and not args.no_bulk client = build_client(sisense_cfg) plugins_obj = Plugins(api_client=client) if args.enable_one: cmd_enable_one( plugins_obj, args.enable_one, dry_run=args.dry_run, bulk=bulk, skip_snapshot=args.skip_snapshot, include_version=args.log_version, ) elif args.save_snapshot is not None: name = ( datetime.now().strftime("snapshot_%Y%m%d_%H%M%S") if args.save_snapshot == "__auto__" else args.save_snapshot ) cmd_save_named_snapshot(plugins_obj, name, args.log_version) elif args.restore_snapshot is not None: name = ( latest_snapshot_name() if args.restore_snapshot == "__latest__" else args.restore_snapshot ) cmd_restore_snapshot( plugins_obj, name, dry_run=args.dry_run, bulk=bulk, enable_only=args.enable_only, disable_only=args.disable_only, skip_snapshot=args.skip_snapshot, include_version=args.log_version, ) else: enable_names = cfg.get("enable_plugins") or cfg.get("plugins", []) disable_names = cfg.get("disable_plugins", []) if not enable_names and not disable_names: print("config.yaml must include at least one entry in 'enable_plugins'.") sys.exit(1) cmd_sync_to_config( plugins_obj, enable_names, disable_names, dry_run=args.dry_run, bulk=bulk, strict=args.strict, enable_only=args.enable_only, disable_only=args.disable_only, skip_snapshot=args.skip_snapshot, include_version=args.log_version, ) if __name__ == "__main__": main() 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 • Last reply 2 months ago
      1
               
    • Idea
      • Help and How-ToChevronRightIcon
      Are your end users losing filter context every time they switch dashboards?
                                                                                       
      Mia Isaacson
      Mia IsaacsonPosted 2 months ago
               
      0
               
    • Idea
      • Help and How-ToChevronRightIcon
      Your Sisense charts are clean, but are your tooltips telling the whole story?
                                                                                               
      Mia Isaacson
      Mia IsaacsonPosted 2 months ago
               
      0
               
    • Blog banner
      • Add-ons & Plug-InsChevronRightIcon

      Plugin - Custom Coloring of Bar and Column Chart Type Widgets (barChartCustomColor)

                                                       

      Plugin - Custom Coloring of Bar and Column chart-type widgets This plugin  modifies the color scheme of bar and column chart-type widgets to match the current color palette of the dashboard. Installation To install this plugin, download and unzip the attachment. Then drop the barChartCustomColor folder into your plugins folder (/opt/sisense/storage/plugins). Enable the plugin on the Add-ons tab in the Admin section, wait for the plugin to build, and the plugin will be enabled. The plugin API can also be used, as well as the file management UI . Notes When a bar or column chart type Sisense widget is very straightforward and focused and does not have multiple values, breaks by's, categories, or conditional coloring, it can sometimes appear relatively mono-color but still be the most effective way to quickly visually communicate important data.   While Sisense includes numerous powerful and varied methods and options to change widget styling  and includes functionality to set bar color to vary based on the bar value , it sometimes may be desired to use the dashboard palette to vary the bar colors independently of bar value and guarantee a colorful widget regardless of the data and current filter state of the current dashboard and widget. This plugin allows the current dashboard palette to be used in this manner in a simple and quick matter and allows this to be applied quickly to an entire dashboard at once. Linking the widget coloring to the dashboard palette allows this color scheme to be quickly changed in the native UI without modifying individual widgets. This plugin also allows using a color dictionary config option to modify the coloring of a value in all widgets this plugin is enabled in.       This plugin  uses the processresult widget event to modify the color parameter of each bar or column in a series.   The dashboard palette is retrieved using the getPalette() function within the dashboard style parameter, which returns the current dashboard palette. This can be used in other plugins and scripts.     dashboard.style.getPalette()       This plugin is enabled via dashboard or widget scripts that set the widget parameter changeColor to true for a widget.   For a widget script, this is as simple as adding this one line to the script:     widget.changeColor = true;       Adding this three-line dashboard script will set this parameter to true for all widgets in the dashboard:     dashboard.on('widgetinitialized', function (_, dashObj) { dashObj.widget.changeColor = true; });     This can be modified with custom logic as needed, for example, based on widget title or ordering in the dashboard. If this option is enabled for a type of widget this plugin does not apply to, it will simply be ignored by the plugin. This plugin ignores widgets with  Break By's or Date Categories, or widgets that are not bar or column chart-type widgets. The color scheme is based on the current dashboard palette.    Changing the dashboard palette using the standard Sisense palette UI will result in a matching change of all widgets in the dashboard where this plugin is enabled. The config file of this plugin includes an option called colorDictionary, this can be used to override the standard palette color for all instances this value appears as the bar category, for all widgets this plugin is enabled for, regardless of the dashboard palette. Any standard HTML color naming format may be used in the dictionary config value. For example, if the colorDictionary is set to:     colorDictionary : { 'Bikes': '#AA6C39', 'ABC' : 'yellow' }      Then the ABC bar or column will be yellow in the widget, regardless of the current widget palette or positioning. This will apply to all widgets where this plugin is active. This plugin can be used as a basic template for your own Sisense plugins that run on a specific widget or dashboard event, that can be enabled or disabled via widget script, and that includes a configuration file to modify settings without modifying code files.        How did the plugin work for you? What other type of plugin are you looking to learn more about? Let me know in the comments!

      Jeremy Friedel
      Jeremy FriedelPosted 2 years ago • Last reply 3 months ago
      1
               
    • Blog banner
      • Widget & Dashboard ScriptsChevronRightIcon

      Programmatically Formatting Bar Chart Widget Value Labels in Sisense

                                                                                               

      Programmatically Formatting Bar Chart Widget Value Labels in Sisense This article outlines ways to programmatically format Sisense Bar Chart Widget Value labels via widget scripts , covering methods to prevent label overlap and apply consistent styling across all labels. Custom Styling for Data Labels The script below enables the formatting of Chart Widget Value labels by setting a custom background color, padding, and border-radius. Ensure the default data label UI option is disabled. Other CSS and Highcharts settings can be added as needed.         widget.on('render', function (se, ev) { ev.widget.queryResult.plotOptions.bar.dataLabels = { backgroundColor: '#f5d142', color: 'white', padding: 5, borderRadius: 5, enabled: true } })       Preventing Label Overlap The script below manually adjusts value label positioning to prevent overlap in densely populated bar chart widgets. The exact formulas for label positioning can be changed as needed.         widget.on('domready', function (se, ev) { var barWidth = $('.highcharts-series-group .highcharts-series rect', element).width(); $('.highcharts-data-labels .highcharts-label', element).each(function () { var labelWidth = $(this).find('rect').width(); var labelHeight = $(this).find('rect').height(); $(this).find('rect').attr('x', ($(this).find('rect').attr('x') + 2)); $(this).find('rect').attr('height', barWidth); $(this).find('rect').attr('y', ((labelHeight - barWidth) / 2)); }) })         Dynamically Increase Space for Labels If bar value labels overlap with the chart bars, you can dynamically adjust the maximum value on the y-axis to create additional space. A different formula, or a hard-coded value, can also be used as the y-axis maximum value.         widget.on('processresult', function (se, ev) { var maxValue = 0; var increasePercent = 0.2; ev.result.series.forEach(function (series) { series.data.forEach(function (dataItem) { if (dataItem.y > maxValue) maxValue = dataItem.y; }) ev.result.yAxis[0].max = maxValue + (increasePercent * maxValue); }) })       Conclusion These scripts enable customizing dynamically formatted and well-positioned data labels in your Sisense charts, enhancing readability and aesthetics beyond the default Sisense data bar data labels in bar chart widgets. For further discussion of these types of scripts, see the  Dynamically Formatted Data Labels article Example Of Custom Labels Added via Scripting   Y-Axis Maximum Set To a Very Large Value Check out this related content:  Academy Documentation

      Jeremy Friedel
      Jeremy FriedelPosted 1 year ago • Last reply 3 months ago
      1
               
    • 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
      • News & UpdatesChevronRightIcon

      Every Metric Happens Somewhere: Why the “Where” Dimension Is the Missing Layer in Modern Analytics

                                               

      In our recent QBeeQ webinar, Every Metric Happens Somewhere, we explored why geospatial context is no longer a niche feature for specialized industries. It’s a core analytical dimension and one that can dramatically elevate engagement, insight discovery, and decision-making. You can view the recording , or keep scrolling for the highlights and key takeaways from the webinar. The Problem: Dashboards Without Geography Miss Patterns Traditional dashboards rely heavily on tables, bar charts, and line graphs. They show totals, trends, and rankings well. But they struggle to reveal spatial relationships. Consider common business questions: How many claims do we have? What’s our revenue by region? Which territories are underperforming? These are valuable questions, but they’re incomplete without geographic context. When you introduce the where dimension, new insights emerge: Clusters that only appear spatially Boundary effects between adjacent territories Pockets of unusually high or low performance Regional anomalies masked in aggregated totals From Niche Feature to Core Capability Maps were once considered specialized and useful for specific industries such as logistics, real estate, or field operations. That’s no longer true. Revenue, risk, compliance, performance, claims, customer distribution – these all happen somewhere. Geography cuts across industries. Approximately 80% of enterprise data already contains a spatial component. Most organizations simply aren’t leveraging it. Instead of placing a map as a static, standalone widget, leading teams are making it central to the analytical experience. Research shows interactive dashboards increase engagement and insight discovery. When you layer interactivity (zooming, filtering, panning) on top of geography, users uncover patterns faster and spend more time exploring. But friction often gets in the way. The Friction: Where Native Mapping Falls Short Many teams start with basic mapping tools that allow simple point plotting or polygon mapping. That works until analysis shifts to become more strategic. Common limitations appear quickly: Needing both points and polygons on the same map Wanting density visualizations or clustering Requiring multiple layers Enabling advanced drill behavior Supporting large-scale or time-based data When mapping tools can’t support these needs, one of three things happens: Maps sit unused on dashboards. Designers revert to traditional charts. Spatial thinking never becomes core to analysis.  Remove frictions with the QBeeQ mapping solutions SuperMap: Practical Spatial Analytics for Everyday Use The SuperMap is built for operational decision-making, understanding what’s happening right now, and acting on it. This plugin includes a range of flexible, scalable features, delivered as a zero-code solution, designed for both dashboard builders and end users. Multi-Layer Mapping (Points + Polygons) - Overlay geographic territories such as counties or states with individual location points, allowing each layer to have its own KPIs, category breakdowns, and sizing logic for richer comparative analysis. Heatmaps and Radius-Based Points - Color polygons by one performance metric while simultaneously sizing map points by another, delivering multi-dimensional insight within a single, unified view. Clustering with Drill-In Behavior - As users zoom in, clustered points automatically separate to reveal individual locations, with clusters capable of displaying proportional category breakdowns such as hospitals, police stations, and post offices. Advanced Tooltips - Enhance map interactivity with tooltips that go beyond a single value by displaying multiple KPIs, raw metrics, and calculated insights to support deeper exploration. Jump-to-Dashboard Navigation - Enable users to click on a territory to instantly filter the entire dashboard or navigate directly to a more detailed analytical view for focused investigation. Measure Switching - Allow users to toggle between different KPIs on the same map without duplicating visuals, reducing dashboard clutter while increasing analytical flexibility. Geographic Hierarchy - Support seamless geographic drill-down across states, counties, zip codes, and other levels—either directly within the map interface or through a structured dropdown selection. Deck.gl Map: High-Performance, Large-Scale Exploration While the Super Map focuses on operational clarity, Deck.gl is designed for scale and trends over time. For teams working with event streams, logistics data, or large geospatial datasets, Deck.gl provides performance without sacrificing interactivity. This plugin enables: Arc layers Hex bin density visualizations 2D and 3D polygon layers High-volume point plotting Hierarchical spatial analysis The Bigger Picture: Making “Where” a First-Class Dimension Time has long been treated as a foundational analytical dimension. Geography deserves the same status. When you: Align metrics to business geography Enable exploration instead of passive observation Remove friction from advanced spatial analysis Combine interactivity with spatial context You don’t just make dashboards prettier. You make them more useful. Every metric happens somewhere.  When you show that somewhere clearly, insights accelerate and decisions improve. At QBeeQ , we believe geography should be a first-class dimension in every analytical experience. Our mapping solutions are designed to remove friction, scale with your data, and make spatial insight accessible to every dashboard builder and decision-maker. Because when every metric happens somewhere, QBeeQ helps you see exactly where it matters most. QBeeQ is data consulting firm and also a Sisense Gold Implementation Partner. 

      Mia Isaacson
      Mia IsaacsonPosted 4 months ago
      0
               
    …