---
title: Copilot Basics
sidebar_position: 7
description: Learn the basics of OpenBB Copilot interface and functionality
keywords:
- OpenBB Copilot
- copilot basics
- AI assistant
- interface
- prompt library
---
import HeadTitle from '@site/src/components/General/HeadTitle.tsx';
Additionally, when you click “Add matching widget to dashboard,” the widget will be automatically added to your current dashboard, using the same parameters applied by Copilot.
---
---
title: Orchestrator Mode (Beta)
sidebar_position: 14
description: Understanding OpenBB Copilot's Orchestrator Mode for multi-agent workflows
keywords:
- Orchestrator Mode
- Multi-agent
- Workflow coordination
- Task delegation
- Agent routing
---
import HeadTitle from '@site/src/components/General/HeadTitle.tsx';
3. Enter your base URL; Workspace fetches `/agents.json` and uses `/query` for conversations.
Ensure CORS settings are correct and SSE are configured on your service.
## Example
Simplistic example that allows users to communicate with an agent that can optimize the user prompt. The code is open source [and available here](https://github.com/OpenBB-finance/agents-for-openbb/tree/main/20-financial-prompt-optimizer).
This agent does nothing else - it doesn't parse data added to context, doesn't pass data in the dashboard, doesn't share step-by-step reasoning or citations, doesn't create artifacts, etc. We will dive on each of these features in the AI features tab.
```python
from typing import AsyncGenerator
import openai
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import JSONResponse
from sse_starlette.sse import EventSourceResponse
from openbb_ai.models import MessageChunkSSE, QueryRequest
from openbb_ai import message_chunk
from openai.types.chat import (
ChatCompletionMessageParam,
ChatCompletionUserMessageParam,
ChatCompletionAssistantMessageParam,
ChatCompletionSystemMessageParam,
)
app = FastAPI()
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
@app.get("/agents.json")
def get_copilot_description():
"""Agent descriptor for the OpenBB Workspace."""
return JSONResponse(
content={
"financial_prompt_optimizer": {
"name": "Financial Prompt Optimizer",
"description": "Optimizes a user's prompt for finance: clearer, more specific, and actionable.",
"image": "https://github.com/OpenBB-finance/copilot-for-terminal-pro/assets/14093308/7da2a512-93b9-478d-90bc-b8c3dd0cabcf",
"endpoints": {"query": "http://localhost:7777/v1/query"},
"features": {
"streaming": True,
"widget-dashboard-select": False,
"widget-dashboard-search": False,
},
}
}
)
@app.post("/v1/query")
async def query(request: QueryRequest) -> EventSourceResponse:
"""Stream a concise optimized prompt and rationale."""
openai_messages: list[ChatCompletionMessageParam] = [
ChatCompletionSystemMessageParam(
role="system",
content=(
"You are a concise Financial Prompt Optimizer.\n"
"Rewrite the user's prompt to be clearer, more specific, and immediately actionable for financial analysis.\n"
"Always return exactly the improved prompt:\n"
"Optimized Prompt:
## Architecture
This pattern extends widget citations by adding document-level text extraction and positioning. Extract PDF content with character-level precision for accurate highlighting.
`agents.json` configuration:
```python
return JSONResponse(content={
"vanilla_agent_pdf_citations": {
"name": "Vanilla Agent PDF Citations",
"description": "A vanilla agent that handles PDF data with citation support.",
"endpoints": {"query": "http://localhost:7777/v1/query"},
"features": {
"streaming": True,
"widget-dashboard-select": True,
"widget-dashboard-search": False,
},
}
})
```
### Query flow
- Extract PDF content when widget data contains PDF format
- Use `pdfplumber` to get text with character positions
- Store text positions for citation highlighting
- Create multiple citation types:
- Basic widget citation for data source
- Highlighted citation for specific text passages
- Stream citations with bounding boxes for visual highlighting
### OpenBB AI SDK
- `CitationHighlightBoundingBox`: Precise text highlighting with coordinates
- `PdfDataFormat`: Identifies PDF content in widget data
- `quote_bounding_boxes`: Attach visual highlights to citations
- Text position tracking: `x0`, `top`, `x1`, `bottom` for accurate placement
## Core logic
Extract PDF text with positions for precise highlighting:
```python
import pdfplumber
from openbb_ai import cite, citations
from openbb_ai.models import CitationHighlightBoundingBox
def extract_pdf_with_positions(pdf_bytes: bytes) -> Tuple[str, List[Dict[str, Any]]]:
"""Extract text and positions from PDF."""
document_text = ""
text_positions = []
with pdfplumber.open(io.BytesIO(pdf_bytes)) as pdf:
for page_num, page in enumerate(pdf.pages, 1):
# Extract character-level data for positioning
if page.chars:
# Group characters into lines
lines = {}
for char in page.chars:
y = round(char['top'])
if y not in lines:
lines[y] = {'chars': [], 'x0': char['x0'], 'x1': char['x1']}
lines[y]['chars'].append(char['text'])
lines[y]['x0'] = min(lines[y]['x0'], char['x0'])
lines[y]['x1'] = max(lines[y]['x1'], char['x1'])
# Get first meaningful line for citation
sorted_lines = sorted(lines.items())
for y_pos, line_data in sorted_lines[:5]:
line_text = ''.join(line_data['chars']).strip()
if line_text and len(line_text) > 10:
text_positions.append({
'text': line_text,
'page': page_num,
'x0': line_data['x0'],
'top': y_pos,
'x1': line_data['x1'],
'bottom': y_pos + 12
})
break
# Extract full text for context
page_text = page.extract_text()
if page_text:
document_text += page_text + "\\n\\n"
return document_text, text_positions
```
Create citations with line/word level highlighting in the PDF:
```python
async def handle_widget_data(data: list[DataContent | DataFileReferences]):
"""Process widget data and create PDF citations."""
widget_text, pdf_text_positions = await handle_widget_data(message.data)
context_str += widget_text
# Create citations for widget data
for widget_data_request in message.input_arguments["data_sources"]:
widget = matching_widgets[0]
# Basic widget citation
basic_citation = cite(
widget=widget,
input_arguments=widget_data_request["input_args"],
)
citations_list.append(basic_citation)
# PDF citation with highlighting
if pdf_text_positions and len(pdf_text_positions) > 0:
first_line = pdf_text_positions[0]
pdf_citation = cite(
widget=widget,
input_arguments=widget_data_request["input_args"],
extra_details={
"Page": first_line['page'],
"Reference": "First sentence of document"
}
)
# Add precise text highlighting
pdf_citation.quote_bounding_boxes = [[
CitationHighlightBoundingBox(
text=first_line['text'][:100],
page=first_line['page'],
x0=first_line['x0'],
top=first_line['top'],
x1=first_line['x1'],
bottom=first_line['bottom']
)
]]
citations_list.append(pdf_citation)
```
---
---
title: Create charts
sidebar_position: 6
description: Stream inline charts as part of your agent’s response
keywords:
- charts
- visualization
- artifacts
- SSE
---
import HeadTitle from '@site/src/components/General/HeadTitle.tsx';
## Architecture
Emit chart artifacts so visualizations render below the answer. The example shows multiple chart types in one response.
`agents.json` configuration with `widget-dashboard-select` feature enabled:
```python
return JSONResponse(content={
"vanilla_agent_charts": {
"endpoints": {"query": "http://localhost:7777/v1/query"},
"features": {
"widget-dashboard-select": False,
"widget-dashboard-search": False,
},
}
})
```
### Query flow
- Process user request and prepare data for visualization
- Stream explanatory text with `message_chunk()`
- Create chart data as list of dictionaries
- Choose appropriate chart type based on data characteristics:
- **Line/Bar/Scatter**: Use `x_key` and `y_keys` for XY data
- **Pie/Donut**: Use `angle_key` for values, `callout_label_key` for labels
- Emit `chart()` artifacts with proper configuration
- Charts render interactively below streamed content
### OpenBB AI SDK
- `chart(type, data, x_key, y_keys, name, description)`: Creates `MessageArtifactSSE` for chart display
- Chart types: `"line"`, `"bar"`, `"scatter"`, `"pie"`, `"donut"`
- Chart parameters handled by specific models:
- `LineChartParameters`, `BarChartParameters`, `ScatterChartParameters`
- `PieChartParameters`, `DonutChartParameters`
- `message_chunk(text)`: Streams explanatory text around charts
- Charts support interactive features like zoom, hover, and data export
## Core logic
```python
from openbb_ai import message_chunk, chart
from openbb_ai.models import QueryRequest
import datetime
async def query(request: QueryRequest) -> EventSourceResponse:
async def execution_loop():
# Stream introduction
yield message_chunk("Let me create some visualizations to illustrate the data trends.\n\n").model_dump()
# Prepare time series data
price_data = [
{"date": "2024-01-01", "price": 150.0, "volume": 1200000},
{"date": "2024-01-02", "price": 152.5, "volume": 1350000},
{"date": "2024-01-03", "price": 148.2, "volume": 1100000},
{"date": "2024-01-04", "price": 155.8, "volume": 1450000},
]
# Create line chart for price trend
yield chart(
type="line",
data=price_data,
x_key="date",
y_keys=["price"],
name="Stock Price Trend",
description="Daily stock price movement over time"
).model_dump()
yield message_chunk("\n\nThe line chart shows an overall upward trend. Now let's look at volume distribution:\n\n").model_dump()
# Create bar chart for volume
yield chart(
type="bar",
data=price_data,
x_key="date",
y_keys=["volume"],
name="Trading Volume",
description="Daily trading volume by date"
).model_dump()
# Portfolio allocation pie chart
portfolio_data = [
{"asset": "Stocks", "allocation": 60},
{"asset": "Bonds", "allocation": 25},
{"asset": "Cash", "allocation": 10},
{"asset": "Real Estate", "allocation": 5}
]
yield message_chunk("\n\nHere's the portfolio allocation breakdown:\n\n").model_dump()
yield chart(
type="pie",
data=portfolio_data,
angle_key="allocation",
callout_label_key="asset",
name="Portfolio Allocation",
description="Investment portfolio distribution by asset class"
).model_dump()
return EventSourceResponse(execution_loop(), media_type="text/event-stream")
```
---
---
title: Create tables
sidebar_position: 5
description: Stream tabular data as an artifact in the conversation
keywords:
- tables
- artifacts
- SSE
---
import HeadTitle from '@site/src/components/General/HeadTitle.tsx';
## Architecture
Emit table artifacts in-line so tabular data renders below the answer.
`agents.json` configuration:
```python
return JSONResponse(content={
"vanilla_agent_tables": {
"endpoints": {"query": "http://localhost:7777/v1/query"},
"features": {
"widget-dashboard-select": False,
"widget-dashboard-search": False,
},
}
})
```
### Query flow
- Process user request and generate data (from widgets, analysis, or computation)
- Stream explanatory text with `message_chunk()`
- Create table data as list of dictionaries (column names from object keys)
- Emit `table()` artifact with data, name, and description
- Table renders below the streamed text in Workspace UI
- Continue with additional content or complete response
### OpenBB AI SDK
- `table(data, name, description)`: Creates `MessageArtifactSSE` for table display
- `data`: List of dictionaries where keys become column headers
- `name`: Table title displayed in UI
- `description`: Optional table description
- `message_chunk(text)`: Streams explanatory text before/after tables
- Tables automatically handle data formatting and sorting in UI
## Core logic
```python
from openbb_ai import message_chunk, table
from openbb_ai.models import QueryRequest
async def query(request: QueryRequest) -> EventSourceResponse:
async def execution_loop():
# Stream introduction
yield message_chunk("Let me analyze the financial data and create a summary table.\n\n").model_dump()
# Generate or process data (from widgets, calculations, etc.)
financial_data = [
{"symbol": "AAPL", "price": 150.25, "change": 2.5, "volume": 1200000},
{"symbol": "MSFT", "price": 280.75, "change": -1.2, "volume": 890000},
{"symbol": "GOOGL", "price": 2650.80, "change": 15.3, "volume": 560000},
]
# Create table artifact
yield table(
data=financial_data,
name="Stock Market Summary",
description="Current stock prices with daily changes and trading volume"
).model_dump()
# Stream additional analysis
yield message_chunk("\n\nThe table above shows the current market status. AAPL and GOOGL are up, while MSFT is down slightly.").model_dump()
return EventSourceResponse(execution_loop(), media_type="text/event-stream")
```
---
---
title: Custom agent features
sidebar_position: 7
description: Configure and manage custom agent features based on workspace options
keywords:
- features
- configuration
- workspace options
- custom agents
- SSE
---
import HeadTitle from '@site/src/components/General/HeadTitle.tsx';
## Architecture
Configure custom features in your agent's descriptor and access them through the query request payload. Features can have default states, descriptions, and labels.
`agents.json` configuration with custom features:
```python
return JSONResponse(content={
"vanilla_agent_custom_features": {
"name": "Vanilla Agent Custom Features",
"description": "A simple agent that reports its feature status.",
"endpoints": {"query": "/v1/query"},
"features": {
"streaming": True,
"widget-dashboard-select": False,
"widget-dashboard-search": False,
"deep-research": {
"label": "Deep Research",
"default": False,
"description": "Allows the copilot to do deep research",
},
"web-search": {
"label": "Web Search",
"default": True,
"description": "Allows the copilot to search the web.",
},
},
}
})
```
### Feature configuration
- **Simple features**: Boolean values for basic on/off features
- **Complex features**: Objects with `label`, `default`, and `description` properties
- **Built-in features**: Standard features like `streaming`, `widget-dashboard-select`, `widget-dashboard-search`
- **Custom features**: User-defined features with custom behavior
### Query flow
- User enables/disables features in workspace settings
- Features are passed to agent via `workspace_options` in request payload
- Agent checks enabled features and adjusts behavior accordingly
- Agent can report feature status back to user
- Response content varies based on active features
### OpenBB AI SDK
- `QueryRequest.workspace_options`: List of enabled feature names
- `message_chunk(text)`: Streams response content with feature-aware messaging
- Feature checking via simple list membership: `"feature-name" in workspace_options`
## Core logic
```python
from typing import AsyncGenerator
from openbb_ai import message_chunk
from openbb_ai.models import MessageChunkSSE, QueryRequest
from fastapi.responses import JSONResponse
from sse_starlette.sse import EventSourceResponse
@app.post("/v1/query")
async def query(request: QueryRequest) -> EventSourceResponse:
# Access workspace options from request payload
workspace_options = getattr(request, "workspace_options", [])
# Check which features are enabled
deep_research_enabled = "deep-research" in workspace_options
web_search_enabled = "web-search" in workspace_options
# Build feature status message
features_msg = (
f"- Deep Research: {'✅ Enabled' if deep_research_enabled else '❌ Disabled'}\n"
f"- Web Search: {'✅ Enabled' if web_search_enabled else '❌ Disabled'}"
)
# Include feature status in system prompt
openai_messages = [
{
"role": "system",
"content": (
"You are a simple greeting agent.\n"
"Greet the user and let them know their current feature settings:\n"
f"{features_msg}\n"
"Keep your response brief and friendly."
),
}
]
# Add conversation history
for message in request.messages:
if message.role == "human":
openai_messages.append({"role": "user", "content": message.content})
elif message.role == "ai" and isinstance(message.content, str):
openai_messages.append({"role": "assistant", "content": message.content})
async def execution_loop() -> AsyncGenerator[MessageChunkSSE, None]:
client = openai.AsyncOpenAI()
async for event in await client.chat.completions.create(
model="gpt-4o",
messages=openai_messages,
stream=True,
):
if chunk := event.choices[0].delta.content:
yield message_chunk(chunk)
return EventSourceResponse(
content=(
event.model_dump(exclude_none=True) async for event in execution_loop()
),
media_type="text/event-stream",
)
```
## Feature types
### Boolean features
Simple on/off switches in the agent descriptor:
```python
"features": {
"streaming": True,
"some-feature": False
}
```
### Complex features
Rich feature objects with metadata:
```python
"features": {
"research-mode": {
"label": "Research Mode",
"default": True,
"description": "Enables comprehensive research capabilities"
}
}
```
### Conditional behavior
Adjust agent behavior based on enabled features:
```python
workspace_options = getattr(request, "workspace_options", [])
if "research-mode" in workspace_options:
# Enable research capabilities
pass
if "web-search" in workspace_options:
# Enable web search functionality
pass
```
---
---
title: Highlight widget citations
sidebar_position: 4
description: Cite widget data sources in your responses and display them in Workspace
keywords:
- citations
- cite
- widgets
- provenance
---
import HeadTitle from '@site/src/components/General/HeadTitle.tsx';
## Architecture
This pattern extends "Parse widget data" by adding attribution. After data retrieval, emit citations to show provenance in Workspace.
`agents.json` configuration with `widget-dashboard-select` feature enabled:
```python
return JSONResponse(content={
"vanilla_agent_raw_widget_data_citations": {
"endpoints": {"query": "http://localhost:7777/v1/query"},
"features": {
"streaming": True,
"widget-dashboard-select": True,
"widget-dashboard-search": False,
},
}
})
```
### Query flow
- Early exit: fetch widget data when human message contains `widgets.primary`
- On subsequent tool message:
- Process widget data and include in LLM context
- Match widget UUIDs from tool `input_arguments` to `request.widgets.primary`
- Build `cite()` objects for each data source used
- Stream citations with `citations()` after LLM response
- Citations appear in Workspace UI panel for source verification
### OpenBB AI SDK
- `cite(widget, input_arguments, extra_details)`: Creates `Citation` objects
- `citations(citation_list)`: Emits `CitationCollectionSSE` events
- `Citation`: Links outputs to data sources with metadata
- `SourceInfo`: Provides detailed source attribution data
## Core logic
Build citations by matching the tool input arguments to widgets in the request:
```python
from openbb_ai import cite, citations
from openbb_ai.models import Citation, CitationHighlightBoundingBox
async def execution_loop():
# ... stream LLM response ...
# Build citations after response
citations_list = []
# Process tool message to find data sources
for message in request.messages:
if message.role == "tool":
for widget_data_request in message.input_arguments["data_sources"]:
# Match widget by UUID
matching_widgets = [
w for w in request.widgets.primary
if str(w.uuid) == widget_data_request["widget_uuid"]
]
if matching_widgets:
widget = matching_widgets[0]
citation = cite(
widget=widget,
input_arguments=widget_data_request["input_args"],
extra_details={
"Widget Name": widget.name,
"Data Source": widget.type,
"Parameters Used": widget_data_request["input_args"]
}
)
citations_list.append(citation)
# Emit citations for UI display
if citations_list:
yield citations(citations_list).model_dump()
```
---
---
title: Interact with dashboard
sidebar_position: 7
description: Receive full dashboard widget metadata and conditionally fetch data
keywords:
- dashboard widgets
- widget-dashboard-search
- WidgetRequest
- get_widget_data
---
import HeadTitle from '@site/src/components/General/HeadTitle.tsx';
Example that highlights that the agent still has access to data on the dashboard (secondary) but also has explicit context (primary) as a multi-file viewer widget.
Example that highlights that the agent has access to data that lives on the dashboard (and on all the tabs!).
## Architecture
Receive dashboard metadata and selected widgets, summarize what's available, and fetch a sample widget's data.
`agents.json` configuration with `widget-dashboard-select` enabled so it accepts explicit context and `widget-dashboard-search` so it can retrieve widgets from the dashboard.
```python
return JSONResponse(content={
"vanilla_agent_dashboard_widgets": {
"endpoints": {"query": "http://localhost:7777/v1/query"},
"features": {
"widget-dashboard-select": True,
"widget-dashboard-search": True,
},
}
})
```
### Query flow
- Access both `widgets.primary` (user-selected) and `widgets.secondary` (dashboard) widget collections
- Combine widget lists for comprehensive dashboard overview
- Check `workspace_state.current_dashboard_info` for tab information
- Stream formatted widget inventory with `message_chunk()`:
- Widget names, types, and parameter configurations
- Tab organization if present
- Data availability status
- Demonstrate data retrieval by fetching sample widget with `get_widget_data()`
- Process returned data and show preview with metadata
### OpenBB AI SDK
- `WidgetCollection`: Contains `primary`, `secondary`, and `extra` widget groups
- `Widget`: Individual widget with `uuid`, `name`, `type`, and `params`
- `WidgetParam`: Parameter definition with `name`, `type`, `current_value`
- `get_widget_data(widget_requests)`: Fetches data from specified widgets
- `WorkspaceState`: Provides dashboard context and tab information
- `message_chunk(text)`: Streams widget summaries and data previews
## Core logic
Unify primary and secondary widgets, render a summary, then fetch data for one widget:
```python
from openbb_ai import get_widget_data, WidgetRequest, message_chunk
from openbb_ai.models import QueryRequest
async def query(request: QueryRequest) -> EventSourceResponse:
async def execution_loop():
# Combine all available widgets
all_widgets = []
primary_count = 0
secondary_count = 0
if request.widgets:
if request.widgets.primary:
all_widgets.extend(request.widgets.primary)
primary_count = len(request.widgets.primary)
if request.widgets.secondary:
all_widgets.extend(request.widgets.secondary)
secondary_count = len(request.widgets.secondary)
if not all_widgets:
yield message_chunk("No widgets found on your dashboard.").model_dump()
return
# Stream dashboard overview
dashboard_info = ""
if request.workspace_state and request.workspace_state.current_dashboard_info:
dashboard_name = request.workspace_state.current_dashboard_info.name
tab_count = len(request.workspace_state.current_dashboard_info.tabs)
dashboard_info = f"Dashboard: **{dashboard_name}** ({tab_count} tabs)\n\n"
widget_summary = f"""# Dashboard Widget Analysis
{dashboard_info}## Widget Inventory
- **Selected widgets (primary)**: {primary_count}
- **Dashboard widgets (secondary)**: {secondary_count}
- **Total available**: {len(all_widgets)}
## Available Widgets
"""
for i, widget in enumerate(all_widgets[:5]): # Show first 5
widget_type = "🎯 Selected" if i < primary_count else "📊 Dashboard"
param_count = len(widget.params) if widget.params else 0
widget_summary += f"- **{widget.name}** ({widget_type}) - {param_count} parameters\n"
if len(all_widgets) > 5:
widget_summary += f"- ... and {len(all_widgets) - 5} more widgets\n"
yield message_chunk(widget_summary + "\n").model_dump()
# Demonstrate data retrieval with last widget
if all_widgets:
sample_widget = all_widgets[-1]
yield message_chunk(f"Let me fetch data from **{sample_widget.name}** as an example:\n\n").model_dump()
yield get_widget_data([
WidgetRequest(
widget=sample_widget,
input_arguments={p.name: p.current_value for p in sample_widget.params} if sample_widget.params else {}
)
]).model_dump()
return EventSourceResponse(execution_loop(), media_type="text/event-stream")
```
---
---
title: Parse PDF context
sidebar_position: 3
description: Parse PDF content from widget data and cite sources
keywords:
- PDF
- DataContent
- PdfDataFormat
- citations
---
import HeadTitle from '@site/src/components/General/HeadTitle.tsx';
## Architecture
Handle PDF data delivered by the UI through the widget data tool. Support both URL and base64 PDFs and add text to the LLM context.
`agents.json` configuration with `widget-dashboard-select` feature enabled:
```python
return JSONResponse(content={
"vanilla_agent_pdf": {
"endpoints": {"query": "http://localhost:7777/v1/query"},
"features": {
"widget-dashboard-select": True,
"widget-dashboard-search": False,
},
}
})
```
### Query flow
- Check for human message with `widgets.primary` containing PDF data
- Early exit: yield `get_widget_data()` for UI execution
- On subsequent tool message:
- Iterate through `DataContent` items
- Detect `PdfDataFormat` using `isinstance()` check
- Handle both `SingleDataContent` (base64) and `SingleFileReference` (URL)
- Extract text using `pdfplumber.open()` with `io.BytesIO()`
- Append extracted text to context string
- Process with LLM and stream response
- Optionally create `cite()` with `CitationHighlightBoundingBox` for text highlighting
### OpenBB AI SDK
- `PdfDataFormat`: Identifies PDF content in widget data
- `SingleDataContent`: Contains base64-encoded PDF data
- `SingleFileReference`: Contains URL reference to PDF
- `DataContent`/`DataFileReferences`: Containers for data items
- `CitationHighlightBoundingBox`: Defines text highlighting coordinates
- `cite(widget, input_arguments, extra_details)`: Creates citations with bounding boxes
- `get_widget_data()`: Requests PDF data from widgets
## Core logic
Detect PDF data, extract text, and accumulate as context:
```python
import base64
import io
import pdfplumber
import httpx
from openbb_ai import get_widget_data, cite, citations
from openbb_ai.models import (
QueryRequest, WidgetRequest, PdfDataFormat,
SingleDataContent, SingleFileReference,
DataContent, DataFileReferences,
CitationHighlightBoundingBox
)
async def _download_file(url: str) -> bytes:
async with httpx.AsyncClient() as client:
response = await client.get(url)
return response.content
async def _get_pdf_text(item) -> str:
if isinstance(item, SingleDataContent):
# Handle base64 PDF
file_content = base64.b64decode(item.content)
elif isinstance(item, SingleFileReference):
# Handle URL PDF
file_content = await _download_file(str(item.url))
with pdfplumber.open(io.BytesIO(file_content)) as pdf:
document_text = ""
for page in pdf.pages:
document_text += page.extract_text() + "\n\n"
return document_text
async def handle_widget_data(data: list[DataContent | DataFileReferences]) -> str:
result_str = "--- PDF Content ---\n"
for result_item in data:
for item in result_item.items:
if isinstance(item.data_format, PdfDataFormat):
result_str += f"===== {item.data_format.filename} =====\n"
result_str += await _get_pdf_text(item)
result_str += "------\n"
else:
result_str += str(item.content) + "\n"
return result_str
```
Add citation highlights with bounding boxes:
```python
# Create citations with text highlighting
citations_list = []
for widget in request.widgets.primary:
citation = cite(
widget=widget,
input_arguments={p.name: p.current_value for p in widget.params},
extra_details={"filename": "document.pdf"}
)
# Add bounding boxes for specific text regions
citation.quote_bounding_boxes = [
[
CitationHighlightBoundingBox(
text="Key financial metrics",
page=1,
x0=72.0, top=117, x1=259, bottom=135
),
CitationHighlightBoundingBox(
text="Revenue increased 15%",
page=1,
x0=110.0, top=140, x1=275, bottom=160
)
]
]
citations_list.append(citation)
if citations_list:
yield citations(citations_list).model_dump()
```
---
---
title: Parse widget data
sidebar_position: 2
description: Retrieve data from selected widgets and pass it as raw context to your LLM
keywords:
- widgets
- get_widget_data
- WidgetRequest
- SSE
- OpenAI
---
import HeadTitle from '@site/src/components/General/HeadTitle.tsx';
## Architecture
This pattern uses a minimal FastAPI backend with two endpoints and OpenBB AI SDK helpers to retrieve widget data and stream results.
`agents.json` configuration with `widget-dashboard-select` feature enabled:
```python
return JSONResponse(content={
"vanilla_agent_raw_context": {
"endpoints": {"query": "http://localhost:7777/v1/query"},
"features": {
"widget-dashboard-select": True,
"widget-dashboard-search": False,
},
}
})
```
### Query flow
- Check if latest message is human with `widgets.primary` populated
- Build `WidgetRequest` objects with current parameter values
- Early exit: yield `get_widget_data()` SSE immediately for UI to execute
- On subsequent request with tool results:
- Parse `DataContent` items from tool message
- Extract and format widget data into context string
- Append context to user messages for LLM processing
- Stream LLM response with `message_chunk()`
### OpenBB AI SDK
- `get_widget_data(widget_requests)`: Creates `FunctionCallSSE` for widget data retrieval
- `WidgetRequest(widget, input_arguments)`: Specifies widget and parameter values
- `Widget`: Contains widget metadata (uuid, name, type, params)
- `WidgetParam`: Individual parameter with name, type, current_value
- `DataContent`: Container for widget response data
- `message_chunk(text)`: Creates `MessageChunkSSE` for streaming text
## Core logic
```python
from openbb_ai import get_widget_data, WidgetRequest, message_chunk
@app.post("/v1/query")
async def query(request: QueryRequest) -> EventSourceResponse:
if (
request.messages[-1].role == "human"
and request.widgets
and request.widgets.primary
):
widget_requests = [
WidgetRequest(
widget=w,
input_arguments={p.name: p.current_value for p in w.params},
)
for w in request.widgets.primary
]
async def retrieve_widget_data():
# Function-call SSE that Workspace interprets and executes
yield get_widget_data(widget_requests).model_dump()
return EventSourceResponse(retrieve_widget_data(), media_type="text/event-stream")
# Process tool message with widget data
openai_messages = [
ChatCompletionSystemMessageParam(
role="system",
content="You are a helpful financial assistant."
)
]
context_str = ""
for message in request.messages:
if message.role == "human":
openai_messages.append(
ChatCompletionUserMessageParam(role="user", content=message.content)
)
elif message.role == "tool":
# Extract widget data from latest tool result
for data_content in message.data:
for item in data_content.items:
context_str += str(item.content) + "\n"
# Append context to last user message
if context_str and openai_messages:
openai_messages[-1]["content"] += "\n\nContext:\n" + context_str
async def execution_loop():
async for event in await client.chat.completions.create(
model="gpt-4o",
messages=openai_messages,
stream=True
):
if chunk := event.choices[0].delta.content:
yield message_chunk(chunk).model_dump()
return EventSourceResponse(execution_loop(), media_type="text/event-stream")
```
## Dashboard widgets vs explicit context
The example above uses `request.widgets.primary` which contains widgets explicitly selected by the user. If you want to access all widgets available on the current dashboard instead, you can use `request.widgets.secondary`:
```python
# Access dashboard widgets instead of explicit context
if (
request.messages[-1].role == "human"
and request.widgets
and request.widgets.secondary # Dashboard widgets
):
widget_requests = [
WidgetRequest(
widget=w,
input_arguments={p.name: p.current_value for p in w.params},
)
for w in request.widgets.secondary # Use secondary instead of primary
]
```
**Important**: To access dashboard widgets, you must enable the `widget-dashboard-search` feature in your `agents.json`:
```python
"features": {
...
"widget-dashboard-search": True, # Dashboard widgets
}
```
This gives your agent broader context about the user's dashboard setup and available data sources, rather than just the widgets they've explicitly selected.
---
---
title: Share step-by-step reasoning
sidebar_position: 1
description: Stream status updates alongside model output during long operations
keywords:
- reasoning_step
- SSE
- status updates
- progress
---
import HeadTitle from '@site/src/components/General/HeadTitle.tsx';
## Architecture
Stream status updates alongside tokens so users see what the agent is doing.
`agents.json` configuration:
```python
return JSONResponse(content={
"vanilla_agent_reasoning_steps": {
"endpoints": {"query": "http://localhost:7777/v1/query"},
"features": {
"streaming": True,
"widget-dashboard-select": False,
"widget-dashboard-search": False,
},
}
})
```
### Query flow
- Parse `QueryRequest.messages` and convert to OpenAI-compatible format
- Add system message to define agent role and capabilities
- Emit `reasoning_step()` at key processing stages:
- Before starting LLM processing
- During data preparation or analysis steps
- After completing major operations
- Stream LLM response tokens with `message_chunk()`
- Send final reasoning step upon completion
### OpenBB AI SDK
- `reasoning_step(event_type, message, details)`: Creates `StatusUpdateSSE` events
- `event_type`: `"INFO"`, `"SUCCESS"`, `"WARNING"`, `"ERROR"`
- `message`: Human-readable status description
- `details`: Optional dictionary with key-value pairs for additional context
- `message_chunk(text)`: Creates `MessageChunkSSE` for streaming LLM output
- `LlmClientMessage`: Handles message conversion between formats
## Core logic
```python
from openbb_ai import reasoning_step, message_chunk
from openbb_ai.models import QueryRequest, LlmClientMessage
from openai.types.chat import ChatCompletionSystemMessageParam, ChatCompletionUserMessageParam
async def query(request: QueryRequest) -> EventSourceResponse:
# Convert messages to OpenAI format
openai_messages = [
ChatCompletionSystemMessageParam(
role="system",
content="You are a helpful financial assistant."
)
]
for message in request.messages:
if message.role == "human":
openai_messages.append(
ChatCompletionUserMessageParam(role="user", content=message.content)
)
async def execution_loop():
# Pre-processing reasoning
yield reasoning_step(
event_type="INFO",
message="Processing your request...",
details={"total_messages": len(request.messages)}
).model_dump()
# Stream LLM response
yield reasoning_step(
event_type="INFO",
message="Generating response..."
).model_dump()
async for event in await client.chat.completions.create(
model="gpt-4o",
messages=openai_messages,
stream=True
):
if chunk := event.choices[0].delta.content:
yield message_chunk(chunk).model_dump()
# Completion reasoning
yield reasoning_step(
event_type="SUCCESS",
message="Response generated successfully!"
).model_dump()
return EventSourceResponse(execution_loop(), media_type="text/event-stream")
```
---
---
title: Apps
sidebar_position: 33
description: Create and customize your own OpenBB Apps for optimized workflows
keywords:
- OpenBB Apps
- Custom Apps
- Workflow Optimization
- Dashboard Templates
- AI Agents
- Data Integration
- Custom Solutions
---
import HeadTitle from '@site/src/components/General/HeadTitle.tsx';
5. This will create a `apps.json` file with your configuration.
This is what you should expect as a file:
```json
[
{
"name": "Fama French Factors and Research Portfolio",
"img": "https://github.com/user-attachments/assets/8b2409d6-5ddc-4cbc-b20c-89a29b1bd923",
"img_dark": "",
"img_light": "",
"description": "Examine sample portfolio holdings distribution across countries, sectors, and industries, while also understanding how different assets correlate with each other over various time periods. This app provides insights into how portfolios respond to different market factors using Fama-French analysis, helping investors understand their portfolio's underlying drivers of returns and risk exposures.",
"allowCustomization": true,
"tabs": {
"reference-data": {
"id": "reference-data",
"name": "Reference Data",
"layout": [
{
"i": "fama_french_info_custom_obb",
"x": 0,
"y": 2,
"w": 12,
"h": 25
},
{
"i": "load_factors_custom_obb",
"x": 12,
"y": 13,
"w": 28,
"h": 14,
"state": {
"params": {
"frequency": "monthly",
"start_date": "2021-01-01",
"end_date": "2025-03-27"
},
"chartView": {
"enabled": false,
"chartType": "line"
}
}
},
{
"i": "load_portfolios_custom_obb",
"x": 12,
"y": 2,
"w": 28,
"h": 11,
"state": {
"params": {
"portfolio": "Portfolios_Formed_on_OP",
"start_date": "2021-01-01",
"end_date": "2025-03-27"
},
"chartView": {
"enabled": false,
"chartType": "line"
}
}
}
]
},
"portfolio-price--performance": {
"id": "portfolio-price--performance",
"name": "Portfolio Price & Performance",
"layout": [
{
"i": "portfolio_unit_price_custom_obb",
"x": 0,
"y": 2,
"w": 40,
"h": 24,
"state": {
"params": {
"portfolio": "Client 2",
"returns": "True"
},
"chartView": {
"enabled": true,
"chartType": "line"
}
}
}
]
},
"portfolio-region-and-sector-exposure": {
"id": "portfolio-region-and-sector-exposure",
"name": "Portfolio Region and Sector Exposure",
"layout": [
{
"i": "portfolio_sectors_custom_obb",
"x": 0,
"y": 13,
"w": 19,
"h": 14,
"state": {
"chartView": {
"enabled": true,
"chartType": "pie"
}
}
},
{
"i": "portfolio_countries_custom_obb",
"x": 0,
"y": 2,
"w": 19,
"h": 11,
"state": {
"chartView": {
"enabled": true,
"chartType": "pie"
}
}
},
{
"i": "portfolio_industries_custom_obb",
"x": 19,
"y": 2,
"w": 21,
"h": 25,
"state": {
"params": {
"portfolio": "Client 3"
},
"chartView": {
"enabled": true,
"chartType": "pie"
}
}
}
]
},
"portfolio-holdings": {
"id": "portfolio-holdings",
"name": "Portfolio Holdings",
"layout": [
{
"i": "portfolio_holdings_custom_obb",
"x": 0,
"y": 2,
"w": 40,
"h": 25,
"state": {
"params": {
"portfolio": "Client 2"
},
"chartView": {
"enabled": true,
"chartType": "bar"
}
}
}
]
},
"portfolio-holdings-correlations": {
"id": "portfolio-holdings-correlations",
"name": "Portfolio Holdings Correlations",
"layout": [
{
"i": "holdings_correlation_custom_obb",
"x": 0,
"y": 2,
"w": 40,
"h": 26,
"state": {
"params": {
"portfolio": "Client 2"
}
}
}
]
},
"portfolio-factor-correlations": {
"id": "portfolio-factor-correlations",
"name": "Portfolio Factor Attributions",
"layout": [
{
"i": "portfolio_factors_custom_obb",
"x": 0,
"y": 2,
"w": 30,
"h": 20,
"state": {
"params": {
"portfolio": "Client 2"
}
}
}
]
}
},
"groups": [
{
"name": "Group 3",
"type": "param",
"paramName": "frequency",
"defaultValue": "monthly",
"widgetIds": [
"load_factors_custom_obb",
"load_portfolios_custom_obb"
]
},
{
"name": "Group 2",
"type": "param",
"paramName": "start_date",
"defaultValue": "2021-01-01",
"widgetIds": [
"load_factors_custom_obb",
"load_portfolios_custom_obb"
]
},
{
"name": "Group 4",
"type": "param",
"paramName": "end_date",
"defaultValue": "2025-03-27",
"widgetIds": [
"load_factors_custom_obb",
"load_portfolios_custom_obb"
]
},
{
"name": "Group 5",
"type": "param",
"paramName": "region",
"defaultValue": "america",
"widgetIds": [
"load_factors_custom_obb",
"load_portfolios_custom_obb"
]
},
{
"name": "Group 6",
"type": "endpointParam",
"paramName": "factor",
"defaultValue": "america",
"widgetIds": [
"load_factors_custom_obb",
"load_portfolios_custom_obb"
]
},
{
"name": "Group 7",
"type": "param",
"paramName": "portfolio",
"defaultValue": "Client 1",
"widgetIds": [
"portfolio_sectors_custom_obb",
"portfolio_countries_custom_obb",
"portfolio_industries_custom_obb",
"portfolio_holdings_custom_obb",
"portfolio_unit_price_custom_obb",
"holdings_correlation_custom_obb",
"portfolio_factors_custom_obb"
]
}
],
"prompts": [
"Please analyze my current portfolio holdings. What are the top 5 positions by weight? Are there any concentration risks I should be aware of? How has each position performed over the last month?",
"What are the strongest correlations between my portfolio holdings? Which positions might provide good diversification benefits? How do my holdings correlate with major market factors?",
"What is my current sector exposure? Are there any sectors where I'm over or underweight compared to the market? What are the risks and opportunities in my current sector allocation?",
"How does my portfolio respond to different market factors? What are my current factor exposures? Are there any factor tilts I should consider adjusting?"
]
}
]
---
---
title: Data Integration
sidebar_position: 1
description: Learn how to integrate your own data sources and APIs into OpenBB Workspace with a custom backend solution.
keywords:
- Data Integration
- Custom Backend
- API Endpoints
- Widget Configuration
- Data Connectors
- User Interface
- Real-time Updates
- Single Widget
- Data Key Parameter
- Nested JSON
---
import HeadTitle from '@site/src/components/General/HeadTitle.tsx';
import TutorialVideo from '@site/src/components/General/TutorialVideo.tsx';
### 6. Set up Authentication / or API keys
Generally, it’s recommended to enable authentication for your backend to ensure secure access. Another common use case is when connecting to a third-party data provider that requires an API key. To handle this securely, we recommend including the key in a custom header (for example, X-API-KEY).
Here are some benefits of using a custom header:
- Security: you can keep credentials or API keys out of URLs and logs.
- Flexibility: this method supports multiple API keys / credentials per request.
Here’s an example of how you can add this to your main.py:
```
from fastapi import FastAPI, Request, HTTPException
from dotenv import load_dotenv
import os
# Load environment variables from .env file (recommended)
load_dotenv()
app = FastAPI(title="Custom Header Example", version="0.0.1")
@app.get("/secure_hello")
def secure_hello(request: Request, name: str = ""):
"""Returns a personalized message if the correct API key is provided."""
# -------------------------------------------------------------------------
# NOTE:
# It is recommended to set your API keys as environment variables.
# Example: create a `.env` file with the line below:
# VALID_API_KEYS=my-secret-key,another-valid-key
#
# For quick local testing, you can also define keys directly in code:
# valid_api_keys = ["my-secret-key", "another-valid-key"]
# (Make sure to comment out the environment variable line below.)
# -------------------------------------------------------------------------
# Get allowed API keys from environment variables
valid_api_keys = os.getenv("VALID_API_KEYS", "").split(",")
# Get API key from header
api_key = request.headers.get("X-API-KEY")
if not api_key:
raise HTTPException(
status_code=401,
detail="API key required. Please include 'X-API-KEY' in the request headers."
)
# Validate provided API key
if api_key.strip() not in [k.strip() for k in valid_api_keys]:
raise HTTPException(status_code=403, detail="Invalid API key")
# Return a markdown-formatted greeting
return f"# Hello {name}, your API key is valid!"
```
Once configured, you can add your header values in the following way. These values will then be automatically passed along with any subsequent API calls.
### 7. Voila
## Reference Backend
The [reference backend](https://github.com/OpenBB-finance/backends-for-openbb/tree/main/getting-started/reference-backend) serves as a comprehensive example repository showcasing implementations of most widget types and configuration patterns available in OpenBB Workspace.
This backend provides practical examples for:
- **Complete Widget Type Coverage**: Examples for table, chart, metric, markdown, file viewer, and specialized widget implementations
- **Advanced Configuration Patterns**: Demonstrations of parameter linking, dynamic dropdowns, render functions, and custom formatting
- **Real-world Data Integration**: Working examples with external APIs, data transformation, and error handling
- **Best Practice Implementations**: Production-ready code patterns with proper authentication, caching, and performance optimization
The reference backend functions both as a learning resource and a foundation for your own backend development. You can clone the repository, study the implementations, and adapt the patterns to your specific data sources and analytical requirements.
Whether you're implementing your first widget or exploring advanced features like sparklines and custom formatters, the reference backend provides tested, documented examples that demonstrate the full capabilities of the OpenBB integration framework.
---
---
title: agents.json Reference
sidebar_position: 3
description: Complete reference guide for configuring custom AI agents in OpenBB Workspace using the agents.json endpoint
keywords:
- agents.json
- AI configuration
- custom agents
- agent metadata
- OpenBB AI SDK
- agent features
- SSE
- streaming
---
import HeadTitle from '@site/src/components/General/HeadTitle.tsx';
**Note**: The ideal image size is 250x200px
Remember, the best part is that you can build your own apps tailored to your specific needs.
## Examples
### DTCC Trade Apps
```python
@register_widget({
"name": "Markdown Widget with Category and Subcategory",
"description": "A markdown widget with category and subcategory",
"type": "markdown",
"category": "Widgets",
"subcategory": "Markdown Widgets",
"endpoint": "markdown_widget_with_category_and_subcategory",
"gridData": {"w": 12, "h": 4},
})
@app.get("/markdown_widget_with_category_and_subcategory")
def markdown_widget_with_category_and_subcategory():
"""Returns a markdown widget with category and subcategory"""
return f"# Markdown Widget with Category and Subcategory"
```
---
---
title: Error Handling
sidebar_position: 17
description: Learn how to handle errors in your widgets in OpenBB Workspace.
keywords:
- error handling
- HTTPException
- error management
- widgets
- status codes
---
import HeadTitle from '@site/src/components/General/HeadTitle.tsx';
```python
@register_widget({
"name": "Markdown Widget with Error Handling",
"description": "A markdown widget with error handling",
"type": "markdown",
"endpoint": "markdown_widget_with_error_handling",
"gridData": {"w": 12, "h": 4},
})
@app.get("/markdown_widget_with_error_handling")
def markdown_widget_with_error_handling():
"""Returns a markdown widget with error handling"""
raise HTTPException(
status_code=500,
detail="Error that just occurred"
)
```
---
---
title: Grid Size
sidebar_position: 15
description: Learn about the grid-based layout system for widgets in OpenBB Workspace, including width and height specifications.
keywords:
- grid
- layout
- width
- height
- widgets
- gridData
---
import HeadTitle from '@site/src/components/General/HeadTitle.tsx';
```python
@register_widget({
"name": "Markdown Widget",
"description": "A markdown widget",
"type": "markdown",
"endpoint": "markdown_widget",
"gridData": {"w": 12, "h": 4},
})
@app.get("/markdown_widget")
def markdown_widget():
"""Returns a markdown widget"""
return "# Markdown Widget"
```
The grid system works as follows:
**Width (w)**: Horizontal span (10-40 units)
- 12 units is a good default for most widgets
- Use 40 units for full-width widgets
**Height (h)**: Vertical span (4-100 units)
- 4-8 units for simple widgets
- 8-20 units for standard widgets
- Larger values for detailed charts or tables
### Example
This is the code utilized to add the widgets in the image above.
```python
@register_widget({
"name": "Markdown Widget w-12 x h-20",
"description": "A markdown widget",
"type": "markdown",
"endpoint": "markdown_widget2",
"gridData": {"w": 12, "h": 20},
})
@app.get("/markdown_widget2")
def markdown_widget2():
"""Returns a markdown widget"""
return "# Markdown Widget w-12 x h-20"
@register_widget({
"name": "Markdown Widget w-40 x h-4",
"description": "A markdown widget",
"type": "markdown",
"endpoint": "markdown_widget3",
"gridData": {"w": 40, "h": 4},
})
@app.get("/markdown_widget3")
def markdown_widget3():
"""Returns a markdown widget"""
return "# Markdown Widget w-40 x h-4"
@register_widget({
"name": "Markdown Widget w-14 x h-12",
"description": "A markdown widget",
"type": "markdown",
"endpoint": "markdown_widget4",
"gridData": {"w": 14, "h": 12},
})
@app.get("/markdown_widget4")
def markdown_widget4():
"""Returns a markdown widget"""
return "# Markdown Widget w-14 x h-12"
@register_widget({
"name": "Markdown Widget w-28 x h-8",
"description": "A markdown widget",
"type": "markdown",
"endpoint": "markdown_widget5",
"gridData": {"w": 28, "h": 8},
})
@app.get("/markdown_widget5")
def markdown_widget5():
"""Returns a markdown widget"""
return "# Markdown Widget w-28 x h-8"
@register_widget({
"name": "Markdown Widget w-14 x h-6",
"description": "A markdown widget",
"type": "markdown",
"endpoint": "markdown_widget6",
"gridData": {"w": 14, "h": 6},
})
@app.get("/markdown_widget6")
def markdown_widget6():
"""Returns a markdown widget"""
return "# Markdown Widget w-14 x h-6"
```
---
---
title: Matching widget to MCP tool
sidebar_position: 22
description: Learn how you can make it so an MCP server and tool are associated with a specific widget
keywords:
- MCP tool
- matching widget
- MCP server
- widgets.json
- citations
---
import HeadTitle from '@site/src/components/General/HeadTitle.tsx';
In the case above, note that the name of MCP Server is "Financial Data" - this will be the value you need for the `mcp_server` field in your `widgets.json`.
Similarly, the MCP tool is "get_company_revenue_data" - this will be the value you need for the `tool_id` field in your `widgets.json`.
### 2. Configure OpenBB widget with matching MCP
The important is for the widget configuration to include `mcp_tool` with the **EXACT** same names as the ones from your MCP Server:
Notice how the `mcp_server` value "Financial Data" and `tool_id` value "get_company_revenue_data" match exactly with the MCP server configuration from step 1.
The name and URL of the backend can be ANYTHING.
### 3. Matching widget citation
This is when the magic happens. If the user has that MCP tool enabled, and asks something like:
> "Utilize financial data MCP tool and get company revenue data for AAPL"
Then when the MCP tool is utilized, the user will see a toast like the following:
This indicates that a matching widget was found.
The user will be able to see that this is a matching widget, by the property of having a `*` at the end.
When hovering on top of that widget, the user will be able to add it to the dashboard to the same parameters as the ones that were utilized by the copilot.
The advantage now is that the user can interact with the parameters, charting and all other OpenBB widgets properties.
---
---
title: Refetch Interval
sidebar_position: 20
description: Learn about configuring refetch intervals for widgets in OpenBB Workspace.
keywords:
- refetch interval
- auto refresh
- data updates
- refresh rate
- widget updates
---
import HeadTitle from '@site/src/components/General/HeadTitle.tsx';
```python
@register_widget({
"name": "Markdown Widget with Short Refetch Interval",
"description": "A markdown widget with a short refetch interval",
"type": "markdown",
"endpoint": "markdown_widget_with_short_refetch_interval",
"gridData": {"w": 12, "h": 4},
"refetchInterval": 1000
})
@app.get("/markdown_widget_with_short_refetch_interval")
def markdown_widget_with_short_refetch_interval():
"""Returns a markdown widget with current time"""
current_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
return f"### Current time: {current_time}"
```
## Refetch Interval with Stale Time
The refetch interval is set to 10000ms (10 seconds) and the stale time is set to 5000ms (5 seconds). Data older than stale time will make the refresh button in the widget become orange to indicate that the data is stale, and once it reaches the refetch interval, the widget will be refreshed and the indicator will turn green again.
```python
@register_widget({
"name": "Markdown Widget with Refetch Interval and Shorter Stale Time",
"description": "A markdown widget with a short refetch interval and a shorter stale time",
"type": "markdown",
"endpoint": "markdown_widget_with_refetch_interval_and_shorter_stale_time",
"gridData": {"w": 12, "h": 4},
"refetchInterval": 10000,
"staleTime": 5000
})
@app.get("/markdown_widget_with_refetch_interval_and_shorter_stale_time")
def markdown_widget_with_refetch_interval_and_shorter_stale_time():
"""Returns a markdown widget with current time"""
current_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
return f"### Current time: {current_time}"
```
## Refetch interval with Run Button
The refresh interval is set to 10000ms (10 seconds) but the run button is enabled, which means that the user can refresh the widget manually.
```python
@register_widget({
"name": "Markdown Widget with Short Refetch Interval and a Run Button",
"description": "A markdown widget with a short refetch interval and a run button",
"type": "markdown",
"endpoint": "markdown_widget_with_short_refetch_interval_and_run_button",
"gridData": {"w": 12, "h": 4},
"refetchInterval": 10000,
"runButton": True
})
@app.get("/markdown_widget_with_short_refetch_interval_and_run_button")
def markdown_widget_with_short_refetch_interval_and_run_button():
"""Returns a markdown widget with current time"""
current_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
return f"### Current time: {current_time}"
```
---
---
title: Render Functions
sidebar_position: 21
description: Learn how to configure and use custom render functions in OpenBB Workspace to customize data display and interactions.
keywords:
- custom render functions
- OpenBB API
- widget configuration
- data visualization
- interactive widgets
---
import HeadTitle from '@site/src/components/General/HeadTitle.tsx';
```python
@register_widget({
"name": "Markdown Widget with Run Button",
"description": "A markdown widget with a run button",
"type": "markdown",
"endpoint": "markdown_widget_with_run_button",
"gridData": {"w": 12, "h": 4},
"runButton": True,
})
@app.get("/markdown_widget_with_run_button")
def markdown_widget_with_run_button():
"""Returns a markdown widget with current time"""
current_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
return f"### Current time: {current_time}"
```
---
---
title: Stale Time
sidebar_position: 19
description: Learn about configuring stale time for widgets in OpenBB Workspace.
keywords:
- stale time
- data freshness
- refresh indicators
- data staleness
- widget updates
---
import HeadTitle from '@site/src/components/General/HeadTitle.tsx';
```python
@register_widget({
"name": "Markdown Widget with Stale Time",
"description": "A markdown widget with stale time",
"type": "markdown",
"endpoint": "markdown_widget_with_stale_time",
"gridData": {"w": 12, "h": 4},
"staleTime": 5000
})
@app.get("/markdown_widget_with_stale_time")
def markdown_widget_with_stale_time():
"""Returns a markdown widget with current time"""
current_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
return f"### Current time: {current_time}"
```
---
---
title: Advanced Dropdown
sidebar_position: 28
description: Learn how to implement and use advanced dropdown parameters in OpenBB Workspace widgets, including dynamic options from endpoints and additional information display
keywords:
- advanced dropdown
- dynamic dropdown
- endpoint dropdown
- widget parameters
- enhanced selection
---
import HeadTitle from '@site/src/components/General/HeadTitle.tsx';
The dropdown options are fetched from an endpoint and can include extra details.
```python
@app.get("/advanced_dropdown_options")
def advanced_dropdown_options():
"""Returns a list of stocks with their details"""
return [
{
"label": "Apple Inc.",
"value": "AAPL",
"extraInfo": {
"description": "Technology Company",
"rightOfDescription": "NASDAQ"
}
},
{
"label": "Microsoft Corporation",
"value": "MSFT",
"extraInfo": {
"description": "Software Company",
"rightOfDescription": "NASDAQ"
}
},
{
"label": "Google",
"value": "GOOGL",
"extraInfo": {
"description": "Search Engine",
"rightOfDescription": "NASDAQ"
}
}
]
```
Note how the `optionsEndpoint` is pointing to the endpoint `"/advanced_dropdown_options"`.
We also set the `style` parameter to `popupWidth` to `450` to set the width of the dropdown.
```python
@register_widget({
"name": "Markdown Widget with Multi Select Advanced Dropdown",
"description": "A markdown widget with a multi select advanced dropdown parameter",
"endpoint": "markdown_widget_with_multi_select_advanced_dropdown",
"gridData": {"w": 16, "h": 6},
"type": "markdown",
"params": [
{
"paramName": "stock_picker",
"description": "Select a stock to analyze",
"value": "AAPL",
"label": "Select Stock",
"type": "endpoint",
"multiSelect": True,
"optionsEndpoint": "/advanced_dropdown_options",
"style": {
"popupWidth": 450
}
}
]
})
@app.get("/markdown_widget_with_multi_select_advanced_dropdown")
def markdown_widget_with_multi_select_advanced_dropdown(stock_picker: str):
"""Returns a markdown widget with multi select advanced dropdown parameter"""
return f"""# Multi Select Advanced Dropdown
Selected stocks: {stock_picker}
"""
---
---
title: Boolean Toggle
sidebar_position: 25
description: Learn how to implement and use boolean toggle parameters in OpenBB Workspace widgets, including configuration options and example usage
keywords:
- boolean toggle
- switch
- toggle
- widget parameters
- enable/disable
---
import HeadTitle from '@site/src/components/General/HeadTitle.tsx';
```python
@register_widget({
"name": "Markdown Widget with Boolean Toggle",
"description": "A markdown widget with a boolean parameter",
"endpoint": "markdown_widget_with_boolean",
"gridData": {"w": 16, "h": 6},
"type": "markdown",
"params": [
{
"paramName": "condition",
"description": "Enable or disable this feature",
"label": "Toggle Option",
"type": "boolean",
"value": True,
}
]
})
@app.get("/markdown_widget_with_boolean")
def markdown_widget_with_boolean(condition: bool):
"""Returns a markdown widget with boolean parameter"""
return f"""# Boolean Toggle
Current state: {'Enabled' if condition else 'Disabled'}
"""
---
---
title: Cell Click Grouping
sidebar_position: 31
description: Learn how to implement cell click grouping in OpenBB Workspace widgets, allowing users to click on cells in a table to update related widgets
keywords:
- cell click grouping
- table cell click
- interactive tables
- widget parameters
- cell interaction
---
import HeadTitle from '@site/src/components/General/HeadTitle.tsx';
The implementation consists of three main components:
1. An endpoint that provides a list of available stock symbols that can be selected in the widgets.
```python
@app.get("/get_tickers_list")
def get_tickers_list():
"""Returns a list of available stock symbols"""
return [
{"label": "Apple Inc.", "value": "AAPL"},
{"label": "Microsoft Corporation", "value": "MSFT"},
{"label": "Google", "value": "GOOGL"},
{"label": "Amazon", "value": "AMZN"},
{"label": "Tesla", "value": "TSLA"}
]
```
2. A table widget that displays stock data and allows users to click on symbol cells to update related widgets. The key feature is the `cellOnClick` renderFn in the symbol column, which triggers the `groupBy` action when a cell is clicked.
```python
@register_widget({
"name": "Table widget with grouping by cell click",
"description": "A table widget that groups data when clicking on symbols. Click on a symbol to update all related widgets.",
"type": "table",
"endpoint": "table_widget_with_grouping_by_cell_click",
"params": [
{
"paramName": "symbol", # This parameter name is crucial - it's used for grouping
"description": "Select stocks to display",
"value": "AAPL",
"label": "Symbol",
"type": "endpoint",
"optionsEndpoint": "/get_tickers_list",
"multiSelect": False,
"show": True
}
],
"data": {
"table": {
"showAll": True,
"columnsDefs": [
{
"field": "symbol",
"headerName": "Symbol",
"cellDataType": "text",
"width": 120,
"pinned": "left",
"renderFn": "cellOnClick",
"renderFnParams": {
"actionType": "groupBy",
"groupBy": {
"paramName": "symbol"
}
}
},
{
"field": "price",
"headerName": "Price",
"cellDataType": "number",
"formatterFn": "none",
"width": 120
},
{
"field": "change",
"headerName": "Change",
"cellDataType": "number",
"formatterFn": "percent",
"renderFn": "greenRed",
"width": 120
},
{
"field": "volume",
"headerName": "Volume",
"cellDataType": "number",
"formatterFn": "int",
"width": 150
}
]
}
},
"gridData": {
"w": 20,
"h": 9
}
})
```
3. A markdown widget that displays detailed information about the selected stock. This widget uses the same `symbol` parameter as the table widget, so it automatically updates when a symbol is clicked in the table.
```python
@register_widget({
"name": "Widget managed by parameter from cell click on table widget",
"description": "This widget demonstrates how to use the grouped symbol parameter from a table widget. When a symbol is clicked in the table, this widget will automatically update to show details for the selected symbol.",
"type": "markdown",
"endpoint": "widget_managed_by_parameter_from_cell_click_on_table_widget",
"params": [
{
"paramName": "symbol", # Must match the groupBy.paramName in the table widget
"description": "The symbol to get details for",
"value": "AAPL",
"label": "Symbol",
"type": "endpoint",
"optionsEndpoint": "/get_tickers_list",
"show": True
}
],
"gridData": {
"w": 20,
"h": 6
}
})
```
This functionality is achieved through three key components:
1. Both widgets must share the same `paramName` (in this case "symbol") to enable parameter synchronization
2. The table widget's `cellOnClick` renderFn must be configured with `actionType: "groupBy"` and specify the `groupBy.paramName` as "symbol"
3. Both widgets must reference the same endpoint (`/get_tickers_list`) for their options data
The interaction flow works as follows:
1. When a user clicks a symbol cell in the table, the `cellOnClick` renderFn activates the `groupBy` action
2. The `groupBy` action then updates the shared `symbol` parameter value
3. Any widget that uses the `symbol` parameter will automatically refresh to display data for the newly selected symbol
This implementation creates an intuitive user experience where selecting a symbol in the table instantly updates all connected widgets with the corresponding stock information.
## Using valueField for Custom Value Mapping
The `valueField` option allows you to pass a different value than what's displayed in the cell. This is useful when your table shows human-readable values (like company names) but your API expects different values (like IDs or codes).
**Example:** Display company names but pass company IDs when clicked:
```python
@register_widget({
"name": "Company List with ID Mapping",
"description": "Table showing company names but passing IDs on click",
"type": "table",
"endpoint": "company_list",
"params": [
{
"paramName": "companyId", # Parameter expects ID, not name
"description": "Company identifier",
"value": "AAPL",
"label": "Company ID",
"type": "endpoint",
"optionsEndpoint": "/company_options",
"show": True
}
],
"data": {
"table": {
"showAll": True,
"columnsDefs": [
{
"field": "companyName", # Display name in cell
"headerName": "Company",
"cellDataType": "text",
"width": 200,
"renderFn": "cellOnClick",
"renderFnParams": {
"actionType": "groupBy",
"groupBy": {
"paramName": "companyId",
"valueField": "companyId" # Use ID field instead of companyName
}
}
},
{
"field": "price",
"headerName": "Price",
"cellDataType": "number",
"formatterFn": "none",
"width": 120
}
]
}
}
})
```
**Data structure:**
```python
@app.get("/company_list")
def get_company_list():
return [
{
"companyName": "Apple Inc.", # Displayed in cell
"companyId": "AAPL", # Passed to parameter
"price": 150.25
},
{
"companyName": "Microsoft Corporation",
"companyId": "MSFT",
"price": 350.50
}
]
```
In this example:
- The table cell displays "Apple Inc." (from `companyName` field)
- When clicked, it passes "AAPL" (from `companyId` field) to the `companyId` parameter
- This allows you to show user-friendly names while using IDs for API calls
**When to use valueField:**
- When displaying human-readable text but needing to pass IDs or codes
- When the displayed value differs from the parameter value format
- When you want to decouple the display value from the API parameter value
---
---
title: Date Picker
sidebar_position: 23
description: Learn how to implement and use date picker parameters in OpenBB Workspace widgets, including configuration options and example usage
keywords:
- date picker
- date input
- calendar
- widget parameters
- date selection
- datetime
---
import HeadTitle from '@site/src/components/General/HeadTitle.tsx';
```python
@register_widget({
"name": "Markdown Widget with Date Picker",
"description": "A markdown widget with a date picker parameter",
"endpoint": "markdown_widget_with_date_picker",
"gridData": {"w": 16, "h": 6},
"type": "markdown",
"params": [
{
"paramName": "date_picker",
"description": "Choose a date to display",
"value": "$currentDate-1d",
"label": "Select Date",
"type": "date"
}
]
})
@app.get("/markdown_widget_with_date_picker")
def markdown_widget_with_date_picker(
date_picker: str = (datetime.now() - timedelta(days=1)).strftime("%Y-%m-%d")
):
"""Returns a markdown widget with date picker parameter"""
return f"""# Date Picker
Selected date: {date_picker}
"""
```
Note : We use the `$currentDate` variable to get the current date. More info here : [Date Modifier in widgets.json](../json-specs/widgets-json-reference#date-modifier)
---
---
title: Dependent Dropdown
sidebar_position: 29
description: Learn how to implement and use dependent dropdown parameters in OpenBB Workspace widgets, where options in one dropdown depend on the selection in another
keywords:
- dependent dropdown
- cascading dropdown
- linked dropdown
- widget parameters
- dynamic options
---
import HeadTitle from '@site/src/components/General/HeadTitle.tsx';
First, we create an endpoint that will provide the filtered list of documents based on the selected category. This endpoint takes a `category` parameter and returns only the documents that match that category.
```python
@app.get("/document_options")
def get_document_options(category: str = "all"):
"""Get filtered list of documents based on category"""
SAMPLE_DOCUMENTS = [
{
"name": "Q1 Report",
"category": "reports"
},
{
"name": "Q2 Report",
"category": "reports"
},
{
"name": "Investor Presentation",
"category": "presentations"
},
{
"name": "Product Roadmap",
"category": "presentations"
}
]
filtered_docs = (
SAMPLE_DOCUMENTS if category == "all"
else [doc for doc in SAMPLE_DOCUMENTS if doc["category"] == category]
)
return [
{
"label": doc["name"],
"value": doc["name"]
}
for doc in filtered_docs
]
```
Next, we create the widget that uses these dependent dropdowns. The widget has two parameters:
1. A category dropdown that lets users select between "All", "Reports", or "Presentations"
2. A document dropdown that shows documents filtered based on the selected category
The key to making this work is the `optionsParams` field in the second parameter, which uses `$category` to reference the value from the first parameter. This creates the dependency between the two dropdowns. These parameters are passed to the endpoint as a query parameter.
```python
@register_widget({
"name": "Dropdown Dependent Widget",
"description": "A simple widget with a dropdown depending on another dropdown",
"endpoint": "dropdown_dependent_widget",
"gridData": {"w": 16, "h": 6},
"type": "markdown",
"params": [
{
"paramName": "category",
"description": "Category of documents to fetch",
"value": "all",
"label": "Category",
"type": "text",
"options": [
{"label": "All", "value": "all"},
{"label": "Reports", "value": "reports"},
{"label": "Presentations", "value": "presentations"}
]
},
{
"paramName": "document_type",
"description": "Document to display",
"label": "Select Document",
"type": "endpoint",
"optionsEndpoint": "/document_options",
"optionsParams": {
"category": "$category"
}
},
]
})
@app.get("/dropdown_dependent_widget")
def dropdown_dependent_widget(category: str = "all", document_type: str = "all"):
"""Returns a dropdown dependent widget"""
return f"""# Dropdown Dependent Widget
- Selected category: **{category}**
- Selected document: **{document_type}**
"""
```
When a user selects a category in the first dropdown, the second dropdown will automatically update to show only the documents that belong to that category. For example:
- If "Reports" is selected, only "Q1 Report" and "Q2 Report" will appear in the second dropdown
- If "Presentations" is selected, only "Investor Presentation" and "Product Roadmap" will appear
- If "All" is selected, all documents will be shown
---
---
title: Dropdown
sidebar_position: 27
description: Learn how to implement and use dropdown parameters in OpenBB Workspace widgets, including configuration options and example usage
keywords:
- dropdown
- select
- combobox
- widget parameters
- selection list
---
import HeadTitle from '@site/src/components/General/HeadTitle.tsx';
```python
@register_widget({
"name": "Markdown Widget with Dropdown",
"description": "A markdown widget with a dropdown parameter",
"endpoint": "markdown_widget_with_dropdown",
"gridData": {"w": 16, "h": 6},
"type": "markdown",
"params": [
{
"paramName": "days_picker",
"description": "Number of days to look back",
"value": "1",
"label": "Select Days",
"type": "text",
"multiSelect": True,
"options": [
{
"value": "1",
"label": "1"
},
{
"value": "5",
"label": "5"
},
{
"value": "10",
"label": "10"
},
{
"value": "20",
"label": "20"
},
{
"value": "30",
"label": "30"
}
]
}
]
})
@app.get("/markdown_widget_with_dropdown")
def markdown_widget_with_dropdown(days_picker: str):
"""Returns a markdown widget with dropdown parameter"""
return f"""# Dropdown
Selected days: {days_picker}
"""
---
---
title: Input Form
sidebar_position: 30
description: Input Form
keywords:
- input
- form
- configuration
---
import HeadTitle from '@site/src/components/General/HeadTitle.tsx';
A form can include various input types:
- **Date**: Select a specific date for the data
- **Number**: Numeric input field
- **Dropdown**: Selection menu (can be populated dynamically from an API)
- **Text**: Text input field
- **Button**: Button to trigger the data submission
```python
@register_widget({
"name": "Markdown Widget with Number Input",
"description": "A markdown widget with a number input parameter",
"endpoint": "markdown_widget_with_number_input",
"gridData": {"w": 16, "h": 6},
"type": "markdown",
"params": [
{
"paramName": "number_box",
"description": "Enter a number",
"value": 20,
"label": "Enter Number",
"type": "number"
}
]
})
@app.get("/markdown_widget_with_number_input")
def markdown_widget_with_number_input(number_box: int):
"""Returns a markdown widget with number input parameter"""
return f"""# Number Input
Entered number: {number_box}
"""
---
---
title: Parameter Grouping
sidebar_position: 32
description: Learn how to implement parameter grouping in OpenBB Workspace widgets, allowing multiple widgets to share and respond to the same parameter input
keywords:
- parameter grouping
- shared parameters
- synchronized parameters
- widget parameters
- parameter synchronization
---
import HeadTitle from '@site/src/components/General/HeadTitle.tsx';
The implementation consists of three main components that work together to create a cohesive user interface:
1. Provides a list of available car manufacturers that can be selected in the widgets.
```python
@app.get("/company_options")
def get_company_options():
"""Returns a list of available car manufacturers"""
return [
{"label": "Toyota Motor Corporation", "value": "TM"},
{"label": "Volkswagen Group", "value": "VWAGY"},
{"label": "General Motors", "value": "GM"},
{"label": "Ford Motor Company", "value": "F"},
{"label": "Tesla Inc.", "value": "TSLA"}
]
```
2. Displays performance metrics for the selected car manufacturer in a table format. This widget uses parameters (`company` and `year`) that are grouped with other widgets. As it is possible to see by the color box before the parameter and the same id. When a user selects a company or year, all widgets using these parameters will update automatically.
```python
@register_widget({
"name": "Car Manufacturer Performance",
"description": "Displays performance metrics for the selected car manufacturer",
"type": "table",
"endpoint": "company_performance",
"gridData": {"w": 16, "h": 8},
"params": [
{
"paramName": "company", # Shared paramName with company_details widget
"description": "Select a car manufacturer to view performance",
"value": "TM",
"label": "Manufacturer",
"type": "endpoint",
"optionsEndpoint": "/company_options" # Shared endpoint with company_details widget
},
{
"paramName": "year", # Shared paramName with company_details widget
"description": "Select model year to view performance",
"value": "2024",
"label": "Model Year",
"type": "text",
"options": [
{"label": "2024", "value": "2024"},
{"label": "2023", "value": "2023"},
{"label": "2022", "value": "2022"}
]
}
],
"data": {
"table": {
"showAll": True,
"columnsDefs": [
{
"field": "metric",
"headerName": "Metric",
"cellDataType": "text",
"width": 150
},
{
"field": "value",
"headerName": "Value",
"cellDataType": "text",
"width": 150
},
{
"field": "change",
"headerName": "Change",
"cellDataType": "number",
"formatterFn": "percent",
"renderFn": "greenRed",
"width": 150
}
]
}
}
})
@app.get("/company_performance")
def get_company_performance(company: str, year: str = "2024"):
"""Returns car manufacturer performance metrics"""
performance_data = {
"TM": {
"2024": [
{"metric": "Global Sales", "value": "10.5M", "change": 5.2},
{"metric": "EV Sales", "value": "1.2M", "change": 45.8},
{"metric": "Operating Margin", "value": "8.5%", "change": 1.2},
{"metric": "R&D Investment", "value": "$12.5B", "change": 15.3}
],
"2023": [
{"metric": "Global Sales", "value": "9.98M", "change": 3.1},
{"metric": "EV Sales", "value": "0.82M", "change": 35.2},
{"metric": "Operating Margin", "value": "7.3%", "change": 0.8},
{"metric": "R&D Investment", "value": "$10.8B", "change": 12.5}
],
"2022": [
{"metric": "Global Sales", "value": "9.67M", "change": 1.2},
{"metric": "EV Sales", "value": "0.61M", "change": 25.4},
{"metric": "Operating Margin", "value": "6.5%", "change": -0.5},
{"metric": "R&D Investment", "value": "$9.6B", "change": 8.7}
]
},
"VWAGY": {
"2024": [
{"metric": "Global Sales", "value": "9.2M", "change": 4.8},
{"metric": "EV Sales", "value": "1.5M", "change": 52.3},
{"metric": "Operating Margin", "value": "7.8%", "change": 1.5},
{"metric": "R&D Investment", "value": "$15.2B", "change": 18.5}
],
"2023": [
{"metric": "Global Sales", "value": "8.78M", "change": 3.2},
{"metric": "EV Sales", "value": "0.98M", "change": 42.1},
{"metric": "Operating Margin", "value": "6.3%", "change": 0.9},
{"metric": "R&D Investment", "value": "$12.8B", "change": 15.2}
],
"2022": [
{"metric": "Global Sales", "value": "8.5M", "change": 1.8},
{"metric": "EV Sales", "value": "0.69M", "change": 32.5},
{"metric": "Operating Margin", "value": "5.4%", "change": -0.7},
{"metric": "R&D Investment", "value": "$11.1B", "change": 10.8}
]
},
"GM": {
"2024": [
{"metric": "Global Sales", "value": "6.8M", "change": 3.5},
{"metric": "EV Sales", "value": "0.8M", "change": 48.2},
{"metric": "Operating Margin", "value": "8.2%", "change": 1.8},
{"metric": "R&D Investment", "value": "$9.5B", "change": 16.5}
],
"2023": [
{"metric": "Global Sales", "value": "6.57M", "change": 2.1},
{"metric": "EV Sales", "value": "0.54M", "change": 38.5},
{"metric": "Operating Margin", "value": "6.4%", "change": 1.2},
{"metric": "R&D Investment", "value": "$8.15B", "change": 14.2}
],
"2022": [
{"metric": "Global Sales", "value": "6.43M", "change": 0.8},
{"metric": "EV Sales", "value": "0.39M", "change": 28.7},
{"metric": "Operating Margin", "value": "5.2%", "change": -0.5},
{"metric": "R&D Investment", "value": "$7.13B", "change": 9.8}
]
},
"F": {
"2024": [
{"metric": "Global Sales", "value": "4.2M", "change": 2.8},
{"metric": "EV Sales", "value": "0.6M", "change": 42.5},
{"metric": "Operating Margin", "value": "7.5%", "change": 1.5},
{"metric": "R&D Investment", "value": "$8.2B", "change": 15.8}
],
"2023": [
{"metric": "Global Sales", "value": "4.08M", "change": 1.5},
{"metric": "EV Sales", "value": "0.42M", "change": 35.2},
{"metric": "Operating Margin", "value": "6.0%", "change": 1.0},
{"metric": "R&D Investment", "value": "$7.08B", "change": 13.5}
],
"2022": [
{"metric": "Global Sales", "value": "4.02M", "change": 0.5},
{"metric": "EV Sales", "value": "0.31M", "change": 25.8},
{"metric": "Operating Margin", "value": "5.0%", "change": -0.8},
{"metric": "R&D Investment", "value": "$6.24B", "change": 8.9}
]
},
"TSLA": {
"2024": [
{"metric": "Global Sales", "value": "2.1M", "change": 35.2},
{"metric": "EV Sales", "value": "2.1M", "change": 35.2},
{"metric": "Operating Margin", "value": "15.5%", "change": 3.7},
{"metric": "R&D Investment", "value": "$4.5B", "change": 25.8}
],
"2023": [
{"metric": "Global Sales", "value": "1.55M", "change": 28.5},
{"metric": "EV Sales", "value": "1.55M", "change": 28.5},
{"metric": "Operating Margin", "value": "11.8%", "change": 2.5},
{"metric": "R&D Investment", "value": "$3.58B", "change": 22.3}
],
"2022": [
{"metric": "Global Sales", "value": "1.21M", "change": 21.8},
{"metric": "EV Sales", "value": "1.21M", "change": 21.8},
{"metric": "Operating Margin", "value": "9.3%", "change": 1.8},
{"metric": "R&D Investment", "value": "$2.93B", "change": 18.5}
]
}
}
return performance_data.get(company, {}).get(year, [
{"metric": "No Data", "value": "N/A", "change": 0}
])
```
3. Displays detailed information about the selected car manufacturer in a markdown format. Like the Performance widget, it uses the same shared parameters and updates automatically when they change. The widget includes error handling to display fallback data when the selected company or year is not found.
```python
@register_widget({
"name": "Car Manufacturer Details",
"description": "Displays detailed information about the selected car manufacturer",
"type": "markdown",
"endpoint": "company_details",
"gridData": {"w": 16, "h": 8},
"params": [
{
"paramName": "company", # Shared paramName with company_performance widget
"description": "Select a car manufacturer to view details",
"value": "TM",
"label": "Manufacturer",
"type": "endpoint",
"optionsEndpoint": "/company_options" # Shared endpoint with company_performance widget
},
{
"paramName": "year", # Shared paramName with company_performance widget
"description": "Select model year to view details",
"value": "2024",
"label": "Model Year",
"type": "text",
"options": [
{"label": "2024", "value": "2024"},
{"label": "2023", "value": "2023"},
{"label": "2022", "value": "2022"}
]
}
]
})
@app.get("/company_details")
def get_company_details(company: str, year: str = "2024"):
"""Returns car manufacturer details in markdown format"""
company_info = {
"TM": {
"name": "Toyota Motor Corporation",
"sector": "Automotive",
"market_cap": "280B",
"pe_ratio": 9.5,
"dividend_yield": 2.1,
"description": "Toyota Motor Corporation designs, manufactures, assembles, and sells passenger vehicles, minivans, commercial vehicles, and related parts and accessories worldwide.",
"models": {
"2024": ["Camry", "Corolla", "RAV4", "Highlander"],
"2023": ["Camry", "Corolla", "RAV4", "Highlander"],
"2022": ["Camry", "Corolla", "RAV4", "Highlander"]
}
},
"VWAGY": {
"name": "Volkswagen Group",
"sector": "Automotive",
"market_cap": "75B",
"pe_ratio": 4.2,
"dividend_yield": 3.5,
"description": "Volkswagen Group manufactures and sells automobiles worldwide. The company offers passenger cars, commercial vehicles, and power engineering systems.",
"models": {
"2024": ["Golf", "Passat", "Tiguan", "ID.4"],
"2023": ["Golf", "Passat", "Tiguan", "ID.4"],
"2022": ["Golf", "Passat", "Tiguan", "ID.4"]
}
},
"GM": {
"name": "General Motors",
"sector": "Automotive",
"market_cap": "45B",
"pe_ratio": 5.8,
"dividend_yield": 1.2,
"description": "General Motors designs, builds, and sells cars, trucks, crossovers, and automobile parts worldwide.",
"models": {
"2024": ["Silverado", "Equinox", "Malibu", "Corvette"],
"2023": ["Silverado", "Equinox", "Malibu", "Corvette"],
"2022": ["Silverado", "Equinox", "Malibu", "Corvette"]
}
},
"F": {
"name": "Ford Motor Company",
"sector": "Automotive",
"market_cap": "48B",
"pe_ratio": 7.2,
"dividend_yield": 4.8,
"description": "Ford Motor Company designs, manufactures, markets, and services a line of Ford trucks, cars, sport utility vehicles, electrified vehicles, and Lincoln luxury vehicles.",
"models": {
"2024": ["F-150", "Mustang", "Explorer", "Mach-E"],
"2023": ["F-150", "Mustang", "Explorer", "Mach-E"],
"2022": ["F-150", "Mustang", "Explorer", "Mach-E"]
}
},
"TSLA": {
"name": "Tesla Inc.",
"sector": "Automotive",
"market_cap": "800B",
"pe_ratio": 65.3,
"dividend_yield": 0.0,
"description": "Tesla Inc. designs, develops, manufactures, leases, and sells electric vehicles, and energy generation and storage systems in the United States, China, and internationally.",
"models": {
"2024": ["Model 3", "Model Y", "Model S", "Model X"],
"2023": ["Model 3", "Model Y", "Model S", "Model X"],
"2022": ["Model 3", "Model Y", "Model S", "Model X"]
}
}
}
details = company_info.get(company, {
"name": "Unknown",
"sector": "Unknown",
"market_cap": "N/A",
"pe_ratio": 0,
"dividend_yield": 0,
"description": "No information available for this manufacturer.",
"models": {"2024": [], "2023": [], "2022": []}
})
models = details['models'].get(year, [])
return f"""# {details['name']} ({company}) - {year} Models
**Sector:** {details['sector']}
**Market Cap:** ${details['market_cap']}
**P/E Ratio:** {details['pe_ratio']}
**Dividend Yield:** {details['dividend_yield']}%
{details['description']}
## {year} Model Lineup
{', '.join(models)}
"""
```
This implementation demonstrates how to create a cohesive user interface where multiple widgets work together to provide different views of the same data, while maintaining parameter synchronization across all components.
---
---
title: Parameter Positioning
sidebar_position: 22
description: Learn how to control the layout and positioning of widget parameters in OpenBB Workspace, including row positioning and parameter ordering
keywords:
- parameter positioning
- parameter layout
- parameter rows
- widget parameters
- parameter ordering
---
import HeadTitle from '@site/src/components/General/HeadTitle.tsx';
## Row Positioning
To position parameters in different rows, you can structure the `params` array as a nested array where each sub-array represents a row of parameters.
### Single Row (Default)
```python
"params": [
{
"paramName": "param1",
"type": "text",
"value": "value1"
},
{
"paramName": "param2",
"type": "text",
"value": "value2"
}
]
```
### Multiple Rows
```python
"params": [
[], # Empty first row
[ # Second row with parameters
{
"paramName": "param1",
"type": "text",
"value": "value1"
},
{
"paramName": "param2",
"type": "text",
"value": "value2"
}
]
]
```
## Complete Example
Here's a comprehensive example showing how to position different types of parameters across multiple rows:
```python
@register_widget({
"name": "Moving Parameters Example",
"description": "Show example of moving parameter positions",
"endpoint": "moving_parameters_example",
"gridData": {"w": 20, "h": 9},
"type": "table",
"params": [
[], # Empty first row - pushes all parameters to second row
[ # Second row with all parameters
{
"paramName": "datePicker1",
"value": "$currentDate-1d",
"label": "Param 1",
"description": "I'm a Date Picker!",
"type": "date"
},
{
"paramName": "textBox1",
"value": "Hello!",
"label": "Param 2",
"description": "I'm a text input box!",
"type": "text"
},
{
"paramName": "TrueFalse",
"value": True,
"label": "True/False",
"description": "I'm a True/False selector!",
"type": "boolean"
},
{
"paramName": "daysPicker1",
"value": "1",
"label": "Days",
"type": "text",
"multiSelect": True,
"description": "Number of days to look back",
"options": [
{"value": "1", "label": "1"},
{"value": "5", "label": "5"},
{"value": "10", "label": "10"},
{"value": "20", "label": "20"},
{"value": "30", "label": "30"}
]
}
]
]
})
@app.get("/moving_parameters_example")
def moving_parameters_example(
datePicker1: str = None,
textBox1: str = None,
daysPicker1: str = "1",
TrueFalse: bool = True
):
"""Show example of how to move parameters - This will put them all on the second row of the widget"""
return {
"datePicker1": datePicker1,
"textBox1": textBox1,
"daysPicker1": daysPicker1.split(","),
"TrueFalse": TrueFalse
}
```
## Advanced Positioning Examples
### Parameters Across Multiple Rows
```python
"params": [
[ # First row
{
"paramName": "symbol",
"type": "text",
"value": "AAPL",
"label": "Symbol"
}
],
[ # Second row
{
"paramName": "start_date",
"type": "date",
"value": "$currentDate-30d",
"label": "Start Date"
},
{
"paramName": "end_date",
"type": "date",
"value": "$currentDate",
"label": "End Date"
}
],
[ # Third row
{
"paramName": "show_volume",
"type": "boolean",
"value": True,
"label": "Show Volume"
}
]
]
### Skipping Multiple Rows
```python
"params": [
[], # Skip first row
[], # Skip second row
[ # Parameters on third row
{
"paramName": "param1",
"type": "text",
"value": "value1"
}
]
]
```
## Parameter Ordering
Within each row, parameters are displayed in the order they appear in the array. You can change the visual order by rearranging the parameter objects:
```python
# Original order: Date, Text, Boolean, Dropdown
[
{"paramName": "date_param", "type": "date", ...},
{"paramName": "text_param", "type": "text", ...},
{"paramName": "bool_param", "type": "boolean", ...},
{"paramName": "dropdown_param", "type": "text", "options": [...], ...}
]
# Reordered: Boolean, Dropdown, Date, Text
[
{"paramName": "bool_param", "type": "boolean", ...},
{"paramName": "dropdown_param", "type": "text", "options": [...], ...},
{"paramName": "date_param", "type": "date", ...},
{"paramName": "text_param", "type": "text", ...}
]
```
In the above example, the parameters are displayed in the order: Date, Text, Boolean, Dropdown. Then, the parameters are reordered to: Boolean, Dropdown, Date, Text based on the order of the parameters in the array.
---
---
title: Text Input
sidebar_position: 24
description: Learn how to implement and use text input parameters in OpenBB Workspace widgets, including configuration options and example usage
keywords:
- text input
- text box
- input field
- widget parameters
- text entry
---
import HeadTitle from '@site/src/components/General/HeadTitle.tsx';
```python
@register_widget({
"name": "Markdown Widget with Text Input",
"description": "A markdown widget with a text input parameter",
"endpoint": "markdown_widget_with_text_input",
"gridData": {"w": 16, "h": 6},
"type": "markdown",
"params": [
{
"paramName": "text_box",
"value": "hello",
"label": "Enter Text",
"description": "Type something to display",
"type": "text"
}
]
})
@app.get("/markdown_widget_with_text_input")
def markdown_widget_with_text_input(text_box: str):
"""Returns a markdown widget with text input parameter"""
return f"""# Text Input
Entered text: {text_box}
"""
```
## Multiple Options Text Input
```python
@register_widget({
"name": "Markdown Widget with Text Input",
"description": "A markdown widget with a text input parameter",
"endpoint": "markdown_widget_with_text_input",
"gridData": {"w": 16, "h": 6},
"type": "markdown",
"params": [
{
"paramName": "text_box",
"value": "var1,var2,var3",
"label": "Enter Text",
"description": "Type something to display",
"multiple": true,
"type": "text"
}
]
})
@app.get("/markdown_widget_with_text_input")
def markdown_widget_with_text_input(text_box: str):
"""Returns a markdown widget with text input parameter"""
return f"""# Text Input
Entered text: {text_box}
"""
```
---
---
title: AgGrid Table Charts
sidebar_position: 7
description: AgGrid Table Charts
keywords:
- asd
---
import HeadTitle from '@site/src/components/General/HeadTitle.tsx';
```python
@register_widget({
"name": "Table Widget",
"description": "A table widget",
"type": "table",
"endpoint": "table_widget",
"gridData": {"w": 12, "h": 4},
})
@app.get("/table_widget")
def table_widget():
"""Returns a mock table data for demonstration"""
mock_data = [
{
"name": "Ethereum",
"tvl": 45000000000,
"change_1d": 2.5,
"change_7d": 5.2
},
{
"name": "Bitcoin",
"tvl": 35000000000,
"change_1d": 1.2,
"change_7d": 4.8
},
{
"name": "Solana",
"tvl": 8000000000,
"change_1d": -0.5,
"change_7d": 2.1
}
]
return mock_data
```
## Table Widget from API
A widget that fetches and displays data from an external API. This example demonstrates integration with the DeFi Llama API to show chain TVL data.
```python
@register_widget({
"name": "Table Widget from API Endpoint",
"description": "A table widget from an API endpoint",
"type": "table",
"endpoint": "table_widget_from_api_endpoint",
"gridData": {"w": 12, "h": 4},
})
@app.get("/table_widget_from_api_endpoint")
def table_widget_from_api_endpoint():
"""Get current TVL of all chains using Defi LLama"""
response = requests.get("https://api.llama.fi/v2/chains")
if response.status_code == 200:
return response.json()
print(f"Request error {response.status_code}: {response.text}")
raise HTTPException(
status_code=response.status_code,
detail=response.text
)
```
## Table Widget with Column Definitions
A widget that displays data in a tabular format with customizable column definitions. The most important part of this widget is the "columnsDefs" key in the data object which allows for detailed column configuration.
```python
@register_widget({
"name": "Table Widget with Column Definitions",
"description": "A table widget with column definitions",
"type": "table",
"endpoint": "table_widget_with_column_definitions",
"gridData": {"w": 20, "h": 6},
"data": {
"table": {
"columnsDefs": [
{
"field": "name",
"headerName": "Asset",
"cellDataType": "text",
"formatterFn": "none",
"renderFn": "titleCase",
"width": 120,
"pinned": "left"
},
{
"field": "tvl",
"headerName": "TVL (USD)",
"headerTooltip": "Total Value Locked",
"cellDataType": "number",
"formatterFn": "int",
"width": 150
},
{
"field": "change_1d",
"headerName": "24h Change",
"cellDataType": "number",
"formatterFn": "percent",
"width": 120,
"maxWidth": 150,
"minWidth": 70,
},
{
"field": "change_7d",
"headerName": "7d Change",
"cellDataType": "number",
"formatterFn": "percent",
"width": 120,
"maxWidth": 150,
"minWidth": 70,
"hide": True
},
]
}
},
})
@app.get("/table_widget_with_column_definitions")
def table_widget_with_column_definitions():
"""Returns a mock table data for demonstration"""
mock_data = [
{
"name": "Ethereum",
"tvl": 45000000000,
"change_1d": 2.5,
"change_7d": 5.2
},
{
"name": "Bitcoin",
"tvl": 35000000000,
"change_1d": 1.2,
"change_7d": 4.8
},
{
"name": "Solana",
"tvl": 8000000000,
"change_1d": -0.5,
"change_7d": 2.1
}
]
return mock_data
```
## Table Widget with Render Functions
A widget that demonstrates various rendering functions for table cells. The key feature is the "renderFn" key in the columnsDefs object which allows for custom cell rendering.
```python
@register_widget({
"name": "Table Widget with Render Functions",
"description": "A table widget with render functions",
"type": "table",
"endpoint": "table_widget_with_render_functions",
"gridData": {"w": 20, "h": 6},
"data": {
"table": {
"columnsDefs": [
{
"field": "name",
"headerName": "Asset",
"cellDataType": "text",
"formatterFn": "none",
"renderFn": "titleCase",
"width": 120,
"pinned": "left"
},
{
"field": "tvl",
"headerName": "TVL (USD)",
"headerTooltip": "Total Value Locked",
"cellDataType": "number",
"formatterFn": "int",
"width": 150,
"renderFn": "columnColor",
"renderFnParams": {
"colorRules": [
{
"condition": "between",
"range": {
"min": 30000000000,
"max": 40000000000
},
"color": "blue",
"fill": False
},
{
"condition": "lt",
"value": 10000000000,
"color": "#FFA500",
"fill": False
},
{
"condition": "gt",
"value": 40000000000,
"color": "green",
"fill": True
}
]
}
},
{
"field": "change_1d",
"headerName": "24h Change",
"cellDataType": "number",
"formatterFn": "percent",
"renderFn": "greenRed",
"width": 120,
"maxWidth": 150,
"minWidth": 70,
},
{
"field": "change_7d",
"headerName": "7d Change",
"cellDataType": "number",
"formatterFn": "percent",
"renderFn": "greenRed",
"width": 120,
"maxWidth": 150,
"minWidth": 70,
}
]
}
},
})
@app.get("/table_widget_with_render_functions")
def table_widget_with_render_functions():
"""Returns a mock table data for demonstration"""
mock_data = [
{
"name": "Ethereum",
"tvl": 45000000000,
"change_1d": 2.5,
"change_7d": 5.2
},
{
"name": "Bitcoin",
"tvl": 35000000000,
"change_1d": 1.2,
"change_7d": 4.8
},
{
"name": "Solana",
"tvl": 8000000000,
"change_1d": -0.5,
"change_7d": 2.1
}
]
return mock_data
```
For more information on this, check [Render functions](../widget-configuration/render-functions).
## Table Widget with Hover Card
A widget that demonstrates the hover card feature, allowing additional information to be displayed when hovering over table cells.
```python
@register_widget({
"name": "Table Widget with Hover Card",
"description": "A table widget with hover card",
"type": "table",
"endpoint": "table_widget_with_hover_card",
"gridData": {"w": 20, "h": 6},
"data": {
"table": {
"columnsDefs": [
{
"field": "name",
"headerName": "Asset",
"cellDataType": "text",
"formatterFn": "none",
"width": 120,
"pinned": "left",
"renderFn": "hoverCard",
"renderFnParams": {
"hoverCard": {
"cellField": "value",
"title": "Project Details",
"markdown": "### {value} (since {foundedDate})\n**Description:** {description}"
}
}
},
{
"field": "tvl",
"headerName": "TVL (USD)",
"headerTooltip": "Total Value Locked",
"cellDataType": "number",
"formatterFn": "int",
"width": 150,
"renderFn": "columnColor",
},
{
"field": "change_1d",
"headerName": "24h Change",
"cellDataType": "number",
"formatterFn": "percent",
"renderFn": "greenRed",
"width": 120,
"maxWidth": 150,
"minWidth": 70,
},
{
"field": "change_7d",
"headerName": "7d Change",
"cellDataType": "number",
"formatterFn": "percent",
"renderFn": "greenRed",
"width": 120,
"maxWidth": 150,
"minWidth": 70,
}
]
}
},
})
@app.get("/table_widget_with_hover_card")
def table_widget_with_hover_card():
"""Returns a mock table data for demonstration"""
mock_data = [
{
"name": {
"value": "Ethereum",
"description": "A decentralized, open-source blockchain with smart contract functionality",
"foundedDate": "2015-07-30"
},
"tvl": 45000000000,
"change_1d": 2.5,
"change_7d": 5.2
},
{
"name": {
"value": "Bitcoin",
"description": "The first decentralized cryptocurrency",
"foundedDate": "2009-01-03"
},
"tvl": 35000000000,
"change_1d": 1.2,
"change_7d": 4.8
},
{
"name": {
"value": "Solana",
"description": "A high-performance blockchain supporting builders around the world",
"foundedDate": "2020-03-16"
},
"tvl": 8000000000,
"change_1d": -0.5,
"change_7d": 2.1
}
]
return mock_data
```
## Table to Chart Widget
A widget that demonstrates how to convert table data into a chart view. The key feature is the "chartView" configuration in the data object.
```python
@register_widget({
"name": "Table to Chart Widget",
"description": "A table widget",
"type": "table",
"endpoint": "table_to_chart_widget",
"gridData": {"w": 20, "h": 12},
"data": {
"table": {
"enableCharts": True,
"showAll": False,
"chartView": {
"enabled": True,
"chartType": "column"
},
"columnsDefs": [
{
"field": "name",
"headerName": "Asset",
"chartDataType": "category",
},
{
"field": "tvl",
"headerName": "TVL (USD)",
"chartDataType": "series",
},
]
}
},
})
@app.get("/table_to_chart_widget")
def table_to_chart_widget():
"""Returns a mock table data for demonstration"""
mock_data = [
{
"name": "Ethereum",
"tvl": 45000000000,
"change_1d": 2.5,
"change_7d": 5.2
},
{
"name": "Bitcoin",
"tvl": 35000000000,
"change_1d": 1.2,
"change_7d": 4.8
},
{
"name": "Solana",
"tvl": 8000000000,
"change_1d": -0.5,
"change_7d": 2.1
}
]
return mock_data
```
## Table to Time Series Widget
A widget that demonstrates how to display time series data in a chart format. The key feature is the use of "chartDataType": "time" for date fields.
```python
@register_widget({
"name": "Table to Time Series Widget",
"description": "A table widget",
"type": "table",
"endpoint": "table_to_time_series_widget",
"gridData": {"w": 20, "h": 12},
"data": {
"table": {
"enableCharts": True,
"showAll": False,
"chartView": {
"enabled": True,
"chartType": "line"
},
"columnsDefs": [
{
"field": "date",
"headerName": "Date",
"chartDataType": "time",
},
{
"field": "Ethereum",
"headerName": "Ethereum",
"chartDataType": "series",
},
{
"field": "Bitcoin",
"headerName": "Bitcoin",
"chartDataType": "series",
},
{
"field": "Solana",
"headerName": "Solana",
"chartDataType": "series",
}
]
}
},
})
@app.get("/table_to_time_series_widget")
def table_to_time_series_widget():
"""Returns a mock table data for demonstration"""
mock_data = [
{
"date": "2024-06-06",
"Ethereum": 1.0000,
"Bitcoin": 1.0000,
"Solana": 1.0000
},
{
"date": "2024-06-07",
"Ethereum": 1.0235,
"Bitcoin": 0.9822,
"Solana": 1.0148
},
{
"date": "2024-06-08",
"Ethereum": 0.9945,
"Bitcoin": 1.0072,
"Solana": 0.9764
},
{
"date": "2024-06-09",
"Ethereum": 1.0205,
"Bitcoin": 0.9856,
"Solana": 1.0300
},
{
"date": "2024-06-10",
"Ethereum": 0.9847,
"Bitcoin": 1.0195,
"Solana": 0.9897
}
]
return mock_data
```
## Table Widget with Sparklines
Sparklines allow you to display small charts directly within table cells, providing at-a-glance data visualization. Our implementation supports line, area, and bar sparklines with comprehensive styling options.
### Basic Sparkline with Min/Max Points
This example shows a basic sparkline with minimum and maximum points highlighted:
```python
@register_widget({
"name": "Table Widget with Basic Sparklines",
"description": "A table widget with basic sparklines showing min/max points",
"type": "table",
"endpoint": "table_widget_basic_sparklines",
"gridData": {"w": 20, "h": 6},
"data": {
"table": {
"columnsDefs": [
{
"field": "stock",
"headerName": "Stock",
"cellDataType": "text",
"width": 120,
"pinned": "left"
},
{
"field": "price_history",
"headerName": "Price History",
"width": 200,
"sparkline": {
"type": "line",
"options": {
"stroke": "#2563eb",
"strokeWidth": 2,
"markers": {
"enabled": True,
"size": 3
},
"pointsOfInterest": {
"maximum": {
"fill": "#22c55e",
"stroke": "#16a34a",
"size": 6
},
"minimum": {
"fill": "#ef4444",
"stroke": "#dc2626",
"size": 6
}
}
}
}
},
{
"field": "volume",
"headerName": "Volume",
"width": 150,
"sparkline": {
"type": "bar",
"options": {
"fill": "#6b7280",
"stroke": "#4b5563",
"pointsOfInterest": {
"maximum": {
"fill": "#22c55e",
"stroke": "#16a34a"
},
"minimum": {
"fill": "#ef4444",
"stroke": "#dc2626"
}
}
}
}
}
]
}
},
})
@app.get("/table_widget_basic_sparklines")
def table_widget_basic_sparklines():
"""Returns mock data with sparklines"""
mock_data = [
{
"stock": "AAPL",
"price_history": [150, 155, 148, 162, 158, 165, 170],
"volume": [1000, 1200, 900, 1500, 1100, 1300, 1800]
},
{
"stock": "GOOGL",
"price_history": [2800, 2750, 2900, 2850, 2950, 3000, 2980],
"volume": [800, 950, 700, 1200, 850, 1100, 1400]
},
{
"stock": "MSFT",
"price_history": [340, 335, 350, 345, 360, 355, 365],
"volume": [900, 1100, 800, 1300, 950, 1200, 1600]
}
]
return mock_data
```
### Sparklines with Custom Formatters
For complete control over styling individual data points, use custom formatters. This example shows profit/loss data with dynamic coloring:
```python
@register_widget({
"name": "Table Widget with Custom Formatter",
"description": "A table widget with custom sparkline formatter for profit/loss",
"type": "table",
"endpoint": "table_widget_custom_formatter",
"gridData": {"w": 20, "h": 6},
"data": {
"table": {
"columnsDefs": [
{
"field": "company",
"headerName": "Company",
"cellDataType": "text",
"width": 150,
"pinned": "left"
},
{
"field": "profit_loss",
"headerName": "P&L Trend",
"width": 200,
"sparkline": {
"type": "bar",
"options": {
"customFormatter": "(params) => ({ fill: params.yValue >= 0 ? '#22c55e' : '#ef4444', stroke: params.yValue >= 0 ? '#16a34a' : '#dc2626' })"
}
}
},
{
"field": "revenue_growth",
"headerName": "Revenue Growth",
"width": 200,
"sparkline": {
"type": "bar",
"options": {
"customFormatter": "(params) => ({ fill: params.yValue > 10 ? '#22c55e' : params.yValue < 0 ? '#ef4444' : '#f59e0b', fillOpacity: 0.3, stroke: params.yValue > 10 ? '#16a34a' : params.yValue < 0 ? '#dc2626' : '#d97706' })"
}
}
}
]
}
},
})
@app.get("/table_widget_custom_formatter")
def table_widget_custom_formatter():
"""Returns mock data with custom formatter"""
mock_data = [
{
"company": "TechCorp",
"profit_loss": [5, -2, 8, -3, 12, 7, -1],
"revenue_growth": [15, 8, -5, 20, -8, 25, 18]
},
{
"company": "DataSoft",
"profit_loss": [10, -5, 15, -8, 20, 12, -3],
"revenue_growth": [12, 5, 8, -2, 15, 10, 22]
},
{
"company": "CloudInc",
"profit_loss": [8, -15, 25, 12, -5, 18, 28],
"revenue_growth": [8, -3, 12, 18, 6, 14, 9]
}
]
return mock_data
```
### Sparkline Configuration Options
#### Supported Sparkline Types
- **`line`** - Line chart
- **`area`** - Area chart (filled line)
- **`bar`** - Bar chart
#### Points of Interest
- **`firstLast`** - First and last data points
- **`minimum`** - Points with minimum values
- **`maximum`** - Points with maximum values
- **`positiveNegative`** - Separate styling for positive and negative values
- **`highlighted`** - Points highlighted on hover/interaction
#### Custom Formatter Parameters
The formatter function receives parameters including:
- ***`yValue`*** - The data value
- ***`first`*** - Whether this is the first point
- ***`last`*** - Whether this is the last point
- ***`min`*** - Whether this is a minimum point
- ***`max`*** - Whether this is a maximum point
- ***`highlighted`*** - Whether this point is highlighted
#### Styling Options
- **Basic styling**: `stroke`, `strokeWidth`, `fill`, `fillOpacity`
- **Markers**: `enabled`, `size`, `fill`, `stroke`, `strokeWidth`
- **Padding**: `top`, `right`, `bottom`, `left`
- **Direction**: `vertical`, `horizontal` (for bar charts)
For more detailed configuration options, refer to the [Widgets JSON Reference](../json-specs/widgets-json-reference) or our examples backends [here](https://github.com/OpenBB-finance/backends-for-openbb/tree/main/getting-started/reference-backend).
## OTHERS
### Table Interface
The Table widget offers comprehensive data manipulation and visualization capabilities:
- **Column Resizing**: Adjust column widths manually or use the "Autosize all columns" feature for automatic optimization.
- **Column Reorganization**: Implement drag-and-drop functionality to reorder columns. Click and hold any column header to reposition it.
- **Column Filtering**: Toggle column visibility through column settings to focus on relevant data for your analysis.
- **Sorting**: Click column headers to sort data in ascending or descending order.
- **Data Selection**: Select specific data points or ranges to generate visualizations.
### Table to Chart Conversion
The widget supports two primary methods for converting table data into charts:
1. **Selection-based Charting**: Select desired data points, choose a chart type, and generate visualizations instantly. This feature is particularly useful for quantitative analysis.
The example below demonstrates data selection and right-click menu options for creating a line chart:
```python
@register_widget({
"name": "PDF Widget with Base64",
"description": "Display a PDF file with base64 encoding",
"endpoint": "pdf_widget_base64",
"gridData": {
"w": 20,
"h": 20
},
"type": "pdf",
})
@app.get("/pdf_widget_base64")
def get_pdf_widget_base64():
"""Serve a file through base64 encoding."""
try:
name = "sample.pdf"
with open(ROOT_PATH / name, "rb") as file:
file_data = file.read()
encoded_data = base64.b64encode(file_data)
content = encoded_data.decode("utf-8")
except FileNotFoundError as exc:
raise HTTPException(
status_code=404,
detail="File not found"
) from exc
return JSONResponse(
headers={"Content-Type": "application/json"},
content={
"data_format": {
"data_type": "pdf",
"filename": name,
},
"content": content,
},
)
```
### PDF Widget with URL
A widget that displays a PDF file using a direct URL. This method is more efficient for larger PDFs as it doesn't require base64 encoding.
```python
@register_widget({
"name": "PDF Widget with URL",
"description": "Display a PDF file",
"type": "pdf",
"endpoint": "pdf_widget_url",
"gridData": {
"w": 20,
"h": 20
},
})
@app.get("/pdf_widget_url")
def get_pdf_widget_url():
"""Serve a file through URL."""
file_reference = "https://openbb-assets.s3.us-east-1.amazonaws.com/testing/sample.pdf"
if not file_reference:
raise HTTPException(status_code=404, detail="File not found")
return JSONResponse(
headers={"Content-Type": "application/json"},
content={
"data_format": {
"data_type": "pdf",
"filename": "Sample.pdf",
},
"url": file_reference,
},
)
```
## Multi File Viewer
### Multi File Viewer with Base64
A widget that displays multiple PDF files using base64 encoding. This method is useful for smaller files that you want to serve directly (vs URL).
```python
from pydantic import BaseModel
from typing import List, Union
from fastapi import Body
class FileOption(BaseModel):
label: str
value: str
class FileDataFormat(BaseModel):
data_type: str
filename: str
class DataContent(BaseModel):
content: str
data_format: FileDataFormat
class DataUrl(BaseModel):
url: str
data_format: FileDataFormat
class DataError(BaseModel):
error_type: str
content: str
# Sample PDF files data
SAMPLE_PDFS = [
{
"name": "Sample",
"location": "sample.pdf",
"url": "https://openbb-assets.s3.us-east-1.amazonaws.com/testing/sample.pdf",
},
{
"name": "Bitcoin Whitepaper",
"location": "bitcoin.pdf",
"url": "https://openbb-assets.s3.us-east-1.amazonaws.com/testing/bitcoin.pdf",
}
]
# Options endpoint for file selection
@app.get("/get_pdf_options")
async def get_pdf_options() -> List[FileOption]:
"""Get list of available PDFs"""
return [
FileOption(label=pdf["name"], value=pdf["name"])
for pdf in SAMPLE_PDFS
]
@register_widget({
"name": "Multi PDF Viewer - Base64",
"description": "View multiple PDF files using base64 encoding",
"type": "multi_file_viewer",
"endpoint": "/multi_pdf_base64",
"gridData": {
"w": 20,
"h": 10
},
"params": [
{
"paramName": "pdf_name",
"description": "PDF file to display",
"type": "endpoint",
"label": "PDF File",
"optionsEndpoint": "/get_pdf_options",
"show": False,
"value": ["Bitcoin Whitepaper"],
"multiSelect": True,
"roles": ["fileSelector"]
}
]
})
@app.post("/multi_pdf_base64")
async def get_multi_pdf_base64(
pdf_name: List[str] = Body(..., embed=True)
) -> List[Union[DataContent, DataError]]:
"""Get multiple PDF files in base64 format"""
files = []
for name in pdf_name:
pdf = next((p for p in SAMPLE_PDFS if p["name"] == name), None)
if not pdf:
files.append(
DataError(
error_type="not_found",
content=f"PDF '{name}' not found"
).model_dump()
)
continue
file_path = ROOT_PATH / pdf["location"]
if not file_path.exists():
files.append(
DataError(
error_type="not_found",
content=f"PDF file '{pdf['location']}' not found on disk"
).model_dump()
)
continue
with open(file_path, "rb") as file:
base64_content = base64.b64encode(file.read()).decode("utf-8")
files.append(
DataContent(
content=base64_content,
data_format=FileDataFormat(
data_type="pdf",
filename=f"{pdf['name']}.pdf"
)
).model_dump()
)
return JSONResponse(
headers={"Content-Type": "application/json"},
content=files
)
```
### Multi File Viewer with URL
A widget that displays multiple PDF files using URLs. This method is more efficient for larger files as they're loaded directly from the URL.
```python
from pydantic import BaseModel
from typing import List, Union
from fastapi import Body
class FileOption(BaseModel):
label: str
value: str
class FileDataFormat(BaseModel):
data_type: str
filename: str
class DataContent(BaseModel):
content: str
data_format: FileDataFormat
class DataUrl(BaseModel):
url: str
data_format: FileDataFormat
class DataError(BaseModel):
error_type: str
content: str
@register_widget({
"name": "Multi PDF Viewer - URL",
"description": "View multiple PDF files using URLs",
"type": "multi_file_viewer",
"endpoint": "/multi_pdf_url",
"gridData": {
"w": 20,
"h": 10
},
"params": [
{
"paramName": "pdf_name",
"description": "PDF file to display",
"type": "endpoint",
"label": "PDF File",
"optionsEndpoint": "/get_pdf_options",
"value": ["Sample"],
"show": False,
"multiSelect": True,
"roles": ["fileSelector"]
}
]
})
@app.post("/multi_pdf_url")
async def get_multi_pdf_url(
pdf_name: List[str] = Body(..., embed=True)
) -> List[Union[DataUrl, DataError]]:
"""Get multiple PDF files via URLs"""
files = []
for name in pdf_name:
pdf = next((p for p in SAMPLE_PDFS if p["name"] == name), None)
if not pdf:
files.append(
DataError(
error_type="not_found",
content=f"PDF '{name}' not found"
).model_dump()
)
continue
if url := pdf.get("url"):
files.append(
DataUrl(
url=url,
data_format=FileDataFormat(
data_type="pdf",
filename=f"{pdf['name']}.pdf"
)
).model_dump()
)
else:
files.append(
DataError(
error_type="not_found",
content=f"URL not found for '{name}'"
).model_dump()
)
return JSONResponse(
headers={"Content-Type": "application/json"},
content=files
)
```
## Multi File Viewer with Category Filtering
This multi-file-viewer widget introduces a parameter called `optionsParams` which allows you to pass the options to an endpoint from a different parameter. More information [here](../widget-parameters/dependent-dropdown.md).
In our case we want to pass the options in the `type` parameter to the `/whitepapers/options` endpoint to filter the list of whitepapers.
```python
from pydantic import BaseModel
from typing import List, Union
from fastapi import Body
class FileOption(BaseModel):
label: str
value: str
class FileDataFormat(BaseModel):
data_type: str
filename: str
class DataContent(BaseModel):
content: str
data_format: FileDataFormat
class DataUrl(BaseModel):
url: str
data_format: FileDataFormat
class DataError(BaseModel):
error_type: str
content: str
# Sample whitepaper data with categories
WHITEPAPERS = [
{
"name": "Bitcoin",
"location": "bitcoin.pdf",
"url": "https://openbb-assets.s3.us-east-1.amazonaws.com/testing/bitcoin.pdf",
"category": "l1",
},
{
"name": "Ethereum",
"location": "ethereum.pdf",
"url": "https://openbb-assets.s3.us-east-1.amazonaws.com/testing/ethereum.pdf",
"category": "l1",
},
{
"name": "Chainlink",
"location": "chainlink.pdf",
"url": "https://openbb-assets.s3.us-east-1.amazonaws.com/testing/chainlink.pdf",
"category": "oracles",
},
{
"name": "Solana",
"location": "solana.pdf",
"url": "https://openbb-assets.s3.us-east-1.amazonaws.com/testing/solana.pdf",
"category": "l1",
},
]
@app.get("/whitepapers/options")
async def get_whitepaper_options(category: str = Query("all")) -> List[FileOption]:
"""Get list of available whitepapers filtered by category"""
if category == "all":
return [
FileOption(label=wp["name"], value=wp["name"])
for wp in WHITEPAPERS
]
return [
FileOption(label=wp["name"], value=wp["name"])
for wp in WHITEPAPERS
if wp["category"] == category
]
@register_widget({
"name": "Whitepapers",
"description": "A collection of crypto whitepapers.",
"type": "multi_file_viewer",
"endpoint": "/whitepapers/view-base64",
"gridData": {
"w": 40,
"h": 10
},
"params": [
{
"type": "endpoint",
"paramName": "whitepaper",
"value": ["Bitcoin"],
"label": "Whitepaper",
"description": "Whitepaper to display.",
"optionsEndpoint": "/whitepapers/options",
"show": False,
"optionsParams": {
"category": "$category"
},
"multiSelect": True,
"roles": ["fileSelector"]
},
{
"type": "text",
"paramName": "category",
"value": "all",
"label": "Category",
"description": "Category of whitepaper to fetch.",
"options": [
{
"label": "All",
"value": "all"
},
{
"label": "L1",
"value": "l1"
},
{
"label": "L2",
"value": "l2"
},
{
"label": "Oracles",
"value": "oracles"
},
{
"label": "Defi",
"value": "defi"
}
]
}
]
})
@app.post("/whitepapers/view-base64")
async def view_whitepapers_base64(
whitepaper: List[str] = Body(..., embed=True),
) -> List[Union[DataContent, DataError]]:
"""Get multiple whitepapers in base64 format"""
files = []
for name in whitepaper:
wp = next((w for w in WHITEPAPERS if w["name"] == name), None)
if not wp:
files.append(
DataError(
error_type="not_found",
content=f"Whitepaper '{name}' not found"
).model_dump()
)
continue
file_path = ROOT_PATH / wp["location"]
if file_path.exists():
with open(file_path, "rb") as file:
base64_content = base64.b64encode(file.read()).decode("utf-8")
files.append(
DataContent(
content=base64_content,
data_format=FileDataFormat(
data_type="pdf",
filename=f"{wp['name']}.pdf"
),
).model_dump()
)
else:
files.append(
DataError(
error_type="not_found",
content="Whitepaper file not found"
).model_dump()
)
return JSONResponse(headers={"Content-Type": "application/json"}, content=files)
# Alternative URL version
@app.post("/whitepapers/view-url")
async def view_whitepapers_url(
whitepaper: List[str] = Body(..., embed=True),
) -> List[Union[DataUrl, DataError]]:
"""Get multiple whitepapers via URLs"""
files = []
for name in whitepaper:
wp = next((w for w in WHITEPAPERS if w["name"] == name), None)
if not wp:
files.append(
DataError(
error_type="not_found",
content=f"Whitepaper '{name}' not found"
).model_dump()
)
continue
if url := wp.get("url"):
files.append(
DataUrl(
url=url,
data_format=FileDataFormat(
data_type="pdf",
filename=f"{wp['name']}.pdf"
),
).model_dump()
)
else:
files.append(
DataError(
error_type="not_found",
content=f"URL not found for '{name}'"
).model_dump()
)
return JSONResponse(headers={"Content-Type": "application/json"}, content=files)
```
More examples can be found on the github repository at https://github.com/OpenBB-finance/backends-for-openbb
---
---
title: Highcharts Chart
sidebar_position: 14
description: Learn how to create Highcharts widgets for OpenBB Workspace, with step-by-step instructions for backend integration, configuration, and theme support.
keywords:
- widgets.json
- OpenBB API
- Endpoint integration
- widget configuration
- Highcharts
- Chart widgets
- API implementation
- Python
- FastAPI
- Workspace widgets
- Widget definitions
---
import HeadTitle from '@site/src/components/General/HeadTitle.tsx';
### Theme Support
The example doesn't include theme support, but you can easily add it by adapting the endpoint to include the `theme` parameter.
## Additional Resources
You can find more examples of how to set up your own backend in the [Backend for OpenBB Workspace GitHub](https://github.com/OpenBB-finance/backend-examples-for-openbb-workspace).
For more information on Highcharts configuration options, visit the [Highcharts API Documentation](https://api.highcharts.com/highcharts/).
---
---
title: HTML
sidebar_position: 5
description: Learn how to create and customize HTML widgets in OpenBB Workspace, enabling complete control over visualization and interaction design with custom HTML, CSS, and JavaScript.
keywords:
- html widget
- widget configuration
- custom visualization
- interactive dashboard
- widget development
- HTML content
- CSS styling
- JavaScript interactivity
- OpenBB Workspace
- custom widgets
---
import HeadTitle from '@site/src/components/General/HeadTitle.tsx';
```python
@register_widget({
"name": "HTML Widget",
"description": "A HTML widget with interactive dashboard",
"type": "html",
"endpoint": "html_widget",
"gridData": {"w": 40, "h": 20},
})
@app.get("/html_widget", response_class=HTMLResponse)
def html_widget():
"""Returns an HTML widget with mockup data"""
return HTMLResponse(content="""
Real-time market overview and analytics
## Additional Resources
You can find more examples of how to set up your own backend in the [Backend for OpenBB Workspace GitHub](https://github.com/OpenBB-finance/backend-examples-for-openbb-workspace).
---
---
title: Markdown
sidebar_position: 4
description: Learn how to create and customize markdown widgets in OpenBB Workspace, including basic markdown display and data-rich markdown with dynamic content integration.
keywords:
- markdown widget
- widget configuration
- dynamic markdown
- data integration
- widget display
- markdown formatting
- widget customization
- OpenBB Workspace
- widget development
- markdown content
---
import HeadTitle from '@site/src/components/General/HeadTitle.tsx';
```python
@register_widget({
"name": "Markdown Widget",
"description": "A markdown widget",
"type": "markdown",
"endpoint": "markdown_widget",
"gridData": {"w": 12, "h": 4},
})
@app.get("/markdown_widget")
def markdown_widget():
"""Returns a markdown widget"""
return "# Markdown Widget"
```
The gridData parameter specifies the widget's size in the OpenBB Workspace grid system. More on that can be found [here](../widget-configuration/grid-size).
## Data rich markdown
```python
@register_widget({
"name": "Defi Llama Protocol Details",
"description": "Details for a given protocol",
"category": "Crypto",
"defaultViz": "markdown",
"endpoint": "defi_llama_protocol_details",
"gridData": {"w": 20, "h": 9},
"source": "Defi Llama",
"params": [
{
"paramName": "protocol_id",
"value": "aave",
"label": "Protocol",
"type": "text",
"description": "Defi Llama ID of the protocol"
}
]
})
@app.get("/defi_llama_protocol_details")
def defi_llama_protocol_details(protocol_id: str = None):
"""Get details for a given protocol using Defi Llama"""
data = requests.get(f'https://api.llama.fi/protocol/{protocol_id}')
if data.status_code == 200:
data = data.json()
else:
return JSONResponse(content={"error": data.text}, status_code=data.status_code)
github_links = ""
if 'github' in data and data['github']:
github_links = "**GitHub:** " + ", ".join(data['github'])
# Use HTML for multi-column layout
markdown = dedent(f"""
})
# {data.get('name', 'N/A')} ({data.get('symbol', 'N/A').upper()})
**Description:** {data.get('description', 'N/A')}
---
## Twitter
**Twitter:** {data.get('twitter', 'N/A')}
## Links
**Website:** {data.get('url', 'N/A')}
{github_links}
""")
return markdown
```
**Note:** The `dedent` function is used to remove leading whitespace from the markdown string. This is a good practice to ensure the markdown is formatted correctly.
## Markdown Widget with Local Image
A widget that displays markdown content with an embedded local image. The image is converted to base64 for display.
```python
@register_widget({
"name": "Markdown Widget with Local Image",
"description": "A markdown widget with a local image",
"type": "markdown",
"endpoint": "markdown_widget_with_local_image",
"gridData": {"w": 20, "h": 20},
})
@app.get("/markdown_widget_with_local_image")
def markdown_widget_with_local_image():
"""Returns a markdown widget with a local image"""
try:
with open("img.png", "rb") as image_file:
image_base64 = base64.b64encode(image_file.read()).decode('utf-8')
return f""
except FileNotFoundError:
raise HTTPException(
status_code=500,
detail="Image file not found"
) from e
except Exception as e:
raise HTTPException(
status_code=500,
detail=f"Error reading image: {str(e)}"
) from e
```
## Markdown Widget with Image from URL
The Markdown widget also provides image handling capabilities, supporting both local and remote images. Images are converted to base64 format for displaying.
Below is a markdown widget that displays markdown content with an image fetched from a URL. The image is converted to base64 for display.
```python
@register_widget({
"name": "Markdown Widget with Image from URL",
"description": "A markdown widget with an image from a URL",
"type": "markdown",
"endpoint": "markdown_widget_with_image_from_url",
"gridData": {"w": 20, "h": 20},
})
@app.get("/markdown_widget_with_image_from_url")
def markdown_widget_with_image_from_url():
"""Returns a markdown widget with an image from a URL"""
image_url = "https://api.star-history.com/svg?repos=openbb-finance/OpenBB&type=Date&theme=dark"
try:
response = requests.get(image_url, timeout=10)
response.raise_for_status()
content_type = response.headers.get('content-type', '')
if not content_type.startswith('image/'):
raise HTTPException(
status_code=500,
detail=f"URL did not return an image. Content-Type: {content_type}"
)
image_base64 = base64.b64encode(response.content).decode('utf-8')
return f""
except requests.RequestException as e:
raise HTTPException(
status_code=500,
detail=f"Failed to fetch image: {str(e)}"
) from e
except Exception as e:
raise HTTPException(
status_code=500,
detail=f"Error processing image: {str(e)}"
) from e
```
---
---
title: Metric
sidebar_position: 5
description: Learn how to integrate your own backend with OpenBB Workspace using the cookie-cutter or language-agnostic API approaches, with illustrative guides and principles for handling widget.json files, APIs, interfaces, Python, FastAPI, and more.
keywords:
- widgets.json
- OpenBB API
- Endpoint integration
- widget configuration
- Language-Agnostic API
- API implementation
- Python
- FastAPI
- Workspace widgets
- Widget definitions
---
import HeadTitle from '@site/src/components/General/HeadTitle.tsx';
```python
@register_widget({
"name": "Metric Widget",
"description": "A metric widget example",
"category": "Test",
"endpoint": "test_metric",
"type": "metric"
})
@app.get("/test_metric")
def test_metric():
"""Example endpoint to provide metric data."""
# Example data structure
data = {
"label": "Example Label",
"value": "12345",
"delta": "5.67"
}
return JSONResponse(content=data)
```
As you can see in the example the data structure is as follows:
- `label`: The label of the metric.
- `value`: The value of the metric.
- `delta`: The delta of the metric.
## Multiple metrics
```python
@register_widget({
"name": "Metric Widget",
"description": "A metric widget",
"endpoint": "metric_widget",
"gridData": {
"w": 5,
"h": 5
},
"type": "metric"
})
@app.get("/metric_widget")
def metric_widget():
data = [
{
"label": "Total Users",
"value": "1,234,567",
"delta": "12.5"
},
{
"label": "Active Sessions",
"value": "45,678",
"delta": "-2.3"
},
{
"label": "Revenue (USD)",
"value": "$89,432",
"delta": "8.9"
},
{
"label": "Conversion Rate",
"value": "3.2%",
"delta": "0.0"
},
{
"label": "Avg. Session Duration",
"value": "4m 32s",
"delta": "0.5"
}
]
return JSONResponse(content=data)
```
---
---
title: Newsfeed
sidebar_position: 10
description: Learn how to create a newsfeed widget for OpenBB Workspace that displays articles in a clean, organized format.
keywords:
- widgets.json
- OpenBB API
- Newsfeed widget
- Article display
- News integration
- FastAPI
- Custom Backend
---
import HeadTitle from '@site/src/components/General/HeadTitle.tsx';
## Data Structure
The newsfeed widget expects articles in a specific format:
```python
{
"title": string, # Article title
"date": string, # ISO 8601 formatted date
"author": string, # Article author
"excerpt": string, # Short preview of the article
"body": string, # Full article text (can include markdown)
}
```
## Basic Example
Here's a simple example that returns sample news articles:
```python
@register_widget({
"name": "Sample News Feed",
"description": "A simple newsfeed widget with example articles",
"type": "newsfeed",
"endpoint": "sample_newsfeed",
"gridData": {
"w": 40,
"h": 20
},
"source": "example",
"params": [
{
"paramName": "category",
"label": "Category",
"description": "Filter news by category",
"type": "text",
"value": "all",
"options": [
{"label": "All", "value": "all"},
{"label": "Technology", "value": "tech"},
{"label": "Business", "value": "business"},
{"label": "Science", "value": "science"}
]
},
{
"paramName": "limit",
"label": "Number of Articles",
"description": "Maximum number of articles to display",
"type": "number",
"value": "5"
}
]
})
@app.get("/sample_newsfeed")
def get_sample_newsfeed(category: str = "all", limit: int = 5):
"""Returns sample news articles in the required newsfeed format"""
# Sample news data organized by category
sample_articles = {
"tech": [
{
"title": "AI Breakthrough: New Model Achieves Human-Level Reasoning",
"date": (datetime.now() - timedelta(hours=2)).isoformat(),
"author": "Sarah Johnson",
"excerpt": "Researchers at TechLab have unveiled a groundbreaking AI model that demonstrates unprecedented reasoning capabilities...",
"body": """# AI Breakthrough: New Model Achieves Human-Level Reasoning
Researchers at TechLab have unveiled a groundbreaking AI model that demonstrates unprecedented reasoning capabilities, marking a significant milestone in artificial intelligence development.
## Key Features
- **Advanced reasoning**: The model can solve complex logical problems
- **Multimodal understanding**: Processes text, images, and audio simultaneously
- **Energy efficient**: Uses 40% less computational resources than previous models
The implications of this breakthrough extend across multiple industries, from healthcare to education, promising to revolutionize how we interact with AI systems."""
},
{
"title": "Quantum Computing Startup Raises $500M in Series C Funding",
"date": (datetime.now() - timedelta(hours=5)).isoformat(),
"author": "Michael Chen",
"excerpt": "QuantumLeap Technologies secures major funding round to accelerate development of commercial quantum processors...",
"body": """# Quantum Computing Startup Raises $500M in Series C Funding
QuantumLeap Technologies announced today that it has secured $500 million in Series C funding, led by prominent venture capital firms.
The company plans to use the funding to:
1. Scale manufacturing capabilities
2. Expand research team by 200 engineers
3. Develop partnerships with major cloud providers
CEO Jane Smith stated, "This investment validates our approach to making quantum computing accessible to enterprises worldwide." """
}
],
"business": [
{
"title": "Global Markets Rally on Positive Economic Data",
"date": (datetime.now() - timedelta(hours=1)).isoformat(),
"author": "Robert Williams",
"excerpt": "Stock markets across the globe surged today following the release of better-than-expected employment figures...",
"body": """# Global Markets Rally on Positive Economic Data
Stock markets worldwide experienced significant gains today as investors responded positively to robust employment data and inflation reports.
## Market Performance
- S&P 500: +2.3%
- NASDAQ: +2.8%
- FTSE 100: +1.9%
- Nikkei 225: +2.1%
Analysts attribute the rally to renewed confidence in economic recovery and expectations of stable monetary policy."""
}
],
"science": [
{
"title": "Scientists Discover New Earth-like Exoplanet in Habitable Zone",
"date": (datetime.now() - timedelta(hours=3)).isoformat(),
"author": "Dr. Emily Rogers",
"excerpt": "Astronomers using the James Webb Space Telescope have identified a potentially habitable exoplanet just 40 light-years away...",
"body": """# Scientists Discover New Earth-like Exoplanet in Habitable Zone
A team of international astronomers has announced the discovery of an Earth-like exoplanet orbiting within the habitable zone of its star system.
## Planet Characteristics
- **Size**: 1.2 times Earth's radius
- **Orbital period**: 385 days
- **Surface temperature**: Estimated 15°C average
- **Atmosphere**: Preliminary data suggests presence of water vapor
The discovery opens new possibilities for studying potentially habitable worlds beyond our solar system."""
}
]
}
# Collect articles based on category
if category == "all":
# Combine all categories
all_articles = []
for cat_articles in sample_articles.values():
all_articles.extend(cat_articles)
articles = all_articles
else:
# Get specific category or empty list if category doesn't exist
articles = sample_articles.get(category, [])
# Sort by date (newest first) and limit results
articles.sort(key=lambda x: x["date"], reverse=True)
articles = articles[:limit]
# Add some variety with random additional recent articles if needed
if len(articles) < limit and category == "all":
# Generate some generic filler articles
for i in range(limit - len(articles)):
articles.append({
"title": f"Breaking News: Important Update #{i+1}",
"date": (datetime.now() - timedelta(hours=8+i)).isoformat(),
"author": "News Team",
"excerpt": f"This is a sample news article demonstrating the newsfeed widget functionality...",
"body": f"""# Breaking News: Important Update #{i+1}
This is a sample article created to demonstrate the newsfeed widget's ability to display multiple articles.
## Summary
The newsfeed widget can display articles with:
- Rich markdown formatting
- Timestamps and author information
- Excerpts for quick preview
- Full article body with detailed content
Stay tuned for more updates!"""
})
return articles
```
## Live API Example with CoinDesk
Here's an example that fetches real news from CoinDesk's API:
```python
from typing import Optional, List, TypedDict
from datetime import datetime
import requests
class TransformedArticle(TypedDict):
title: str
date: str
author: str
excerpt: str
body: str
@register_widget({
"name": "CoinDesk News",
"description": "Get the latest crypto news from CoinDesk",
"type": "newsfeed",
"endpoint": "coindesk_news",
"gridData": {
"w": 40,
"h": 20
},
"source": "coindesk",
"params": [
{
"paramName": "limit",
"label": "Number of Articles",
"description": "The number of news articles to fetch",
"type": "number",
"value": "10"
},
{
"paramName": "lang",
"label": "Language",
"description": "The language of the news articles",
"type": "text",
"value": "EN",
"options": [
{"label": "English", "value": "EN"},
{"label": "Spanish", "value": "ES"}
]
},
{
"paramName": "categories",
"label": "Categories",
"description": "Filter by news categories",
"type": "text",
"value": ""
}
]
})
@app.get("/coindesk_news")
def get_coindesk_news(limit: str = "10", lang: str = "EN", categories: Optional[str] = None):
"""Get news from CoinDesk API"""
try:
url = f"https://data-api.coindesk.com/news/v1/article/list?lang={lang}&limit={limit}"
if categories:
url += f"&categories={categories}"
response = requests.get(url)
if response.status_code != 200:
raise HTTPException(
status_code=response.status_code,
detail=f"Failed to fetch news: {response.reason}"
)
data = response.json()
articles = []
for article in data.get("Data", []):
# Convert UNIX timestamp to ISO format
date = datetime.fromtimestamp(article["PUBLISHED_ON"]).isoformat()
# Create excerpt from body (first 150 characters)
body = article["BODY"]
excerpt = f"{body[:150]}..." if len(body) > 150 else body
articles.append({
"title": article["TITLE"],
"date": date,
"author": article["AUTHORS"],
"excerpt": excerpt,
"body": body
})
return articles
except Exception as e:
return JSONResponse(
content={"error": f"Failed to fetch news: {str(e)}"},
status_code=500
)
```
---
---
title: Omni
sidebar_position: 12
description: Learn how to create versatile Omni widgets for OpenBB Workspace that can dynamically return different content types based on input parameters.
keywords:
- omni widget
- dynamic content
- POST request
- multi-format output
- widget configuration
- citations
- flexible widgets
- OpenBB Workspace
- widget development
---
import HeadTitle from '@site/src/components/General/HeadTitle.tsx';
```python
from pydantic import BaseModel, Field
from typing import Any, List, Literal
from uuid import UUID
from fastapi import FastAPI, Body, Query
import json
class DataFormat(BaseModel):
data_type: str
parse_as: Literal["text", "table", "chart"]
class ExtraCitation(BaseModel):
source_info: SourceInfo | None = Field(default=None)
details: List[dict] | None = Field(default=None)
class OmniWidgetResponse(BaseModel):
content: Any
data_format: DataFormat
extra_citations: list[ExtraCitation] | None = Field(default_factory=list)
citable: bool = Field(default=True)
@register_widget({
"name": "Omni Widget Example",
"description": "A versatile omni widget that can display multiple types of content",
"category": "General",
"type": "omni",
"endpoint": "omni-widget",
"params": [
{
"paramName": "prompt",
"type": "text",
"description": "The prompt to send to the LLM to make queries or ask questions.",
"label": "Prompt",
"show": False
},
{
"paramName": "type",
"type": "text",
"description": "Type of content to return",
"label": "Content Type",
"show": True,
"options": [
{"value": "markdown", "label": "Markdown"},
{"value": "chart", "label": "Chart"},
{"value": "table", "label": "Table"}
]
}
],
"gridData": {"w": 30, "h": 12}
})
@app.post("/omni-widget")
async def get_omni_widget(
data: str | dict = Body(...)
):
if isinstance(data, str):
data = json.loads(data)
# Return table format
if data.get("type") == "table":
content = [
{"col1": "value1", "col2": "value2", "col3": "value3", "col4": "value4"},
{"col1": "value5", "col2": "value6", "col3": "value7", "col4": "value8"},
{"col1": "value9", "col2": "value10", "col3": "value11", "col4": "value12"},
]
return OmniWidgetResponse(
content=content,
data_format=DataFormat(data_type="object", parse_as="table")
)
# Return chart format
if data.get("type") == "chart":
content = {
"data": [
{"x": [1, 2, 3], "y": [4, 1, 2], "type": "bar"},
{"x": [1, 2, 3], "y": [2, 4, 5], "type": "bar"},
{"x": [1, 2, 3], "y": [2, 3, 6], "type": "bar"},
],
"layout": {
"title": "Dynamic Chart",
"template": "plotly_dark"
},
}
return OmniWidgetResponse(
content=content,
data_format=DataFormat(data_type="object", parse_as="chart")
)
# Return markdown format (default)
content = f"""### Dynamic Omni Widget Response
**Input Parameters:**
- **Search Query:** `{search}`
- **Content Type:** `{data.get('type', 'markdown')}`
- **Prompt:** `{data.get('prompt', 'No prompt provided')}`
#### Raw Data:
{json.dumps(data, indent=2)}
"""
return OmniWidgetResponse(
content=content,
data_format=DataFormat(data_type="object", parse_as="text")
)
```
## Omni Widget with Citations
This example demonstrates how to add citation support to your Omni widget, which is useful when you want to use the widget in conjunction with an agent. The citations are added to the response if the `citable` parameter is set to `True`. This is shown in the example below and returned when the user is interacting with the widget through an agent.
```python
@register_widget({
"name": "Omni Widget with Citations",
"description": "An omni widget that includes citation information",
"category": "General",
"type": "omni",
"endpoint": "omni-widget-citations",
"params": [
{
"paramName": "prompt",
"type": "text",
"description": "The prompt to send to the LLM to make queries or ask questions.",
"label": "Prompt",
"show": True
},
{
"paramName": "include_metadata",
"type": "boolean",
"description": "Include metadata in response",
"label": "Include Metadata",
"show": True,
"value": True
}
],
"gridData": {"w": 30, "h": 15}
})
@app.post("/omni-widget-citations")
async def get_omni_widget_with_citations(
data: str | dict = Body(...)
):
if isinstance(data, str):
data = json.loads(data)
# Create citation information
source_info = SourceInfo(
type="widget",
widget_id="omni_widget_citations",
origin="custom_backend",
name="Omni Widget with Citations",
description="Example widget demonstrating citation functionality",
metadata={
"prompt": data.get("prompt", ""),
"search_term": search,
"timestamp": "2024-01-01T00:00:00Z",
"data_source": "Custom API"
}
)
extra_citation = ExtraCitation(
source_info=source_info,
details=[
{
"Source": "Custom Backend API",
"Prompt": data.get("prompt", ""),
"Search": search,
"Metadata_Included": data.get("include_metadata", False),
"Response_Type": "Dynamic Content"
}
]
)
# Generate content based on parameters
content = f"""# Query Results
**Search Query:** {search}
**User Prompt:** {data.get('prompt', 'No prompt provided')}
## Results
This is dynamically generated content based on your input parameters.
### Metadata
"""
if data.get("include_metadata"):
content += f"""
- **Widget ID:** omni_widget_citations
- **Timestamp:** 2024-01-01T00:00:00Z
- **Data Source:** Custom API
- **Parameters:** {json.dumps(data, indent=2)}
"""
else:
content += "Metadata hidden (set 'Include Metadata' to true to view)"
return OmniWidgetResponse(
content=content,
data_format=DataFormat(data_type="object", parse_as="text"),
extra_citations=[extra_citation],
citable=True
)
```
## Important Implementation Notes
### POST Request Method
Unlike other widget types that use GET requests, the Omni widget uses POST requests. This allows for more complex parameter handling and larger payloads:
```python
@app.post("/omni-widget") # Note: POST, not GET
async def omni_endpoint(
data: str | dict = Body(...) # Main parameters in request body
):
# Handle both string and dict formats
if isinstance(data, str):
data = json.loads(data)
# All widget parameters are available in the 'data' object
param_value = data.get("paramName")
```
### Dynamic Output Control
The Omni widget can return different content types based on the parse_as field in the DataFormat:
"text": For markdown/text content
"table": For tabular data (list of dictionaries)
"chart": For Plotly chart objects
```python
# Text/Markdown output
return OmniWidgetResponse(
content="# Markdown content",
data_format=DataFormat(data_type="object", parse_as="text")
)
```
```python
# Table output
return OmniWidgetResponse(
content=[{"col1": "val1", "col2": "val2"}],
data_format=DataFormat(data_type="object", parse_as="table")
)
```
```python
# Chart output
return OmniWidgetResponse(
content={"data": [...], "layout": {...}},
data_format=DataFormat(data_type="object", parse_as="chart")
)
```
### Parameter Handling
All widget parameters defined in the widget configuration are passed in the POST request body, and the `prompt` parameter is required:
```json
{
"params": [
{
// Required parameter for the LLM to make queries or ask questions
"paramName": "prompt",
"type": "text",
"description": "The prompt to send to the LLM to make queries or ask questions.",
"label": "Prompt",
"show": False
}
{
"paramName": "user_input",
"type": "text",
"label": "User Input"
},
{
"paramName": "option_select",
"type": "text",
"options": [...]
}
]
}
```
These parameters are accessible in your endpoint:
```python
@app.post("/omni-widget")
async def omni_endpoint(data: dict = Body(...)):
user_input = data.get("user_input")
selected_option = data.get("option_select")
prompt = data.get("prompt")
# Process parameters...
```
## Use Cases
The Omni widget is particularly useful for:
- AI/LLM Integration: Dynamic content generation based on user prompts
- Multi-format Data Display: Single endpoint that can return different visualizations
- Citation-heavy Applications: Research tools that need to track data sources.
---
---
title: Plotly Charts
sidebar_position: 9
description: Plotly Charts
keywords:
- plotly
- charts
- visualization
---
import HeadTitle from '@site/src/components/General/HeadTitle.tsx';
```python
@register_widget({
"name": "Plotly Chart",
"description": "Plotly chart",
"type": "chart",
"endpoint": "plotly_chart",
"gridData": {"w": 40, "h": 15}
})
@app.get("/plotly_chart")
def get_plotly_chart():
# Generate mock time series data
mock_data = [
{"date": "2023-01-01", "return": 2.5, "transactions": 1250},
{"date": "2023-01-02", "return": -1.2, "transactions": 1580},
{"date": "2023-01-03", "return": 3.1, "transactions": 1820},
{"date": "2023-01-04", "return": 0.8, "transactions": 1450},
{"date": "2023-01-05", "return": -2.3, "transactions": 1650},
{"date": "2023-01-06", "return": 1.5, "transactions": 1550},
{"date": "2023-01-07", "return": 2.8, "transactions": 1780},
{"date": "2023-01-08", "return": -0.9, "transactions": 1620},
{"date": "2023-01-09", "return": 1.2, "transactions": 1480},
{"date": "2023-01-10", "return": 3.5, "transactions": 1920}
]
dates = [datetime.strptime(d["date"], "%Y-%m-%d") for d in mock_data]
returns = [d["return"] for d in mock_data]
transactions = [d["transactions"] for d in mock_data]
# Create the figure with secondary y-axis
fig = go.Figure()
# Add the line trace for returns
fig.add_trace(go.Scatter(
x=dates,
y=returns,
mode='lines',
name='Returns',
line=dict(width=2)
))
# Add the bar trace for transactions
fig.add_trace(go.Bar(
x=dates,
y=transactions,
name='Transactions',
opacity=0.5
))
# Update layout with axis titles and secondary y-axis
fig.update_layout(
xaxis_title='Date',
yaxis_title='Returns (%)',
yaxis2=dict(
title="Transactions",
overlaying="y",
side="right"
),
legend=dict(
orientation="h",
yanchor="bottom",
y=1.02,
xanchor="right",
x=1
)
)
# Update the bar trace to use secondary y-axis
fig.data[1].update(yaxis="y2")
return json.loads(fig.to_json())
```
## Plotly Chart with Theme
This endpoint extends the basic Plotly chart by adding theme support. The theme parameter is automatically provided by OpenBB Workspace based on the user's current display mode (dark/light). This enables dynamic chart styling that matches the workspace theme. The theme parameter is optional - if unused, OpenBB will still pass it but the endpoint will ignore it.
Note: OpenBB widget UI dark mode is #151518 and light mode is #FFFFFF, using these background colors make the chart look consistent with the widgets in the OpenBB Workspace.
```python
@register_widget({
"name": "Plotly Chart with Theme",
"description": "Plotly chart with theme",
"type": "chart",
"endpoint": "plotly_chart_with_theme",
"gridData": {"w": 40, "h": 15}
})
@app.get("/plotly_chart_with_theme")
def get_plotly_chart_with_theme(theme: str = "dark"):
# Generate mock time series data
mock_data = [
{"date": "2023-01-01", "return": 2.5, "transactions": 1250},
{"date": "2023-01-02", "return": -1.2, "transactions": 1580},
{"date": "2023-01-03", "return": 3.1, "transactions": 1820},
{"date": "2023-01-04", "return": 0.8, "transactions": 1450},
{"date": "2023-01-05", "return": -2.3, "transactions": 1650},
{"date": "2023-01-06", "return": 1.5, "transactions": 1550},
{"date": "2023-01-07", "return": 2.8, "transactions": 1780},
{"date": "2023-01-08", "return": -0.9, "transactions": 1620},
{"date": "2023-01-09", "return": 1.2, "transactions": 1480},
{"date": "2023-01-10", "return": 3.5, "transactions": 1920}
]
dates = [datetime.strptime(d["date"], "%Y-%m-%d") for d in mock_data]
returns = [d["return"] for d in mock_data]
transactions = [d["transactions"] for d in mock_data]
# Create the figure with secondary y-axis
fig = go.Figure()
if theme == "dark":
# Dark theme colors and styling
line_color = "#FF8000" # Orange
bar_color = "#2D9BF0" # Blue
text_color = "#FFFFFF" # White
grid_color = "rgba(51, 51, 51, 0.3)"
bg_color = "#151518" # Dark background
else:
# Light theme colors and styling
line_color = "#2E5090" # Navy blue
bar_color = "#00AA44" # Forest green
text_color = "#333333" # Dark gray
grid_color = "rgba(221, 221, 221, 0.3)"
bg_color = "#FFFFFF" # White background
# Add the line trace for returns with theme-specific color
fig.add_trace(go.Scatter(
x=dates,
y=returns,
mode='lines',
name='Returns',
line=dict(width=2, color=line_color)
))
# Add the bar trace for transactions with theme-specific color
fig.add_trace(go.Bar(
x=dates,
y=transactions,
name='Transactions',
opacity=0.5,
marker_color=bar_color
))
# Update layout with theme-specific styling
fig.update_layout(
xaxis_title='Date',
yaxis_title='Returns (%)',
yaxis2=dict(
title="Transactions",
overlaying="y",
side="right",
gridcolor=grid_color,
tickfont=dict(color=text_color)
),
legend=dict(
orientation="h",
yanchor="bottom",
y=1.02,
xanchor="right",
x=1,
font=dict(color=text_color)
),
paper_bgcolor=bg_color,
plot_bgcolor=bg_color,
font=dict(color=text_color),
xaxis=dict(
gridcolor=grid_color,
tickfont=dict(color=text_color)
),
yaxis=dict(
gridcolor=grid_color,
tickfont=dict(color=text_color)
)
)
# Update the bar trace to use secondary y-axis
fig.data[1].update(yaxis="y2")
return json.loads(fig.to_json())
```
## Plotly Chart with Theme and Toolbar
This endpoint extends the basic Plotly chart by adding a toolbar to the chart. The toolbar is a set of buttons that allows the user to interact with the chart.
Note: As you can see, all the settings and styling utilized by plotly can be too much boilerplate code, so it is recommended to create a plotly_config.py file and use the functions defined in that file to create the chart.
```python
@register_widget({
"name": "Plotly Chart with Theme and Toolbar",
"description": "Plotly chart with Theme and toolbar",
"type": "chart",
"endpoint": "plotly_chart_with_theme_and_toolbar",
"gridData": {"w": 40, "h": 15}
})
@app.get("/plotly_chart_with_theme_and_toolbar")
def get_plotly_chart_with_theme_and_toolbar(theme: str = "dark"):
# Generate mock time series data
mock_data = [
{"date": "2023-01-01", "return": 2.5, "transactions": 1250},
{"date": "2023-01-02", "return": -1.2, "transactions": 1580},
{"date": "2023-01-03", "return": 3.1, "transactions": 1820},
{"date": "2023-01-04", "return": 0.8, "transactions": 1450},
{"date": "2023-01-05", "return": -2.3, "transactions": 1650},
{"date": "2023-01-06", "return": 1.5, "transactions": 1550},
{"date": "2023-01-07", "return": 2.8, "transactions": 1780},
{"date": "2023-01-08", "return": -0.9, "transactions": 1620},
{"date": "2023-01-09", "return": 1.2, "transactions": 1480},
{"date": "2023-01-10", "return": 3.5, "transactions": 1920}
]
dates = [datetime.strptime(d["date"], "%Y-%m-%d") for d in mock_data]
returns = [d["return"] for d in mock_data]
transactions = [d["transactions"] for d in mock_data]
# Create the figure with secondary y-axis
fig = go.Figure()
if theme == "dark":
# Dark theme colors and styling
line_color = "#FF8000" # Orange
bar_color = "#2D9BF0" # Blue
text_color = "#FFFFFF" # White
grid_color = "rgba(51, 51, 51, 0.3)"
bg_color = "#151518" # Dark background
else:
# Light theme colors and styling
line_color = "#2E5090" # Navy blue
bar_color = "#00AA44" # Forest green
text_color = "#333333" # Dark gray
grid_color = "rgba(221, 221, 221, 0.3)"
bg_color = "#FFFFFF" # White background
# Add the line trace for returns with theme-specific color
fig.add_trace(go.Scatter(
x=dates,
y=returns,
mode='lines',
name='Returns',
line=dict(width=2, color=line_color)
))
# Add the bar trace for transactions with theme-specific color
fig.add_trace(go.Bar(
x=dates,
y=transactions,
name='Transactions',
opacity=0.5,
marker_color=bar_color
))
# Update layout with theme-specific styling
fig.update_layout(
xaxis_title='Date',
yaxis_title='Returns (%)',
yaxis2=dict(
title="Transactions",
overlaying="y",
side="right",
gridcolor=grid_color,
tickfont=dict(color=text_color)
),
legend=dict(
orientation="h",
yanchor="bottom",
y=1.02,
xanchor="right",
x=1,
font=dict(color=text_color)
),
paper_bgcolor=bg_color,
plot_bgcolor=bg_color,
font=dict(color=text_color),
xaxis=dict(
gridcolor=grid_color,
tickfont=dict(color=text_color)
),
yaxis=dict(
gridcolor=grid_color,
tickfont=dict(color=text_color)
)
)
# Update the bar trace to use secondary y-axis
fig.data[1].update(yaxis="y2")
# Configure the toolbar and other display settings
toolbar_config = {
'displayModeBar': True,
'responsive': True,
'scrollZoom': True,
'modeBarButtonsToRemove': [
'lasso2d',
'select2d',
'autoScale2d',
'toggleSpikelines',
'hoverClosestCartesian',
'hoverCompareCartesian'
],
'modeBarButtonsToAdd': [
'drawline',
'drawcircle',
'drawrect',
'eraseshape'
],
'doubleClick': 'reset+autosize',
'showTips': True,
'watermark': False,
'staticPlot': False,
'locale': 'en',
'showAxisDragHandles': True,
'showAxisRangeEntryBoxes': True,
'displaylogo': False,
'modeBar': {
'bgcolor': 'rgba(0, 0, 0, 0.1)' if theme == 'light' else 'rgba(255, 255, 255, 0.1)',
'color': text_color,
'activecolor': line_color,
'orientation': 'v',
'yanchor': 'top',
'xanchor': 'right',
'x': 1.05, # Increased spacing from chart
'y': 1,
'opacity': 0, # Start hidden
'hovermode': True, # Show on hover
'hoverdelay': 0, # No delay on hover
'hoverduration': 0 # No delay on hover out
}
}
# Convert figure to JSON and add config
figure_json = json.loads(fig.to_json())
figure_json['config'] = toolbar_config
return figure_json
```
## Plotly Chart with Theme and Config File
This widget demonstrates how to create a chart using the Plotly library and use the config file to minimize the amount of code needed to create the chart.
For reference, here's where this particular `plotly_config.py` was used: https://github.com/OpenBB-finance/backend-examples-for-openbb-workspace/blob/main/getting-started/reference-backend/plotly_config.py
```python
@register_widget({
"name": "Plotly Chart with Theme and Toolbar using Config File",
"description": "Plotly chart with theme and toolbar using config file",
"type": "chart",
"endpoint": "plotly_chart_with_theme_and_toolbar_using_config_file",
"gridData": {"w": 40, "h": 15}
})
@app.get("/plotly_chart_with_theme_and_toolbar_using_config_file")
def get_plotly_chart_with_theme_and_toolbar_using_config_file(theme: str = "dark"):
# Generate mock time series data
mock_data = [
{"date": "2023-01-01", "return": 2.5, "transactions": 1250},
{"date": "2023-01-02", "return": -1.2, "transactions": 1580},
{"date": "2023-01-03", "return": 3.1, "transactions": 1820},
{"date": "2023-01-04", "return": 0.8, "transactions": 1450},
{"date": "2023-01-05", "return": -2.3, "transactions": 1650},
{"date": "2023-01-06", "return": 1.5, "transactions": 1550},
{"date": "2023-01-07", "return": 2.8, "transactions": 1780},
{"date": "2023-01-08", "return": -0.9, "transactions": 1620},
{"date": "2023-01-09", "return": 1.2, "transactions": 1480},
{"date": "2023-01-10", "return": 3.5, "transactions": 1920}
]
dates = [datetime.strptime(d["date"], "%Y-%m-%d") for d in mock_data]
returns = [d["return"] for d in mock_data]
transactions = [d["transactions"] for d in mock_data]
# Get theme colors
colors = get_theme_colors(theme)
# Create the figure
fig = go.Figure()
# Add the line trace for returns
fig.add_trace(go.Scatter(
x=dates,
y=returns,
mode='lines',
name='Returns',
line=dict(width=2, color=colors["main_line"])
))
# Add the bar trace for transactions
fig.add_trace(go.Bar(
x=dates,
y=transactions,
name='Transactions',
opacity=0.5,
marker_color=colors["neutral"]
))
fig.update_layout(**base_layout(theme=theme))
# Add secondary y-axis for transactions
fig.update_layout(
yaxis2=dict(
title="Transactions",
overlaying="y",
side="right",
gridcolor=colors["grid"],
tickfont=dict(color=colors["text"])
)
)
# Update the bar trace to use secondary y-axis
fig.data[1].update(yaxis="y2")
figure_json = json.loads(fig.to_json())
figure_json['config'] = get_toolbar_config()
return figure_json
```
## Plotly Heatmap
This widget demonstrates that with Plotly you can create any type of chart including heatmaps, scatter plots, line charts, 3d charts, etc. and also demonstrates how parameters can influence a plotly chart.
**Note that the theme parameter always comes at the end of the function.**
```python
@register_widget({
"name": "Plotly Heatmap",
"description": "Plotly heatmap",
"type": "chart",
"endpoint": "plotly_heatmap",
"gridData": {"w": 40, "h": 15},
"params": [
{
"paramName": "color_scale",
"description": "Select the color scale for the heatmap",
"value": "RdBu_r",
"label": "Color Scale",
"type": "text",
"show": True,
"options": [
{"label": "Red-Blue (RdBu_r)", "value": "RdBu_r"},
{"label": "Viridis", "value": "Viridis"},
{"label": "Plasma", "value": "Plasma"},
{"label": "Inferno", "value": "Inferno"},
{"label": "Magma", "value": "Magma"},
{"label": "Greens", "value": "Greens"},
{"label": "Blues", "value": "Blues"},
{"label": "Reds", "value": "Reds"}
]
}
]
})
@app.get("/plotly_heatmap")
def get_plotly_heatmap(color_scale: str = "RdBu_r", theme: str = "dark"):
# Create mock stock symbols
symbols = ['AAPL', 'MSFT', 'GOOGL', 'AMZN', 'TSLA']
# Create mock correlation matrix directly
corr_matrix = [
[1.00, 0.65, 0.45, 0.30, 0.20], # AAPL correlations
[0.65, 1.00, 0.55, 0.40, 0.25], # MSFT correlations
[0.45, 0.55, 1.00, 0.35, 0.15], # GOOGL correlations
[0.30, 0.40, 0.35, 1.00, 0.10], # AMZN correlations
[0.20, 0.25, 0.15, 0.10, 1.00] # TSLA correlations
]
# Get theme colors
colors = get_theme_colors(theme)
# Create the figure
fig = go.Figure()
# Apply base layout configuration
layout_config = base_layout(theme=theme)
# This allows users to modify the layout configuration further
# in case they want to steer from the default settings.
layout_config['title'] = {
'text': "Correlation Matrix",
'x': 0.5,
'y': 0.95,
'xanchor': 'center',
'yanchor': 'top',
'font': {'size': 20}
}
layout_config['margin'] = {'t': 50, 'b': 50, 'l': 50, 'r': 50}
# Update figure with complete layout
fig.update_layout(layout_config)
# Add the heatmap trace
fig.add_trace(go.Heatmap(
z=corr_matrix,
x=symbols,
y=symbols,
colorscale=color_scale,
zmid=colors["heatmap"]["zmid"],
text=[[f'{val:.2f}' for val in row] for row in corr_matrix],
texttemplate='%{text}',
textfont={"color": colors["heatmap"]["text_color"]},
hoverongaps=False,
hovertemplate='%{x} - %{y}
```python
@register_widget({
"name": "Chains Chart Example Plotly with Raw Data",
"description": "Get current TVL of all chains and plot it with Plotly",
"type": "chart",
"endpoint": "chains",
"gridData": {"w": 40, "h": 9},
"raw": true,
})
@app.get("/chains")
def get_chains(raw: bool = False, theme: str = "dark"):
"""Get current TVL of all chains using Defi Llama"""
params = {}
response = requests.get("https://api.llama.fi/v2/chains", params=params)
if response.status_code == 200:
# Create a DataFrame from the JSON data
df = pd.DataFrame(response.json())
# OPTIONAL - If raw is True, return the data as a list of dictionaries
# Otherwise, return the data as a Plotly figure
# This is useful when you want to make sure the AI can see the data
if raw:
return df.to_dict(orient="records")
# Sort the DataFrame by 'tvl' in descending order and select the top 30
top_30_df = df.sort_values(by="tvl", ascending=False).head(30)
# Get theme colors
colors = get_theme_colors(theme)
# Create a bar chart using Plotly
fig = go.Figure()
fig.add_trace(go.Bar(
x=top_30_df["tokenSymbol"],
y=top_30_df["tvl"],
marker_color=colors["main_line"],
opacity=0.8
))
# Apply base layout configuration
layout_config = base_layout(theme=theme)
layout_config.update({
'title': {
'text': "Top 30 Chains by TVL",
'x': 0.5,
'y': 0.95,
'xanchor': 'center',
'yanchor': 'top',
'font': {'size': 20}
},
'xaxis_title': "Token Symbol",
'yaxis_title': "Total Value Locked (TVL)",
'margin': {'t': 80, 'b': 80, 'l': 80, 'r': 80}
})
fig.update_layout(layout_config)
# Convert figure to JSON and apply config
figure_json = json.loads(fig.to_json())
figure_json['config'] = get_toolbar_config()
return figure_json
print(f"Request error {response.status_code}: {response.text}")
return JSONResponse(
content={"error": response.text}, status_code=response.status_code
)
```
## Additional Resources
For more information on setting up and configuring Plotly charts within the OpenBB Workspace, you can visit the [OpenBB Backend Examples for Chart Widgets](https://github.com/OpenBB-finance/backends-for-openbb/tree/main/widget-examples/widget-types/chart_widget). This resource provides detailed examples and guidance on how to effectively utilize Plotly for creating interactive and dynamic chart widgets.
---
---
title: SSRM Mode
sidebar_position: 8
description: Server-Side Rendered Mode for handling large datasets efficiently in OpenBB Workspace widgets
keywords:
- SSRM
- server-side rendering
- large datasets
- performance
- data optimization
- advanced widgets
---
import HeadTitle from '@site/src/components/General/HeadTitle.tsx';
## Enterprise Security Features
The platform integrates with existing security infrastructure including SSO support for Azure and Google authentication systems. Granular role-based access control (RBAC) manages permissions across applications, widgets, data sources, and AI features. Multi-factor authentication enforcement adds security layers for all user accounts.
Detailed audit logging captures all user activities, system access, and configuration changes. The platform operates with no external product analytics and minimal operational logging to maintain privacy and security standards.
## Compliance and Audit Support
Meet regulatory requirements through complete activity logging that tracks user actions, data access patterns, and system modifications. Export controls manage how data can be extracted or shared outside the platform. Retention policies automate data lifecycle management according to organizational and regulatory requirements.
SOC 2 Type II compliance support provides standardized security controls and audit procedures. The platform maintains audit trails that support various regulatory frameworks and compliance requirements.
Installing as a PWA offers several advantages:
- **App-like Experience**: Runs in its own window without browser UI, providing a cleaner, focused workspace
- **Quick Access**: Launch directly from your dock, taskbar, or home screen without opening a browser
- **Cross-Platform**: Consistent experience across desktop and mobile devices
- **Seamless Updates**: Automatically receives the latest features when you're online
- **Better Performance**: Cached resources provide faster load times for frequently used components
## Desktop Installation
The desktop PWA provides the most complete OpenBB experience, with full keyboard support and optimized screen real estate.
### Installation Steps
1. Launch Chrome, Edge, or any Chromium-based browser
2. Navigate to [https://pro.openbb.co](https://pro.openbb.co)
3. Click the install icon in the address bar (typically a plus sign or computer icon)
4. Follow the installation prompts to add OpenBB to your desktop
## Mobile Installation
### iOS Devices
1. Open Safari and navigate to [https://pro.openbb.co](https://pro.openbb.co)
2. Tap the share button (square with upward arrow) at the bottom of the screen
3. Select "Add to Home Screen" from the share menu
4. Name the app "OpenBB" and tap "Add"
5. The OpenBB icon will appear on your home screen
### Android Devices
1. Open Chrome and navigate to [https://pro.openbb.co](https://pro.openbb.co)
2. Tap the three-dot menu in the top-right corner
3. Select "Add to Home screen" from the menu
4. Name the app "OpenBB" and tap "Add"
5. The OpenBB icon will appear on your home screen
## Using OpenBB PWA
The PWA version of OpenBB Workspace provides a consistent experience across all your devices while maintaining full functionality. Key features include:
- **Responsive Design**: Optimized layouts for desktop, tablet, and mobile screens
- **Touch Support**: Intuitive touch controls for mobile devices
- **Offline Capabilities**: Access key features without an internet connection
- **Cross-Device Sync**: Seamlessly continue your work across different devices
### Best Practices
- Use Chrome or Edge for the best desktop experience
- Keep your browser updated to the latest version
- Ensure you have sufficient storage space on your device
- Connect to a stable internet connection for initial setup
### Troubleshooting
If you encounter any issues during installation:
1. Clear your browser cache and try again
2. Ensure you're using a supported browser version
3. Check your device's storage space
4. Verify your internet connection
## Learn More
For a deeper dive into the OpenBB PWA, including its development journey and technical implementation, read our detailed blog post: [OpenBB Workspace is Now Available on Mobile](https://openbb.co/blog/openbb-terminal-is-now-available-on-mobile)
---
---
title: Workspace Overview
sidebar_position: 0
description: OpenBB Workspace is a secure enterprise UI application for AI workflows, featuring data integration, AI model deployment, flexible UI customization, and on-premises deployment capabilities.
keywords:
- enterprise AI application
- data integration
- AI model deployment
- flexible UI
- on-premises deployment
- secure application
- team collaboration
- OpenBB Apps
- proprietary data
- licensed data
- AI workflows
- enterprise security
- private cloud
- data privacy
---
import HeadTitle from '@site/src/components/General/HeadTitle.tsx';
### Dashboards
Dashboards are your personal spaces in OpenBB Workspace, designed to adapt to your unique analytical style and requirements. Each dashboard starts as a blank canvas where you can create your perfect analytical environment by combining elements from your entire widget library, organizing and customizing them in ways that make sense for your specific workflows.
The power of dashboards lies in their flexibility. You can add widgets and group them by linking their parameters - when you update a date range or a ticker symbol in one widget, related widgets will update automatically. This parameter grouping ensures your analysis remains synchronized across multiple data views.
Beyond dynamic data widgets, dashboards support a rich variety of content types. You can enhance your analysis by adding static files like PDFs, images, text documents, and spreadsheets, incorporating AI-generated artifacts directly from your chats, or writing notes to document your thought process and findings. This combination of dynamic data, static resources, and personal annotations creates a wide-ranging analytical workspace that captures both the quantitative and qualitative aspects of your research.
Once you've crafted your perfect dashboard, sharing it with your organization is effortless. Team members can access your shared dashboards to benefit from your analytical setup. This transforms individual insights into collective intelligence, elevating your dashboards to become organizational assets and fostering collaboration and knowledge sharing.
### AI Agents
AI Agents make OpenBB Workspace an intelligent system that understands your data and automates complex workflows. These agents leverage the metadata from your widgets to query the right datasets with appropriate parameters.
What makes AI Agents particularly powerful is their contextual awareness. They can access your entire dashboard ecosystem, understand relationships between different data sources and maintain context across multiple queries. This enables sophisticated multi-step analysis where agents can gather data from various widgets, perform calculations, generate insights, and even create new visualizations based on their findings.
AI Agents excel at both reactive and proactive analysis. They can respond to your specific queries about market conditions, company performance, or economic indicators while also monitoring your dashboards for anomalies, trends, or opportunities that require attention. You can configure agents with custom instructions and prompts, creating specialized assistants for different analytical tasks – from earnings analysis to risk assessment to market surveillance.
The integration between AI Agents and your widget library creates a multiplier effect. Agents can dynamically combine data from multiple sources to apply advanced analytical techniques, and present results in formats that best suit your needs.
Finally, AI agents can produce artifacts (text, tables, charts) that are added back to the dashboard, closing the feedback loop.
### Apps
Apps are pre-built dashboard templates designed for specific workflows. Each app includes a curated set of widgets with parameters already linked, a pre-selection of an AI agent for the task at hand, and custom prompts relevant to that use case. When you change a ticker or date range, all connected widgets update together.
Examples include portfolio management apps with position tracking and risk metrics, market surveillance apps with data monitoring and anomaly detection, and research apps with fundamental analysis and news sentiment tools. The workspace supports unlimited apps that can be shared across teams and customized to fit your exact needs.
Explore our app gallery and use cases at [openbb.co/solutions](https://openbb.co/solutions).
#### Prompts: Context-Aware Suggestions
Prompts are query suggestions specific to each app. The AI agent knows your widget library and can automatically tag relevant widgets when responding to prompts. For example, in a portfolio app, prompts might suggest "Show me today's top performers" and automatically reference the position tracking and performance widgets. This context awareness means prompts always work with the actual data available in your app, eliminating guesswork and ensuring accurate, relevant responses.