powerbi-visuals-tools 7.0.2 → 7.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Changelog.md +17 -0
- package/MCP.md +234 -0
- package/bin/pbiviz.js +12 -0
- package/eslint.config.mjs +12 -9
- package/lib/CertificateTools.js +1 -1
- package/lib/CommandManager.js +9 -1
- package/lib/ConsoleWriter.js +4 -0
- package/lib/FeatureManager.js +9 -3
- package/lib/Visual.js +6 -0
- package/lib/VisualGenerator.js +2 -2
- package/lib/VisualManager.js +22 -6
- package/lib/WebPackWrap.js +8 -4
- package/lib/features/AuthorInfo.js +14 -0
- package/lib/features/BaseFeature.js +1 -0
- package/lib/features/RenderingEvents.js +1 -0
- package/lib/features/index.js +2 -1
- package/lib/mcp/McpServer.js +122 -0
- package/lib/mcp/tools/availableApis.js +608 -0
- package/lib/mcp/tools/bestPractices.js +391 -0
- package/lib/mcp/tools/certification.js +380 -0
- package/lib/mcp/tools/visualInfo.js +133 -0
- package/lib/mcp/tools/vulnerabilities.js +211 -0
- package/lib/utils.js +27 -0
- package/package.json +22 -18
- package/templates/visuals/_global/.vscode/mcp.json +8 -0
- package/templates/visuals/default/src/visual.ts +17 -4
- package/templates/visuals/rhtml/src/visual.ts +27 -11
- package/templates/visuals/rvisual/src/visual.ts +34 -20
- package/templates/visuals/slicer/src/visual.ts +16 -4
- package/templates/visuals/table/src/visual.ts +14 -1
- package/tsconfig.json +2 -1
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Power BI Visual CLI - MCP Server - Best Practices Tool
|
|
3
|
+
*
|
|
4
|
+
* Copyright (c) Microsoft Corporation
|
|
5
|
+
* All rights reserved.
|
|
6
|
+
* MIT License
|
|
7
|
+
*/
|
|
8
|
+
"use strict";
|
|
9
|
+
import fs from 'fs-extra';
|
|
10
|
+
import path from 'path';
|
|
11
|
+
import { getSourceFiles, existsIgnoreCase } from '../../utils.js';
|
|
12
|
+
async function buildProjectContext(rootPath) {
|
|
13
|
+
const readJsonSafe = async (filePath) => {
|
|
14
|
+
try {
|
|
15
|
+
return await fs.readFile(filePath, 'utf-8');
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
const srcPath = path.join(rootPath, 'src');
|
|
22
|
+
const sourceFiles = await getSourceFiles(srcPath);
|
|
23
|
+
let sourceCode = '';
|
|
24
|
+
for (const file of sourceFiles) {
|
|
25
|
+
sourceCode += await fs.readFile(file, 'utf-8') + '\n';
|
|
26
|
+
}
|
|
27
|
+
return {
|
|
28
|
+
rootPath,
|
|
29
|
+
tsconfigContent: await readJsonSafe(path.join(rootPath, 'tsconfig.json')),
|
|
30
|
+
capabilitiesContent: await readJsonSafe(path.join(rootPath, 'capabilities.json')),
|
|
31
|
+
packageJsonContent: await readJsonSafe(path.join(rootPath, 'package.json')),
|
|
32
|
+
pbivizContent: await readJsonSafe(path.join(rootPath, 'pbiviz.json')),
|
|
33
|
+
sourceCode,
|
|
34
|
+
sourceFiles: sourceFiles.map(f => path.relative(rootPath, f)),
|
|
35
|
+
hasReadme: existsIgnoreCase(path.join(rootPath, 'README.md')),
|
|
36
|
+
hasChangelog: existsIgnoreCase(path.join(rootPath, 'Changelog.md')) || existsIgnoreCase(path.join(rootPath, 'CHANGELOG.md')),
|
|
37
|
+
hasEslint: fs.existsSync(path.join(rootPath, 'eslint.config.mjs')) || fs.existsSync(path.join(rootPath, '.eslintrc.json')) || fs.existsSync(path.join(rootPath, '.eslintrc.js')),
|
|
38
|
+
hasTests: fs.existsSync(path.join(rootPath, 'test')) || fs.existsSync(path.join(rootPath, 'spec')) || fs.existsSync(path.join(rootPath, '__tests__')),
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
const PRACTICES = [
|
|
42
|
+
{
|
|
43
|
+
id: 1, section: 'API & Version Management', title: 'Use Latest API Version',
|
|
44
|
+
description: 'Always use the latest stable powerbi-visuals-api (currently v5.x)',
|
|
45
|
+
check: (ctx) => {
|
|
46
|
+
if (!ctx.packageJsonContent)
|
|
47
|
+
return { status: '⚠️', detail: 'package.json not found' };
|
|
48
|
+
const match = ctx.packageJsonContent.match(/"powerbi-visuals-api"\s*:\s*"[~^]?(\d+)/);
|
|
49
|
+
if (!match)
|
|
50
|
+
return { status: '⚠️', detail: 'powerbi-visuals-api not found in dependencies' };
|
|
51
|
+
const major = parseInt(match[1]);
|
|
52
|
+
if (major >= 5)
|
|
53
|
+
return { status: '✅', detail: `Using API v${match[1]}.x` };
|
|
54
|
+
return { status: '⚠️', detail: `Using API v${match[1]}.x — consider upgrading to v5.x` };
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
id: 2, section: 'API & Version Management', title: 'TypeScript Strict Mode',
|
|
59
|
+
description: 'Enable strict mode in tsconfig.json for better type safety',
|
|
60
|
+
check: (ctx) => {
|
|
61
|
+
if (!ctx.tsconfigContent)
|
|
62
|
+
return { status: '⚠️', detail: 'tsconfig.json not found' };
|
|
63
|
+
if (/"strict"\s*:\s*true/.test(ctx.tsconfigContent))
|
|
64
|
+
return { status: '✅', detail: 'Strict mode enabled' };
|
|
65
|
+
return { status: '❌', detail: 'Strict mode not enabled — add `"strict": true` to tsconfig.json' };
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
id: 3, section: 'Performance', title: 'Minimize Update Loop Work',
|
|
70
|
+
description: 'Cache DOM selections, use data binding efficiently',
|
|
71
|
+
check: (ctx) => {
|
|
72
|
+
const hasUpdate = /update\s*\(/.test(ctx.sourceCode);
|
|
73
|
+
if (!hasUpdate)
|
|
74
|
+
return { status: '⚠️', detail: 'No update() method found' };
|
|
75
|
+
const hasD3Join = /\.join\(/.test(ctx.sourceCode) || /\.enter\(\)/.test(ctx.sourceCode);
|
|
76
|
+
if (hasD3Join)
|
|
77
|
+
return { status: '✅', detail: 'D3 data binding patterns detected' };
|
|
78
|
+
return { status: '⚠️', detail: 'Consider using D3 .join() pattern for efficient data binding' };
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
id: 4, section: 'Performance', title: 'FetchMoreData for Large Datasets',
|
|
83
|
+
description: 'Implement pagination for datasets exceeding initial row limit',
|
|
84
|
+
check: (ctx) => {
|
|
85
|
+
if (/fetchMoreData/.test(ctx.sourceCode))
|
|
86
|
+
return { status: '✅', detail: 'fetchMoreData is implemented' };
|
|
87
|
+
return { status: '⚠️', detail: 'Consider implementing fetchMoreData for large datasets' };
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
id: 5, section: 'Performance', title: 'Optimize Data Processing',
|
|
92
|
+
description: 'Process data once in update(), store results',
|
|
93
|
+
check: (ctx) => {
|
|
94
|
+
const hasViewModel = /viewModel|ViewModel|dataModel|DataModel/.test(ctx.sourceCode);
|
|
95
|
+
if (hasViewModel)
|
|
96
|
+
return { status: '✅', detail: 'Data model/view model pattern detected' };
|
|
97
|
+
return { status: '⚠️', detail: 'Consider creating a ViewModel to separate data processing from rendering' };
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
id: 6, section: 'Security', title: 'No External Network Calls',
|
|
102
|
+
description: 'Avoid fetch/XMLHttpRequest to external URLs',
|
|
103
|
+
check: (ctx) => {
|
|
104
|
+
const hasExternal = /fetch\s*\(\s*['"`]http/.test(ctx.sourceCode) || /XMLHttpRequest/.test(ctx.sourceCode);
|
|
105
|
+
if (hasExternal)
|
|
106
|
+
return { status: '❌', detail: 'External network calls detected — remove for certification' };
|
|
107
|
+
return { status: '✅', detail: 'No external network calls found' };
|
|
108
|
+
}
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
id: 7, section: 'Security', title: 'No eval() or Function()',
|
|
112
|
+
description: 'Never use dynamic code execution',
|
|
113
|
+
check: (ctx) => {
|
|
114
|
+
const hasEval = /eval\s*\(/.test(ctx.sourceCode) || /new\s+Function\s*\(/.test(ctx.sourceCode);
|
|
115
|
+
if (hasEval)
|
|
116
|
+
return { status: '❌', detail: 'Dynamic code execution detected — security risk' };
|
|
117
|
+
return { status: '✅', detail: 'No dynamic code execution found' };
|
|
118
|
+
}
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
id: 8, section: 'Security', title: 'Sanitize User Input',
|
|
122
|
+
description: 'Escape HTML in tooltips and labels',
|
|
123
|
+
check: (ctx) => {
|
|
124
|
+
const hasInnerHTML = /innerHTML\s*=/.test(ctx.sourceCode);
|
|
125
|
+
if (hasInnerHTML)
|
|
126
|
+
return { status: '⚠️', detail: 'innerHTML usage detected — ensure data is sanitized' };
|
|
127
|
+
return { status: '✅', detail: 'No direct innerHTML assignment found' };
|
|
128
|
+
}
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
id: 9, section: 'Accessibility', title: 'Keyboard Navigation',
|
|
132
|
+
description: 'Support Tab navigation and keyboard shortcuts',
|
|
133
|
+
check: (ctx) => {
|
|
134
|
+
const capHasKeys = ctx.capabilitiesContent && /supportsKeyboardFocus/.test(ctx.capabilitiesContent);
|
|
135
|
+
const codeHasKeys = /keydown|keyup|keypress|tabindex|focusable/.test(ctx.sourceCode);
|
|
136
|
+
if (capHasKeys || codeHasKeys)
|
|
137
|
+
return { status: '✅', detail: 'Keyboard navigation support detected' };
|
|
138
|
+
return { status: '⚠️', detail: 'No keyboard navigation detected — required for certification' };
|
|
139
|
+
}
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
id: 10, section: 'Accessibility', title: 'High Contrast Mode',
|
|
143
|
+
description: 'Support all high contrast themes',
|
|
144
|
+
check: (ctx) => {
|
|
145
|
+
const hasHC = /highContrast|isHighContrast|allowHighContrast/.test(ctx.sourceCode);
|
|
146
|
+
if (hasHC)
|
|
147
|
+
return { status: '✅', detail: 'High contrast mode support detected' };
|
|
148
|
+
return { status: '⚠️', detail: 'No high contrast support detected — required for certification' };
|
|
149
|
+
}
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
id: 11, section: 'Accessibility', title: 'Screen Reader Support',
|
|
153
|
+
description: 'Add proper ARIA labels to interactive elements',
|
|
154
|
+
check: (ctx) => {
|
|
155
|
+
const hasAria = /aria-label|aria-role|role=/.test(ctx.sourceCode);
|
|
156
|
+
if (hasAria)
|
|
157
|
+
return { status: '✅', detail: 'ARIA attributes detected' };
|
|
158
|
+
return { status: '⚠️', detail: 'No ARIA labels found — add for better accessibility' };
|
|
159
|
+
}
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
id: 12, section: 'Rendering Events', title: 'Rendering Events (Required)',
|
|
163
|
+
description: 'Report renderingStarted/renderingFinished/renderingFailed events',
|
|
164
|
+
check: (ctx) => {
|
|
165
|
+
const hasStarted = /renderingStarted/.test(ctx.sourceCode);
|
|
166
|
+
const hasFinished = /renderingFinished/.test(ctx.sourceCode);
|
|
167
|
+
const hasFailed = /renderingFailed/.test(ctx.sourceCode);
|
|
168
|
+
if (hasStarted && hasFinished && hasFailed)
|
|
169
|
+
return { status: '✅', detail: 'All 3 rendering events implemented' };
|
|
170
|
+
if (hasStarted || hasFinished)
|
|
171
|
+
return { status: '⚠️', detail: 'Partial rendering events — need renderingStarted, renderingFinished, AND renderingFailed' };
|
|
172
|
+
return { status: '❌', detail: 'No rendering events found — required for certification' };
|
|
173
|
+
}
|
|
174
|
+
},
|
|
175
|
+
{
|
|
176
|
+
id: 13, section: 'Formatting', title: 'Modern Formatting Pane',
|
|
177
|
+
description: 'Use getFormattingModel() for the modern format pane',
|
|
178
|
+
check: (ctx) => {
|
|
179
|
+
if (/getFormattingModel/.test(ctx.sourceCode))
|
|
180
|
+
return { status: '✅', detail: 'Modern formatting pane (getFormattingModel) detected' };
|
|
181
|
+
if (/enumerateObjectInstances/.test(ctx.sourceCode))
|
|
182
|
+
return { status: '⚠️', detail: 'Using legacy format pane — consider migrating to getFormattingModel()' };
|
|
183
|
+
return { status: '⚠️', detail: 'No formatting pane implementation detected' };
|
|
184
|
+
}
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
id: 14, section: 'Project Structure', title: 'Modular Code',
|
|
188
|
+
description: 'Split code into logical modules',
|
|
189
|
+
check: (ctx) => {
|
|
190
|
+
if (ctx.sourceFiles.length > 2)
|
|
191
|
+
return { status: '✅', detail: `${ctx.sourceFiles.length} source files — good modular structure` };
|
|
192
|
+
return { status: '⚠️', detail: `Only ${ctx.sourceFiles.length} source file(s) — consider splitting into modules` };
|
|
193
|
+
}
|
|
194
|
+
},
|
|
195
|
+
{
|
|
196
|
+
id: 15, section: 'Project Structure', title: 'Error Handling',
|
|
197
|
+
description: 'Wrap rendering logic in try/catch',
|
|
198
|
+
check: (ctx) => {
|
|
199
|
+
if (/try\s*\{/.test(ctx.sourceCode) && /catch\s*\(/.test(ctx.sourceCode))
|
|
200
|
+
return { status: '✅', detail: 'Error handling (try/catch) detected' };
|
|
201
|
+
return { status: '⚠️', detail: 'No try/catch found — add error handling around rendering logic' };
|
|
202
|
+
}
|
|
203
|
+
},
|
|
204
|
+
{
|
|
205
|
+
id: 16, section: 'Testing', title: 'Tests Present',
|
|
206
|
+
description: 'Unit tests and visual tests',
|
|
207
|
+
check: (ctx) => {
|
|
208
|
+
if (ctx.hasTests)
|
|
209
|
+
return { status: '✅', detail: 'Test directory found' };
|
|
210
|
+
return { status: '⚠️', detail: 'No tests found — add unit tests for data transformation logic' };
|
|
211
|
+
}
|
|
212
|
+
},
|
|
213
|
+
{
|
|
214
|
+
id: 17, section: 'Testing', title: 'ESLint Configured',
|
|
215
|
+
description: 'Use ESLint for code quality',
|
|
216
|
+
check: (ctx) => {
|
|
217
|
+
if (ctx.hasEslint)
|
|
218
|
+
return { status: '✅', detail: 'ESLint configuration found' };
|
|
219
|
+
return { status: '⚠️', detail: 'No ESLint config — add for consistent code quality' };
|
|
220
|
+
}
|
|
221
|
+
},
|
|
222
|
+
{
|
|
223
|
+
id: 18, section: 'Documentation', title: 'README.md',
|
|
224
|
+
description: 'Document visual capabilities and usage',
|
|
225
|
+
check: (ctx) => {
|
|
226
|
+
if (ctx.hasReadme)
|
|
227
|
+
return { status: '✅', detail: 'README.md found' };
|
|
228
|
+
return { status: '❌', detail: 'No README.md — add documentation' };
|
|
229
|
+
}
|
|
230
|
+
},
|
|
231
|
+
{
|
|
232
|
+
id: 19, section: 'Documentation', title: 'Changelog',
|
|
233
|
+
description: 'Track version changes',
|
|
234
|
+
check: (ctx) => {
|
|
235
|
+
if (ctx.hasChangelog)
|
|
236
|
+
return { status: '✅', detail: 'Changelog found' };
|
|
237
|
+
return { status: '⚠️', detail: 'No Changelog — consider adding one' };
|
|
238
|
+
}
|
|
239
|
+
},
|
|
240
|
+
{
|
|
241
|
+
id: 20, section: 'Context Menu', title: 'Context Menu Support',
|
|
242
|
+
description: 'Right-click context menu for drill, filter, and other actions',
|
|
243
|
+
check: (ctx) => {
|
|
244
|
+
if (/contextMenuService/.test(ctx.sourceCode))
|
|
245
|
+
return { status: '✅', detail: 'Context menu implemented' };
|
|
246
|
+
return { status: '⚠️', detail: 'No context menu — add right-click support for better UX' };
|
|
247
|
+
}
|
|
248
|
+
},
|
|
249
|
+
];
|
|
250
|
+
export async function getBestPractices(rootPath) {
|
|
251
|
+
// If no rootPath, return static list
|
|
252
|
+
if (!rootPath || !fs.existsSync(rootPath)) {
|
|
253
|
+
return getStaticBestPractices();
|
|
254
|
+
}
|
|
255
|
+
// Build project context and check each practice
|
|
256
|
+
const ctx = await buildProjectContext(rootPath);
|
|
257
|
+
let output = `# Power BI Custom Visual Best Practices\n\n`;
|
|
258
|
+
output += `**Project scan results for**: ${path.basename(rootPath)}\n\n`;
|
|
259
|
+
let passed = 0;
|
|
260
|
+
let warnings = 0;
|
|
261
|
+
let failed = 0;
|
|
262
|
+
const sections = new Map();
|
|
263
|
+
for (const practice of PRACTICES) {
|
|
264
|
+
const result = practice.check(ctx);
|
|
265
|
+
if (result.status === '✅')
|
|
266
|
+
passed++;
|
|
267
|
+
else if (result.status === '⚠️')
|
|
268
|
+
warnings++;
|
|
269
|
+
else
|
|
270
|
+
failed++;
|
|
271
|
+
const line = `${result.status} **${practice.id}. ${practice.title}**: ${result.detail}`;
|
|
272
|
+
if (!sections.has(practice.section)) {
|
|
273
|
+
sections.set(practice.section, []);
|
|
274
|
+
}
|
|
275
|
+
sections.get(practice.section).push(line);
|
|
276
|
+
}
|
|
277
|
+
output += `## 📊 Summary: ${passed} passed, ${warnings} warnings, ${failed} issues\n\n`;
|
|
278
|
+
for (const [section, lines] of sections) {
|
|
279
|
+
const sectionEmoji = {
|
|
280
|
+
'API & Version Management': '🎯',
|
|
281
|
+
'Performance': '⚡',
|
|
282
|
+
'Security': '🔒',
|
|
283
|
+
'Accessibility': '♿',
|
|
284
|
+
'Rendering Events': '📡',
|
|
285
|
+
'Formatting': '🎨',
|
|
286
|
+
'Project Structure': '📦',
|
|
287
|
+
'Testing': '🧪',
|
|
288
|
+
'Documentation': '📝',
|
|
289
|
+
'Context Menu': '🖱️',
|
|
290
|
+
};
|
|
291
|
+
output += `### ${sectionEmoji[section] || '📋'} ${section}\n\n`;
|
|
292
|
+
for (const line of lines) {
|
|
293
|
+
output += `${line}\n\n`;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
output += `---\nFor more details, visit: https://learn.microsoft.com/en-us/power-bi/developer/visuals/\n`;
|
|
297
|
+
return output;
|
|
298
|
+
}
|
|
299
|
+
function getStaticBestPractices() {
|
|
300
|
+
return `# Power BI Custom Visual Best Practices
|
|
301
|
+
|
|
302
|
+
## 🎯 API & Version Management
|
|
303
|
+
|
|
304
|
+
1. **Use Latest API Version**: Always use the latest stable powerbi-visuals-api (currently v5.x)
|
|
305
|
+
- Run: \`npm install powerbi-visuals-api@latest\`
|
|
306
|
+
- Update apiVersion in pbiviz.json
|
|
307
|
+
|
|
308
|
+
2. **TypeScript Strict Mode**: Enable strict mode in tsconfig.json for better type safety
|
|
309
|
+
\`\`\`json
|
|
310
|
+
{ "compilerOptions": { "strict": true } }
|
|
311
|
+
\`\`\`
|
|
312
|
+
|
|
313
|
+
## ⚡ Performance Optimization
|
|
314
|
+
|
|
315
|
+
3. **Minimize Update Loop Work**: The \`update()\` method is called frequently
|
|
316
|
+
- Cache DOM selections
|
|
317
|
+
- Use data binding efficiently (D3.js .join() pattern)
|
|
318
|
+
- Avoid heavy computations in update()
|
|
319
|
+
|
|
320
|
+
4. **Use Lazy Loading**: Load resources only when needed
|
|
321
|
+
- Defer non-critical rendering
|
|
322
|
+
- Use requestAnimationFrame for smooth animations
|
|
323
|
+
|
|
324
|
+
5. **Optimize Data Processing**:
|
|
325
|
+
- Process data once in update(), store results
|
|
326
|
+
- Use Web Workers for heavy calculations
|
|
327
|
+
- Implement pagination with fetchMoreData for large datasets
|
|
328
|
+
|
|
329
|
+
## 🔒 Security Guidelines
|
|
330
|
+
|
|
331
|
+
6. **No External Network Calls**: Avoid fetch/XMLHttpRequest to external URLs
|
|
332
|
+
- Use only Power BI host services
|
|
333
|
+
- Required for certification
|
|
334
|
+
|
|
335
|
+
7. **Sanitize User Input**: Always sanitize data before rendering
|
|
336
|
+
- Escape HTML in tooltips and labels
|
|
337
|
+
- Prevent XSS vulnerabilities
|
|
338
|
+
|
|
339
|
+
8. **No eval() or Function()**: Never use dynamic code execution
|
|
340
|
+
|
|
341
|
+
## ♿ Accessibility (Required for Certification)
|
|
342
|
+
|
|
343
|
+
9. **Keyboard Navigation**: Implement IVisualHost.hostCapabilities
|
|
344
|
+
- Support Tab navigation
|
|
345
|
+
- Provide keyboard shortcuts
|
|
346
|
+
|
|
347
|
+
10. **High Contrast Mode**: Support all high contrast themes
|
|
348
|
+
- Use host.colorPalette for colors
|
|
349
|
+
- Test with Windows High Contrast
|
|
350
|
+
|
|
351
|
+
11. **Screen Reader Support**: Add proper ARIA labels
|
|
352
|
+
- role attributes on interactive elements
|
|
353
|
+
- aria-label for data points
|
|
354
|
+
|
|
355
|
+
## 📦 Project Structure
|
|
356
|
+
|
|
357
|
+
12. **Modular Code**: Split code into logical modules
|
|
358
|
+
- Separate data transformation, rendering, and formatting
|
|
359
|
+
- Use ES6 modules
|
|
360
|
+
|
|
361
|
+
13. **Proper Error Handling**: Graceful degradation
|
|
362
|
+
\`\`\`typescript
|
|
363
|
+
try {
|
|
364
|
+
// rendering logic
|
|
365
|
+
} catch (e) {
|
|
366
|
+
console.error('Visual error:', e);
|
|
367
|
+
}
|
|
368
|
+
\`\`\`
|
|
369
|
+
|
|
370
|
+
## 🎨 Formatting Pane (Modern)
|
|
371
|
+
|
|
372
|
+
14. **Use FormattingModel**: Implement getFormattingModel() for modern formatting
|
|
373
|
+
- Provides better UX than legacy format pane
|
|
374
|
+
- Required for new visuals
|
|
375
|
+
|
|
376
|
+
## 🧪 Testing
|
|
377
|
+
|
|
378
|
+
15. **Unit Tests**: Test data transformation logic
|
|
379
|
+
16. **Visual Tests**: Use Playwright or similar for E2E tests
|
|
380
|
+
17. **Test Edge Cases**: Empty data, single point, large datasets
|
|
381
|
+
|
|
382
|
+
## 📝 Documentation
|
|
383
|
+
|
|
384
|
+
18. **README.md**: Document visual capabilities and usage
|
|
385
|
+
19. **Changelog**: Track version changes
|
|
386
|
+
20. **Inline Comments**: Document complex logic
|
|
387
|
+
|
|
388
|
+
---
|
|
389
|
+
For more details, visit: https://learn.microsoft.com/en-us/power-bi/developer/visuals/
|
|
390
|
+
`;
|
|
391
|
+
}
|