PythonLangGraphReactVite

Rendering Interactive Charts with LangGraph and React

Ryan EgglestonRyan Eggleston
Rendering Interactive Charts with LangGraph and React

Building AI agents that can do more than just talk is the new frontier. At Enso Labs, we're building DeepAgents—composed AI workflows that interact with complex tools and data.

One common challenge developers face is displaying rich data, like interactive charts, within a chat interface. LLMs are great at text, but they can't natively generate a Plotly chart or a React component.

In this post, we'll explore how we solved this using LangGraph's content_and_artifact feature and a custom React renderer.

The Challenge: Text vs. Visuals

When an agent analyzes stock prices, it might retrieve a CSV of historical data.

  • The LLM needs this data in text format (like CSV) to reason about it ("The price went up on Tuesday").
  • The User wants to see a line chart, not a wall of numbers.

We need a way to send two different representations of the same result: one for the AI, and one for the UI.

The Solution: content_and_artifact

LangChain and LangGraph introduced a tool feature called content_and_artifact (see LangChain Tools docs). This allows a tool to return a tuple: (content, artifact).

  • content: The text representation for the model (e.g., CSV).
  • artifact: Arbitrary data for the system/UI (e.g., a JSON object representing a Plotly chart).

Installation

Before diving in, let’s install the required dependencies for both backend (Python) and frontend (React):

Backend (Python) Dependencies

You'll need yfinance, plotly, and LangGraph/LangChain Core packages. If you use uv:

uv pip install yfinance plotly langchain

(You may need to add any other packages your project requires)

Frontend (React) Dependencies

For rendering Plotly charts in React, you’ll need:

npm install plotly.js-dist-min react-plotly.js

If you don’t have React/TypeScript set up, refer to your local setup or try Vite.


1. The Backend: Python Tool

Here is our implementation of a stock price tool. We use yfinance to get data and plotly.express to generate the chart.

import yfinance as yf
from langchain.tools import tool
import plotly.express as px

@tool(response_format="content_and_artifact")
def get_stock_price_history(ticker: str, period: str = "1mo") -> str:
    """Get the stock price history and return a Plotly chart."""

    # 1. Fetch data
    ticker_obj = yf.Ticker(ticker)
    hist = ticker_obj.history(period=period)
    hist = hist.reset_index()

    # 2. Create Plotly figure
    fig = px.line(
        hist,
        x="Date",
        y="Close",
        title=f"{ticker} Closing Price"
    )

    # 3. Return Tuple: (CSV for LLM, JSON for UI)
    return hist.to_csv(index=False), fig.to_json()

Notice the response_format="content_and_artifact" in the decorator. This tells LangGraph to handle the return value as a tuple.


2. The Frontend: React Renderer

On the frontend, we receive the tool execution result. When the artifact contains a Plotly JSON structure, we pass it to our ChartRenderWidget.

Here is the React component that renders the chart:

import Plotly from "plotly.js-dist-min";
import createPlotlyComponent from "react-plotly.js/factory";
import { useEffect, useState } from "react";

const Plot = createPlotlyComponent(Plotly);

const ChartRenderWidget = ({ content }: { content: any }) => {
  const [plotData, setPlotData] = useState<any[]>([]);
  const [layout, setLayout] = useState<any>({});

  useEffect(() => {
    try {
      // Parse the JSON artifact from the backend
      const parsedContent = JSON.parse(content);
      setPlotData(parsedContent?.data || []);
      setLayout(parsedContent?.layout || {});
    } catch (e) {
      console.error("Failed to parse chart data", e);
    }
  }, [content]);

  return (
    <div style={{ width: "100%", height: "400px" }}>
      <Plot
        data={plotData}
        layout={{ ...layout, autosize: true }}
        useResizeHandler={false}
        style={{ width: "100%", height: "100%" }}
      />
    </div>
  );
};

export default ChartRenderWidget;

This component takes the raw JSON string generated by fig.to_json() in Python, parses it, and feeds it directly into react-plotly.js.


Why This Matters

This pattern unlocks powerful capabilities for AI Agents:

  1. Optimization: The LLM token context isn't cluttered with massive JSON objects describing chart layouts. It just sees the concise CSV.
  2. User Experience: Users get interactive, professional-grade visualizations instantly.
  3. Separation of Concerns: Your data logic stays in Python, and your rendering logic stays in React.

Try It Out

We're building Orchestra to make patterns like this easy to deploy.

Check out our code on GitHub or follow us for more updates on our Socials.

Happy coding!