Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
2ccea5c
Initial plan
Copilot Oct 20, 2025
053a4a6
Add inspector plugin implementation and example
Copilot Oct 20, 2025
8008fbc
Add validation support for inspector element
Copilot Oct 20, 2025
9ca19c8
Add interactive array expand/collapse to inspector plugin
Copilot Oct 20, 2025
855d484
Fix inspector element hydration by changing pre to div
Copilot Oct 20, 2025
f2659a9
Add raw option to inspector for copy/paste use case
Copilot Oct 21, 2025
1a01afc
Make arrays start collapsed by default
Copilot Oct 21, 2025
1cfa5e0
Remove all coloring and unnecessary styling from inspector
Copilot Oct 21, 2025
1eaed6d
Refactor raw mode handling in inspector to simplify value display logic
danmarshall Oct 21, 2025
1b84dbb
Add inspector case to markdown compiler
Copilot Oct 21, 2025
b8d3593
Add nested array demo and remove CSS styling
Copilot Oct 21, 2025
8ec4793
Add Vega transforms example with filter, aggregate, pivot, and nest
Copilot Oct 21, 2025
52260db
Fix validation error: change dataSources to dataLoaders
Copilot Oct 21, 2025
93c374e
Add wildcard (*) feature to inspect all variables via signalDeps
Copilot Oct 21, 2025
02ddee2
Change wildcard from * to optional variableId for all variables inspe…
Copilot Oct 21, 2025
0fb5aa4
Refactor InspectorSpec to extend InspectorElementProps and remove unn…
danmarshall Oct 21, 2025
b9ce8d8
Remove unused label property from inspectorSpec in groupMarkdown func…
danmarshall Oct 21, 2025
2c94b53
Fix type assertion for variableId in inspector element validation
danmarshall Oct 21, 2025
e155ede
Remove label properties from inspector outputs for consistency
danmarshall Oct 21, 2025
3cb626e
Remove label, add duplicate validation, and add wildcard (*) for all-…
Copilot Oct 21, 2025
1d2ee64
Remove incorrect duplicate validation from document.ts and fix demo
Copilot Oct 21, 2025
5ec7ad8
Refactor inspector plugin: simplify initialSignals, extract repeated …
Copilot Oct 21, 2025
f8af03b
Extract renderValue helper to eliminate duplicate code for rendering …
Copilot Oct 21, 2025
9744af0
Move renderValue outside renderArray and use it in displayValue to el…
Copilot Oct 21, 2025
1c4a5b1
Remove displayValue wrapper and use renderValue directly everywhere
Copilot Oct 21, 2025
6a5a55e
Fix * in signalDeps, simplify title, and fix nest transform
Copilot Oct 21, 2025
2df6eed
Remove unused destroy method from inspectorPlugin
danmarshall Oct 23, 2025
1b56550
Rename title
danmarshall Oct 23, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 15 additions & 2 deletions docs/schema/idoc_v1.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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[];
Expand Down Expand Up @@ -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 };
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 };
25 changes: 25 additions & 0 deletions docs/schema/idoc_v1.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -3216,6 +3238,9 @@
{
"$ref": "#/definitions/ImageElement"
},
{
"$ref": "#/definitions/InspectorElement"
},
{
"$ref": "#/definitions/MermaidElement"
},
Expand Down
14 changes: 13 additions & 1 deletion packages/compiler/src/md.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, "json" | "yaml">) {
const mdElements: string[] = [];
Expand Down Expand Up @@ -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) {
Expand Down
9 changes: 8 additions & 1 deletion packages/compiler/src/validate/element.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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;

Expand Down
2 changes: 2 additions & 0 deletions packages/markdown/src/plugins/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -34,6 +35,7 @@ export function registerNativePlugins() {
registerMarkdownPlugin(googleFontsPlugin);
registerMarkdownPlugin(dropdownPlugin);
registerMarkdownPlugin(imagePlugin);
registerMarkdownPlugin(inspectorPlugin);
registerMarkdownPlugin(mermaidPlugin);
registerMarkdownPlugin(numberPlugin);
registerMarkdownPlugin(placeholdersPlugin);
Expand Down
174 changes: 174 additions & 0 deletions packages/markdown/src/plugins/inspector.ts
Original file line number Diff line number Diff line change
@@ -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<InspectorSpec> = {
...flaggablePlugin<InspectorSpec>(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 = `<div class="inspector">
<div class="inspector-value" id="${spec.variableId || 'all'}-value"></div>
</div>`;
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;
},
};
2 changes: 2 additions & 0 deletions packages/markdown/src/plugins/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -25,6 +26,7 @@ export type PluginNames =
'dsv' |
'image' |
'google-fonts' |
'inspector' |
'mermaid' |
'number' |
'placeholders' |
Expand Down
2 changes: 1 addition & 1 deletion packages/markdown/src/signalbus.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
||
Expand Down
16 changes: 16 additions & 0 deletions packages/schema-doc/src/interactive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -243,6 +258,7 @@ export type InteractiveElement =
| CheckboxElement
| DropdownElement
| ImageElement
| InspectorElement
| MermaidElement
| NumberElement
| PresetsElement
Expand Down
Loading