Skip to content
Open
Show file tree
Hide file tree
Changes from 20 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
18 changes: 18 additions & 0 deletions packages/compiler/src/validate/document.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,24 @@ export async function validateDocument(page: InteractiveDocument) {
const variables = page.variables || [];
const tabulatorElements = page.groups.flatMap(group => group.elements.filter(e => typeof e !== 'string' && e.type === 'tabulator'));

// Check for duplicate dataLoader names
const dataLoaderNames = new Set<string>();
for (const dataLoader of dataLoaders) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot there is a typescript error here because those of type=spec do not have this name. please look at typescript compilation errors. Also I dont even think you need this, see packages\compiler\src\validate\data.ts

what was lacking a check for a variable colliding with a data source. please look at other files in this library before you implement junk like this again.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in commit (will be in next push). Removed the incorrect duplicate validation from document.ts - you're right that the existing validation in data.ts already handles this properly. The issue was:

  1. My code had TypeScript errors because I didn't check types properly
  2. The existing validateDataSource() in data.ts already checks for:
    • Collisions between variables and data sources (lines 35-41)
    • Duplicate data sources (lines 44-47)

Also fixed the demo to remove the section trying to inspect salesData (which is a dataLoader, not a variable).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in commit 1d2ee64. Removed the incorrect duplicate validation - the existing validation in data.ts already handles this correctly.

if (dataLoaderNames.has(dataLoader.dataSourceName)) {
errors.push(`Duplicate dataLoader name: '${dataLoader.dataSourceName}' - each dataLoader must have a unique dataSourceName`);
}
dataLoaderNames.add(dataLoader.dataSourceName);
}

// Check for duplicate variable IDs
const variableIds = new Set<string>();
for (const variable of variables) {
if (variableIds.has(variable.variableId)) {
errors.push(`Duplicate variable ID: '${variable.variableId}' - each variable must have a unique variableId`);
}
variableIds.add(variable.variableId);
}

for (const dataLoader of dataLoaders) {
const otherDataLoaders = dataLoaders.filter(dl => dl !== dataLoader);
errors.push(...await validateDataLoader(dataLoader, variables, tabulatorElements, otherDataLoaders));
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
195 changes: 195 additions & 0 deletions packages/markdown/src/plugins/inspector.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
/**
* 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 = isInspectAll ? [{
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot collapse this with just a switch on the name prop

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in commit 5ec7ad8. Simplified to single array with ternary on the name property: name: isInspectAll ? '*' : spec.variableId

name: '*',
value: null,
priority: -1,
isData: false,
}] : [{
name: spec.variableId,
value: null,
priority: -1,
isData: false,
}];

const updateDisplay = (value: unknown) => {
element.innerHTML = ''; // Clear previous content

// If raw mode is enabled, always use JSON.stringify without interactivity
if (spec.raw) {
element.textContent = JSON.stringify(value, null, 2);
return;
}

// Interactive mode (default)
if (Array.isArray(value)) {
// Create interactive collapsible array display
renderArray(element, value);
} else if (typeof value === 'object') {
// For objects, use JSON.stringify with indentation
element.textContent = JSON.stringify(value, null, 2);
} else {
element.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');

if (Array.isArray(item)) {
// Nested array
renderArray(valueSpan, item, depth + 1);
} else if (typeof item === 'object') {
valueSpan.textContent = JSON.stringify(item, null, 2);
valueSpan.style.whiteSpace = 'pre';
} else {
valueSpan.textContent = JSON.stringify(item);
}

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);
};

return {
...inspectorInstance,
initialSignals,
receiveBatch: async (batch) => {
if (isInspectAll) {
// Extract all variable values from signalDeps
const allVars: { [key: string]: unknown } = {};
for (const signalName in signalBus.signalDeps) {
allVars[signalName] = signalBus.signalDeps[signalName].value;
}
updateDisplay(allVars);
} else if (batch[spec.variableId]) {
const value = batch[spec.variableId].value;
updateDisplay(value);
}
},
beginListening() {
// Inspector is read-only, no event listeners needed
// For inspect-all mode, do initial display
if (isInspectAll) {
const allVars: { [key: string]: unknown } = {};
for (const signalName in signalBus.signalDeps) {
allVars[signalName] = signalBus.signalDeps[signalName].value;
}
updateDisplay(allVars);
}
},
getCurrentSignalValue: () => {
// Inspector doesn't modify the signal, return undefined
return undefined;
},
destroy: () => {
// No cleanup needed
},
};
});
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
Loading