Skip to content

Commit e16bc76

Browse files
feat(component): add component for visualization (#4)
* feat(component): add component for visualization GH-0 * feat(component): use number.parseFloat as per sonar suggestion GH-0
1 parent 4ce4ec1 commit e16bc76

39 files changed

Lines changed: 1575 additions & 46 deletions

README.md

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,117 @@ Users can provide feedback on generated datasets through the dataset actions end
173173

174174
![alt text](https://raw.githubusercontent.com/sourcefuse/llm-chat-component/refs/heads/main/db-query-graph.png)
175175

176+
## VisualizerComponent
177+
178+
The `VisualizerComponent` extends the LLM Chat functionality by providing intelligent data visualization capabilities. This component automatically generates charts and graphs based on database query results, making data insights more accessible and visually appealing.
179+
180+
### Features
181+
182+
- **Automatic Visualization Selection** - The system intelligently selects the most appropriate visualization type based on the data structure and user prompt
183+
- **Multiple Chart Types** - Supports bar charts, line charts, and pie charts out of the box
184+
- **LLM-Powered Configuration** - Uses AI to generate optimal chart configurations including axis selection, orientation, and styling
185+
- **Seamless Integration** - Works directly with datasets generated by the DbQueryComponent
186+
187+
### Available Visualizers
188+
189+
The component includes three built-in visualizers:
190+
191+
#### Bar Chart Visualizer (`src/components/visualization/visualizers/bar.visualizer.ts:13`)
192+
193+
- **Best for**: Comparing values across different categories or showing trends over time
194+
- **Configuration**: Automatically determines category column (x-axis) and value column (y-axis)
195+
- **Options**: Supports both vertical and horizontal orientations
196+
197+
#### Line Chart Visualizer (`src/components/visualization/visualizers/line.visualizer.ts`)
198+
199+
- **Best for**: Displaying trends and changes over time
200+
- **Configuration**: Optimized for time-series data and continuous variables
201+
202+
#### Pie Chart Visualizer (`src/components/visualization/visualizers/pie.visualizer.ts`)
203+
204+
- **Best for**: Showing proportions and percentages of a whole
205+
- **Configuration**: Automatically identifies categorical data and corresponding values
206+
207+
### Usage
208+
209+
#### Basic Setup
210+
211+
```ts
212+
import {VisualizerComponent} from 'lb4-llm-chat-component';
213+
214+
export class MyApplication extends BootMixin(
215+
ServiceMixin(RepositoryMixin(RestApplication)),
216+
) {
217+
constructor(options: ApplicationConfig = {}) {
218+
// Add the visualizer component
219+
this.component(VisualizerComponent);
220+
221+
// ... other configuration
222+
}
223+
}
224+
```
225+
226+
#### Generate Visualization Tool
227+
228+
The component provides a `generate-visualization` tool that can be used by the LLM to create visualizations:
229+
230+
- **Input**: Takes a user prompt and dataset ID from a previously generated query
231+
- **Process**: Automatically selects the best visualization type and generates optimal configuration
232+
- **Output**: Renders the visualization in the UI for the user
233+
234+
#### Example Usage Flow
235+
236+
1. User asks: "Show me sales by region as a chart"
237+
2. LLM uses `generate-query` tool to create a dataset with sales data by region
238+
3. LLM uses `generate-visualization` tool with the dataset ID
239+
4. System selects bar chart as the most appropriate visualization
240+
5. Chart is rendered with regions on x-axis and sales values on y-axis
241+
242+
### Visualization Graph Flow
243+
244+
The visualization process follows a structured graph workflow (`src/components/visualization/visualization.graph.ts:9`):
245+
246+
1. **Get Dataset Data** - Retrieves the dataset and query information
247+
2. **Select Visualization** - Chooses the most appropriate chart type based on data structure
248+
3. **Render Visualization** - Generates the final chart configuration and displays it
249+
250+
### Creating Custom Visualizers
251+
252+
You can extend the system with custom visualizers by implementing the `IVisualizer` interface (`src/components/visualization/types.ts:4`):
253+
254+
```ts
255+
import {visualizer} from 'lb4-llm-chat-component';
256+
import {IVisualizer, VisualizationGraphState} from 'lb4-llm-chat-component';
257+
258+
@visualizer()
259+
export class CustomVisualizer implements IVisualizer {
260+
name = 'custom-chart';
261+
description = 'Description of when to use this visualizer';
262+
263+
async getConfig(state: VisualizationGraphState): Promise<AnyObject> {
264+
// Generate configuration based on the data and user prompt
265+
return {
266+
// your custom chart configuration
267+
};
268+
}
269+
}
270+
```
271+
272+
### Configuration
273+
274+
The visualizer component automatically registers all available visualizers and makes them available to the LLM. No additional configuration is required for basic usage. You can register a new visualizer using the `@visualizer` decorator on a class following the IVisualizer interface.
275+
276+
### Integration with DbQueryComponent
277+
278+
The visualizer component works seamlessly with the `DbQueryComponent`:
279+
280+
1. Use database query tools to generate datasets
281+
2. The visualization tool automatically accesses dataset metadata including:
282+
- SQL query structure
283+
- Query description
284+
- User's original prompt
285+
3. This context helps generate more accurate and relevant visualizations
286+
176287
## Providing Context
177288

178289
There are two ways to provide context to the LLM -

src/.DS_Store

6 KB
Binary file not shown.
Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
import {expect, sinon} from '@loopback/testlab';
2+
import {BarVisualizer} from '../../../../components/visualization/visualizers/bar.visualizer';
3+
import {LLMProvider} from '../../../../types';
4+
import {fail} from 'assert';
5+
import {VisualizationGraphState} from '../../../../components';
6+
7+
describe('BarVisualizer Unit', function () {
8+
let visualizer: BarVisualizer;
9+
let llmProvider: sinon.SinonStubbedInstance<LLMProvider>;
10+
let withStructuredOutputStub: sinon.SinonStub;
11+
12+
beforeEach(() => {
13+
// Create stub for LLM provider
14+
withStructuredOutputStub = sinon.stub();
15+
llmProvider = {
16+
withStructuredOutput: withStructuredOutputStub,
17+
} as sinon.SinonStubbedInstance<LLMProvider>;
18+
19+
visualizer = new BarVisualizer(llmProvider);
20+
});
21+
22+
afterEach(() => {
23+
sinon.restore();
24+
});
25+
26+
it('should have correct name and description', () => {
27+
expect(visualizer.name).to.equal('bar');
28+
expect(visualizer.description).to.match(/bar chart/);
29+
expect(visualizer.description).to.match(/comparing values/);
30+
});
31+
32+
it('should have valid schema with required fields', () => {
33+
const schema = visualizer.schema;
34+
expect(schema).to.be.ok();
35+
36+
// Test schema structure by trying to parse valid data
37+
const validData = {
38+
categoryColumn: 'category',
39+
valueColumn: 'value',
40+
orientation: 'vertical',
41+
};
42+
43+
const result = schema.safeParse(validData);
44+
expect(result.success).to.be.true();
45+
46+
if (result.success) {
47+
expect(result.data).to.deepEqual(validData);
48+
}
49+
});
50+
51+
it('should validate schema with default orientation', () => {
52+
const schema = visualizer.schema;
53+
const dataWithoutOrientation = {
54+
categoryColumn: 'category',
55+
valueColumn: 'value',
56+
};
57+
58+
const result = schema.safeParse(dataWithoutOrientation);
59+
expect(result.success).to.be.true();
60+
61+
if (result.success) {
62+
expect(result.data.orientation).to.equal('vertical');
63+
}
64+
});
65+
66+
it('should reject invalid orientation values', () => {
67+
const schema = visualizer.schema;
68+
const invalidData = {
69+
categoryColumn: 'category',
70+
valueColumn: 'value',
71+
orientation: 42, // invalid type
72+
};
73+
74+
const result = schema.safeParse(invalidData);
75+
expect(result.success).to.be.false();
76+
});
77+
78+
it('should throw error when state is invalid (missing sql)', async () => {
79+
const invalidState = {
80+
prompt: 'test prompt',
81+
datasetId: 'test-id',
82+
queryDescription: 'test description',
83+
// sql is missing - will be undefined
84+
} as unknown as VisualizationGraphState;
85+
86+
try {
87+
await visualizer.getConfig(invalidState);
88+
fail('Should have thrown an error');
89+
} catch (error) {
90+
expect(error).to.have.property('message', 'Invalid State');
91+
}
92+
});
93+
94+
it('should throw error when state is invalid (missing queryDescription)', async () => {
95+
const invalidState = {
96+
prompt: 'test prompt',
97+
datasetId: 'test-id',
98+
sql: 'SELECT * FROM test',
99+
// queryDescription is missing - will be undefined
100+
} as unknown as VisualizationGraphState;
101+
102+
try {
103+
await visualizer.getConfig(invalidState);
104+
fail('Should have thrown an error');
105+
} catch (error) {
106+
expect(error).to.have.property('message', 'Invalid State');
107+
}
108+
});
109+
110+
it('should throw error when state is invalid (missing prompt)', async () => {
111+
const invalidState = {
112+
datasetId: 'test-id',
113+
sql: 'SELECT * FROM test',
114+
queryDescription: 'test description',
115+
// prompt is missing - will be undefined
116+
} as unknown as VisualizationGraphState;
117+
118+
try {
119+
await visualizer.getConfig(invalidState);
120+
fail('Should have thrown an error');
121+
} catch (error) {
122+
expect(error).to.have.property('message', 'Invalid State');
123+
}
124+
});
125+
126+
it('should successfully generate config with valid state', async () => {
127+
const mockLLMResponse = {
128+
categoryColumn: 'department',
129+
valueColumn: 'salary',
130+
orientation: 'vertical',
131+
};
132+
133+
const mockInvoke = sinon.stub().resolves(mockLLMResponse);
134+
withStructuredOutputStub.returns(mockInvoke);
135+
136+
const validState = {
137+
prompt: 'Show me a bar chart of salaries by department',
138+
datasetId: 'test-dataset',
139+
sql: 'SELECT department, AVG(salary) as avg_salary FROM employees GROUP BY department',
140+
queryDescription: 'Average salary by department',
141+
} as unknown as VisualizationGraphState;
142+
143+
const config = await visualizer.getConfig(validState);
144+
145+
expect(config).to.deepEqual(mockLLMResponse);
146+
expect(
147+
withStructuredOutputStub.calledOnceWith(visualizer.schema),
148+
).to.be.true();
149+
expect(mockInvoke.calledOnce).to.be.true();
150+
151+
// Check that the mock was called with a StringPromptValue containing our data
152+
const invokeArgs = mockInvoke.getCall(0).args[0];
153+
expect(invokeArgs).to.have.property('value');
154+
// Escape special regex characters in SQL
155+
const escapedSQL =
156+
validState.sql?.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') ?? '';
157+
expect(invokeArgs.value).to.match(new RegExp(escapedSQL));
158+
expect(invokeArgs.value).to.match(
159+
new RegExp(validState.queryDescription ?? ''),
160+
);
161+
expect(invokeArgs.value).to.match(new RegExp(validState.prompt));
162+
});
163+
164+
it('should handle LLM errors gracefully', async () => {
165+
const mockError = new Error('LLM processing failed');
166+
const mockInvoke = sinon.stub().rejects(mockError);
167+
withStructuredOutputStub.returns(mockInvoke);
168+
169+
const validState = {
170+
prompt: 'test prompt',
171+
datasetId: 'test-dataset',
172+
sql: 'SELECT * FROM test',
173+
queryDescription: 'test description',
174+
} as unknown as VisualizationGraphState;
175+
176+
try {
177+
await visualizer.getConfig(validState);
178+
fail('Should have thrown an error');
179+
} catch (error) {
180+
expect(error).to.equal(mockError);
181+
}
182+
});
183+
184+
it('should contain proper prompt template structure', () => {
185+
const promptTemplate = visualizer.renderPrompt;
186+
expect(promptTemplate).to.be.ok();
187+
188+
const templateText = promptTemplate.template;
189+
expect(templateText).to.match(/bar chart/);
190+
expect(templateText).to.match(/\{sql\}/);
191+
expect(templateText).to.match(/\{description\}/);
192+
expect(templateText).to.match(/\{userPrompt\}/);
193+
expect(templateText).to.match(/x-axis/);
194+
expect(templateText).to.match(/y-axis/);
195+
});
196+
});

0 commit comments

Comments
 (0)