Skip to content

Implemented generatePlotlyChart() to dynamically render charts based … #6

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -134,3 +134,6 @@ dist
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*

.roo/

4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

137 changes: 126 additions & 11 deletions src/handlers/toolHandlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ import { formatErrorResponse } from '../utils/formatUtils.js';
// Import all tool implementations
import { readQuery, writeQuery, exportQuery } from '../tools/queryTools.js';
import { createTable, alterTable, dropTable, listTables, describeTable } from '../tools/schemaTools.js';
import { appendInsight, listInsights } from '../tools/insightTools.js';
import { appendInsight, generatePlotlyChart, listInsights } from '../tools/insightTools.js';
import { PlotlyChartConfig } from '../types/index.js';

/**
* Handle listing available tools
Expand Down Expand Up @@ -118,6 +119,66 @@ export function handleListTools() {
properties: {},
},
},
{
name: 'generate_plotly_chart',
description: 'Convert query results or DataFrame into interactive Plotly chart JSON',
inputSchema: {
type: 'object',
properties: {
data: {
type: 'array',
items: { type: 'object' },
description: 'Array of data objects (rows from query result or DataFrame)'
},
chartType: {
type: 'string',
enum: ['bar', 'line', 'pie', 'scatter', 'histogram', 'box', 'heatmap'],
description: 'Type of chart to generate'
},
xColumn: {
type: 'string',
description: 'Column name for X-axis (required for bar, line, scatter, histogram charts)'
},
yColumn: {
type: 'string',
description: 'Column name for Y-axis (required for bar, line, scatter charts)'
},
valueColumn: {
type: 'string',
description: 'Column name for values (required for pie charts)'
},
labelColumn: {
type: 'string',
description: 'Column name for labels (required for pie charts)'
},
title: {
type: 'string',
description: 'Chart title'
},
colorColumn: {
type: 'string',
description: 'Column name for color grouping (optional)'
},
aggregation: {
type: 'string',
enum: ['sum', 'avg', 'count', 'min', 'max', 'none'],
description: 'Aggregation method for grouped data',
default: 'none'
},
width: {
type: 'number',
description: 'Chart width in pixels',
default: 800
},
height: {
type: 'number',
description: 'Chart height in pixels',
default: 600
}
},
required: ['data', 'chartType']
}
},
],
};
}
Expand All @@ -133,34 +194,88 @@ export async function handleToolCall(name: string, args: any) {
switch (name) {
case "read_query":
return await readQuery(args.query);

case "write_query":
return await writeQuery(args.query);

case "create_table":
return await createTable(args.query);

case "alter_table":
return await alterTable(args.query);

case "drop_table":
return await dropTable(args.table_name, args.confirm);

case "export_query":
return await exportQuery(args.query, args.format);

case "list_tables":
return await listTables();

case "describe_table":
return await describeTable(args.table_name);

case "append_insight":
return await appendInsight(args.insight);

case "list_insights":
return await listInsights();

case 'generate_plotly_chart': {
const config = args as unknown as PlotlyChartConfig;

if (!config?.data || !config?.chartType) {
return {
content: [{
type: 'text',
text: 'Missing required chart config: data and chartType are required.'
}],
isError: true
};
}

// Validate required fields based on chart type
const missingFields = [];
if (['bar', 'line', 'scatter'].includes(config.chartType)) {
if (!config.xColumn) missingFields.push('xColumn');
if (!config.yColumn) missingFields.push('yColumn');
} else if (config.chartType === 'pie') {
if (!config.valueColumn) missingFields.push('valueColumn');
if (!config.labelColumn) missingFields.push('labelColumn');
} else if (config.chartType === 'histogram') {
if (!config.xColumn) missingFields.push('xColumn');
}

if (missingFields.length > 0) {
return {
content: [{
type: 'text',
text: `Missing required fields for ${config.chartType} chart: ${missingFields.join(', ')}.`
}],
isError: true
};
}

try {
const chartJson = await generatePlotlyChart(config);
return {
content: [{
type: 'text',
text: JSON.stringify(chartJson, null, 2)
}]
};
} catch (error) {
return {
content: [{
type: 'text',
text: `Error generating chart: ${error}`
}],
isError: true
};
}
};


default:
throw new Error(`Unknown tool: ${name}`);
}
Expand Down
157 changes: 155 additions & 2 deletions src/tools/insightTools.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { dbAll, dbExec, dbRun } from '../db/index.js';
import { formatSuccessResponse } from '../utils/formatUtils.js';
import { PlotlyChartConfig } from '../types/index.js';
import { formatSuccessResponse, formatSuccessResponseHTML } from '../utils/formatUtils.js';
import { aggregateData } from '../utils/helper.js';

/**
* Add a business insight to the memo
Expand Down Expand Up @@ -61,4 +63,155 @@ export async function listInsights() {
} catch (error: any) {
throw new Error(`Error listing insights: ${error.message}`);
}
}
}

export async function generatePlotlyChart(config: PlotlyChartConfig): Promise<any> {
const {
data,
chartType,
xColumn,
yColumn,
valueColumn,
labelColumn,
title,
colorColumn,
aggregation = 'none',
width = 800,
height = 600
} = config;

if (!data || data.length === 0) {
throw new Error('No data provided for chart generation');
}

let processedData = [...data];

if (aggregation && aggregation !== 'none' && xColumn) {
const targetColumn = yColumn || valueColumn;
if (targetColumn) {
processedData = aggregateData(data, xColumn, targetColumn, aggregation, colorColumn);
}
}

const layout: any = {
title: title || `${chartType.charAt(0).toUpperCase() + chartType.slice(1)} Chart`,
width,
height
};

let chartData: any[] = [];

try {
switch (chartType) {
case 'bar':
chartData = [{
type: 'bar',
x: processedData.map(row => row[xColumn!]),
y: processedData.map(row => row[yColumn!]),
marker: colorColumn ? { color: processedData.map(row => row[colorColumn]) } : undefined
}];
layout.xaxis = { title: xColumn };
layout.yaxis = { title: yColumn };
break;

case 'line':
chartData = [{
type: 'scatter',
mode: 'lines+markers',
x: processedData.map(row => row[xColumn!]),
y: processedData.map(row => row[yColumn!]),
line: colorColumn ? { color: processedData.map(row => row[colorColumn]) } : undefined
}];
layout.xaxis = { title: xColumn };
layout.yaxis = { title: yColumn };
break;

case 'pie':
chartData = [{
type: 'pie',
labels: processedData.map(row => row[labelColumn!]),
values: processedData.map(row => row[valueColumn!])
}];
break;

case 'scatter':
chartData = [{
type: 'scatter',
mode: 'markers',
x: processedData.map(row => row[xColumn!]),
y: processedData.map(row => row[yColumn!]),
marker: colorColumn ? { color: processedData.map(row => row[colorColumn]), colorscale: 'Viridis' } : undefined
}];
layout.xaxis = { title: xColumn };
layout.yaxis = { title: yColumn };
break;

case 'histogram':
chartData = [{
type: 'histogram',
x: processedData.map(row => row[xColumn!]).filter(Boolean)
}];
layout.xaxis = { title: xColumn };
layout.yaxis = { title: 'Frequency' };
break;

case 'box':
chartData = [{
type: 'box',
y: processedData.map(row => row[xColumn!]).filter(Boolean),
name: xColumn
}];
layout.yaxis = { title: xColumn };
break;

case 'heatmap':
if (!xColumn || !yColumn || !valueColumn) {
throw new Error('Heatmap requires xColumn, yColumn, and valueColumn');
}

const xValues = [...new Set(processedData.map(row => row[xColumn]))];
const yValues = [...new Set(processedData.map(row => row[yColumn]))];

const matrix = yValues.map(y =>
xValues.map(x => {
const match = processedData.find(row => row[xColumn] === x && row[yColumn] === y);
return match ? match[valueColumn] : 0;
})
);

chartData = [{
type: 'heatmap',
x: xValues,
y: yValues,
z: matrix,
colorscale: 'Viridis'
}];
layout.xaxis = { title: xColumn };
layout.yaxis = { title: yColumn };
break;

default:
throw new Error(`Unsupported chart type: ${chartType}`);
}

// Return as full HTML string
return formatSuccessResponseHTML(`
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<script src="https://cdn.plot.ly/plotly-latest.min.js"></script>
</head>
<body>
<div id="chart" style="width:${width}px;height:${height}px;"></div>
<script>
Plotly.newPlot('chart', ${JSON.stringify(chartData)}, ${JSON.stringify(layout)});
</script>
</body>
</html>
`);

} catch (error) {
throw new Error(`Failed to generate ${chartType} chart: ${error}`);
}
}
13 changes: 13 additions & 0 deletions src/types/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
export interface PlotlyChartConfig {
data: any[];
chartType: 'bar' | 'line' | 'pie' | 'scatter' | 'histogram' | 'box' | 'heatmap';
xColumn?: string;
yColumn?: string;
valueColumn?: string;
labelColumn?: string;
title?: string;
colorColumn?: string;
aggregation?: 'sum' | 'avg' | 'count' | 'min' | 'max' | 'none';
width?: number;
height?: number;
}
11 changes: 10 additions & 1 deletion src/utils/formatUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,4 +58,13 @@ export function formatSuccessResponse(data: any): { content: Array<{type: string
}],
isError: false
};
}
}
export function formatSuccessResponseHTML(html: string): { content: Array<{ type: string, text: string }>, isError: boolean } {
return {
content: [{
type: "html",
text: html
}],
isError: false
};
}
Loading