Skip to content

Commit add0e56

Browse files
ryan-williamsclaude
andcommitted
add schema-generated types, start replacing \any\ with real types
- Add \`tasks/gen-schema-types.mjs\`: generates TypeScript interfaces from \`plot-schema.json\` (the formal plotly attribute schema) - Generate \`types/schema.d.ts\` (1,368 lines): typed \`ScatterTrace\`, \`SchemaLayout\` with proper \`valType\` → TS mappings - Replace \`gd: any\` → \`GraphDiv\` and \`trace: any\` → \`FullTrace\` in \`drawing/index.ts\` (14 + 12 replacements) - Fix \`CalcData\` type (was double-nested) - Properly type \`fx/calc.ts\` as example of real typing The \`[key: string]: any\` escape hatch on core interfaces means these types provide documentation + IDE autocomplete without breaking anything. Removing the escape hatch requires exhaustively listing all properties (which the schema-generated types enable, as a future step). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent f1055f8 commit add0e56

File tree

5 files changed

+1527
-28
lines changed

5 files changed

+1527
-28
lines changed

src/components/drawing/index.ts

Lines changed: 23 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { select } from 'd3-selection';
2+
import type { GraphDiv, FullTrace, FullAxis } from '../../../types/core';
23
function d3Round(x: number, n: number): number { return n ? Math.round(x * (n = Math.pow(10, n))) / n : Math.round(x); }
34
import { ensureSingle, ensureSingleById, extendFlat, extractOption, identity, isArrayOrTypedArray, nestedProperty, numberFormat, strTranslate, texttemplateString } from '../../lib/index.js';
45
import isNumeric from 'fast-isnumeric';
@@ -132,7 +133,7 @@ export function hideOutsideRangePoints(traceGroups: any, subplot: any): void {
132133
});
133134
}
134135

135-
export function crispRound(gd: any, lineWidth: number, dflt?: number): number {
136+
export function crispRound(gd: GraphDiv, lineWidth: number, dflt?: number): number {
136137
if (!lineWidth || !isNumeric(lineWidth)) return dflt || 0;
137138

138139
if (gd._context.staticPlot) return lineWidth;
@@ -191,7 +192,7 @@ export function dashStyle(dash: string, lineWidth: number): string {
191192
return dash;
192193
}
193194

194-
function setFillStyle(sel: any, trace: any, gd: any, forLegend: boolean): void {
195+
function setFillStyle(sel: any, trace: FullTrace, gd: GraphDiv, forLegend: boolean): void {
195196
var markerPattern = trace.fillpattern;
196197
var fillgradient = trace.fillgradient;
197198
var pAttr = getPatternAttr;
@@ -274,14 +275,14 @@ function setFillStyle(sel: any, trace: any, gd: any, forLegend: boolean): void {
274275
}
275276
}
276277

277-
export function singleFillStyle(sel: any, gd: any): void {
278+
export function singleFillStyle(sel: any, gd: GraphDiv): void {
278279
var node = select(sel.node());
279280
var data = node.data();
280281
var trace = ((data[0] || [])[0] || {}).trace || {};
281282
setFillStyle(sel, trace, gd, false);
282283
}
283284

284-
export function fillGroupStyle(s: any, gd: any, forLegend?: boolean): void {
285+
export function fillGroupStyle(s: any, gd: GraphDiv, forLegend?: boolean): void {
285286
s.style('stroke-width', 0).each(function (this: any, d: any) {
286287
var shape = select(this);
287288
if (d[0].trace) {
@@ -375,7 +376,7 @@ var gradientInfo: Record<string, any> = {
375376
verticalreversed: { type: 'linear', start: { x: 0, y: 1 }, stop: { x: 0, y: 0 }, reversed: true }
376377
};
377378

378-
export function gradient(sel: any, gd: any, gradientID: string, type: string, colorscale: any[], prop: string): void {
379+
export function gradient(sel: any, gd: GraphDiv, gradientID: string, type: string, colorscale: any[], prop: string): void {
379380
var info = gradientInfo[type];
380381
return gradientWithBounds(
381382
sel,
@@ -391,7 +392,7 @@ export function gradient(sel: any, gd: any, gradientID: string, type: string, co
391392
);
392393
}
393394

394-
function gradientWithBounds(sel: any, gd: any, gradientID: string, type: string, colorscale: any[], prop: string, start: any, stop: any, inUserSpace: boolean, reversed: boolean): void {
395+
function gradientWithBounds(sel: any, gd: GraphDiv, gradientID: string, type: string, colorscale: any[], prop: string, start: any, stop: any, inUserSpace: boolean, reversed: boolean): void {
395396
var len = colorscale.length;
396397

397398
var info: any;
@@ -464,7 +465,7 @@ function gradientWithBounds(sel: any, gd: any, gradientID: string, type: string,
464465
export function pattern(
465466
sel: any,
466467
calledBy: string,
467-
gd: any,
468+
gd: GraphDiv,
468469
patternID: string,
469470
shape: string,
470471
size: number,
@@ -760,7 +761,7 @@ export function pattern(
760761
sel.classed('pattern_filled', true);
761762
}
762763

763-
export function initGradients(gd: any): void {
764+
export function initGradients(gd: GraphDiv): void {
764765
var fullLayout = gd._fullLayout;
765766

766767
var gradientsGroup = ensureSingle(fullLayout._defs, 'g', 'gradients');
@@ -769,7 +770,7 @@ export function initGradients(gd: any): void {
769770
select(gd).selectAll('.gradient_filled').classed('gradient_filled', false);
770771
}
771772

772-
export function initPatterns(gd: any): void {
773+
export function initPatterns(gd: GraphDiv): void {
773774
var fullLayout = gd._fullLayout;
774775

775776
var patternsGroup = ensureSingle(fullLayout._defs, 'g', 'patterns');
@@ -785,7 +786,7 @@ export function getPatternAttr(mp: any, i: number, dflt: any): any {
785786
return mp;
786787
}
787788

788-
export function pointStyle(s: any, trace: any, gd: any, pt?: any): void {
789+
export function pointStyle(s: any, trace: FullTrace, gd: GraphDiv, pt?: any): void {
789790
if (!s.size()) return;
790791

791792
var fns = makePointStyleFns(trace);
@@ -795,7 +796,7 @@ export function pointStyle(s: any, trace: any, gd: any, pt?: any): void {
795796
});
796797
}
797798

798-
export function singlePointStyle(d: any, sel: any, trace: any, fns: any, gd: any, pt?: any): void {
799+
export function singlePointStyle(d: any, sel: any, trace: FullTrace, fns: any, gd: GraphDiv, pt?: any): void {
799800
var marker = trace.marker;
800801
var markerLine = marker.line;
801802

@@ -955,7 +956,7 @@ export function singlePointStyle(d: any, sel: any, trace: any, fns: any, gd: any
955956
}
956957
}
957958

958-
export function makePointStyleFns(trace: any): any {
959+
export function makePointStyleFns(trace: FullTrace): any {
959960
var out: Record<string, any> = {};
960961
var marker = trace.marker;
961962

@@ -977,7 +978,7 @@ export function makePointStyleFns(trace: any): any {
977978
return out;
978979
}
979980

980-
export function makeSelectedPointStyleFns(trace: any): any {
981+
export function makeSelectedPointStyleFns(trace: FullTrace): any {
981982
var out: Record<string, any> = {};
982983

983984
var selectedAttrs = trace.selected || {};
@@ -1042,7 +1043,7 @@ export function makeSelectedPointStyleFns(trace: any): any {
10421043
return out;
10431044
}
10441045

1045-
export function makeSelectedTextStyleFns(trace: any): any {
1046+
export function makeSelectedTextStyleFns(trace: FullTrace): any {
10461047
var out: Record<string, any> = {};
10471048

10481049
var selectedAttrs = trace.selected || {};
@@ -1070,7 +1071,7 @@ export function makeSelectedTextStyleFns(trace: any): any {
10701071
return out;
10711072
}
10721073

1073-
export function selectedPointStyle(s: any, trace: any): void {
1074+
export function selectedPointStyle(s: any, trace: FullTrace): void {
10741075
if (!s.size() || !trace.selectedpoints) return;
10751076

10761077
var fns = makeSelectedPointStyleFns(trace);
@@ -1151,12 +1152,12 @@ function textPointPosition(s: any, textPosition: string, fontSize: number, marke
11511152
}
11521153
}
11531154

1154-
function extracTextFontSize(d: any, trace: any): number {
1155+
function extracTextFontSize(d: any, trace: FullTrace): number {
11551156
var fontSize = d.ts || trace.textfont.size;
11561157
return isNumeric(fontSize) && fontSize > 0 ? fontSize : 0;
11571158
}
11581159

1159-
export function textPointStyle(s: any, trace: any, gd: any): void {
1160+
export function textPointStyle(s: any, trace: FullTrace, gd: GraphDiv): void {
11601161
if (!s.size()) return;
11611162

11621163
var selectedTextColorFn: any;
@@ -1215,7 +1216,7 @@ export function textPointStyle(s: any, trace: any, gd: any): void {
12151216
});
12161217
}
12171218

1218-
export function selectedTextStyle(s: any, trace: any): void {
1219+
export function selectedTextStyle(s: any, trace: FullTrace): void {
12191220
if (!s.size() || !trace.selectedpoints) return;
12201221

12211222
var fns = makeSelectedTextStyleFns(trace);
@@ -1498,11 +1499,11 @@ function nodeHash(node: any): string | undefined {
14981499
return inputText + node.getAttribute('data-math') + node.getAttribute('text-anchor') + node.getAttribute('style');
14991500
}
15001501

1501-
export function setClipUrl(s: any, localId: string, gd: any): void {
1502+
export function setClipUrl(s: any, localId: string, gd: GraphDiv): void {
15021503
s.attr('clip-path', getFullUrl(localId, gd));
15031504
}
15041505

1505-
function getFullUrl(localId: string, gd: any): string | null {
1506+
function getFullUrl(localId: string, gd: GraphDiv): string | null {
15061507
if (!localId) return null;
15071508

15081509
var context = gd._context;
@@ -1629,7 +1630,7 @@ export function setTextPointsScale(selection: any, xScale: number, yScale: numbe
16291630
});
16301631
}
16311632

1632-
function getMarkerStandoff(d: any, trace: any): number {
1633+
function getMarkerStandoff(d: any, trace: FullTrace): number {
16331634
var standoff: number;
16341635

16351636
if (d) standoff = d.mf;
@@ -1664,7 +1665,7 @@ var previousY: number;
16641665
var previousI: number;
16651666
var previousTraceUid: string;
16661667

1667-
function getMarkerAngle(d: any, trace: any): number | null {
1668+
function getMarkerAngle(d: any, trace: FullTrace): number | null {
16681669
var angle: any = d.ma;
16691670

16701671
if (angle === undefined) {

src/components/fx/calc.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,20 @@
1-
import { coerceHoverinfo, fillArray, identity } from '../../lib/index.js';
1+
import type { GraphDiv, FullTrace, CalcDatum } from '../../../types/core';
2+
import { fillArray, identity } from '../../lib/index.js';
3+
import { coerceHoverinfo } from '../../lib/index.js';
24
import Registry from '../../registry.js';
35

4-
export default function calc(gd: any): void {
6+
export default function calc(gd: GraphDiv): void {
57
var calcdata = gd.calcdata;
68
var fullLayout = gd._fullLayout;
79

8-
function makeCoerceHoverInfo(trace: any): (val: any) => any {
10+
function makeCoerceHoverInfo(trace: FullTrace): (val: any) => any {
911
return function(val: any): any {
1012
return coerceHoverinfo({hoverinfo: val}, {_module: trace._module}, fullLayout);
1113
};
1214
}
1315

1416
for(var i = 0; i < calcdata.length; i++) {
15-
var cd = calcdata[i];
17+
var cd: CalcDatum[] = calcdata[i];
1618
var trace = cd[0].trace;
1719

1820
// don't include hover calc fields for pie traces
@@ -42,7 +44,7 @@ export default function calc(gd: any): void {
4244
}
4345
}
4446

45-
function paste(traceAttr: any, cd: any, cdAttr: string, fn?: (val: any) => any): void {
47+
function paste(traceAttr: any, cd: CalcDatum[], cdAttr: string, fn?: (val: any) => any): void {
4648
fn = fn || identity;
4749

4850
if(Array.isArray(traceAttr)) {

tasks/gen-schema-types.mjs

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
#!/usr/bin/env node
2+
/**
3+
* Generate TypeScript interfaces from plot-schema.json.
4+
*
5+
* Reads the plotly schema and emits typed interfaces for traces and layout.
6+
* valType mapping:
7+
* string, color, colorscale, subplotid → string
8+
* number, integer, angle → number
9+
* boolean → boolean
10+
* enumerated → union of values
11+
* flaglist → string
12+
* data_array, info_array → any[]
13+
* any → any
14+
* nested objects → recursive interface
15+
*/
16+
import { readFileSync, writeFileSync } from 'fs';
17+
import { join, dirname } from 'path';
18+
import { fileURLToPath } from 'url';
19+
20+
const __dirname = dirname(fileURLToPath(import.meta.url));
21+
const root = join(__dirname, '..');
22+
const schema = JSON.parse(readFileSync(join(root, 'dist/plot-schema.json'), 'utf-8'));
23+
24+
function valTypeToTS(attr) {
25+
if(!attr || !attr.valType) {
26+
if(attr && typeof attr === 'object' && !Array.isArray(attr)) {
27+
return null; // nested object, handle recursively
28+
}
29+
return 'any';
30+
}
31+
switch(attr.valType) {
32+
case 'string': return 'string';
33+
case 'color': return 'string';
34+
case 'colorscale': return 'string | [number, string][]';
35+
case 'subplotid': return 'string';
36+
case 'number': return 'number';
37+
case 'integer': return 'number';
38+
case 'angle': return 'number';
39+
case 'boolean': return 'boolean';
40+
case 'flaglist': return 'string';
41+
case 'data_array': return 'any[]';
42+
case 'info_array': return 'any[]';
43+
case 'any': return 'any';
44+
case 'enumerated':
45+
if(attr.values) {
46+
return attr.values.map(v => {
47+
if(typeof v === 'string') return `'${v}'`;
48+
if(typeof v === 'boolean') return String(v);
49+
return String(v);
50+
}).join(' | ');
51+
}
52+
return 'any';
53+
default: return 'any';
54+
}
55+
}
56+
57+
function generateInterface(name, attrs, indent = '') {
58+
const lines = [];
59+
const skip = new Set(['editType', 'description', 'role', '_isSubplotObj',
60+
'_isLinkedToArray', '_arrayAttrRegexps', '_deprecated', 'impliedEdits',
61+
'uid', '_noTemplating', 'uirevision']);
62+
63+
for(const [key, attr] of Object.entries(attrs)) {
64+
if(skip.has(key)) continue;
65+
if(key.startsWith('_')) continue;
66+
if(key.endsWith('src')) continue; // data source refs
67+
68+
const tsType = valTypeToTS(attr);
69+
if(tsType === null) {
70+
// nested object
71+
const nestedAttrs = Object.fromEntries(
72+
Object.entries(attr).filter(([k]) => !skip.has(k) && !k.startsWith('_'))
73+
);
74+
if(Object.keys(nestedAttrs).length > 0) {
75+
lines.push(`${indent} ${safeName(key)}?: {`);
76+
const inner = generateInterface(key, nestedAttrs, indent + ' ');
77+
lines.push(...inner);
78+
lines.push(`${indent} };`);
79+
}
80+
} else {
81+
lines.push(`${indent} ${safeName(key)}?: ${tsType};`);
82+
}
83+
}
84+
return lines;
85+
}
86+
87+
function safeName(key) {
88+
if(/^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(key)) return key;
89+
return `'${key}'`;
90+
}
91+
92+
// Generate trace interfaces
93+
const output = [];
94+
output.push('/**');
95+
output.push(' * Auto-generated from plot-schema.json');
96+
output.push(' * DO NOT EDIT — regenerate with: node tasks/gen-schema-types.mjs');
97+
output.push(' */');
98+
output.push('');
99+
100+
for(const [traceName, traceSchema] of Object.entries(schema.traces)) {
101+
const attrs = traceSchema.attributes || {};
102+
const iface = `${traceName.charAt(0).toUpperCase() + traceName.slice(1)}Trace`;
103+
output.push(`export interface ${iface} {`);
104+
output.push(` type: '${traceName}';`);
105+
output.push(...generateInterface(iface, attrs));
106+
output.push('}');
107+
output.push('');
108+
}
109+
110+
// Generate layout interface
111+
output.push('export interface SchemaLayout {');
112+
const layoutAttrs = schema.layout.layoutAttributes || {};
113+
output.push(...generateInterface('SchemaLayout', layoutAttrs));
114+
output.push('}');
115+
output.push('');
116+
117+
// Union type for all traces
118+
const traceNames = Object.keys(schema.traces);
119+
output.push('export type AnyTrace = ' + traceNames.map(t =>
120+
`${t.charAt(0).toUpperCase() + t.slice(1)}Trace`
121+
).join(' | ') + ';');
122+
output.push('');
123+
124+
const outPath = join(root, 'types/schema.d.ts');
125+
writeFileSync(outPath, output.join('\n') + '\n');
126+
console.log(`Generated ${outPath}`);
127+
console.log(` ${traceNames.length} trace interfaces`);
128+
console.log(` ${output.length} lines`);

types/core.d.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ export interface GraphDiv extends HTMLDivElement {
1919
_fullData: FullTrace[];
2020
_fullLayout: FullLayout;
2121
_transitionData?: TransitionData;
22-
calcdata: CalcData[][];
22+
calcdata: CalcData[];
2323
_context: PlotConfig;
2424
_promises: Promise<void>[];
2525
_ev?: EventEmitter;

0 commit comments

Comments
 (0)