diff --git a/docs/schema/idoc_v1.d.ts b/docs/schema/idoc_v1.d.ts index 7fb3df38..6ace9bce 100644 --- a/docs/schema/idoc_v1.d.ts +++ b/docs/schema/idoc_v1.d.ts @@ -207,6 +207,19 @@ interface ImageElementProps { height?: number; width?: number; } +/** + * Inspector + * use for examining and displaying the current value of a variable + */ +interface InspectorElement extends InspectorElementProps { + type: 'inspector'; +} +interface InspectorElementProps { + /** Optional variable ID. If omitted, inspects all variables from signalBus.signalDeps */ + variableId?: VariableID; + /** When true, displays raw JSON output without interactive elements (for copy/paste). Default is false. */ + raw?: boolean; +} /** * Treebark * use for rendering cards and structured HTML from templates @@ -256,7 +269,7 @@ interface TabulatorElementProps extends OptionalVariableControl { /** * Union type for all possible interactive elements */ -type InteractiveElement = ChartElement | CheckboxElement | DropdownElement | ImageElement | MermaidElement | NumberElement | PresetsElement | SliderElement | TabulatorElement | TextboxElement | TreebarkElement; +type InteractiveElement = ChartElement | CheckboxElement | DropdownElement | ImageElement | InspectorElement | MermaidElement | NumberElement | PresetsElement | SliderElement | TabulatorElement | TextboxElement | TreebarkElement; interface ElementGroup { groupId: string; elements: PageElement[]; @@ -316,4 +329,4 @@ interface GoogleFontsSpec { type InteractiveDocumentWithSchema = InteractiveDocument & { $schema?: string; }; -export type { Calculation, ChartElement, CheckboxElement, CheckboxProps, DataFrameCalculation, DataLoader, DataLoaderBySpec, DataSource, DataSourceBase, DataSourceBaseFormat, DataSourceByDynamicURL, DataSourceByFile, DataSourceInline, DropdownElement, DropdownElementProps, DynamicDropdownOptions, ElementBase, ElementGroup, GoogleFontsSpec, ImageElement, ImageElementProps, InteractiveDocument, InteractiveDocumentWithSchema, InteractiveElement, MarkdownElement, MermaidElement, MermaidElementProps, MermaidTemplate, NumberElement, NumberElementProps, OptionalVariableControl, PageElement, PageStyle, Preset, PresetsElement, PresetsElementProps, ReturnType, ScalarCalculation, SliderElement, SliderElementProps, TabulatorElement, TabulatorElementProps, TemplatedUrl, TextboxElement, TextboxElementProps, TreebarkElement, TreebarkElementProps, Variable, VariableControl, VariableID, VariableType, VariableValue, VariableValueArray, VariableValuePrimitive, Vega_or_VegaLite_spec }; \ No newline at end of file +export type { Calculation, ChartElement, CheckboxElement, CheckboxProps, DataFrameCalculation, DataLoader, DataLoaderBySpec, DataSource, DataSourceBase, DataSourceBaseFormat, DataSourceByDynamicURL, DataSourceByFile, DataSourceInline, DropdownElement, DropdownElementProps, DynamicDropdownOptions, ElementBase, ElementGroup, GoogleFontsSpec, ImageElement, ImageElementProps, InspectorElement, InspectorElementProps, InteractiveDocument, InteractiveDocumentWithSchema, InteractiveElement, MarkdownElement, MermaidElement, MermaidElementProps, MermaidTemplate, NumberElement, NumberElementProps, OptionalVariableControl, PageElement, PageStyle, Preset, PresetsElement, PresetsElementProps, ReturnType, ScalarCalculation, SliderElement, SliderElementProps, TabulatorElement, TabulatorElementProps, TemplatedUrl, TextboxElement, TextboxElementProps, TreebarkElement, TreebarkElementProps, Variable, VariableControl, VariableID, VariableType, VariableValue, VariableValueArray, VariableValuePrimitive, Vega_or_VegaLite_spec }; \ No newline at end of file diff --git a/docs/schema/idoc_v1.json b/docs/schema/idoc_v1.json index c93a9361..e236c0bf 100644 --- a/docs/schema/idoc_v1.json +++ b/docs/schema/idoc_v1.json @@ -3143,6 +3143,28 @@ ], "type": "object" }, + "InspectorElement": { + "additionalProperties": false, + "description": "Inspector use for examining and displaying the current value of a variable", + "properties": { + "label": { + "description": "optional label if the variableId is not descriptive enough", + "type": "string" + }, + "type": { + "const": "inspector", + "type": "string" + }, + "variableId": { + "$ref": "#/definitions/VariableID" + } + }, + "required": [ + "type", + "variableId" + ], + "type": "object" + }, "InteractiveDocumentWithSchema": { "additionalProperties": false, "description": "JSON Schema version with $schema property for validation", @@ -3216,6 +3238,9 @@ { "$ref": "#/definitions/ImageElement" }, + { + "$ref": "#/definitions/InspectorElement" + }, { "$ref": "#/definitions/MermaidElement" }, diff --git a/packages/compiler/src/md.ts b/packages/compiler/src/md.ts index 1c49d9fd..5af310e8 100644 --- a/packages/compiler/src/md.ts +++ b/packages/compiler/src/md.ts @@ -202,7 +202,7 @@ function dataLoaderMarkdown(dataSources: DataSource[], variables: Variable[], ta return { vegaScope, inlineDataMd }; } -type pluginSpecs = Plugins.CheckboxSpec | Plugins.DropdownSpec | Plugins.ImageSpec | Plugins.MermaidSpec | Plugins.NumberSpec | Plugins.PresetsSpec | Plugins.SliderSpec | Plugins.TabulatorSpec | Plugins.TextboxSpec | Plugins.TreebarkSpec; +type pluginSpecs = Plugins.CheckboxSpec | Plugins.DropdownSpec | Plugins.ImageSpec | Plugins.InspectorSpec | Plugins.MermaidSpec | Plugins.NumberSpec | Plugins.PresetsSpec | Plugins.SliderSpec | Plugins.TabulatorSpec | Plugins.TextboxSpec | Plugins.TreebarkSpec; function groupMarkdown(group: ElementGroup, variables: Variable[], vegaScope: VegaScope, resources: { charts?: { [chartKey: string]: VegaSpec | VegaLiteSpec } }, pluginFormat: Record) { const mdElements: string[] = []; @@ -281,6 +281,18 @@ function groupMarkdown(group: ElementGroup, variables: Variable[], vegaScope: Ve addSpec('image', imageSpec); break; } + case 'inspector': { + const { variableId, raw } = element; + const inspectorSpec: Plugins.InspectorSpec = {} as any; + if (variableId) { + inspectorSpec.variableId = variableId; + } + if (raw) { + inspectorSpec.raw = raw; + } + addSpec('inspector', inspectorSpec, false); + break; + } case 'mermaid': { const { diagramText, template, variableId } = element; if (diagramText) { diff --git a/packages/compiler/src/validate/element.ts b/packages/compiler/src/validate/element.ts index a8b3307b..31291c45 100644 --- a/packages/compiler/src/validate/element.ts +++ b/packages/compiler/src/validate/element.ts @@ -1,4 +1,4 @@ -import { PageElement, Variable, DataLoader, CheckboxElement, DropdownElement, SliderElement, TextboxElement, ChartElement, ImageElement, MermaidElement, TreebarkElement, Vega_or_VegaLite_spec } from "@microsoft/chartifact-schema"; +import { PageElement, Variable, DataLoader, CheckboxElement, DropdownElement, SliderElement, TextboxElement, ChartElement, ImageElement, InspectorElement, MermaidElement, TreebarkElement, Vega_or_VegaLite_spec } from "@microsoft/chartifact-schema"; import { getChartType } from "../util.js"; import { validateVegaLite, validateVegaChart } from "./chart.js"; import { validateVariableID, validateRequiredString, validateOptionalString, validateOptionalPositiveNumber, validateOptionalBoolean, validateOptionalObject, validateInputElementWithVariableId, validateMarkdownString } from "./common.js"; @@ -94,6 +94,13 @@ export async function validateElement(element: PageElement, groupIndex: number, break; } + case 'inspector': { + // Inspector has optional variableId (if omitted, inspects all variables) + if (element.variableId) { + errors.push(...validateInputElementWithVariableId(element as { type: string; variableId: string })); + } + break; + } case 'mermaid': { const mermaidElement = element as MermaidElement; diff --git a/packages/markdown/src/plugins/index.ts b/packages/markdown/src/plugins/index.ts index 9071173e..df2bf23b 100644 --- a/packages/markdown/src/plugins/index.ts +++ b/packages/markdown/src/plugins/index.ts @@ -13,6 +13,7 @@ import { dsvPlugin } from './dsv.js'; import { googleFontsPlugin } from './google-fonts.js'; import { dropdownPlugin } from './dropdown.js'; import { imagePlugin } from './image.js'; +import { inspectorPlugin } from './inspector.js'; import { mermaidPlugin } from './mermaid.js'; import { numberPlugin } from './number.js'; import { placeholdersPlugin } from './placeholders.js'; @@ -34,6 +35,7 @@ export function registerNativePlugins() { registerMarkdownPlugin(googleFontsPlugin); registerMarkdownPlugin(dropdownPlugin); registerMarkdownPlugin(imagePlugin); + registerMarkdownPlugin(inspectorPlugin); registerMarkdownPlugin(mermaidPlugin); registerMarkdownPlugin(numberPlugin); registerMarkdownPlugin(placeholdersPlugin); diff --git a/packages/markdown/src/plugins/inspector.ts b/packages/markdown/src/plugins/inspector.ts new file mode 100644 index 00000000..520f9071 --- /dev/null +++ b/packages/markdown/src/plugins/inspector.ts @@ -0,0 +1,174 @@ +/** +* Copyright (c) Microsoft Corporation. +* Licensed under the MIT License. +*/ + +import { IInstance, Plugin } from '../factory.js'; +import { pluginClassName } from './util.js'; +import { flaggablePlugin } from './config.js'; +import { PluginNames } from './interfaces.js'; +import { InspectorElementProps } from '@microsoft/chartifact-schema'; + +interface InspectorInstance { + id: string; + spec: InspectorSpec; + element: HTMLElement; +} + +export interface InspectorSpec extends InspectorElementProps { +} + +const pluginName: PluginNames = 'inspector'; +const className = pluginClassName(pluginName); + +export const inspectorPlugin: Plugin = { + ...flaggablePlugin(pluginName, className), + hydrateComponent: async (renderer, errorHandler, specs) => { + const { signalBus } = renderer; + const inspectorInstances: InspectorInstance[] = []; + for (let index = 0; index < specs.length; index++) { + const specReview = specs[index]; + if (!specReview.approvedSpec) { + continue; + } + const container = renderer.element.querySelector(`#${specReview.containerId}`); + + const spec: InspectorSpec = specReview.approvedSpec; + + const html = `
+
+
`; + container.innerHTML = html; + const element = container.querySelector('.inspector-value') as HTMLElement; + + const inspectorInstance: InspectorInstance = { id: `${pluginName}-${index}`, spec, element }; + inspectorInstances.push(inspectorInstance); + } + + const instances = inspectorInstances.map((inspectorInstance): IInstance => { + const { element, spec } = inspectorInstance; + + // Special case: if variableId is undefined/omitted, inspect all variables from signalDeps + const isInspectAll = !spec.variableId; + + const initialSignals = [{ + name: isInspectAll ? '*' : spec.variableId, + value: null, + priority: -1, + isData: false, + }]; + + const renderValue = (container: HTMLElement, value: unknown, depth: number = 0) => { + // Clear previous content when rendering at root level + if (depth === 0) { + container.innerHTML = ''; + } + + // If raw mode is enabled, always use JSON.stringify without interactivity + if (spec.raw) { + container.textContent = JSON.stringify(value, null, 2); + return; + } + + // Interactive mode (default) + if (Array.isArray(value)) { + renderArray(container, value, depth); + } else if (typeof value === 'object') { + container.textContent = JSON.stringify(value, null, 2); + container.style.whiteSpace = 'pre'; + } else { + container.textContent = JSON.stringify(value); + } + }; + + const renderArray = (container: HTMLElement, arr: unknown[], depth: number = 0) => { + const indent = ' '.repeat(depth); + + // Create collapsible array structure + const arrayWrapper = document.createElement('div'); + arrayWrapper.className = 'inspector-array'; + + // Array header with toggle + const header = document.createElement('div'); + header.className = 'inspector-array-header'; + header.style.cursor = 'pointer'; + header.style.userSelect = 'none'; + + const toggleIcon = document.createElement('span'); + toggleIcon.className = 'inspector-toggle'; + toggleIcon.textContent = '▶ '; + toggleIcon.style.display = 'inline-block'; + toggleIcon.style.width = '1em'; + + const arrayLabel = document.createElement('span'); + arrayLabel.textContent = `Array(${arr.length})`; + + header.appendChild(toggleIcon); + header.appendChild(arrayLabel); + + // Array content + const content = document.createElement('div'); + content.className = 'inspector-array-content'; + content.style.paddingLeft = '1.5em'; + + arr.forEach((item, index) => { + const itemDiv = document.createElement('div'); + itemDiv.className = 'inspector-array-item'; + + const indexLabel = document.createElement('span'); + indexLabel.textContent = `[${index}]: `; + itemDiv.appendChild(indexLabel); + + const valueSpan = document.createElement('span'); + renderValue(valueSpan, item, depth + 1); + + itemDiv.appendChild(valueSpan); + content.appendChild(itemDiv); + }); + + // Toggle functionality - start collapsed + let isExpanded = false; + content.style.display = 'none'; + const toggle = () => { + isExpanded = !isExpanded; + content.style.display = isExpanded ? 'block' : 'none'; + toggleIcon.textContent = isExpanded ? '▼ ' : '▶ '; + }; + + header.addEventListener('click', toggle); + + arrayWrapper.appendChild(header); + arrayWrapper.appendChild(content); + container.appendChild(arrayWrapper); + }; + + const getAllVariables = () => { + const allVars: { [key: string]: unknown } = {}; + for (const signalName in signalBus.signalDeps) { + allVars[signalName] = signalBus.signalDeps[signalName].value; + } + return allVars; + }; + + return { + ...inspectorInstance, + initialSignals, + receiveBatch: async (batch) => { + if (isInspectAll) { + renderValue(element, getAllVariables()); + } else if (batch[spec.variableId]) { + renderValue(element, batch[spec.variableId].value); + } + }, + beginListening() { + // Inspector is read-only, no event listeners needed + // For inspect-all mode, do initial display + if (isInspectAll) { + renderValue(element, getAllVariables()); + } + }, + }; + }); + return instances; + }, +}; diff --git a/packages/markdown/src/plugins/interfaces.ts b/packages/markdown/src/plugins/interfaces.ts index ab81a30e..f481857b 100644 --- a/packages/markdown/src/plugins/interfaces.ts +++ b/packages/markdown/src/plugins/interfaces.ts @@ -7,6 +7,7 @@ export { CsvSpec } from './csv.js'; export { DropdownSpec } from './dropdown.js'; export { DsvSpec } from './dsv.js'; export { ImageSpec } from './image.js'; +export { InspectorSpec } from './inspector.js'; export { MermaidSpec } from './mermaid.js'; export { NumberSpec } from './number.js'; export { PresetsSpec } from './presets.js'; @@ -25,6 +26,7 @@ export type PluginNames = 'dsv' | 'image' | 'google-fonts' | + 'inspector' | 'mermaid' | 'number' | 'placeholders' | diff --git a/packages/markdown/src/signalbus.ts b/packages/markdown/src/signalbus.ts index ec7faf9a..9c93a5ef 100644 --- a/packages/markdown/src/signalbus.ts +++ b/packages/markdown/src/signalbus.ts @@ -71,7 +71,7 @@ export class SignalBus { let hasBatch = false; for (const signalName in batch) { if ( - peer.initialSignals.some(s => s.name === signalName) + peer.initialSignals.some(s => s.name === signalName || s.name === '*') && ( (batch[signalName].value !== this.signalDeps[signalName].value) || diff --git a/packages/schema-doc/src/interactive.ts b/packages/schema-doc/src/interactive.ts index 70753307..3b9de261 100644 --- a/packages/schema-doc/src/interactive.ts +++ b/packages/schema-doc/src/interactive.ts @@ -137,6 +137,21 @@ export interface ImageElementProps { width?: number; } +/** + * Inspector + * use for examining and displaying the current value of a variable + */ +export interface InspectorElement extends InspectorElementProps { + type: 'inspector'; +} +export interface InspectorElementProps { + /** Optional variable ID. If omitted, inspects all variables from signalBus.signalDeps */ + variableId?: VariableID; + + /** When true, displays raw JSON output without interactive elements (for copy/paste). Default is false. */ + raw?: boolean; +} + /** * Treebark * use for rendering cards and structured HTML from templates @@ -243,6 +258,7 @@ export type InteractiveElement = | CheckboxElement | DropdownElement | ImageElement + | InspectorElement | MermaidElement | NumberElement | PresetsElement diff --git a/packages/web-deploy/json/features/12.inspector.idoc.json b/packages/web-deploy/json/features/12.inspector.idoc.json new file mode 100644 index 00000000..abefd9fc --- /dev/null +++ b/packages/web-deploy/json/features/12.inspector.idoc.json @@ -0,0 +1,197 @@ +{ + "$schema": "../../../../docs/schema/idoc_v1.json", + "title": "Inspector", + "dataLoaders": [ + { + "dataSourceName": "salesData", + "type": "inline", + "format": "json", + "content": [ + {"category": "Electronics", "product": "Laptop", "region": "North", "sales": 1200, "quantity": 3}, + {"category": "Electronics", "product": "Phone", "region": "North", "sales": 800, "quantity": 5}, + {"category": "Electronics", "product": "Laptop", "region": "South", "sales": 1500, "quantity": 4}, + {"category": "Furniture", "product": "Desk", "region": "North", "sales": 600, "quantity": 2}, + {"category": "Furniture", "product": "Chair", "region": "South", "sales": 300, "quantity": 6}, + {"category": "Furniture", "product": "Desk", "region": "South", "sales": 700, "quantity": 3} + ] + } + ], + "variables": [ + { + "variableId": "selectedRegion", + "type": "string", + "initialValue": "North" + }, + { + "variableId": "minSales", + "type": "number", + "initialValue": 500 + }, + { + "variableId": "filteredData", + "type": "object", + "isArray": true, + "initialValue": [], + "calculation": { + "dataSourceNames": ["salesData"], + "dataFrameTransformations": [ + { + "type": "filter", + "expr": "datum.region == selectedRegion && datum.sales >= minSales" + } + ] + } + }, + { + "variableId": "aggregatedByCategory", + "type": "object", + "isArray": true, + "initialValue": [], + "calculation": { + "dataSourceNames": ["filteredData"], + "dataFrameTransformations": [ + { + "type": "aggregate", + "groupby": ["category"], + "ops": ["sum", "sum", "count"], + "fields": ["sales", "quantity", null], + "as": ["totalSales", "totalQuantity", "productCount"] + } + ] + } + }, + { + "variableId": "pivotedData", + "type": "object", + "isArray": true, + "initialValue": [], + "calculation": { + "dataSourceNames": ["salesData"], + "dataFrameTransformations": [ + { + "type": "pivot", + "groupby": ["product"], + "field": "region", + "value": "sales", + "op": "sum" + } + ] + } + }, + { + "variableId": "nestedByCategory", + "type": "object", + "initialValue": [], + "calculation": { + "dataSourceNames": ["salesData"], + "dataFrameTransformations": [ + { + "type": "nest", + "keys": ["category"] + } + ] + } + } + ], + "groups": [ + { + "groupId": "main", + "elements": [ + "# Variable Inspector", + "", + "The inspector plugin is particularly useful for examining transformed data. This example demonstrates Vega data transforms like **filter**, **aggregate**, **pivot**, and **nest**.", + "", + "---", + "", + "## Filter Controls", + "", + "Select a region and minimum sales threshold to filter the data:", + "", + { + "type": "dropdown", + "variableId": "selectedRegion", + "label": "Region", + "options": ["North", "South"] + }, + "", + { + "type": "slider", + "variableId": "minSales", + "label": "Minimum Sales", + "min": 0, + "max": 2000, + "step": 100 + }, + "", + "---", + "", + "## Filtered Data", + "", + "Filtered by selected region and minimum sales using Vega **filter** transform:", + "", + "**Inspector output:**", + { + "type": "inspector", + "variableId": "filteredData" + }, + "", + "---", + "", + "## Aggregated by Category", + "", + "Data grouped by category with totals using Vega **aggregate** transform:", + "", + "**Inspector output:**", + { + "type": "inspector", + "variableId": "aggregatedByCategory" + }, + "", + "---", + "", + "## Pivoted Data", + "", + "Sales pivoted by product and region using Vega **pivot** transform:", + "", + "**Inspector output:**", + { + "type": "inspector", + "variableId": "pivotedData" + }, + "", + "---", + "", + "## Nested by Category", + "", + "Data nested by category using Vega **nest** transform:", + "", + "**Inspector output:**", + { + "type": "inspector", + "variableId": "nestedByCategory" + }, + "", + "---", + "", + "## All Variables (Debug Inspector)", + "", + "Omitting `variableId` to inspect all variables in the document:", + "", + "**Inspector output:**", + { + "type": "inspector" + }, + "", + "---", + "", + "## Use Cases", + "", + "The inspector is particularly useful for:", + "- **Debugging transforms**: See exactly how filter, aggregate, pivot, and nest operations transform your data", + "- **Verifying calculations**: Ensure data transformations produce expected results", + "- **Understanding data flow**: Trace how data changes through the pipeline", + "- **Development**: Build and test complex data transformations interactively" + ] + } + ] +}