├── .eslintignore ├── .eslintrc.js ├── .gitattributes ├── .github ├── FUNDING.yml └── workflows │ └── ci.yml ├── .gitignore ├── .npmrc ├── .prettierrc ├── .syncpackrc ├── CHANGELOG.md ├── LICENSE ├── README.md ├── assets ├── error_indicator_dark.svg ├── error_indicator_light.svg ├── icon.png ├── icon_alpha.png ├── icon_beta.png ├── icon_standalone.png ├── logo.svg ├── store_icon.png ├── store_image_01.png ├── store_image_02.png ├── store_image_03.png ├── store_image_04.png ├── store_image_05.png ├── warning_indicator_dark.svg └── warning_indicator_light.svg ├── bin ├── package-custom.ts └── validate-config-for-commit.ts ├── capabilities.json ├── config ├── features.json ├── index.ts └── package-custom.json ├── doc └── DEVELOPMENT.md ├── package-lock.json ├── package.json ├── packages ├── core-dependencies │ ├── .eslintrc.cjs │ ├── README.md │ ├── package.json │ ├── src │ │ ├── constants │ │ │ ├── dataset.ts │ │ │ ├── debug-area.ts │ │ │ ├── index.ts │ │ │ ├── integration-powerbi.ts │ │ │ ├── json-processing.ts │ │ │ ├── template-usermeta-schema.ts │ │ │ ├── utils-base64.ts │ │ │ └── vega-extensibility.ts │ │ ├── dataset │ │ │ ├── fields.ts │ │ │ └── index.ts │ │ ├── definitions │ │ │ ├── dataset.ts │ │ │ ├── index.ts │ │ │ ├── integration-powerbi.ts │ │ │ ├── json-processing.ts │ │ │ ├── store.ts │ │ │ ├── template-usermeta-schema.ts │ │ │ ├── template.ts │ │ │ ├── utils-base64.ts │ │ │ ├── vega-integration.ts │ │ │ ├── worker-dataset-viewer.ts │ │ │ └── worker-spec-json-processing.ts │ │ ├── index.ts │ │ ├── json │ │ │ ├── __tests__ │ │ │ │ └── json.test.ts │ │ │ ├── index.ts │ │ │ └── processing.ts │ │ └── utils │ │ │ ├── __tests__ │ │ │ ├── base64.test.ts │ │ │ ├── crypto.test.ts │ │ │ ├── object.test.ts │ │ │ ├── string.test..ts │ │ │ └── type-guards.test.ts │ │ │ ├── base64.ts │ │ │ ├── crypto.ts │ │ │ ├── index.ts │ │ │ ├── object.ts │ │ │ ├── string.ts │ │ │ ├── type-conversion.ts │ │ │ └── type-guards.ts │ ├── tsconfig.eslint.json │ ├── tsconfig.json │ └── turbo.json ├── eslint-config │ ├── README.md │ ├── library.js │ ├── next.js │ ├── package.json │ └── react-internal.js ├── integration-powerbi │ ├── .eslintrc.cjs │ ├── README.md │ ├── package.json │ ├── src │ │ ├── __test__ │ │ │ ├── cross-highlight.test.ts │ │ │ └── field-tracking.test.ts │ │ ├── cross-highlight.ts │ │ ├── field-tracking.ts │ │ ├── index.ts │ │ ├── number-formatting.ts │ │ ├── properties │ │ │ ├── index.ts │ │ │ ├── settings-data-limit.ts │ │ │ ├── settings-developer.ts │ │ │ ├── settings-display.ts │ │ │ ├── settings-editor.ts │ │ │ ├── settings-general.ts │ │ │ ├── settings-state-management.ts │ │ │ ├── settings-theme.ts │ │ │ ├── settings-vega.ts │ │ │ ├── visual-formatting-settings-model.ts │ │ │ └── visual-formatting-settings-service.ts │ │ └── signals.ts │ ├── tsconfig.eslint.json │ ├── tsconfig.json │ └── turbo.json ├── jest-presets │ ├── jest │ │ └── node │ │ │ └── jest-preset.js │ └── package.json ├── json-processing │ ├── .eslintrc.js │ ├── README.md │ ├── package.json │ ├── src │ │ ├── __test__ │ │ │ ├── field-tracking.test.ts │ │ │ ├── processing.test.ts │ │ │ ├── template-dataset.test.ts │ │ │ ├── template-usermeta.test.ts │ │ │ └── validation.test.ts │ │ ├── field-tracking.ts │ │ ├── index.ts │ │ ├── processing.ts │ │ ├── template-dataset.ts │ │ ├── template-usermeta.ts │ │ └── validation.ts │ ├── tsconfig.eslint.json │ ├── tsconfig.json │ └── turbo.json ├── monaco-custom │ ├── .eslintrc.js │ ├── README.md │ ├── package.json │ ├── src │ │ ├── index.ts │ │ └── json.worker.ts │ ├── tsconfig.eslint.json │ ├── tsconfig.json │ └── turbo.json ├── template-usermeta-schema │ ├── .eslintrc.js │ ├── README.md │ ├── bin │ │ └── turbowatch.ts │ ├── package.json │ ├── src │ │ └── schema.ts │ ├── tsconfig.eslint.json │ ├── tsconfig.json │ └── turbo.json ├── typescript-config │ ├── base.json │ ├── deneb-pbi.json │ ├── package.json │ └── worker.json ├── worker-common │ ├── .eslintrc.js │ ├── README.md │ ├── package.json │ ├── src │ │ └── index.ts │ ├── tsconfig.eslint.json │ ├── tsconfig.json │ ├── tsup.config.ts │ └── turbo.json ├── worker-dataset-viewer │ ├── .eslintrc.js │ ├── README.md │ ├── package.json │ ├── src │ │ └── index.ts │ ├── tsconfig.eslint.json │ ├── tsconfig.json │ └── turbo.json └── worker-spec-json-processing │ ├── .eslintrc.js │ ├── README.md │ ├── package.json │ ├── src │ ├── __test__ │ │ ├── field-tracking.test.ts │ │ ├── remapping.test.ts │ │ └── tokenizer.test.ts │ ├── field-tracking.ts │ ├── index.ts │ ├── remapping.ts │ └── tokenizer.ts │ ├── tsconfig.eslint.json │ ├── tsconfig.json │ └── turbo.json ├── pbiviz.json ├── src ├── Deneb.ts ├── components │ └── App.tsx ├── constants │ ├── dataset.ts │ ├── editor-layout.ts │ ├── index.ts │ ├── interactivity.ts │ ├── interface-advanced-editor.ts │ ├── interface-debug.ts │ ├── table-view.ts │ ├── toaster.ts │ ├── vega-extensibility.ts │ └── vega-output.ts ├── core │ ├── data │ │ ├── dataView.ts │ │ ├── dataset.ts │ │ ├── fields.ts │ │ ├── index.ts │ │ └── values.ts │ ├── ui │ │ ├── advancedEditor.ts │ │ ├── commands.ts │ │ ├── dom.ts │ │ └── index.ts │ ├── utils │ │ ├── properties.ts │ │ └── versioning.ts │ └── vega │ │ └── index.ts ├── features │ ├── commands │ │ ├── index.ts │ │ └── types.ts │ ├── dataset │ │ ├── drilldown.ts │ │ ├── formatting.ts │ │ └── index.ts │ ├── debug-area │ │ ├── components │ │ │ ├── data-table-cell.tsx │ │ │ ├── data-table-navigation-button.tsx │ │ │ ├── data-table-status-bar.tsx │ │ │ ├── data-table-viewer.tsx │ │ │ ├── dataset-viewer-options.tsx │ │ │ ├── dataset-viewer.tsx │ │ │ ├── debug-area-content.tsx │ │ │ ├── log-level-dropdown.tsx │ │ │ ├── log-viewer.tsx │ │ │ ├── no-data-message.tsx │ │ │ ├── processing-data-message.tsx │ │ │ ├── signal-value.tsx │ │ │ └── signal-viewer.tsx │ │ ├── data-table.ts │ │ ├── index.ts │ │ ├── logging.ts │ │ └── types.ts │ ├── i18n │ │ ├── index.ts │ │ └── types.ts │ ├── interactivity │ │ ├── context-menu.ts │ │ ├── cross-filter.ts │ │ ├── cross-highlight.ts │ │ ├── data-point.ts │ │ ├── index.ts │ │ ├── tooltip.ts │ │ └── types.ts │ ├── interface │ │ ├── components │ │ │ ├── advanced-editor-interface.tsx │ │ │ ├── advanced-editor.tsx │ │ │ ├── capped-text-field.tsx │ │ │ ├── hyperlink.tsx │ │ │ ├── portal-root.tsx │ │ │ ├── report-view-router.tsx │ │ │ ├── status-bar-container.tsx │ │ │ ├── tooltip-custom-mount.tsx │ │ │ ├── visual-interface.tsx │ │ │ └── visual-update-history-overlay.tsx │ │ ├── index.ts │ │ ├── theme.ts │ │ └── types.ts │ ├── json-editor │ │ ├── components │ │ │ ├── editor-operation-container.tsx │ │ │ ├── editor-operation-content.tsx │ │ │ ├── editor-pane-collapsed.tsx │ │ │ ├── editor-pane-expanded.tsx │ │ │ ├── editor-pane.tsx │ │ │ ├── index.tsx │ │ │ ├── json-editor-context-provider.tsx │ │ │ ├── json-editor-status-bar.tsx │ │ │ ├── json-editor.tsx │ │ │ ├── provider-detail.tsx │ │ │ └── tracking-sync-status.tsx │ │ ├── index.ts │ │ └── types.ts │ ├── json-processing │ │ ├── formatting.ts │ │ └── index.ts │ ├── logging │ │ ├── index.ts │ │ └── vega.ts │ ├── modal-dialog │ │ ├── components │ │ │ ├── index.ts │ │ │ ├── modal-dialog.tsx │ │ │ ├── stage-progress-indicator.tsx │ │ │ └── version-change-content.tsx │ │ ├── index.ts │ │ ├── types.ts │ │ └── utils.ts │ ├── preview-area │ │ ├── components │ │ │ ├── fourd3d3d.tsx │ │ │ ├── preview-area.tsx │ │ │ └── visual-preview.tsx │ │ └── index.ts │ ├── remap-fields │ │ ├── components │ │ │ ├── field-remap-pane-progress.tsx │ │ │ ├── field-remap-pane.tsx │ │ │ └── remap-button.tsx │ │ └── index.ts │ ├── settings │ │ ├── components │ │ │ ├── cross-filter-max-data-points.tsx │ │ │ ├── cross-filter-mode-settings.tsx │ │ │ ├── index.ts │ │ │ ├── interactivity-checkbox.tsx │ │ │ ├── interactivity-settings.tsx │ │ │ ├── provider-settings.tsx │ │ │ ├── render-mode-settings.tsx │ │ │ ├── settings-heading-label.tsx │ │ │ ├── settings-pane.tsx │ │ │ └── settings-text-section.tsx │ │ └── index.ts │ ├── specification │ │ ├── index.ts │ │ ├── logic.ts │ │ └── types.ts │ ├── status │ │ ├── components │ │ │ ├── fetching-message.tsx │ │ │ ├── index.ts │ │ │ ├── landing-page-card.tsx │ │ │ ├── landing-page-info-header.tsx │ │ │ ├── landing-page-learn-more.tsx │ │ │ ├── landing-page.tsx │ │ │ ├── progress.tsx │ │ │ ├── splash-initial.tsx │ │ │ ├── status-container.tsx │ │ │ └── status-stack-item.tsx │ │ └── index.ts │ ├── template │ │ ├── components │ │ │ ├── data-assignment-column-cell.tsx │ │ │ ├── data-column-header.tsx │ │ │ ├── data-description-column-cell.tsx │ │ │ ├── data-description-column-field.tsx │ │ │ ├── data-field-dropdown.tsx │ │ │ ├── data-name-column-cell.tsx │ │ │ ├── data-name-column-field.tsx │ │ │ ├── data-type-column-cell.tsx │ │ │ ├── data-type-icon.tsx │ │ │ ├── index.ts │ │ │ ├── preview-image.tsx │ │ │ ├── template-dataset-columns.tsx │ │ │ ├── template-dataset-row.tsx │ │ │ └── template-dataset.tsx │ │ ├── fields.ts │ │ ├── included │ │ │ ├── index.ts │ │ │ ├── thumbnail-images.ts │ │ │ ├── vega-lite │ │ │ │ ├── index.ts │ │ │ │ ├── vl-bar-interactive.ts │ │ │ │ ├── vl-bar-simple.ts │ │ │ │ ├── vl-empty-config.ts │ │ │ │ └── vl-empty.ts │ │ │ └── vega │ │ │ │ ├── index.ts │ │ │ │ ├── v-bar-interactive.ts │ │ │ │ ├── v-bar-simple.ts │ │ │ │ ├── v-empty-config.ts │ │ │ │ └── v-empty.ts │ │ ├── index.ts │ │ └── preview-image.ts │ ├── toaster │ │ ├── components │ │ │ ├── index.ts │ │ │ ├── notification-apply-changes.tsx │ │ │ ├── notification-cross-filter-exceeded.tsx │ │ │ ├── notification-toast.tsx │ │ │ └── notification-toaster.tsx │ │ └── index.ts │ ├── toolbar │ │ ├── components │ │ │ ├── advanced-editor-toolbar-update-operations.tsx │ │ │ ├── advanced-editor-toolbar.tsx │ │ │ ├── apply-menu-button.tsx │ │ │ ├── debug-toolbar.tsx │ │ │ ├── index.ts │ │ │ ├── log-error-indicator.tsx │ │ │ ├── toolbar-button-standard.tsx │ │ │ ├── zoom-level-popover.tsx │ │ │ └── zoom-slider.tsx │ │ ├── index.ts │ │ └── types.ts │ ├── vega-extensibility │ │ ├── extensibility │ │ │ ├── expressions.ts │ │ │ ├── index.ts │ │ │ ├── powerbi-theme.ts │ │ │ └── schemes.ts │ │ ├── index.ts │ │ ├── pattern-fill │ │ │ ├── bindings.ts │ │ │ ├── index.ts │ │ │ └── registry.ts │ │ ├── types.ts │ │ ├── utils.ts │ │ └── view │ │ │ ├── index.ts │ │ │ └── loader.ts │ ├── vega-output │ │ ├── components │ │ │ ├── vega-container.tsx │ │ │ └── vega-render.tsx │ │ └── index.ts │ ├── visual-create │ │ ├── components │ │ │ ├── create-button.tsx │ │ │ ├── create-from-template.tsx │ │ │ ├── create-method.tsx │ │ │ ├── import-dropzone.tsx │ │ │ ├── index.ts │ │ │ ├── no-template-message.tsx │ │ │ ├── select-included-template.tsx │ │ │ ├── template-information.tsx │ │ │ ├── template-placeholder-message.tsx │ │ │ └── visual-create-pane.tsx │ │ └── index.ts │ ├── visual-export │ │ ├── components │ │ │ ├── export-buttons.tsx │ │ │ ├── index.ts │ │ │ ├── visual-export-information.tsx │ │ │ └── visual-export-pane.tsx │ │ └── index.ts │ └── visual-host │ │ ├── index.ts │ │ └── types.ts ├── hooks │ ├── index.ts │ └── usePrevious.ts ├── store │ ├── commands.ts │ ├── create.ts │ ├── dataset.ts │ ├── debug.ts │ ├── editor.ts │ ├── export.ts │ ├── field-usage.ts │ ├── index.ts │ ├── interface.ts │ ├── migration.ts │ ├── processing.ts │ ├── specification.ts │ ├── visual-update.ts │ └── visual.ts └── utils │ ├── crypto.ts │ ├── formatting.ts │ └── index.ts ├── stringResources ├── en-US │ └── resources.resjson └── gd-GB │ └── resources.resjson ├── style └── visual.less ├── tsconfig.build-scripts.json ├── tsconfig.json └── turbo.json /.eslintignore: -------------------------------------------------------------------------------- 1 | bin 2 | node_modules 3 | dist 4 | coverage 5 | test 6 | .eslintrc.js 7 | karma.conf.ts 8 | test.webpack.config.js 9 | src/features/json-editor/theme/*.js 10 | # packages are handled by individual inting 11 | packages -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | "browser": true, 4 | "es6": true, 5 | "es2017": true 6 | }, 7 | root: true, 8 | parser: "@typescript-eslint/parser", 9 | parserOptions: { 10 | project: "tsconfig.json", 11 | tsconfigRootDir: ".", 12 | }, 13 | plugins: [ 14 | "powerbi-visuals" 15 | ], 16 | extends: [ 17 | "plugin:powerbi-visuals/recommended" 18 | ], 19 | rules: {} 20 | }; -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [deneb-viz] # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # TypeScript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # next.js build output 61 | .next 62 | 63 | # Local VS Code config 64 | .vscode/ 65 | 66 | # Power BI Custom Visuals SDK-specific stuff 67 | .api/ 68 | .tmp/ 69 | dist/ 70 | webpack.statistics*.html 71 | 72 | # Yalc 73 | **/.yalc/**/*.md 74 | 75 | # Turbo 76 | .turbo -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | legacy-peer-deps=true -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "trailingComma": "none", 4 | "singleQuote": true, 5 | "jsxSingleQuote": true, 6 | "printWidth": 80, 7 | "tabWidth": 4, 8 | "endOfLine": "lf" 9 | } -------------------------------------------------------------------------------- /.syncpackrc: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/syncpack@11.2.1/dist/schema.json", 3 | "source": [ 4 | "package.json", 5 | "packages/*/package.json" 6 | ], 7 | "indent": " ", 8 | "versionGroups": [ 9 | { 10 | "label": "Power BI Visuals API should match desired version", 11 | "dependencies": [ 12 | "powerbi-visuals-api" 13 | ], 14 | "dependencyTypes": [ 15 | "dev" 16 | ], 17 | "pinVersion": "5.7.0" 18 | }, 19 | { 20 | "label": "Vega should be specific version we wish to package", 21 | "dependencies": [ 22 | "vega" 23 | ], 24 | "dependencyTypes": [ 25 | "dev", 26 | "prod" 27 | ], 28 | "pinVersion": "6.1.2" 29 | }, 30 | { 31 | "label": "Vega-Lite should be specific version we wish to package", 32 | "dependencies": [ 33 | "vega-lite" 34 | ], 35 | "dependencyTypes": [ 36 | "dev", 37 | "prod" 38 | ], 39 | "pinVersion": "6.1.0" 40 | }, 41 | { 42 | "label": "Monaco should be specific version we wish to package", 43 | "dependencies": [ 44 | "monaco-editor" 45 | ], 46 | "dependencyTypes": [ 47 | "dev", 48 | "peer" 49 | ], 50 | "pinVersion": "0.45.0" 51 | }, 52 | { 53 | "label": "Use wildcard when developing local packages", 54 | "dependencies": [ 55 | "@deneb-viz/**" 56 | ], 57 | "dependencyTypes": [ 58 | "dev" 59 | ], 60 | "pinVersion": "*" 61 | } 62 | ] 63 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Daniel Marsh-Patrick 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # deneb 2 | 3 | Declarative visualization in Power BI, using the Vega language. 4 | 5 | Looking for documentation? Head over to https://deneb-viz.github.io/ 6 | -------------------------------------------------------------------------------- /assets/error_indicator_dark.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /assets/error_indicator_light.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deneb-viz/deneb/8a8c732eed6630756b8d978aa2c346a4a7cfd164/assets/icon.png -------------------------------------------------------------------------------- /assets/icon_alpha.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deneb-viz/deneb/8a8c732eed6630756b8d978aa2c346a4a7cfd164/assets/icon_alpha.png -------------------------------------------------------------------------------- /assets/icon_beta.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deneb-viz/deneb/8a8c732eed6630756b8d978aa2c346a4a7cfd164/assets/icon_beta.png -------------------------------------------------------------------------------- /assets/icon_standalone.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deneb-viz/deneb/8a8c732eed6630756b8d978aa2c346a4a7cfd164/assets/icon_standalone.png -------------------------------------------------------------------------------- /assets/store_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deneb-viz/deneb/8a8c732eed6630756b8d978aa2c346a4a7cfd164/assets/store_icon.png -------------------------------------------------------------------------------- /assets/store_image_01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deneb-viz/deneb/8a8c732eed6630756b8d978aa2c346a4a7cfd164/assets/store_image_01.png -------------------------------------------------------------------------------- /assets/store_image_02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deneb-viz/deneb/8a8c732eed6630756b8d978aa2c346a4a7cfd164/assets/store_image_02.png -------------------------------------------------------------------------------- /assets/store_image_03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deneb-viz/deneb/8a8c732eed6630756b8d978aa2c346a4a7cfd164/assets/store_image_03.png -------------------------------------------------------------------------------- /assets/store_image_04.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deneb-viz/deneb/8a8c732eed6630756b8d978aa2c346a4a7cfd164/assets/store_image_04.png -------------------------------------------------------------------------------- /assets/store_image_05.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deneb-viz/deneb/8a8c732eed6630756b8d978aa2c346a4a7cfd164/assets/store_image_05.png -------------------------------------------------------------------------------- /assets/warning_indicator_dark.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /assets/warning_indicator_light.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /bin/validate-config-for-commit.ts: -------------------------------------------------------------------------------- 1 | import { exit } from 'process'; 2 | import { FEATURES, LOG_LEVEL } from '../config'; 3 | 4 | console.log('Checking visual configuration is correct...\n'); 5 | const errors: string[] = []; 6 | 7 | // Developer mode: Should not be set in committed code 8 | if (FEATURES.developer_mode) { 9 | errors.push( 10 | '❌ FEATURES.developer_mode flag is true; this should be false.' 11 | ); 12 | } 13 | // Visual update history overlay: Should not be set in committed code 14 | if (FEATURES.visual_update_history_overlay) { 15 | errors.push( 16 | '❌ FEATURES.visual_update_history_overlay flag is true; this should be false.' 17 | ); 18 | } 19 | // Log level: should be 0 (NONE) in committed code 20 | if (LOG_LEVEL !== 0) { 21 | errors.push(`❌ logLevel is ${LOG_LEVEL}; this should be 0 (NONE).`); 22 | } 23 | // External URIs: Not permitted in certified visual, so needs to be disabled in committed code. 24 | if (FEATURES.enable_external_uri) { 25 | errors.push( 26 | '❌ FEATURES.enable_external_uri is true; this should be false.' 27 | ); 28 | } 29 | 30 | if (errors.length > 0) { 31 | console.error( 32 | '===\nIssues found with configuration. Please resolve the following:\n===\n' 33 | ); 34 | errors.forEach((e, i) => console.error(` ${i + 1}. ${e}`)); 35 | exit(1); 36 | } 37 | console.log('✅ No configuration issues found :)'); 38 | exit(0); 39 | -------------------------------------------------------------------------------- /config/features.json: -------------------------------------------------------------------------------- 1 | { 2 | "developer_mode": false, 3 | "combined_apply_button": false, 4 | "data_drilldown": false, 5 | "enable_external_uri": false, 6 | "visual_update_history_overlay": false, 7 | "advanced_cross_filtering": true 8 | } -------------------------------------------------------------------------------- /config/package-custom.json: -------------------------------------------------------------------------------- 1 | { 2 | "alpha": { 3 | "pbiviz": { 4 | "visual": { 5 | "displayName": "Deneb (Alpha Channel Build)", 6 | "description": "NOT FOR PRODUCTION USE", 7 | "guid": "ALPHA{0}" 8 | }, 9 | "assets": { 10 | "icon": "assets/icon_alpha.png" 11 | } 12 | }, 13 | "features": {} 14 | }, 15 | "beta": { 16 | "pbiviz": { 17 | "visual": { 18 | "displayName": "Deneb (Beta Channel Build)", 19 | "description": "NOT FOR PRODUCTION USE", 20 | "guid": "BETA{0}" 21 | }, 22 | "assets": { 23 | "icon": "assets/icon_beta.png" 24 | } 25 | }, 26 | "features": {} 27 | }, 28 | "standalone": { 29 | "pbiviz": { 30 | "visual": { 31 | "displayName": "Deneb (Standalone Version)", 32 | "guid": "STANDALONE{0}" 33 | }, 34 | "assets": { 35 | "icon": "assets/icon_standalone.png" 36 | } 37 | }, 38 | "features": { 39 | "enable_external_uri": true 40 | }, 41 | "capabilities": { 42 | "privileges": [ 43 | { 44 | "name": "ExportContent", 45 | "essential": true 46 | }, 47 | { 48 | "name": "WebAccess", 49 | "parameters": [ 50 | "*" 51 | ] 52 | } 53 | ] 54 | } 55 | } 56 | } -------------------------------------------------------------------------------- /packages/core-dependencies/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | /** @type {import("eslint").Linter.Config} */ 2 | module.exports = { 3 | root: true, 4 | extends: ['@deneb-viz/eslint-config/library.js'], 5 | parser: '@typescript-eslint/parser', 6 | parserOptions: { 7 | project: 'tsconfig.eslint.json', 8 | tsconfigRootDir: __dirname, 9 | sourceType: 'module' 10 | }, 11 | env: { 12 | jest: true 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /packages/core-dependencies/README.md: -------------------------------------------------------------------------------- 1 | # `@deneb-viz/core-dependencies` 2 | 3 | Where possible, we intent to modularize parts of Deneb that perform specific roles. However, many packages share common types and constants, and this package is intended to consolidate them. 4 | -------------------------------------------------------------------------------- /packages/core-dependencies/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@deneb-viz/core-dependencies", 3 | "version": "1.7.0", 4 | "license": "MIT", 5 | "publishConfig": { 6 | "access": "public" 7 | }, 8 | "repository": { 9 | "type": "git", 10 | "url": "https://github.com/deneb-viz/sadr.git" 11 | }, 12 | "main": "./src/index.ts", 13 | "types": "./src/index.ts", 14 | "type": "module", 15 | "sideEffects": false, 16 | "files": [ 17 | "dist" 18 | ], 19 | "tsup": { 20 | "entry": [ 21 | "src/**/*.ts", 22 | "!src/**/*.test.ts" 23 | ], 24 | "dts": true, 25 | "format": [ 26 | "cjs", 27 | "esm" 28 | ], 29 | "onSuccess": "npx tsc --emitDeclarationOnly --declaration" 30 | }, 31 | "scripts": { 32 | "dev": "tsup --watch", 33 | "build": "tsup", 34 | "test": "jest", 35 | "test:watch": "jest --watch", 36 | "eslint": "eslint . --max-warnings 0" 37 | }, 38 | "jest": { 39 | "preset": "@deneb-viz/jest-presets/jest/node" 40 | }, 41 | "dependencies": { 42 | "vega": "6.1.2", 43 | "vega-lite": "6.1.0" 44 | }, 45 | "devDependencies": { 46 | "@deneb-viz/eslint-config": "*", 47 | "@deneb-viz/jest-presets": "*", 48 | "@deneb-viz/typescript-config": "*", 49 | "powerbi-visuals-api": "5.7.0" 50 | } 51 | } -------------------------------------------------------------------------------- /packages/core-dependencies/src/constants/dataset.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * The name we assign to the main dataset across the application (also the master data role name in Power BI). 3 | */ 4 | export const DATASET_CORE_ROLE_NAME = 'dataset'; 5 | /*** 6 | * The name we use to denote a data point's selection status within the 7 | * dataset. 8 | */ 9 | export const DATASET_CROSS_FILTER_NAME = '__selected__'; 10 | 11 | /** 12 | * The name we use to denote a field in the dataset that holds a selection ID 13 | */ 14 | export const DATASET_IDENTITY_NAME = '__identity__'; 15 | 16 | /** 17 | * The name we use to denote a field in the dataset used to hold the stringified representation of the identity, and 18 | * therefore use for comparison operations and suchlike. 19 | */ 20 | export const DATASET_KEY_NAME = '__key__'; 21 | 22 | /** 23 | * The name we use to denote a row in the datset, which is also used for reconciliation of selectors. 24 | */ 25 | export const DATASET_ROW_NAME = '__row__'; 26 | -------------------------------------------------------------------------------- /packages/core-dependencies/src/constants/debug-area.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Default values for the data table in the preview pane (AKA the debug pane). 3 | */ 4 | export const PREVIEW_PANE_DATA_TABLE = { 5 | rowsPerPage: { 6 | default: 50, 7 | values: [10, 25, 50, 100, 150, 200] 8 | } 9 | }; 10 | -------------------------------------------------------------------------------- /packages/core-dependencies/src/constants/index.ts: -------------------------------------------------------------------------------- 1 | export * from './dataset'; 2 | export * from './debug-area'; 3 | export * from './integration-powerbi'; 4 | export * from './json-processing'; 5 | export * from './template-usermeta-schema'; 6 | export * from './utils-base64'; 7 | export * from './vega-extensibility'; 8 | -------------------------------------------------------------------------------- /packages/core-dependencies/src/constants/json-processing.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Placeholder to use when generating literal tokens for field names. These are u sed to identify field names in a 3 | * specification. 4 | */ 5 | export const JSON_FIELD_TRACKING_TOKEN_PLACEHOLDER = '__FIELD__'; 6 | 7 | /** 8 | * Placeholder to use when generating replacement tokens for fields in a specification. This will be replaced with the 9 | * actual metadata placeholder when ready to tokenize the specification. 10 | */ 11 | export const JSON_FIELD_TRACKING_METADATA_PLACEHOLDER = '__METADATA__'; 12 | 13 | /** 14 | * When resolving JSON to readable strings, this is the default maximum level of depth to stop at. 15 | */ 16 | export const JSON_MAX_PRUNE_DEPTH = 3; 17 | -------------------------------------------------------------------------------- /packages/core-dependencies/src/constants/template-usermeta-schema.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * The current version for Deneb template metadata. 3 | */ 4 | export const TEMPLATE_USERMETA_VERSION = 1; 5 | -------------------------------------------------------------------------------- /packages/core-dependencies/src/constants/utils-base64.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * The base MIME-type used when creating images from data URLs. 3 | */ 4 | export const BASE64_MIME_TYPE_PNG = 'image/png'; 5 | 6 | /** 7 | * Blank image data URI; used to return placeholder images when remote URIs are supplied. 8 | */ 9 | export const BASE64_BLANK_IMAGE_PNG = `iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=`; 10 | -------------------------------------------------------------------------------- /packages/core-dependencies/src/constants/vega-extensibility.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Divergent color scheme from Power BI to Vega view. 3 | */ 4 | export const VEGA_SCHEME_POWERBI_DIVERGENT = 'pbiColorDivergent'; 5 | /** 6 | * Linear color scheme from Power BI to Vega view. 7 | */ 8 | export const VEGA_SCHEME_POWERBI_LINEAR = 'pbiColorLinear'; 9 | /** 10 | * Nominal color scheme from Power BI to Vega view. 11 | */ 12 | export const VEGA_SCHEME_POWERBI_NOMINAL = 'pbiColorNominal'; 13 | /** 14 | * Ordinal color scheme from Power BI to Vega view. 15 | */ 16 | export const VEGA_SCHEME_POWERBI_ORDINAL = 'pbiColorOrdinal'; 17 | -------------------------------------------------------------------------------- /packages/core-dependencies/src/dataset/fields.ts: -------------------------------------------------------------------------------- 1 | import { IDatasetFields } from '../definitions'; 2 | import { pickBy } from '../utils'; 3 | 4 | /** 5 | * For supplied fields, retrieve only those that should be from the data roles. 6 | */ 7 | export function getDatasetFieldsInclusive(fields: IDatasetFields) { 8 | return pickBy(fields, (f) => !f.isExcludedFromTemplate); 9 | } 10 | -------------------------------------------------------------------------------- /packages/core-dependencies/src/dataset/index.ts: -------------------------------------------------------------------------------- 1 | export { getDatasetFieldsInclusive } from './fields'; 2 | -------------------------------------------------------------------------------- /packages/core-dependencies/src/definitions/index.ts: -------------------------------------------------------------------------------- 1 | export * from './dataset'; 2 | export * from './integration-powerbi'; 3 | export * from './json-processing'; 4 | export * from './store'; 5 | export * from './template-usermeta-schema'; 6 | export * from './template'; 7 | export * from './utils-base64'; 8 | export * from './vega-integration'; 9 | export * from './worker-dataset-viewer'; 10 | export * from './worker-spec-json-processing'; 11 | -------------------------------------------------------------------------------- /packages/core-dependencies/src/definitions/integration-powerbi.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Represents the signal patched to the Vega view that contains the current state of the container. This can be used 3 | * within a specification to adjust the rendering of the visualization based on the current state of the container. 4 | */ 5 | export type PowerBIContainerSignal = { 6 | height: number; 7 | width: number; 8 | scrollHeight: number; 9 | scrollWidth: number; 10 | scrollTop: number; 11 | scrollLeft: number; 12 | }; 13 | 14 | /** 15 | * Options for setting the PowerBI container signal. This can come from either the container itself or from the scroll 16 | * event in the container, which has its own properties. 17 | */ 18 | export type PowerBIContainerSignalSetterOptions = { 19 | /** 20 | * The container element that the signal is being set for (if known). 21 | */ 22 | container?: HTMLElement; 23 | /** 24 | * The scroll event properties for the container (if known). 25 | */ 26 | scroll?: PowerBIContainerSignal; 27 | }; 28 | 29 | /** 30 | * Indicates the type of selection mode that is currently active. 31 | * `simple` = legacy selection mode (let Deneb do it for me); 32 | * `advanced` = advanced selection mode (let me do it for Deneb). 33 | */ 34 | export type SelectionMode = 'simple' | 'advanced'; 35 | -------------------------------------------------------------------------------- /packages/core-dependencies/src/definitions/template.ts: -------------------------------------------------------------------------------- 1 | import { Spec } from 'vega'; 2 | import { TopLevelSpec } from 'vega-lite'; 3 | 4 | /** 5 | * Which mode we wish to use to instantiate our new specification. 6 | */ 7 | export type CreateMode = 'import' | 'vegaLite' | 'vega'; 8 | 9 | /** 10 | * Used to manage regex match/replace for portions of a template that represent fields from the dataset. 11 | */ 12 | export type DenebTemplateDataFieldReplacerPattern = { 13 | match: string; 14 | replacer: string; 15 | }; 16 | 17 | /** 18 | * Denotes the role that a column performs, allowing us to switch based on this value. 19 | */ 20 | export type DenebTemplateDatasetColumnRole = 21 | | 'type' 22 | | 'name' 23 | | 'assignment' 24 | | 'description' 25 | | 'originalName' 26 | | 'exportName' 27 | | 'exportDescription'; 28 | 29 | /** 30 | * Stages we go through when importing a template so that the interface can respond accordingly. 31 | */ 32 | export type DenebTemplateImportState = 'None' | 'Supplied' | 'Loading' | 'Validating' | 'Success' | 'Error'; 33 | 34 | /** 35 | * Represents templates that are packaged in the .pbiviz for demo purposes. 36 | */ 37 | export type DenebTemplatesIncluded = { 38 | vega: Spec[]; 39 | vegaLite: TopLevelSpec[]; 40 | }; 41 | 42 | /** 43 | * Because we're using JSONC, it's much easier to store the intended spec and config content as a string when we need 44 | * to work with the APIs (and also when we need to update placeholders etc. prior to create). This interface allows us 45 | * to store both of the intended strings for transport to and from the store, and other places we need them. 46 | */ 47 | export interface IDenebTemplateAllocationComponents { 48 | spec: string; 49 | config: string; 50 | } 51 | -------------------------------------------------------------------------------- /packages/core-dependencies/src/definitions/utils-base64.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Supported encodings for base64 3 | */ 4 | export type TBase64DataEncoding = 'png'; 5 | -------------------------------------------------------------------------------- /packages/core-dependencies/src/definitions/vega-integration.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Valid providers for language specifications. 3 | */ 4 | export type SpecProvider = 'vega' | 'vegaLite'; 5 | 6 | /** 7 | * Valid render modes for language specifications. 8 | */ 9 | export type SpecRenderMode = 'svg' | 'canvas'; 10 | -------------------------------------------------------------------------------- /packages/core-dependencies/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './constants'; 2 | export * from './definitions'; 3 | export * from './dataset'; 4 | export * from './json'; 5 | export * from './utils'; 6 | -------------------------------------------------------------------------------- /packages/core-dependencies/src/json/__tests__/json.test.ts: -------------------------------------------------------------------------------- 1 | import { getJsonPlaceholderKey } from '../processing'; 2 | 3 | describe('getJsonPlaceholderKey', () => { 4 | it('should return a placeholder key', () => { 5 | expect(getJsonPlaceholderKey(0)).toBe('__0__'); 6 | }); 7 | it('should return a placeholder key with positive number', () => { 8 | expect(getJsonPlaceholderKey(5)).toBe('__5__'); 9 | }); 10 | it('should return a placeholder key with negative number', () => { 11 | expect(getJsonPlaceholderKey(-3)).toBe('__3__'); 12 | }); 13 | it('should return a placeholder key with decimal number floored down', () => { 14 | expect(getJsonPlaceholderKey(2.5)).toBe('__2__'); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /packages/core-dependencies/src/json/index.ts: -------------------------------------------------------------------------------- 1 | export { getJsonPlaceholderKey } from './processing'; 2 | -------------------------------------------------------------------------------- /packages/core-dependencies/src/json/processing.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Consistently format a supplied identity into a suitable placeholder. Placeholders are used to represent dataset 3 | * fields in the specification, so that they can be replaced with the actual values when the dataset is accessible. 4 | * - Decimal values are floored to the nearest integer. 5 | * - Negative values are converted to positive values. 6 | */ 7 | export function getJsonPlaceholderKey(i: number) { 8 | return `__${Math.floor(Math.abs(i))}__`; 9 | } 10 | -------------------------------------------------------------------------------- /packages/core-dependencies/src/utils/__tests__/base64.test.ts: -------------------------------------------------------------------------------- 1 | import { BASE64_BLANK_IMAGE_PNG } from '../../constants'; 2 | import { getBase64DataUri, getBase64ImagePngBlank, getBase64MimeType, isBase64Image } from '../base64'; 3 | 4 | describe('getBase64DataUri', () => { 5 | it('should return the correct data URI', () => { 6 | const type = 'png'; 7 | const expectedDataUri = 'data:image/png;base64,'; 8 | const result = getBase64DataUri(type); 9 | expect(result).toBe(expectedDataUri); 10 | }); 11 | }); 12 | 13 | describe('getBase64MimeType', () => { 14 | it('should return the correct MIME type for png', () => { 15 | const type = 'png'; 16 | const expectedMimeType = 'image/png'; 17 | const result = getBase64MimeType(type); 18 | expect(result).toBe(expectedMimeType); 19 | }); 20 | }); 21 | 22 | describe('isBase64Image', () => { 23 | it('should return true if just the boilerplate URI is used', () => { 24 | const str = getBase64DataUri('png'); 25 | const result = isBase64Image(str); 26 | expect(result).toBe(true); 27 | }); 28 | it('should return true if the blank URI is used', () => { 29 | const str = getBase64ImagePngBlank(); 30 | const result = isBase64Image(str); 31 | expect(result).toBe(true); 32 | }); 33 | it('should return false if a regular URL is used', () => { 34 | const str = 'https://www.google.com'; 35 | const result = isBase64Image(str); 36 | expect(result).toBe(false); 37 | }); 38 | it('should return false if a regular URL is used with the base64 prefix', () => { 39 | const str = `${getBase64DataUri('png')}https://www.google.com`; 40 | const result = isBase64Image(str); 41 | expect(result).toBe(false); 42 | }); 43 | it('should return false if a base64 URI is used with the wrong prefix', () => { 44 | const str = `data:image/jpeg;base64,${BASE64_BLANK_IMAGE_PNG}`; 45 | const result = isBase64Image(str); 46 | expect(result).toBe(false); 47 | }); 48 | }); 49 | -------------------------------------------------------------------------------- /packages/core-dependencies/src/utils/__tests__/crypto.test.ts: -------------------------------------------------------------------------------- 1 | import { getNewUuid } from '../crypto'; 2 | 3 | const UUID_REGEX = 4 | /^[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12}$/i; 5 | 6 | describe('getNewUuid', () => { 7 | it('should return a valid UUID when crypto is available', () => { 8 | const uuid = getNewUuid(); 9 | expect(uuid).toMatch(UUID_REGEX); 10 | }); 11 | it('should return a valid UUID ', () => { 12 | const uuid = getNewUuid(); 13 | expect(uuid).toMatch(UUID_REGEX); 14 | global.crypto = crypto; 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /packages/core-dependencies/src/utils/__tests__/string.test..ts: -------------------------------------------------------------------------------- 1 | import { getEscapedReplacerPattern } from '../string'; 2 | 3 | describe('getEscapedReplacerPattern', () => { 4 | it('should escape special characters in the input string', () => { 5 | const input = '$ Sales.Current'; 6 | const expectedOutput = '\\$ Sales\\.Current'; 7 | expect(getEscapedReplacerPattern(input)).toBe(expectedOutput); 8 | }); 9 | 10 | it('should leave underscores alone', () => { 11 | const input = 'Sales_Current'; 12 | expect(getEscapedReplacerPattern(input)).toBe(input); 13 | }); 14 | 15 | it('should return the same string if there are no special characters', () => { 16 | const input = 'abc123'; 17 | expect(getEscapedReplacerPattern(input)).toBe(input); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /packages/core-dependencies/src/utils/base64.ts: -------------------------------------------------------------------------------- 1 | import { BASE64_BLANK_IMAGE_PNG, BASE64_MIME_TYPE_PNG } from '../constants'; 2 | import { TBase64DataEncoding } from '../definitions'; 3 | 4 | /** 5 | * For the given data type, create a data URL prefix for base64 data. 6 | */ 7 | export function getBase64DataUri(type: TBase64DataEncoding) { 8 | return `data:${getBase64MimeType(type)};base64,`; 9 | } 10 | 11 | /** 12 | * For the given data type, return a valid MIME type string. 13 | */ 14 | export function getBase64MimeType(type: TBase64DataEncoding) { 15 | switch (type) { 16 | case 'png': 17 | return BASE64_MIME_TYPE_PNG; 18 | } 19 | } 20 | 21 | /** 22 | * Return a base64-encoded PNG image data URI that represents a blank image. 23 | */ 24 | export function getBase64ImagePngBlank() { 25 | return `${getBase64DataUri('png')}${BASE64_BLANK_IMAGE_PNG}`; 26 | } 27 | 28 | /** 29 | * Test an image URL to determine if it's base64 or not. 30 | */ 31 | export function isBase64Image(str: string) { 32 | const prefix = getBase64DataUri('png'); 33 | try { 34 | const b64 = str.replace(prefix, '').trim(); 35 | return btoa(atob(b64)) === b64 && str.trim().indexOf(prefix) === 0; 36 | } catch (err) { 37 | return false; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /packages/core-dependencies/src/utils/crypto.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Generate a new UUID. 3 | */ 4 | export function getNewUuid() { 5 | return 'xxxxxxxx-xxxx-4xxx-Nxxx-xxxxxxxxxxxx' 6 | .replace(/x/g, () => ((Math.random() * 16) | 0).toString(16)) 7 | .replace(/N/g, () => ((Math.random() * 4) | (0 + 8)).toString(16)); 8 | } 9 | -------------------------------------------------------------------------------- /packages/core-dependencies/src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './crypto'; 2 | export { 3 | getBase64DataUri, 4 | getBase64ImagePngBlank, 5 | getBase64MimeType, 6 | isBase64Image 7 | } from './base64'; 8 | export * from './object'; 9 | export * from './string'; 10 | export * from './type-conversion'; 11 | export * from './type-guards'; 12 | -------------------------------------------------------------------------------- /packages/core-dependencies/src/utils/object.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @privateRemarks 3 | * This file exists so that we have our own versions of lodash functions, which are tree-shakeable. 4 | */ 5 | /* eslint-disable @typescript-eslint/no-explicit-any */ 6 | import { isObject } from './type-guards'; 7 | 8 | /** 9 | * A predicate function that takes a value and a key, and returns a boolean. 10 | */ 11 | type Predicate = (value: T[keyof T], key: keyof T) => boolean; 12 | 13 | /** 14 | * Deep clone an object. 15 | */ 16 | export function merge>( 17 | target: T, 18 | ...sources: any[] 19 | ): T { 20 | const result: T = structuredClone(target); 21 | sources.forEach((source) => { 22 | const clonedSource = structuredClone(source); 23 | if (isObject(result) && isObject(clonedSource)) { 24 | Object.keys(clonedSource).forEach((key) => { 25 | if (isObject(clonedSource[key])) { 26 | if (!result[key]) { 27 | (result as any)[key] = {}; 28 | } 29 | (result as any)[key] = merge( 30 | result[key], 31 | clonedSource[key] 32 | ); 33 | } else { 34 | (result as any)[key] = clonedSource[key]; 35 | } 36 | }); 37 | } 38 | }); 39 | return result; 40 | } 41 | 42 | /** 43 | * Pick the properties of an object that satisfy a predicate. 44 | */ 45 | export function pickBy>( 46 | object: T, 47 | predicate: Predicate 48 | ): Partial { 49 | const result: Partial = {}; 50 | for (const key in object) { 51 | if (predicate(object[key], key)) { 52 | result[key] = object[key]; 53 | } 54 | } 55 | return result; 56 | } 57 | -------------------------------------------------------------------------------- /packages/core-dependencies/src/utils/string.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * When performing placeholder replacements, we need to ensure that special characters used in regex qualifiers are 3 | * suitably escaped so that we don't inadvertently mangle them. Returns escaped string, suitable for pattern matching 4 | * if any special characters are used. 5 | */ 6 | export function getEscapedReplacerPattern(value: string) { 7 | return (value ?? '').replace(/[-/\\^$*+?.()&|[\]{}]/g, '\\$&'); 8 | } 9 | -------------------------------------------------------------------------------- /packages/core-dependencies/src/utils/type-conversion.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Converts a string to a Uint8Array, suitable for inclusion in a worker as a `Transferable`. 3 | */ 4 | export function stringToUint8Array(str: string): Uint8Array { 5 | return new TextEncoder().encode(str); 6 | } 7 | 8 | /* 9 | * Converts a Uint8Array to a string. 10 | */ 11 | export function uint8ArrayToString(arr: ArrayBuffer): string { 12 | return new TextDecoder().decode(arr); 13 | } 14 | -------------------------------------------------------------------------------- /packages/core-dependencies/src/utils/type-guards.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Tests for boolean type compatibility. 3 | */ 4 | export function isBoolean(_: unknown) { 5 | return typeof _ === 'boolean'; 6 | } 7 | 8 | /** 9 | * Tests for date type compatibility. 10 | */ 11 | export function isDate(_: unknown) { 12 | return Object.prototype.toString.call(_) === '[object Date]'; 13 | } 14 | 15 | /** 16 | * Tests for number type compatibility. 17 | */ 18 | export function isNumber(_: unknown) { 19 | return typeof _ === 'number'; 20 | } 21 | 22 | /** 23 | * Tests for object type compatibility. 24 | */ 25 | export function isObject(_: unknown) { 26 | return ( 27 | _ !== null && 28 | _ !== undefined && 29 | typeof _ === 'object' && 30 | !Array.isArray(_) 31 | ); 32 | } 33 | 34 | /** 35 | * Tests for string type compatibility. 36 | */ 37 | export function isString(_: unknown) { 38 | return typeof _ === 'string'; 39 | } 40 | -------------------------------------------------------------------------------- /packages/core-dependencies/tsconfig.eslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "include": ["src", "tests", ".eslintrc.js"] 4 | } 5 | -------------------------------------------------------------------------------- /packages/core-dependencies/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@deneb-viz/typescript-config/base.json", 3 | "compilerOptions": { 4 | "outDir": "./dist", 5 | "declarationDir": "./dist" 6 | }, 7 | "include": ["src"], 8 | "exclude": ["node_modules", "dist"] 9 | } 10 | -------------------------------------------------------------------------------- /packages/core-dependencies/turbo.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://turbo.build/schema.json", 3 | "extends": [ 4 | "//" 5 | ], 6 | "tasks": { 7 | "build": { 8 | "outputs": [ 9 | "dist/**" 10 | ] 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /packages/eslint-config/README.md: -------------------------------------------------------------------------------- 1 | # `@turbo/eslint-config` 2 | 3 | Collection of internal eslint configurations. 4 | -------------------------------------------------------------------------------- /packages/eslint-config/library.js: -------------------------------------------------------------------------------- 1 | const { resolve } = require('node:path'); 2 | 3 | const project = resolve(process.cwd(), 'tsconfig.json'); 4 | 5 | /** @type {import("eslint").Linter.Config} */ 6 | module.exports = { 7 | extends: [ 8 | 'eslint:recommended', 9 | 'prettier', 10 | 'eslint-config-turbo', 11 | 'plugin:@typescript-eslint/recommended' 12 | ], 13 | plugins: ['only-warn'], 14 | globals: { 15 | React: true, 16 | JSX: true 17 | }, 18 | env: { 19 | node: true 20 | }, 21 | settings: { 22 | 'import/resolver': { 23 | typescript: { 24 | project 25 | } 26 | } 27 | }, 28 | ignorePatterns: [ 29 | // Ignore dotfiles 30 | '.*.js', 31 | 'node_modules/', 32 | 'dist/', 33 | 'coverage/', 34 | 'bin/' 35 | ], 36 | overrides: [ 37 | { 38 | files: ['*.js?(x)', '*.ts?(x)'] 39 | } 40 | ], 41 | rules: { 42 | 'no-unused-vars': 'off', 43 | '@typescript-eslint/no-unused-vars': ['error'] 44 | } 45 | }; 46 | -------------------------------------------------------------------------------- /packages/eslint-config/next.js: -------------------------------------------------------------------------------- 1 | const { resolve } = require('node:path'); 2 | 3 | const project = resolve(process.cwd(), 'tsconfig.json'); 4 | 5 | /** @type {import("eslint").Linter.Config} */ 6 | module.exports = { 7 | extends: [ 8 | 'eslint:recommended', 9 | 'prettier', 10 | require.resolve('@vercel/style-guide/eslint/next'), 11 | 'eslint-config-turbo' 12 | ], 13 | globals: { 14 | React: true, 15 | JSX: true 16 | }, 17 | env: { 18 | node: true, 19 | browser: true 20 | }, 21 | plugins: ['only-warn'], 22 | settings: { 23 | 'import/resolver': { 24 | typescript: { 25 | project 26 | } 27 | } 28 | }, 29 | ignorePatterns: [ 30 | // Ignore dotfiles 31 | '.*.js', 32 | 'node_modules/' 33 | ], 34 | overrides: [{ files: ['*.js?(x)', '*.ts?(x)'] }] 35 | }; 36 | -------------------------------------------------------------------------------- /packages/eslint-config/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@deneb-viz/eslint-config", 3 | "version": "0.0.0", 4 | "private": true, 5 | "files": [ 6 | "library.js", 7 | "next.js", 8 | "react-internal.js" 9 | ], 10 | "devDependencies": { 11 | "@typescript-eslint/eslint-plugin": "^6.17.0", 12 | "@typescript-eslint/parser": "^6.17.0", 13 | "@vercel/style-guide": "^5.1.0", 14 | "eslint-config-prettier": "^9.1.0", 15 | "eslint-config-turbo": "^2.0.4", 16 | "eslint-plugin-only-warn": "^1.1.0", 17 | "typescript": "^5.4.5" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /packages/eslint-config/react-internal.js: -------------------------------------------------------------------------------- 1 | const { resolve } = require('node:path'); 2 | 3 | const project = resolve(process.cwd(), 'tsconfig.json'); 4 | 5 | /* 6 | * This is a custom ESLint configuration for use with 7 | * internal (bundled by their consumer) libraries 8 | * that utilize React. 9 | * 10 | * This config extends the Vercel Engineering Style Guide. 11 | * For more information, see https://github.com/vercel/style-guide 12 | * 13 | */ 14 | 15 | /** @type {import("eslint").Linter.Config} */ 16 | module.exports = { 17 | extends: ['eslint:recommended', 'prettier', 'eslint-config-turbo'], 18 | plugins: ['only-warn'], 19 | globals: { 20 | React: true, 21 | JSX: true 22 | }, 23 | env: { 24 | browser: true 25 | }, 26 | settings: { 27 | 'import/resolver': { 28 | typescript: { 29 | project 30 | } 31 | } 32 | }, 33 | ignorePatterns: [ 34 | // Ignore dotfiles 35 | '.*.js', 36 | 'node_modules/', 37 | 'dist/' 38 | ], 39 | overrides: [ 40 | // Force ESLint to detect .tsx files 41 | { files: ['*.js?(x)', '*.ts?(x)'] } 42 | ] 43 | }; 44 | -------------------------------------------------------------------------------- /packages/integration-powerbi/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | /** @type {import("eslint").Linter.Config} */ 2 | module.exports = { 3 | root: true, 4 | extends: ['@deneb-viz/eslint-config/library.js'], 5 | parser: '@typescript-eslint/parser', 6 | parserOptions: { 7 | project: 'tsconfig.eslint.json', 8 | tsconfigRootDir: __dirname, 9 | sourceType: 'module' 10 | }, 11 | env: { 12 | jest: true 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /packages/integration-powerbi/README.md: -------------------------------------------------------------------------------- 1 | # `@deneb-viz/integration-powerbi` 2 | 3 | Contains APIs for working with Power BI interactivity. 4 | -------------------------------------------------------------------------------- /packages/integration-powerbi/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@deneb-viz/integration-powerbi", 3 | "version": "1.7.0", 4 | "license": "MIT", 5 | "publishConfig": { 6 | "access": "public" 7 | }, 8 | "repository": { 9 | "type": "git", 10 | "url": "https://github.com/deneb-viz/sadr.git" 11 | }, 12 | "main": "./dist/index.cjs", 13 | "module": "./dist/index.js", 14 | "types": "./dist/index.d.ts", 15 | "type": "module", 16 | "sideEffects": false, 17 | "files": [ 18 | "dist" 19 | ], 20 | "browser": { 21 | "canvas": false, 22 | "encoding": false, 23 | "fs": false, 24 | "fs/promises": false, 25 | "net": false, 26 | "tls": false 27 | }, 28 | "tsup": { 29 | "entry": [ 30 | "src/**/*.ts", 31 | "!src/**/*.test.ts" 32 | ], 33 | "dts": true, 34 | "format": [ 35 | "cjs", 36 | "esm" 37 | ], 38 | "onSuccess": "npx tsc --emitDeclarationOnly --declaration" 39 | }, 40 | "scripts": { 41 | "dev": "tsup --watch", 42 | "build": "tsup", 43 | "test": "jest", 44 | "test:watch": "jest --watch", 45 | "eslint": "eslint . --max-warnings 0" 46 | }, 47 | "jest": { 48 | "preset": "@deneb-viz/jest-presets/jest/node" 49 | }, 50 | "devDependencies": { 51 | "@deneb-viz/core-dependencies": "*", 52 | "@deneb-viz/eslint-config": "*", 53 | "@deneb-viz/jest-presets": "*", 54 | "@deneb-viz/typescript-config": "*", 55 | "powerbi-visuals-utils-formattingmodel": "^6.0.2" 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /packages/integration-powerbi/src/__test__/cross-highlight.test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | HIGHLIGHT_COMPARATOR_SUFFIX, 3 | HIGHLIGHT_FIELD_SUFFIX, 4 | HIGHLIGHT_STATUS_SUFFIX 5 | } from '@deneb-viz/core-dependencies'; 6 | import { getCrossHighlightRegExpAlternation } from '../cross-highlight'; 7 | 8 | describe('getCrossHighlightRegExpAlternation', () => { 9 | it('should return the expected alternation string', () => { 10 | const result = getCrossHighlightRegExpAlternation(); 11 | const expected = `${HIGHLIGHT_COMPARATOR_SUFFIX}|${HIGHLIGHT_STATUS_SUFFIX}|${HIGHLIGHT_FIELD_SUFFIX}`; 12 | expect(result).toEqual(expected); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /packages/integration-powerbi/src/cross-highlight.ts: -------------------------------------------------------------------------------- 1 | import { 2 | HIGHLIGHT_COMPARATOR_SUFFIX, 3 | HIGHLIGHT_FIELD_SUFFIX, 4 | HIGHLIGHT_STATUS_SUFFIX 5 | } from '@deneb-viz/core-dependencies'; 6 | 7 | /** 8 | * Provides all highlight field suffixes, suitable for a RegExp expression. 9 | */ 10 | export const getCrossHighlightRegExpAlternation = () => 11 | `${HIGHLIGHT_COMPARATOR_SUFFIX}|${HIGHLIGHT_STATUS_SUFFIX}|${HIGHLIGHT_FIELD_SUFFIX}`; 12 | -------------------------------------------------------------------------------- /packages/integration-powerbi/src/index.ts: -------------------------------------------------------------------------------- 1 | export { getCrossHighlightRegExpAlternation as getPowerBiCrossHighlightRegExpAlternation } from './cross-highlight'; 2 | export { 3 | getPowerBiTokenPatternsLiteral, 4 | getPowerBiTokenPatternsReplacement 5 | } from './field-tracking'; 6 | export { 7 | getVisualFormattingModel, 8 | getVisualFormattingService, 9 | VisualFormattingSettingsModel, 10 | VisualFormattingSettingsService 11 | } from './properties'; 12 | export { getPowerBiSignalContainer } from './signals'; 13 | -------------------------------------------------------------------------------- /packages/integration-powerbi/src/number-formatting.ts: -------------------------------------------------------------------------------- 1 | import { DATASET_FIELD_FORMATED_VALUE_SUFFIX, DATASET_FIELD_FORMAT_STRING_SUFFIX } from '@deneb-viz/core-dependencies'; 2 | 3 | /** 4 | * Provides all number formatting field suffixes, suitable for a RegExp expression. 5 | */ 6 | export const getNumberFormatRegExpAlternation = () => 7 | `${DATASET_FIELD_FORMAT_STRING_SUFFIX}|${DATASET_FIELD_FORMATED_VALUE_SUFFIX}`; 8 | -------------------------------------------------------------------------------- /packages/integration-powerbi/src/properties/index.ts: -------------------------------------------------------------------------------- 1 | export { VisualFormattingSettingsModel } from './visual-formatting-settings-model'; 2 | export { 3 | getVisualFormattingModel, 4 | getVisualFormattingService, 5 | VisualFormattingSettingsService 6 | } from './visual-formatting-settings-service'; 7 | -------------------------------------------------------------------------------- /packages/integration-powerbi/src/properties/settings-data-limit.ts: -------------------------------------------------------------------------------- 1 | import { formattingSettings } from 'powerbi-visuals-utils-formattingmodel'; 2 | import { PROPERTIES_DEFAULTS } from '@deneb-viz/core-dependencies'; 3 | 4 | export class SettingsDataLimit extends formattingSettings.CompositeCard { 5 | name = 'dataLimit'; 6 | displayNameKey = 'Objects_DataLimit'; 7 | descriptionKey = 'Objects_DataLimit_Description'; 8 | loading = new SettingsDataLimitGroupLoading(Object()); 9 | groups = [this.loading]; 10 | onPreProcess(): void { 11 | if (!this.loading.override.value) { 12 | this.loading.showCustomVisualNotes.visible = false; 13 | } 14 | } 15 | } 16 | 17 | class SettingsDataLimitGroupLoading extends formattingSettings.Group { 18 | name = 'loading'; 19 | displayNameKey = 'Objects_DataLimit_Group_Loading'; 20 | override = new formattingSettings.ToggleSwitch({ 21 | name: 'override', 22 | displayNameKey: 'Objects_DataLimit_Override', 23 | descriptionKey: 'Objects_DataLimit_Override_Description', 24 | value: PROPERTIES_DEFAULTS.dataLimit.override 25 | }); 26 | showCustomVisualNotes = new formattingSettings.ToggleSwitch({ 27 | name: 'showCustomVisualNotes', 28 | displayNameKey: 'Objects_DataLimit_ShowCustomVisualNotes', 29 | descriptionKey: 'Objects_DataLimit_ShowCustomVisualNotes_Description', 30 | value: PROPERTIES_DEFAULTS.dataLimit.showCustomVisualNotes 31 | }); 32 | slices = [this.override, this.showCustomVisualNotes]; 33 | } 34 | -------------------------------------------------------------------------------- /packages/integration-powerbi/src/properties/settings-developer.ts: -------------------------------------------------------------------------------- 1 | import { formattingSettings } from 'powerbi-visuals-utils-formattingmodel'; 2 | import { PROPERTIES_DEFAULTS } from '@deneb-viz/core-dependencies'; 3 | 4 | export class SettingsDeveloper extends formattingSettings.CompositeCard { 5 | name = 'developer'; 6 | displayNameKey = 'Objects_Developer'; 7 | descriptionKey = 'Objects_Developer_Description'; 8 | versioning = new SettingsDeveloperGroupVersioning(Object()); 9 | localization = new SettingsDeveloperGroupLocalization(Object()); 10 | groups = [this.versioning, this.localization]; 11 | } 12 | 13 | class SettingsDeveloperGroupVersioning extends formattingSettings.Group { 14 | name = 'versioning'; 15 | displayNameKey = 'Objects_Developer_Group_Version'; 16 | version = new formattingSettings.ReadOnlyText({ 17 | name: 'version', 18 | displayNameKey: 'Objects_Developer_Version', 19 | descriptionKey: 'Objects_Developer_Version_Description', 20 | value: PROPERTIES_DEFAULTS.developer.version 21 | }); 22 | slices = [this.version]; 23 | } 24 | 25 | class SettingsDeveloperGroupLocalization extends formattingSettings.Group { 26 | name = 'localization'; 27 | displayNameKey = 'Objects_Developer_Group_i18n'; 28 | locale = new formattingSettings.AutoDropdown({ 29 | name: 'locale', 30 | displayNameKey: 'Objects_Developer_Locale', 31 | descriptionKey: 'Objects_Developer_Locale_Description', 32 | value: PROPERTIES_DEFAULTS.developer.locale 33 | }); 34 | slices = [this.locale]; 35 | } 36 | -------------------------------------------------------------------------------- /packages/integration-powerbi/src/properties/settings-general.ts: -------------------------------------------------------------------------------- 1 | import { formattingSettings } from 'powerbi-visuals-utils-formattingmodel'; 2 | 3 | export class SettingsGeneral extends formattingSettings.CompositeCard { 4 | name = 'general'; 5 | groups = []; 6 | } 7 | -------------------------------------------------------------------------------- /packages/integration-powerbi/src/properties/settings-state-management.ts: -------------------------------------------------------------------------------- 1 | import { formattingSettings } from 'powerbi-visuals-utils-formattingmodel'; 2 | import { PROPERTIES_DEFAULTS } from '@deneb-viz/core-dependencies'; 3 | 4 | export class SettingsStateManagement extends formattingSettings.CompositeCard { 5 | name = 'stateManagement'; 6 | displayNameKey = 'Objects_StateManagement'; 7 | descriptionKey = 'Objects_StateManagementDisplay_Description'; 8 | viewport = new SettingsStateManagementGroupViewport(Object()); 9 | groups = [this.viewport]; 10 | } 11 | 12 | class SettingsStateManagementGroupViewport extends formattingSettings.Group { 13 | name = 'viewport'; 14 | displayNameKey = 'Objects_StateManagement_Group_Viewport'; 15 | viewportHeight = new formattingSettings.ReadOnlyText({ 16 | name: 'viewportHeight', 17 | displayNameKey: 'Objects_StateManagement_ViewportHeight', 18 | descriptionKey: 'Objects_StateManagement_ViewportHeight_Description', 19 | value: PROPERTIES_DEFAULTS.stateManagement.viewportHeight 20 | }); 21 | viewportWidth = new formattingSettings.ReadOnlyText({ 22 | name: 'viewportWidth', 23 | displayNameKey: 'Objects_StateManagement_ViewportWidth', 24 | descriptionKey: 'Objects_StateManagement_ViewportWidth_Description', 25 | value: PROPERTIES_DEFAULTS.stateManagement.viewportWidth 26 | }); 27 | slices = [this.viewportHeight, this.viewportWidth]; 28 | } 29 | -------------------------------------------------------------------------------- /packages/integration-powerbi/src/properties/settings-theme.ts: -------------------------------------------------------------------------------- 1 | import { formattingSettings } from 'powerbi-visuals-utils-formattingmodel'; 2 | import { PROPERTIES_DEFAULTS } from '@deneb-viz/core-dependencies'; 3 | 4 | export class SettingsTheme extends formattingSettings.CompositeCard { 5 | name = 'theme'; 6 | displayNameKey = 'Objects_Theme'; 7 | descriptionKey = 'Objects_Theme_Description'; 8 | ordinal = new SettingsThemeGroupOrdinal(Object()); 9 | groups = [this.ordinal]; 10 | } 11 | 12 | class SettingsThemeGroupOrdinal extends formattingSettings.Group { 13 | name = 'ordinal'; 14 | displayNameKey = 'Objects_DataLimit_Group_Loading'; 15 | ordinalColorCount = new formattingSettings.NumUpDown({ 16 | name: 'ordinalColorCount', 17 | displayNameKey: 'Objects_Theme_OrdinalColorCount', 18 | descriptionKey: 'Objects_Theme_OrdinalColorCount_Description', 19 | options: { 20 | minValue: { 21 | value: PROPERTIES_DEFAULTS.theme.ordinalColorCount.min, 22 | type: 0 23 | }, 24 | maxValue: { 25 | value: PROPERTIES_DEFAULTS.theme.ordinalColorCount.max, 26 | type: 1 27 | } 28 | }, 29 | instanceKind: 3 /* VisualEnumerationInstanceKinds.ConstantOrRule */, 30 | value: PROPERTIES_DEFAULTS.theme.ordinalColorCount.default 31 | }); 32 | slices = [this.ordinalColorCount]; 33 | } 34 | -------------------------------------------------------------------------------- /packages/integration-powerbi/src/properties/visual-formatting-settings-model.ts: -------------------------------------------------------------------------------- 1 | import { formattingSettings } from 'powerbi-visuals-utils-formattingmodel'; 2 | 3 | import { SettingsDataLimit } from './settings-data-limit'; 4 | import { SettingsDeveloper } from './settings-developer'; 5 | import { SettingsDisplay } from './settings-display'; 6 | import { SettingsGeneral } from './settings-general'; 7 | import { SettingsTheme } from './settings-theme'; 8 | import { SettingsVega } from './settings-vega'; 9 | import { SettingsEditor } from './settings-editor'; 10 | import { SettingsStateManagement } from './settings-state-management'; 11 | 12 | /** 13 | * Master formatting model for the Power BI formatting pane. 14 | */ 15 | export class VisualFormattingSettingsModel extends formattingSettings.Model { 16 | general = new SettingsGeneral(); 17 | editor = new SettingsEditor(); 18 | theme = new SettingsTheme(); 19 | dataLimit = new SettingsDataLimit(); 20 | display = new SettingsDisplay(); 21 | vega = new SettingsVega(); 22 | stateManagement = new SettingsStateManagement(); 23 | developer = new SettingsDeveloper(); 24 | cards = [ 25 | this.editor, 26 | this.theme, 27 | this.display, 28 | this.dataLimit, 29 | this.stateManagement, 30 | this.vega, 31 | this.developer 32 | ]; 33 | /** 34 | * Check/resolve card visibility based on developer settings. 35 | */ 36 | resolveDeveloperSettings = (developerMode: boolean) => { 37 | if (!developerMode) { 38 | this.developer.visible = false; 39 | this.vega.visible = false; 40 | this.stateManagement.visible = false; 41 | } 42 | }; 43 | } 44 | -------------------------------------------------------------------------------- /packages/integration-powerbi/src/properties/visual-formatting-settings-service.ts: -------------------------------------------------------------------------------- 1 | import powerbi from 'powerbi-visuals-api'; 2 | import { FormattingSettingsService } from 'powerbi-visuals-utils-formattingmodel'; 3 | import { VisualFormattingSettingsModel } from './visual-formatting-settings-model'; 4 | 5 | let formattingSettingsService: FormattingSettingsService = 6 | new FormattingSettingsService(); 7 | 8 | /** 9 | * Used to manage visual formatting settings. 10 | * 11 | * VISUAL FORMATTING SERVICES WILL NOT BE ACCESSIBLE UNLESS THIS IS BOUND. 12 | */ 13 | export const VisualFormattingSettingsService = { 14 | bind: (localizationManager: powerbi.extensibility.ILocalizationManager) => { 15 | formattingSettingsService = new FormattingSettingsService( 16 | localizationManager 17 | ); 18 | } 19 | }; 20 | 21 | /** 22 | * Process the supplied data view into a formatting model. This is a wrapper for the MS `populateFormattingSettingsModel()` method, using 23 | * the bound formatting settings service. 24 | */ 25 | export const getVisualFormattingModel = ( 26 | dataView?: powerbi.DataView 27 | ): VisualFormattingSettingsModel => { 28 | return formattingSettingsService.populateFormattingSettingsModel( 29 | VisualFormattingSettingsModel, 30 | dataView || {} 31 | ); 32 | }; 33 | 34 | /** 35 | * Get the visual formatting service, with the current bound localization manager. 36 | */ 37 | export const getVisualFormattingService = () => formattingSettingsService; 38 | -------------------------------------------------------------------------------- /packages/integration-powerbi/src/signals.ts: -------------------------------------------------------------------------------- 1 | import { 2 | PowerBIContainerSignal, 3 | PowerBIContainerSignalSetterOptions, 4 | SIGNALS_POWERBI 5 | } from '@deneb-viz/core-dependencies'; 6 | 7 | /** 8 | * Get the PowerBI container signal from the provided options. 9 | */ 10 | export const getPowerBiSignalContainer = ( 11 | options?: PowerBIContainerSignalSetterOptions 12 | ): { name: string; value: PowerBIContainerSignal } => { 13 | return { 14 | name: SIGNALS_POWERBI.container, 15 | value: { 16 | height: 17 | options?.container?.clientHeight || 18 | options?.scroll?.height || 19 | 0, 20 | width: 21 | options?.container?.clientWidth || options?.scroll?.width || 0, 22 | scrollHeight: 23 | options?.container?.scrollHeight || 24 | options?.scroll?.scrollHeight || 25 | 0, 26 | scrollWidth: 27 | options?.container?.scrollWidth || 28 | options?.scroll?.scrollWidth || 29 | 0, 30 | scrollTop: 31 | options?.container?.scrollTop || 32 | options?.scroll?.scrollTop || 33 | 0, 34 | scrollLeft: 35 | options?.container?.scrollLeft || 36 | options?.scroll?.scrollLeft || 37 | 0 38 | } 39 | }; 40 | }; 41 | -------------------------------------------------------------------------------- /packages/integration-powerbi/tsconfig.eslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "include": ["src", "tests", ".eslintrc.js"] 4 | } 5 | -------------------------------------------------------------------------------- /packages/integration-powerbi/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@deneb-viz/typescript-config/base.json", 3 | "compilerOptions": { 4 | "outDir": "./dist", 5 | "declarationDir": "./dist" 6 | }, 7 | "include": ["src"], 8 | "exclude": ["node_modules", "dist"] 9 | } 10 | -------------------------------------------------------------------------------- /packages/integration-powerbi/turbo.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://turbo.build/schema.json", 3 | "extends": [ 4 | "//" 5 | ], 6 | "tasks": { 7 | "build": { 8 | "outputs": [ 9 | "dist/**" 10 | ] 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /packages/jest-presets/jest/node/jest-preset.js: -------------------------------------------------------------------------------- 1 | /** @type {import('jest').Config} */ 2 | module.exports = { 3 | maxWorkers: 2, 4 | roots: [''], 5 | transform: { 6 | '^.+\\.tsx?$': 'ts-jest' 7 | }, 8 | moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'], 9 | modulePathIgnorePatterns: [ 10 | '/test/__fixtures__', 11 | '/node_modules', 12 | '/dist' 13 | ], 14 | moduleNameMapper: { 15 | '@deneb-viz/template-usermeta-schema': 16 | '/../template-usermeta-schema/dist/deneb-template-usermeta.json', 17 | // Not ideal, mitigates issues with powerbi-visuals-utils-formattingmodel import 18 | '@deneb-viz/integration-powerbi': 19 | '/../integration-powerbi/dist/index.cjs', 20 | '@deneb-viz/(.*)': '/../$1/src' 21 | }, 22 | preset: 'ts-jest', 23 | collectCoverage: true 24 | }; 25 | -------------------------------------------------------------------------------- /packages/jest-presets/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@deneb-viz/jest-presets", 3 | "version": "0.0.0", 4 | "private": true, 5 | "license": "MIT", 6 | "devDependencies": { 7 | "@types/jest": "^29.5.10", 8 | "bs-logger": "^0.2.6", 9 | "jest": "^29.7.0", 10 | "jest-environment-jsdom": "^29.7.0", 11 | "ts-jest": "^29.1.1" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /packages/json-processing/.eslintrc.js: -------------------------------------------------------------------------------- 1 | /** @type {import("eslint").Linter.Config} */ 2 | module.exports = { 3 | root: true, 4 | extends: ['@deneb-viz/eslint-config/library.js'], 5 | parser: '@typescript-eslint/parser', 6 | parserOptions: { 7 | project: 'tsconfig.eslint.json', 8 | tsconfigRootDir: __dirname, 9 | sourceType: 'module' 10 | }, 11 | env: { 12 | jest: true 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /packages/json-processing/README.md: -------------------------------------------------------------------------------- 1 | # `@deneb-viz/json-processing` 2 | 3 | We do a lot of JSON and [JSONC](https://github.com/microsoft/node-jsonc-parser) processing in Deneb. This package centralizes as much of this as possible. 4 | -------------------------------------------------------------------------------- /packages/json-processing/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@deneb-viz/json-processing", 3 | "version": "0.0.1", 4 | "license": "MIT", 5 | "publishConfig": { 6 | "access": "public" 7 | }, 8 | "repository": { 9 | "type": "git", 10 | "url": "https://github.com/deneb-viz/sadr.git" 11 | }, 12 | "main": "./dist/index.js", 13 | "module": "./dist/index.js", 14 | "types": "./dist/index.d.ts", 15 | "files": [ 16 | "dist" 17 | ], 18 | "browser": { 19 | "canvas": false, 20 | "encoding": false, 21 | "fs": false, 22 | "fs/promises": false, 23 | "net": false, 24 | "tls": false 25 | }, 26 | "tsup": { 27 | "entry": [ 28 | "src/index.ts" 29 | ], 30 | "dts": true, 31 | "format": [ 32 | "cjs", 33 | "esm" 34 | ], 35 | "onSuccess": "npx tsc --emitDeclarationOnly --declaration" 36 | }, 37 | "scripts": { 38 | "dev": "tsup-node --watch", 39 | "build": "tsup-node", 40 | "test": "jest", 41 | "test:watch": "jest --watch", 42 | "eslint": "eslint . --max-warnings 0" 43 | }, 44 | "jest": { 45 | "preset": "@deneb-viz/jest-presets/jest/node" 46 | }, 47 | "devDependencies": { 48 | "@deneb-viz/core-dependencies": "*", 49 | "@deneb-viz/eslint-config": "*", 50 | "@deneb-viz/integration-powerbi": "*", 51 | "@deneb-viz/jest-presets": "*", 52 | "@deneb-viz/template-usermeta-schema": "*", 53 | "@deneb-viz/typescript-config": "*", 54 | "@types/lodash": "^4.14.202", 55 | "ajv": "^8.12.0", 56 | "ajv-formats": "^2.1.1", 57 | "jsonc-parser": "^3.2.1", 58 | "lodash": "^4.17.21", 59 | "powerbi-visuals-api": "5.7.0", 60 | "vega": "6.1.2", 61 | "vega-lite": "6.1.0", 62 | "vscode-json-languageservice": "^5.3.11" 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /packages/json-processing/src/index.ts: -------------------------------------------------------------------------------- 1 | // istanbul ignore file 2 | export { 3 | areAllRemapDataRequirementsMet, 4 | getRemapEligibleFields, 5 | isMappingDialogRequired 6 | } from './field-tracking'; 7 | export { 8 | getJsoncNodeValue, 9 | getJsoncStringAsObject, 10 | getJsoncTree, 11 | getModifiedJsoncByPath, 12 | getParsedJsonWithResult, 13 | getTextFormattedAsJsonC 14 | } from './processing'; 15 | export { areAllCreateDataRequirementsMet } from './template-dataset'; 16 | export { 17 | getExportTemplate, 18 | getNewCreateFromTemplateSliceProperties, 19 | getNewTemplateMetadata, 20 | getResolvedVisualMetadataToDatasetField, 21 | getTemplateMetadata, 22 | getTemplateReplacedForDataset, 23 | getTemplateResolvedForPlaceholderAssignment, 24 | getUpdatedExportMetadata, 25 | getValidatedTemplate 26 | } from './template-usermeta'; 27 | export { 28 | getFriendlyValidationErrors, 29 | getProviderSchema, 30 | getProviderValidator 31 | } from './validation'; 32 | -------------------------------------------------------------------------------- /packages/json-processing/src/template-dataset.ts: -------------------------------------------------------------------------------- 1 | import powerbi from 'powerbi-visuals-api'; 2 | import { 3 | ICreateSliceProperties, 4 | UsermetaDatasetField, 5 | UsermetaDatasetFieldType, 6 | UsermetaTemplate 7 | } from '@deneb-viz/core-dependencies'; 8 | 9 | /** 10 | * Ensure that all requirements are tested and validated before we can create. 11 | */ 12 | export const areAllCreateDataRequirementsMet = (metadata: UsermetaTemplate): Partial => { 13 | const metadataAllFieldsAssigned = areAllTemplateFieldsAssigned(metadata?.dataset); 14 | const metadataDrilldownAssigned = true; // to be implemented when drilldown is implemented 15 | const metadataAllDependenciesAssigned = metadataAllFieldsAssigned && metadataDrilldownAssigned; 16 | return { 17 | metadataAllDependenciesAssigned, 18 | metadataAllFieldsAssigned, 19 | metadataDrilldownAssigned 20 | }; 21 | }; 22 | 23 | /** 24 | * For a given array of template dataset fields, confirm that they all have a field allocated for assignment later on. 25 | */ 26 | export const areAllTemplateFieldsAssigned = (fields: UsermetaDatasetField[]) => 27 | fields?.length === 0 || fields?.filter((f) => !f.suppliedObjectKey)?.length === 0 || false; 28 | 29 | /** 30 | * For a given column or measure (or template placeholder), resolve its type against the corresponding Power BI value 31 | * descriptor. 32 | */ 33 | export const getTemplateDatasetFieldType = (type: powerbi.ValueTypeDescriptor): UsermetaDatasetFieldType => { 34 | switch (true) { 35 | case type?.bool: 36 | return 'bool'; 37 | case type?.text: 38 | return 'text'; 39 | case type?.numeric: 40 | return 'numeric'; 41 | case type?.dateTime: 42 | return 'dateTime'; 43 | default: 44 | return 'other'; 45 | } 46 | }; 47 | -------------------------------------------------------------------------------- /packages/json-processing/tsconfig.eslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "include": ["src", "tests", ".eslintrc.js"] 4 | } 5 | -------------------------------------------------------------------------------- /packages/json-processing/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@deneb-viz/typescript-config/base.json", 3 | "compilerOptions": { 4 | "outDir": "./dist", 5 | "declarationDir": "./dist" 6 | }, 7 | "include": ["src"], 8 | "exclude": ["node_modules", "dist"] 9 | } 10 | -------------------------------------------------------------------------------- /packages/json-processing/turbo.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://turbo.build/schema.json", 3 | "extends": [ 4 | "//" 5 | ], 6 | "tasks": { 7 | "build": { 8 | "outputs": [ 9 | "dist/**" 10 | ] 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /packages/monaco-custom/.eslintrc.js: -------------------------------------------------------------------------------- 1 | /** @type {import("eslint").Linter.Config} */ 2 | module.exports = { 3 | root: true, 4 | extends: ['@deneb-viz/eslint-config/library.js'], 5 | parser: '@typescript-eslint/parser', 6 | parserOptions: { 7 | project: 'tsconfig.eslint.json', 8 | tsconfigRootDir: __dirname, 9 | sourceType: 'module' 10 | }, 11 | env: { 12 | jest: true 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /packages/monaco-custom/README.md: -------------------------------------------------------------------------------- 1 | # `@deneb-viz/worker-spec-tokenizer` 2 | 3 | Once we have tracking information for a spec, we need to be able to tokenize this with the correct metadata, where field names are used. This can be very expensive for extremely large specifications, so this is encapsulated as a web worker so that we can handle this on a separate thread. 4 | -------------------------------------------------------------------------------- /packages/monaco-custom/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@deneb-viz/monaco-custom", 3 | "version": "1.7.0", 4 | "license": "MIT", 5 | "publishConfig": { 6 | "access": "public" 7 | }, 8 | "repository": { 9 | "type": "git", 10 | "url": "https://github.com/deneb-viz/sadr.git" 11 | }, 12 | "main": "./dist/index.js", 13 | "types": "./dist/index.d.ts", 14 | "files": [ 15 | "dist" 16 | ], 17 | "tsup": { 18 | "entry": [ 19 | "./src/index.ts", 20 | "./src/json.worker.ts" 21 | ], 22 | "dts": true, 23 | "format": [ 24 | "cjs" 25 | ], 26 | "onSuccess": "npx tsc --emitDeclarationOnly --declaration", 27 | "treeshake": true 28 | }, 29 | "scripts": { 30 | "dev": "tsup --watch", 31 | "build": "tsup --clean --minify" 32 | }, 33 | "devDependencies": { 34 | "@deneb-viz/typescript-config": "*", 35 | "monaco-editor": "0.45.0" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /packages/monaco-custom/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from 'monaco-editor'; 2 | -------------------------------------------------------------------------------- /packages/monaco-custom/src/json.worker.ts: -------------------------------------------------------------------------------- 1 | // @ts-ignore 2 | export * from 'monaco-editor/esm/vs/language/json/json.worker.js'; 3 | -------------------------------------------------------------------------------- /packages/monaco-custom/tsconfig.eslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "include": ["src", "tests", ".eslintrc.js"] 4 | } 5 | -------------------------------------------------------------------------------- /packages/monaco-custom/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@deneb-viz/typescript-config/base.json", 3 | "compilerOptions": { 4 | "outDir": "./dist", 5 | "declarationDir": "./dist" 6 | }, 7 | "include": ["src"], 8 | "exclude": ["node_modules", "dist"] 9 | } 10 | -------------------------------------------------------------------------------- /packages/monaco-custom/turbo.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://turbo.build/schema.json", 3 | "extends": [ 4 | "//" 5 | ], 6 | "tasks": { 7 | "build": { 8 | "outputs": [ 9 | "dist/**" 10 | ] 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /packages/template-usermeta-schema/.eslintrc.js: -------------------------------------------------------------------------------- 1 | /** @type {import("eslint").Linter.Config} */ 2 | module.exports = { 3 | root: true, 4 | extends: ['@deneb-viz/eslint-config/library.js'], 5 | parser: '@typescript-eslint/parser', 6 | parserOptions: { 7 | project: 'tsconfig.eslint.json', 8 | tsconfigRootDir: __dirname, 9 | sourceType: 'module' 10 | }, 11 | env: { 12 | jest: true 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /packages/template-usermeta-schema/README.md: -------------------------------------------------------------------------------- 1 | # `@deneb-viz/template-usermeta-schema` 2 | 3 | API for working with Deneb's template metadata, which is supplied in the `usermeta` object in a [Vega](https://vega.github.io/vega/) or [Vega-Lite](https://vega.github.io/vega-lite/) specification. The `usermeta` object has no effect on the behavior or parsing of a specification, but is used to indicate that the specification is a valid Deneb template. 4 | 5 | This package is responsible for the production of the JSON schema used to validate this metadata, exists to ensure that there are no circular internal dependencies and contains no other logic. 6 | 7 | ## JSON Schema 8 | 9 | The dev and build process will also compile the interfaces to a JSON schema that you can also use for validation of the structure. This is output to `dist/deneb-template-usermeta.json`. This can be used in tools such as [Ajv](https://ajv.js.org/) to validate an object against the schema if needed. 10 | 11 | #### For Developers of this Module 12 | 13 | Because we are trying to consolidate all declarations in `@deneb-viz/core-dependencies`, the typings are also contained in that package. To keep the build process localized to this package, the root type is exported in [`./src/schema.ts`](./src/schema.ts), and this is referred to by the `schemagen:template-usermeta` npm task. The monorepo `dev` and `build` tasks also just point to this. 14 | -------------------------------------------------------------------------------- /packages/template-usermeta-schema/bin/turbowatch.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import { watch } from 'turbowatch'; 3 | 4 | void watch({ 5 | project: path.resolve(__dirname, '../..'), 6 | triggers: [ 7 | { 8 | expression: [ 9 | 'anyof', 10 | [ 11 | 'allof', 12 | ['dirname', 'node_modules'], 13 | ['dirname', 'dist'], 14 | ['match', '*', 'basename'] 15 | ], 16 | [ 17 | 'allof', 18 | ['not', ['dirname', 'node_modules']], 19 | ['dirname', 'src'], 20 | ['match', '*', 'basename'] 21 | ] 22 | ], 23 | name: 'build', 24 | onChange: async ({ spawn }) => { 25 | return spawn`npm run build`; 26 | } 27 | } 28 | ] 29 | }); 30 | -------------------------------------------------------------------------------- /packages/template-usermeta-schema/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@deneb-viz/template-usermeta-schema", 3 | "version": "1.7.0", 4 | "license": "MIT", 5 | "publishConfig": { 6 | "access": "public" 7 | }, 8 | "repository": { 9 | "type": "git", 10 | "url": "https://github.com/deneb-viz/sadr.git" 11 | }, 12 | "main": "dist/deneb-template-usermeta.json", 13 | "module": "dist/deneb-template-usermeta.json", 14 | "files": [ 15 | "dist" 16 | ], 17 | "scripts": { 18 | "dev": "turbowatch ./bin/turbowatch.ts", 19 | "build": "npm run schemagen:template-usermeta-schema", 20 | "eslint": "eslint . --max-warnings 0", 21 | "schemagen:template-usermeta-schema": "typescript-json-schema ./src/schema.ts UsermetaTemplate -o ./dist/deneb-template-usermeta.json --required --topRef" 22 | }, 23 | "devDependencies": { 24 | "@deneb-viz/core-dependencies": "*", 25 | "@deneb-viz/eslint-config": "*", 26 | "@deneb-viz/typescript-config": "*", 27 | "turbowatch": "^2.29.4", 28 | "typescript-json-schema": "^0.62.0" 29 | } 30 | } -------------------------------------------------------------------------------- /packages/template-usermeta-schema/src/schema.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This is not connected to the rest of the package and should not be included in any APIs, but is 3 | * used to generate the JSON schema for the usermeta template, which is a build artifact. 4 | */ 5 | export type { UsermetaTemplate } from '@deneb-viz/core-dependencies/src/definitions/template-usermeta-schema'; 6 | -------------------------------------------------------------------------------- /packages/template-usermeta-schema/tsconfig.eslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "include": ["src", "tests", ".eslintrc.js"] 4 | } 5 | -------------------------------------------------------------------------------- /packages/template-usermeta-schema/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@deneb-viz/typescript-config/base.json", 3 | "compilerOptions": { 4 | "outDir": "./dist", 5 | "types": ["jest", "node"] 6 | }, 7 | "include": ["src"], 8 | "exclude": ["node_modules", "dist"] 9 | } 10 | -------------------------------------------------------------------------------- /packages/template-usermeta-schema/turbo.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://turbo.build/schema.json", 3 | "extends": [ 4 | "//" 5 | ], 6 | "tasks": { 7 | "build": { 8 | "outputs": [ 9 | "dist/**" 10 | ] 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /packages/typescript-config/base.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/SchemaStore/schemastore/master/src/schemas/json/tsconfig.json", 3 | "display": "Default", 4 | "compilerOptions": { 5 | "declaration": true, 6 | "declarationMap": true, 7 | "esModuleInterop": true, 8 | "incremental": false, 9 | "isolatedModules": true, 10 | "lib": [ 11 | "es2022", 12 | "DOM", 13 | "DOM.Iterable" 14 | ], 15 | "module": "ESNext", 16 | "moduleDetection": "force", 17 | "moduleResolution": "Bundler", 18 | "noUncheckedIndexedAccess": true, 19 | "resolveJsonModule": true, 20 | "skipLibCheck": true, 21 | "strict": true, 22 | "target": "ES2022" 23 | } 24 | } -------------------------------------------------------------------------------- /packages/typescript-config/deneb-pbi.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/SchemaStore/schemastore/master/src/schemas/json/tsconfig.json", 3 | "compilerOptions": { 4 | "allowJs": false, 5 | "jsx": "react", 6 | "types": [ 7 | "react", 8 | "react-dom" 9 | ], 10 | "emitDecoratorMetadata": true, 11 | "experimentalDecorators": true, 12 | "forceConsistentCasingInFileNames": true, 13 | "module": "es6", 14 | "target": "es6", 15 | "sourceMap": true, 16 | "skipLibCheck": true, 17 | "moduleResolution": "bundler", 18 | "declaration": true, 19 | "lib": [ 20 | "es2019", 21 | "dom", 22 | "WebWorker" 23 | ], 24 | "alwaysStrict": true, 25 | "resolveJsonModule": true, 26 | "esModuleInterop": true 27 | } 28 | } -------------------------------------------------------------------------------- /packages/typescript-config/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@deneb-viz/typescript-config", 3 | "version": "0.0.0", 4 | "private": true, 5 | "license": "MIT", 6 | "publishConfig": { 7 | "access": "public" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /packages/typescript-config/worker.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/SchemaStore/schemastore/master/src/schemas/json/tsconfig.json", 3 | "extends": "./base.json", 4 | "compilerOptions": { 5 | "lib": [ 6 | "WebWorker" 7 | ] 8 | } 9 | } -------------------------------------------------------------------------------- /packages/worker-common/.eslintrc.js: -------------------------------------------------------------------------------- 1 | /** @type {import("eslint").Linter.Config} */ 2 | module.exports = { 3 | root: true, 4 | extends: ['@deneb-viz/eslint-config/library.js'], 5 | parser: '@typescript-eslint/parser', 6 | parserOptions: { 7 | project: 'tsconfig.eslint.json', 8 | tsconfigRootDir: __dirname, 9 | sourceType: 'module' 10 | }, 11 | ignorePatterns: ['tsup.config.ts'], 12 | env: { 13 | jest: true 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /packages/worker-common/README.md: -------------------------------------------------------------------------------- 1 | # `@deneb-viz/worker-common` 2 | 3 | Utilities for packaging and encapsulation of web workers, for easier user where they are needed. 4 | 5 | Because workers are bundled .js files (to ensure that all dependencies are present at execution time), this package has a custom loader for the imported .js output from each worker package. This will: 6 | 7 | 1. Import the .js file as raw text. 8 | 2. Convert the text into a blob and object URL. 9 | 3. Load this URL as a new Worker and expose an importable object. 10 | 11 | Anywhere you need a worker, you can import the fully assembled output of the one you want from this package rather than the specific package that defines it (or you can repeat the above in your own implementation). 12 | 13 | # Available Workers 14 | 15 | - `datasetViewerWorker`: Used for the calculation of display widths and formatted values for the dataset viewer in the debug table. 16 | -------------------------------------------------------------------------------- /packages/worker-common/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@deneb-viz/worker-common", 3 | "version": "1.7.0", 4 | "license": "MIT", 5 | "publishConfig": { 6 | "access": "public" 7 | }, 8 | "repository": { 9 | "type": "git", 10 | "url": "https://github.com/deneb-viz/sadr.git" 11 | }, 12 | "main": "./dist/index.js", 13 | "types": "./dist/index.d.ts", 14 | "files": [ 15 | "dist" 16 | ], 17 | "scripts": { 18 | "dev": "tsup --watch ./src --watch ../worker-dataset-viewer/dist/** --watch ../worker-spec-json-processing/dist/** --watch ../monaco-custom/dist/**", 19 | "build": "tsup --clean", 20 | "eslint": "eslint . --max-warnings 0" 21 | }, 22 | "devDependencies": { 23 | "@deneb-viz/core-dependencies": "*", 24 | "@deneb-viz/eslint-config": "*", 25 | "@deneb-viz/typescript-config": "*", 26 | "@deneb-viz/monaco-custom": "*", 27 | "@deneb-viz/worker-dataset-viewer": "*", 28 | "@deneb-viz/worker-spec-json-processing": "*" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /packages/worker-common/tsconfig.eslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "include": ["src", "tests", ".eslintrc.js"] 4 | } 5 | -------------------------------------------------------------------------------- /packages/worker-common/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@deneb-viz/typescript-config/base.json", 3 | "compilerOptions": { 4 | "outDir": "./dist", 5 | "declarationDir": "./dist" 6 | }, 7 | "include": ["src"], 8 | "exclude": ["node_modules", "dist"] 9 | } 10 | -------------------------------------------------------------------------------- /packages/worker-common/tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'tsup'; 2 | import fs from 'fs'; 3 | 4 | export default defineConfig({ 5 | entry: ['src/index.ts'], 6 | dts: true, 7 | format: ['cjs'], 8 | esbuildPlugins: [ 9 | { 10 | name: 'raw', 11 | setup(build) { 12 | build.onLoad({ filter: /\.js$/ }, (args) => { 13 | return { 14 | contents: fs.readFileSync(args.path, 'utf8'), 15 | loader: 'text' 16 | }; 17 | }); 18 | } 19 | } 20 | ], 21 | onSuccess: 'npx tsc --emitDeclarationOnly --declaration' 22 | }); 23 | -------------------------------------------------------------------------------- /packages/worker-common/turbo.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://turbo.build/schema.json", 3 | "extends": [ 4 | "//" 5 | ], 6 | "tasks": { 7 | "build": { 8 | "outputs": [ 9 | "dist/**" 10 | ] 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /packages/worker-dataset-viewer/.eslintrc.js: -------------------------------------------------------------------------------- 1 | /** @type {import("eslint").Linter.Config} */ 2 | module.exports = { 3 | root: true, 4 | extends: ['@deneb-viz/eslint-config/library.js'], 5 | parser: '@typescript-eslint/parser', 6 | parserOptions: { 7 | project: 'tsconfig.eslint.json', 8 | tsconfigRootDir: __dirname, 9 | sourceType: 'module' 10 | }, 11 | env: { 12 | jest: true 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /packages/worker-dataset-viewer/README.md: -------------------------------------------------------------------------------- 1 | # `@deneb-viz/worker-dataset-viewer` 2 | 3 | Web worker used to process dataset viewer table content in a separate thread, so that we don't block the main thread. 4 | -------------------------------------------------------------------------------- /packages/worker-dataset-viewer/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@deneb-viz/worker-dataset-viewer", 3 | "version": "1.7.0", 4 | "license": "MIT", 5 | "publishConfig": { 6 | "access": "public" 7 | }, 8 | "repository": { 9 | "type": "git", 10 | "url": "https://github.com/deneb-viz/sadr.git" 11 | }, 12 | "main": "./dist/index.js", 13 | "types": "./dist/index.d.ts", 14 | "files": [ 15 | "dist" 16 | ], 17 | "tsup": { 18 | "entry": [ 19 | "src/index.ts" 20 | ], 21 | "dts": true, 22 | "format": [ 23 | "cjs" 24 | ], 25 | "onSuccess": "npx tsc --emitDeclarationOnly --declaration" 26 | }, 27 | "scripts": { 28 | "dev": "tsup --watch", 29 | "build": "tsup --clean --minify", 30 | "eslint": "eslint . --max-warnings 0" 31 | }, 32 | "devDependencies": { 33 | "@deneb-viz/core-dependencies": "*", 34 | "@deneb-viz/eslint-config": "*", 35 | "@deneb-viz/typescript-config": "*" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /packages/worker-dataset-viewer/tsconfig.eslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "include": ["src", "tests", ".eslintrc.js"] 4 | } 5 | -------------------------------------------------------------------------------- /packages/worker-dataset-viewer/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@deneb-viz/typescript-config/worker.json", 3 | "compilerOptions": { 4 | "outDir": "./dist", 5 | "declarationDir": "./dist" 6 | }, 7 | "include": ["src"], 8 | "exclude": ["node_modules", "dist"] 9 | } 10 | -------------------------------------------------------------------------------- /packages/worker-dataset-viewer/turbo.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://turbo.build/schema.json", 3 | "extends": [ 4 | "//" 5 | ], 6 | "tasks": { 7 | "build": { 8 | "outputs": [ 9 | "dist/**" 10 | ] 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /packages/worker-spec-json-processing/.eslintrc.js: -------------------------------------------------------------------------------- 1 | /** @type {import("eslint").Linter.Config} */ 2 | module.exports = { 3 | root: true, 4 | extends: ['@deneb-viz/eslint-config/library.js'], 5 | parser: '@typescript-eslint/parser', 6 | parserOptions: { 7 | project: 'tsconfig.eslint.json', 8 | tsconfigRootDir: __dirname, 9 | sourceType: 'module' 10 | }, 11 | env: { 12 | jest: true 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /packages/worker-spec-json-processing/README.md: -------------------------------------------------------------------------------- 1 | # `@deneb-viz/worker-spec-fields-in-use` 2 | 3 | To ensure that we understand which dataset fields are included in a JSON spec, we need to process it. This can be very expensive for extremely large specifications, so this is encapsulated as a web worker so that we can handle this on a separate thread. 4 | -------------------------------------------------------------------------------- /packages/worker-spec-json-processing/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@deneb-viz/worker-spec-json-processing", 3 | "version": "1.7.0", 4 | "license": "MIT", 5 | "publishConfig": { 6 | "access": "public" 7 | }, 8 | "repository": { 9 | "type": "git", 10 | "url": "https://github.com/deneb-viz/sadr.git" 11 | }, 12 | "main": "./dist/index.js", 13 | "types": "./dist/index.d.ts", 14 | "files": [ 15 | "dist" 16 | ], 17 | "tsup": { 18 | "entry": [ 19 | "src/index.ts" 20 | ], 21 | "dts": true, 22 | "format": [ 23 | "cjs" 24 | ], 25 | "onSuccess": "npx tsc --emitDeclarationOnly --declaration", 26 | "treeshake": true 27 | }, 28 | "scripts": { 29 | "dev": "tsup --watch", 30 | "build": "tsup --clean --minify", 31 | "test": "jest", 32 | "test:watch": "jest --watch", 33 | "eslint": "eslint . --max-warnings 0" 34 | }, 35 | "jest": { 36 | "preset": "@deneb-viz/jest-presets/jest/node" 37 | }, 38 | "devDependencies": { 39 | "@deneb-viz/core-dependencies": "*", 40 | "@deneb-viz/eslint-config": "*", 41 | "@deneb-viz/integration-powerbi": "*", 42 | "@deneb-viz/jest-presets": "*", 43 | "@deneb-viz/typescript-config": "*", 44 | "jsonc-parser": "^3.2.1", 45 | "vega-expression": "^5.1.0" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /packages/worker-spec-json-processing/src/index.ts: -------------------------------------------------------------------------------- 1 | import { IDenebJsonProcessingWorkerRequest } from '@deneb-viz/core-dependencies'; 2 | import { getTrackingDataFromSpecification } from './field-tracking'; 3 | import { getTokenizedSpec } from './tokenizer'; 4 | import { getRemappedSpecification } from './remapping'; 5 | 6 | /** 7 | * Handle dataset processing requests from the main thread. 8 | */ 9 | self.onmessage = (e: MessageEvent) => { 10 | switch (e.data.type) { 11 | case 'tracking': 12 | return self.postMessage({ 13 | type: e.data.type, 14 | payload: getTrackingDataFromSpecification(e.data.payload) 15 | }); 16 | case 'tokenization': 17 | return self.postMessage({ 18 | type: e.data.type, 19 | payload: getTokenizedSpec(e.data.payload) 20 | }); 21 | case 'remapping': 22 | return self.postMessage({ 23 | type: e.data.type, 24 | payload: getRemappedSpecification(e.data.payload) 25 | }); 26 | } 27 | }; 28 | -------------------------------------------------------------------------------- /packages/worker-spec-json-processing/src/remapping.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Node, 3 | applyEdits, 4 | findNodeAtLocation, 5 | getNodeValue, 6 | modify, 7 | parseTree 8 | } from 'jsonc-parser'; 9 | import { 10 | IDenebRemapRequestPayload, 11 | IDenebRemapResponsePayload, 12 | stringToUint8Array, 13 | uint8ArrayToString 14 | } from '@deneb-viz/core-dependencies'; 15 | 16 | /** 17 | * For the supplied tokenized specification and re-mapping information, traverse all re-mapping fields needed and 18 | * replace the placeholder with the supplied object name. 19 | */ 20 | export const getRemappedSpecification = ( 21 | options: IDenebRemapRequestPayload 22 | ): IDenebRemapResponsePayload => { 23 | let spec = uint8ArrayToString(options.spec); 24 | const { remapFields, trackedFields } = options; 25 | Object.entries(remapFields).forEach(([, field]) => { 26 | const source = trackedFields[field.key]; 27 | (source?.paths ?? []).forEach((p) => { 28 | const tree = parseTree(spec) as Node; 29 | const node = findNodeAtLocation(tree, p) as Node; 30 | const value = getNodeValue(node); 31 | const newValue = value.replaceAll( 32 | source?.placeholder ?? '', 33 | field.suppliedObjectName 34 | ); 35 | const edit = modify(spec, p, newValue, {}); 36 | spec = applyEdits(spec, edit); 37 | }); 38 | }); 39 | return { spec: stringToUint8Array(spec) }; 40 | }; 41 | -------------------------------------------------------------------------------- /packages/worker-spec-json-processing/tsconfig.eslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "include": ["src", "tests", ".eslintrc.js"] 4 | } 5 | -------------------------------------------------------------------------------- /packages/worker-spec-json-processing/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@deneb-viz/typescript-config/base.json", 3 | "compilerOptions": { 4 | "outDir": "./dist", 5 | "declarationDir": "./dist" 6 | }, 7 | "include": ["src"], 8 | "exclude": ["node_modules", "dist"] 9 | } 10 | -------------------------------------------------------------------------------- /packages/worker-spec-json-processing/turbo.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://turbo.build/schema.json", 3 | "extends": [ 4 | "//" 5 | ], 6 | "tasks": { 7 | "build": { 8 | "outputs": [ 9 | "dist/**" 10 | ] 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /pbiviz.json: -------------------------------------------------------------------------------- 1 | { 2 | "visual": { 3 | "name": "deneb", 4 | "displayName": "Deneb", 5 | "guid": "deneb7E15AEF80B9E4D4F8E12924291ECE89A", 6 | "visualClassName": "Deneb", 7 | "version": "1.7.3.0", 8 | "description": "Use the Vega or Vega-Lite languages to create bespoke visuals, directly inside Power BI.", 9 | "supportUrl": "https://deneb-viz.github.io/", 10 | "gitHubUrl": "https://github.com/deneb-viz/deneb" 11 | }, 12 | "apiVersion": "5.7.0", 13 | "author": { 14 | "name": "Daniel Marsh-Patrick", 15 | "email": "daniel@coacervo.co" 16 | }, 17 | "assets": { 18 | "icon": "assets/icon.png" 19 | }, 20 | "style": "style/visual.less", 21 | "capabilities": "capabilities.json", 22 | "dependencies": null, 23 | "stringResources": [] 24 | } -------------------------------------------------------------------------------- /src/components/App.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { VisualInterface } from '../features/interface'; 4 | import { logRender } from '../features/logging'; 5 | 6 | const App = () => { 7 | logRender('App'); 8 | return ; 9 | }; 10 | 11 | export default App; 12 | -------------------------------------------------------------------------------- /src/constants/dataset.ts: -------------------------------------------------------------------------------- 1 | import { dataRoles } from '../../capabilities.json'; 2 | 3 | /** 4 | * The name to use for the dataset in Deneb, bound from the data view, into the 5 | * Vega view as a named reference. 6 | */ 7 | export const DATASET_NAME = dataRoles[0].name; 8 | 9 | /** 10 | * The name we use to denote a row in the datset, which is also used for 11 | * reconciliation of selectors. 12 | */ 13 | export const DATASET_ROW_NAME = '__row__'; 14 | 15 | /** 16 | * The name we use to denote a field in the dataset that holds a selection ID 17 | */ 18 | export const DATASET_IDENTITY_NAME = '__identity__'; 19 | 20 | /** 21 | * The name we use to denote a field in the dataset used to hold the string- 22 | * ified representation of the identity, and therefore use for comparison 23 | * operations and suchlike. 24 | */ 25 | export const DATASET_KEY_NAME = '__key__'; 26 | 27 | /*** 28 | * The name we use to denote a data point's selection status within the 29 | * dataset. 30 | */ 31 | export const DATASET_SELECTED_NAME = '__selected__'; 32 | 33 | /** 34 | * The name we wish to use for handling the Drilldown data role from the data 35 | * view. 36 | */ 37 | export const DATASET_ROLE_DRILLDOWN = dataRoles[1]?.name; 38 | 39 | /** 40 | * Because Drilldown can be multi-level, and we don't know how many there are, 41 | * we provide a special column which concatenates and formats the supplied 42 | * columns, which can be used like how core charts tend to do this. 43 | */ 44 | export const DATASET_ROLE_DRILLDOWN_FLAT = DATASET_ROLE_DRILLDOWN?.replace( 45 | /(__$)/, 46 | '_flat$1' 47 | ); 48 | -------------------------------------------------------------------------------- /src/constants/index.ts: -------------------------------------------------------------------------------- 1 | export * from './dataset'; 2 | export * from './editor-layout'; 3 | export * from './interactivity'; 4 | export * from './interface-advanced-editor'; 5 | export * from './interface-debug'; 6 | export * from './table-view'; 7 | export * from './toaster'; 8 | export * from './vega-extensibility'; 9 | export * from './vega-output'; 10 | -------------------------------------------------------------------------------- /src/constants/interactivity.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Denotes the name of the dataset that contains the cross filter state. 3 | */ 4 | export const CROSS_FILTER_STATE_DATASET_NAME = 'dataset_cross_filter_context'; 5 | 6 | /** 7 | * Denotes how we suffix fields in the dataset that contain highlight values. 8 | */ 9 | export const HIGHLIGHT_FIELD_SUFFIX = '__highlight'; 10 | 11 | /** 12 | * Denotes how we suffix fields that contain the status of a highlight value, 13 | * which can be used for conditional checks without resorting to more complex 14 | * expressions than necessary. 15 | */ 16 | export const HIGHLIGHT_STATUS_SUFFIX = `${HIGHLIGHT_FIELD_SUFFIX}Status`; 17 | 18 | /** 19 | * Denotes how we suffix fields that contain thew comparison of a highlight 20 | * value to its original value, which can be used for conditional checks 21 | * without resorting to more complex expressions than necessary. 22 | */ 23 | export const HIGHLIGHT_COMPARATOR_SUFFIX = `${HIGHLIGHT_FIELD_SUFFIX}Comparator`; 24 | -------------------------------------------------------------------------------- /src/constants/interface-advanced-editor.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * ID for the element we use to render the editor portal. 3 | */ 4 | export const PORTAL_ROOT_ID = 'portal-root'; 5 | 6 | /** 7 | * Name to use when building classes for error annotations and markers. 8 | */ 9 | export const EDITOR_INDICATOR_ERROR_NAME = 'error'; 10 | /** 11 | * Name to use when building classes for warning annotations and markers. 12 | */ 13 | export const EDITOR_INDICATOR_WARNING_NAME = 'warning'; 14 | -------------------------------------------------------------------------------- /src/constants/interface-debug.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Default name to use in the data viewer. 3 | */ 4 | export const DEBUG_DEFAULT_DATASET_NAME = 'dataset'; 5 | /** 6 | * The name of the root dataset that Vega generates, to filter out when 7 | * deriving from the view. 8 | */ 9 | export const DEBUG_ROOT_DATASET_NAME = 'root'; 10 | -------------------------------------------------------------------------------- /src/constants/table-view.ts: -------------------------------------------------------------------------------- 1 | import { 2 | DATASET_ROW_NAME, 3 | DATASET_IDENTITY_NAME, 4 | DATASET_KEY_NAME, 5 | DATASET_SELECTED_NAME 6 | } from './dataset'; 7 | 8 | /** 9 | * List of reserved words for column headers that can benefit from specific 10 | * tooltips explaining their purpose 11 | */ 12 | export const TABLE_COLUMN_RESERVED_WORDS = [ 13 | DATASET_ROW_NAME, 14 | DATASET_IDENTITY_NAME, 15 | DATASET_KEY_NAME, 16 | DATASET_SELECTED_NAME 17 | ]; 18 | 19 | /** 20 | * The maximum level of recursion to use when evaluating a table value (in case 21 | * it's a complex object. 22 | */ 23 | export const TABLE_VALUE_MAX_DEPTH = 3; 24 | 25 | /** 26 | * The maximum permitted length of a value for a table cell before 'truncating' 27 | * it for display. 28 | */ 29 | export const TABLE_VALUE_MAX_LENGTH = 150; 30 | -------------------------------------------------------------------------------- /src/constants/toaster.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * ID used for the apply changes toast notification. 3 | */ 4 | export const TOAST_NOTIFICATION_ID_APPLY_CHANGES = 'apply-changes'; 5 | 6 | /** 7 | * ID used for the cross filter exceeded toast notification. 8 | */ 9 | export const TOAST_NOTIFICATION_ID_CROSS_FILTER_EXCEEDED = 10 | 'cross-filter-exceeded'; 11 | 12 | /** 13 | * The timeout to apply for toast notifications. 14 | */ 15 | export const TOAST_NOTIFICATION_TIMEOUT = -1; 16 | 17 | /** 18 | * Horizontal offset (in px) for toaster in editor view. 19 | */ 20 | export const TOAST_OFFSET_HORIZONTAL_EDITOR = 5; 21 | 22 | /** 23 | * Vertical offset (in px) for toaster in editor view. 24 | */ 25 | export const TOAST_OFFSET_VERTICAL_EDITOR = 30; 26 | 27 | /** 28 | * Horizontal offset (in px) for toaster in visual view. 29 | */ 30 | export const TOAST_OFFSET_HORIZONTAL_VISUAL = 2; 31 | 32 | /** 33 | * Vertical offset (in px) for toaster in visual view. 34 | */ 35 | export const TOAST_OFFSET_VERTICAL_VISUAL = -15; 36 | 37 | /** 38 | * ID used for the notification toaster. 39 | */ 40 | export const TOASTER_ID = 'deneb-toaster'; 41 | -------------------------------------------------------------------------------- /src/constants/vega-extensibility.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Divergent color scheme from Power BI to Vega view. 3 | */ 4 | export const VEGA_SCHEME_POWERBI_DIVERGENT = 'pbiColorDivergent'; 5 | /** 6 | * Linear color scheme from Power BI to Vega view. 7 | */ 8 | export const VEGA_SCHEME_POWERBI_LINEAR = 'pbiColorLinear'; 9 | /** 10 | * Nominal color scheme from Power BI to Vega view. 11 | */ 12 | export const VEGA_SCHEME_POWERBI_NOMINAL = 'pbiColorNominal'; 13 | /** 14 | * Ordinal color scheme from Power BI to Vega view. 15 | */ 16 | export const VEGA_SCHEME_POWERBI_ORDINAL = 'pbiColorOrdinal'; 17 | -------------------------------------------------------------------------------- /src/constants/vega-output.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * ID of the container element for the Vega output. 3 | */ 4 | export const VEGA_CONTAINER_ID = 'deneb-vega-container'; 5 | 6 | /** 7 | * Number of pixels to adjust the viewport for, to prevent overflow kicking in. 8 | */ 9 | export const VEGA_VIEWPORT_ADJUST = 4; 10 | -------------------------------------------------------------------------------- /src/core/ui/index.ts: -------------------------------------------------------------------------------- 1 | export * as advancedEditor from './advancedEditor'; 2 | export * as commands from './commands'; 3 | export * as dom from './dom'; 4 | 5 | export { TEditorPosition }; 6 | 7 | /** 8 | * Type to allow structure of the value for position of editor within the advanced editor view. 9 | */ 10 | type TEditorPosition = 'left' | 'right'; 11 | -------------------------------------------------------------------------------- /src/core/vega/index.ts: -------------------------------------------------------------------------------- 1 | export { 2 | IVegaViewDatum, 3 | getVegaProvider, 4 | getVegaProvideri18n, 5 | getVegaSettings, 6 | getVegaVersion 7 | }; 8 | 9 | import { getState } from '../../store'; 10 | import { getI18nValue } from '../../features/i18n'; 11 | import { PROVIDER_VERSIONS } from '../../../config'; 12 | import { SpecProvider } from '@deneb-viz/core-dependencies'; 13 | 14 | /** 15 | * Interface specifying a flexible key/value pair object, which is supplied from Vega's tooltip handler and usually casted as `any`. 16 | */ 17 | interface IVegaViewDatum { 18 | [key: string]: any; 19 | } 20 | 21 | /** 22 | * Convenience function to get current Vega provider from persisted properties. 23 | */ 24 | const getVegaProvider = () => 25 | getVegaSettings().output.provider.value; 26 | 27 | /** 28 | * Get the Vega provider, resolved for i18n. 29 | */ 30 | const getVegaProvideri18n = (provider?: SpecProvider) => { 31 | const resolved = provider ?? getVegaProvider(); 32 | return getI18nValue( 33 | resolved === 'vegaLite' ? 'Provider_VegaLite' : 'Provider_Vega' 34 | ); 35 | }; 36 | 37 | /** 38 | * For the current provider, get the version from our package configuration. 39 | */ 40 | const getVegaVersion = () => PROVIDER_VERSIONS[getVegaProvider()]; 41 | 42 | /** 43 | * Convenience function to get current Vega/Spec settings from the visual objects (as we use this a lot). 44 | */ 45 | const getVegaSettings = () => getState().visualSettings.vega; 46 | -------------------------------------------------------------------------------- /src/features/commands/types.ts: -------------------------------------------------------------------------------- 1 | import { InterfaceMode } from '../interface'; 2 | import { ISpecification } from '../specification'; 3 | 4 | /** 5 | * Denotes the command a toolbar button invokes, which allows us to drive the 6 | * rendering and logic whilst using a common component. 7 | */ 8 | export type Command = 9 | | 'applyChanges' 10 | | 'autoApplyToggle' 11 | | 'debugPaneShowData' 12 | | 'debugPaneShowLogs' 13 | | 'debugPaneShowSignals' 14 | | 'debugPaneToggle' 15 | | 'discardChanges' 16 | | 'editorFocusOut' 17 | | 'editorPaneToggle' 18 | | 'fieldMappings' 19 | | 'navigateConfig' 20 | | 'navigateSettings' 21 | | 'navigateSpecification' 22 | | 'newSpecification' 23 | | 'exportSpecification' 24 | | 'helpSite' 25 | | 'themeToggle' 26 | | 'zoomFit' 27 | | 'zoomIn' 28 | | 'zoomLevel' 29 | | 'zoomOut' 30 | | 'zoomReset'; 31 | 32 | export interface IExportSpecCommandTestOptions { 33 | editorIsDirty: boolean; 34 | specification: ISpecification; 35 | interfaceMode: InterfaceMode; 36 | } 37 | 38 | /** 39 | * For other zoom commands, these are the things we need to test. 40 | */ 41 | export interface IZoomOtherCommandTestOptions { 42 | specification: ISpecification; 43 | interfaceMode: InterfaceMode; 44 | } 45 | 46 | /** 47 | * For zoom level-related commands, these are the things we need to test. 48 | */ 49 | export interface IZoomLevelCommandTestOptions { 50 | value: number; 51 | specification: ISpecification; 52 | interfaceMode: InterfaceMode; 53 | } 54 | -------------------------------------------------------------------------------- /src/features/dataset/drilldown.ts: -------------------------------------------------------------------------------- 1 | import powerbi from 'powerbi-visuals-api'; 2 | import PrimitiveValue = powerbi.PrimitiveValue; 3 | 4 | import { powerBiFormatValue } from '../../utils'; 5 | import { FEATURES } from '../../../config'; 6 | 7 | /** 8 | * Convenience check for statis of Drilldown feature flag. 9 | */ 10 | export const isDrilldownFeatureEnabled = () => FEATURES.data_drilldown; 11 | 12 | /** 13 | * For the supplied column/value, process it into an array of all drilldown 14 | * values for that row. Returns a formatted array of all known values. 15 | */ 16 | export const resolveDrilldownComponents = ( 17 | current: PrimitiveValue[], 18 | value: PrimitiveValue, 19 | format: string 20 | ) => { 21 | const formatted = powerBiFormatValue(value, format); 22 | return current ? [...current, formatted] : [formatted]; 23 | }; 24 | 25 | /** 26 | * For the supplied column/value, process it into a single string value, 27 | * delimited by a space, much like how core visuals might do. 28 | */ 29 | export const resolveDrilldownFlat = ( 30 | current: PrimitiveValue, 31 | value: PrimitiveValue, 32 | format: string 33 | ) => { 34 | const formatted = powerBiFormatValue(value, format); 35 | return current ? `${current} ${formatted}` : formatted; 36 | }; 37 | -------------------------------------------------------------------------------- /src/features/dataset/formatting.ts: -------------------------------------------------------------------------------- 1 | import powerbi from 'powerbi-visuals-api'; 2 | import DataViewValueColumn = powerbi.DataViewValueColumn; 3 | 4 | /** 5 | * Test if a data view field is numeric or date/time valued. if so, then we 6 | * should provide formatting support fields for it. 7 | */ 8 | export const isDataViewFieldEligibleForFormatting = ( 9 | field: DataViewValueColumn 10 | ) => field.source.type.numeric || field.source.type.dateTime; 11 | -------------------------------------------------------------------------------- /src/features/dataset/index.ts: -------------------------------------------------------------------------------- 1 | export { 2 | isDrilldownFeatureEnabled, 3 | resolveDrilldownComponents, 4 | resolveDrilldownFlat 5 | } from './drilldown'; 6 | 7 | export { isDataViewFieldEligibleForFormatting } from './formatting'; 8 | -------------------------------------------------------------------------------- /src/features/debug-area/components/data-table-cell.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { getCellTooltip } from '../data-table'; 3 | 4 | interface IDataTableCellProps { 5 | displayValue: string; 6 | field: string; 7 | rawValue: any; 8 | } 9 | 10 | /** 11 | * Handles rendering a cell in the data table, with a tooltip, and allowing 12 | * for any formatting specifics (such as truncating long values). 13 | */ 14 | export const DataTableCell: React.FC = ({ 15 | displayValue, 16 | field, 17 | rawValue 18 | }) => { 19 | const tooltipValue = getCellTooltip(field, rawValue); 20 | return
{displayValue}
; 21 | }; 22 | -------------------------------------------------------------------------------- /src/features/debug-area/components/no-data-message.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { shallow } from 'zustand/shallow'; 3 | import { useDebugStyles } from '..'; 4 | import store from '../../../store'; 5 | import { StatusBarContainer } from '../../interface'; 6 | import { getI18nValue } from '../../i18n'; 7 | import { DatasetViewerOptions } from './dataset-viewer-options'; 8 | 9 | /** 10 | * Displays when no data is available in the data table. 11 | */ 12 | export const NoDataMessage: React.FC = () => { 13 | const { mode } = store( 14 | (state) => ({ mode: state.editorPreviewAreaSelectedPivot }), 15 | shallow 16 | ); 17 | const classes = useDebugStyles(); 18 | const message = getI18nValue( 19 | mode === 'data' 20 | ? 'Text_Debug_Data_No_Data' 21 | : 'Text_Debug_Signal_No_Data' 22 | ); 23 | const options = mode === 'data' ? : null; 24 | return ( 25 |
26 |
27 |
28 |
29 | {message} 30 |
31 | 32 |
{options}
33 |
34 |
35 |
36 |
37 | ); 38 | }; 39 | -------------------------------------------------------------------------------- /src/features/debug-area/components/processing-data-message.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Spinner } from '@fluentui/react-components'; 3 | 4 | import { useDebugStyles } from '..'; 5 | import { StatusBarContainer } from '../../interface'; 6 | import { getI18nValue } from '../../i18n'; 7 | 8 | /** 9 | * Displays when a dataset in the data table is being processed. 10 | */ 11 | export const ProcessingDataMessage: React.FC = () => { 12 | const classes = useDebugStyles(); 13 | const message = getI18nValue('Text_Debug_Data_Processing'); 14 | return ( 15 |
16 |
17 |
18 |
19 | 20 |
21 | 22 |
23 | 24 |
25 |
26 |
27 | ); 28 | }; 29 | -------------------------------------------------------------------------------- /src/features/debug-area/logging.ts: -------------------------------------------------------------------------------- 1 | import { CAPABILITIES } from '../../../config'; 2 | import { ICapabilitiesEnumMember } from '../settings'; 3 | 4 | /** 5 | * Used to exlude debug level entries from the UI. 6 | */ 7 | const DEBUG_LOG_LEVEL = 4; 8 | 9 | /** 10 | * Get the log levels from the capabiliites, adjusting for debug, as we don't 11 | * want to include this in the UI. 12 | */ 13 | export const getDebugLogLevels = (): ICapabilitiesEnumMember[] => 14 | CAPABILITIES.objects.vega.properties.logLevel.type.enumeration.filter( 15 | (e: ICapabilitiesEnumMember) => e.value !== `${DEBUG_LOG_LEVEL}` 16 | ); 17 | -------------------------------------------------------------------------------- /src/features/debug-area/types.ts: -------------------------------------------------------------------------------- 1 | export interface ILogEntry { 2 | level: number; 3 | message: string; 4 | } 5 | 6 | export interface ILogEntryDisplay extends ILogEntry { 7 | i18nLevel: string; 8 | } 9 | 10 | export interface ILogLevel { 11 | level: number; 12 | i18n: string; 13 | } 14 | 15 | /** 16 | * Tracks how a table value should be formatted when working out cell content. 17 | */ 18 | export interface ITableFormattedValue { 19 | formatted: string; 20 | tooLong: boolean; 21 | } 22 | 23 | /** 24 | * Specifes navigation operations on the data table. We can use this as a 25 | * property in a generic component to handle repetitive code. 26 | */ 27 | export type DataTableNavigationType = 'first' | 'last' | 'next' | 'previous'; 28 | 29 | /** 30 | * Represents a row of data in the table for presenting signals and values. 31 | */ 32 | export interface ISignalTableDataRow { 33 | key: string; 34 | value: any; 35 | } 36 | -------------------------------------------------------------------------------- /src/features/i18n/types.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * List of supported locales used for developer mode locale/formatting testing, 3 | * without having to reconfigure the Power BI Service. 4 | */ 5 | export type TLocale = 'en-US' | 'de-DE' | 'fr-FR'; 6 | 7 | /** 8 | * Represents all i18n date and time formats available for D3 that we can add 9 | * to the visual. 10 | */ 11 | export interface ILocaleConfiguration { 12 | default: string; 13 | format: ILocaleFormatConfiguration; 14 | timeFormat: ILocaleTimeConfiguration; 15 | } 16 | 17 | interface ILocaleFormatConfiguration { 18 | [key: string]: Record; 19 | } 20 | 21 | interface ILocaleTimeConfiguration { 22 | [key: string]: Record; 23 | } 24 | -------------------------------------------------------------------------------- /src/features/interactivity/context-menu.ts: -------------------------------------------------------------------------------- 1 | import { ScenegraphEvent, Item } from 'vega'; 2 | 3 | import { getVegaSettings } from '../../core/vega'; 4 | import { getIdentitiesFromData, resolveDataFromItem } from './data-point'; 5 | import { 6 | getVisualInteractionStatus, 7 | getVisualSelectionManager 8 | } from '../visual-host'; 9 | 10 | /** 11 | * If a context menu event is fired over the visual, attempt to retrieve any 12 | * datum and associated identity, before displaying the context menu. 13 | * 14 | * Note that the context menu can only work with a single selector, so we will 15 | * only return a selector if it resolves to a single entry, otherwise drill 16 | * through doesn't actually result in the correct data being displayed in the 17 | * D/T page. This is currently observed in Charticulator and it looks like the 18 | * core visuals avoid this situation, so we'll try to do the same for now. 19 | */ 20 | export const handleContextMenuEvent = (event: ScenegraphEvent, item: Item) => { 21 | event.stopPropagation(); 22 | const mouseEvent: MouseEvent = window.event; 23 | const data = resolveDataFromItem(item); 24 | const identities = getIdentitiesFromData(data); 25 | const identity = 26 | (isContextMenuPropSet() && identities?.length === 1 && identities[0]) || 27 | null; 28 | mouseEvent && mouseEvent.preventDefault(); 29 | getVisualSelectionManager().showContextMenu(identity, { 30 | x: mouseEvent.clientX, 31 | y: mouseEvent.clientY 32 | }); 33 | }; 34 | 35 | /** 36 | * Allows us to validate for all key pre-requisites before we can bind a context 37 | * menu event to the visual. 38 | */ 39 | const isContextMenuPropSet = () => { 40 | const { 41 | interactivity: { 42 | enableContextMenu: { value: enableContextMenu } 43 | } 44 | } = getVegaSettings(); 45 | return (enableContextMenu && getVisualInteractionStatus()) || false; 46 | }; 47 | -------------------------------------------------------------------------------- /src/features/interactivity/index.ts: -------------------------------------------------------------------------------- 1 | export { 2 | clearSelection, 3 | dispatchCrossFilterAbort, 4 | getDataPointCrossFilterStatus, 5 | isCrossFilterPropSet 6 | } from './cross-filter'; 7 | export { 8 | getCrossHighlightFieldBaseMeasureName, 9 | isCrossHighlightField, 10 | isCrossHighlightComparatorField, 11 | isCrossHighlightPropSet, 12 | isCrossHighlightStatusField 13 | } from './cross-highlight'; 14 | export { createSelectionIds } from './data-point'; 15 | export { 16 | getPowerBiTooltipHandler, 17 | getSanitisedTooltipValue, 18 | hidePowerBiTooltip 19 | } from './tooltip'; 20 | export * from './types'; 21 | -------------------------------------------------------------------------------- /src/features/interface/components/advanced-editor.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { shallow } from 'zustand/shallow'; 3 | 4 | import store from '../../../store'; 5 | import { useKonami as fourd3d3d } from 'react-konami-code'; 6 | import { logRender } from '../../logging'; 7 | import { JsonEditorContextProvider } from '../../json-editor'; 8 | import { AdvancedEditorInterface } from './advanced-editor-interface'; 9 | 10 | export const AdvancedEditor: React.FC = () => { 11 | const { setVisual4d3d3d } = store( 12 | (state) => ({ 13 | setVisual4d3d3d: state.setVisual4d3d3d 14 | }), 15 | shallow 16 | ); 17 | fourd3d3d(() => { 18 | setVisual4d3d3d(true); 19 | }); 20 | logRender('AdvancedEditorInterface'); 21 | return ( 22 | 23 | 24 | 25 | ); 26 | }; 27 | -------------------------------------------------------------------------------- /src/features/interface/components/hyperlink.tsx: -------------------------------------------------------------------------------- 1 | import { Link, makeStyles } from '@fluentui/react-components'; 2 | import React from 'react'; 3 | import { launchUrl } from '../../visual-host'; 4 | 5 | interface IHyperlinkProps { 6 | href: string; 7 | children: React.ReactNode; 8 | } 9 | 10 | const useLinkStyles = makeStyles({ 11 | root: {} 12 | }); 13 | 14 | /** 15 | * Provides a suitable hyperlink component that uses the Fluent UI Link 16 | * component as a base, but will ensure that the visual host will . 17 | */ 18 | export const Hyperlink: React.FC = ({ href, children }) => { 19 | const handleClick = ( 20 | event: React.MouseEvent< 21 | HTMLAnchorElement | HTMLElement | HTMLButtonElement, 22 | MouseEvent 23 | > 24 | ) => { 25 | event.preventDefault(); 26 | event.stopPropagation(); 27 | launchUrl(href); 28 | }; 29 | const classes = useLinkStyles(); 30 | return ( 31 | 32 | {children} 33 | 34 | ); 35 | }; 36 | -------------------------------------------------------------------------------- /src/features/interface/components/portal-root.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { PORTAL_ROOT_ID } from '../../../constants'; 3 | 4 | /** 5 | * React portal for handling elements that we cannot render with current 6 | * dependencies (e.g. custom tooltips in the Ace editor). 7 | */ 8 | export const PortalRoot: React.FC = () => { 9 | return
; 10 | }; 11 | -------------------------------------------------------------------------------- /src/features/interface/components/report-view-router.tsx: -------------------------------------------------------------------------------- 1 | import React, { useMemo } from 'react'; 2 | import { shallow } from 'zustand/shallow'; 3 | 4 | import { VegaContainer } from '../../vega-output'; 5 | import { FetchingMessage, SplashInitial } from '../../status'; 6 | import store from '../../../store'; 7 | import { logRender } from '../../logging'; 8 | import { getI18nValue } from '../../i18n'; 9 | 10 | /** 11 | * Handles routing of the main visual display, when in report view. 12 | */ 13 | export const ReportViewRouter: React.FC = () => { 14 | const { datasetProcessingStage, mode } = store( 15 | (state) => ({ 16 | datasetProcessingStage: state.datasetProcessingStage, 17 | mode: state.interface.mode 18 | }), 19 | shallow 20 | ); 21 | const component = useMemo(() => { 22 | switch (datasetProcessingStage) { 23 | case 'Initial': { 24 | return ; 25 | } 26 | case 'Fetching': { 27 | return ; 28 | } 29 | case 'Processing': { 30 | return ( 31 |
32 | {getI18nValue('Fetching_Data_Assistive_Processed')} 33 |
34 | ); 35 | } 36 | case 'Processed': { 37 | return ; 38 | } 39 | } 40 | }, [datasetProcessingStage]); 41 | logRender('DataProcessingRouter', { datasetProcessingStage, mode }); 42 | return component; 43 | }; 44 | -------------------------------------------------------------------------------- /src/features/interface/components/status-bar-container.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { useInterfaceStyles } from '..'; 4 | 5 | interface IStatusBarContainerProps { 6 | children: React.ReactNode; 7 | } 8 | 9 | /** 10 | * PRovides a consisten container for a status bar in the interface. These are 11 | * used underneath the editor, and in the debug area. 12 | */ 13 | export const StatusBarContainer: React.FC = ({ 14 | children 15 | }) => { 16 | const classes = useInterfaceStyles(); 17 | return
{children}
; 18 | }; 19 | -------------------------------------------------------------------------------- /src/features/interface/components/tooltip-custom-mount.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { useInterfaceStyles } from '..'; 4 | 5 | interface ITooltipCustomMountProps { 6 | setRef: React.Dispatch>; 7 | } 8 | 9 | /** 10 | * This provides a generic div that can be used as custom tooltip mount, in 11 | * conjunction with a state setter for the ref from the parent component. 12 | * @privateRemarks 13 | * In Fluent UI 9.20.0, a change was introduced that renders = ({ 21 | setRef 22 | }) => { 23 | const classes = useInterfaceStyles(); 24 | return
; 25 | }; 26 | -------------------------------------------------------------------------------- /src/features/interface/components/visual-interface.tsx: -------------------------------------------------------------------------------- 1 | import React, { useMemo } from 'react'; 2 | import { shallow } from 'zustand/shallow'; 3 | 4 | import { AdvancedEditor } from '../../interface'; 5 | import store from '../../../store'; 6 | import { ReportViewRouter } from './report-view-router'; 7 | import { FetchingMessage, SplashInitial } from '../../status'; 8 | import { NotificationToaster } from '../../toaster'; 9 | import { logRender } from '../../logging'; 10 | import { LandingPage } from '../../status'; 11 | import { VisualUpdateHistoryOverlay } from './visual-update-history-overlay'; 12 | 13 | export const VisualInterface = () => { 14 | const { mode } = store( 15 | (state) => ({ 16 | mode: state.interface.mode 17 | }), 18 | shallow 19 | ); 20 | const mainComponent = useMemo(() => { 21 | switch (mode) { 22 | case 'Initializing': 23 | return ; 24 | case 'Fetching': 25 | return ; 26 | case 'Landing': 27 | case 'NoSpec': 28 | case 'EditorNoData': 29 | return ; 30 | case 'Editor': 31 | return ; 32 | case 'View': 33 | return ; 34 | } 35 | }, [mode]); 36 | logRender('VisualInterface', mode); 37 | return ( 38 | <> 39 | {mainComponent} 40 | 41 | 42 | 43 | ); 44 | }; 45 | -------------------------------------------------------------------------------- /src/features/interface/components/visual-update-history-overlay.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { shallow } from 'zustand/shallow'; 3 | import store from '../../../store'; 4 | import { FEATURES } from '../../../../config'; 5 | 6 | /** 7 | * Provides a simple textarea that we can view the visual update history from 8 | * the store. Is intended for debugging status changes based on the update 9 | * options from the Power BI visual host. 10 | */ 11 | export const VisualUpdateHistoryOverlay: React.FC = () => { 12 | const { history } = store( 13 | (state) => ({ 14 | history: state.visualUpdateOptions.history 15 | }), 16 | shallow 17 | ); 18 | return FEATURES.visual_update_history_overlay ? ( 19 |