Reverse-engineer the chart drawing of ChatGPT

Hello,

I have created a chat bot by Chat Completions API and ReactJS. At the moment, the chatbot asks for answsers in form of markdown from OpenAI, and renders them by react-markdown, rehype-raw, and remark-gfm, etc.

Now, I would like to enable users to draw charts as ChatGPT does:

I understand that the chatbot can ask for Plotly JSON configuration from OpenAI and render the chart by react-plotly.js. But I would like to reverse-engineer how exactly ChatGPT achieves this chart drawing.

For instance, I can set up the system such that

  1. In the system message, I can say, whenever the user wants the bot to draw a graph, please return a valid Plotly JSON configuration (e.g., enclosed by <PlotlyConfiguration> and </PlotlyConfiguration>)
  2. When rendering the message of the assistant, whenever we see <PlotlyConfiguration> and </PlotlyConfiguration>, we render it with react-plotly.js.

Do you think this is how ChatGPT achieves the chart plotting? Do you think asking Chat Completions API to enclose Plotly JSON configurations with <PlotlyConfiguration> and </PlotlyConfiguration> is a good idea? Do you think we need to use advanced features such as function calling or structured outputs, etc?

Edit 1:, I realize that ChatGPT can render several charts in one response. With function calling and structured outputs, I wrote the following code, do you think the idea is good and whether it can be optimized?

async function runRealChart() {
  try {
    const openai = new OpenAI({ apiKey: "sk-ThIdpClUNt..." });

    const draw_chart =
      {
        type: "function",
        function: {
          name: 'get_plotly_configuration_json_to_render_a_chart',
          description: "Get a Plotly configuration JSON generated by AI, based on a two-dimensional array of data provided by the user. The JSON will be used to draw a chart later in our UI.",
          parameters: {
            type: 'object',
            properties: {
              data: {
                type: 'array',
                items: {
                  type: 'array',
                  items: {
                    type: 'string'
                  }
                },
                description: 'A two-dimensional array of data.'
              }
            },
            required: ["data"],
            additionalProperties: false,
          }
        }
      }

    const response_format = {
      type: "json_schema",
      json_schema: {
        name: "response",
        strict: true,
        schema: {
          type: "object",
          properties: {
            plotly_configuration_json: {
              type: "string",
              description: "The Plotly configuration JSON for the chart."
            }
          },
          required: ["plotly_configuration_json"],
          additionalProperties: false
        }
      }
    }

    const m0 = { role: "system", content: "You are a spreadsheet expert." }
    const m1 = { role: "user", content: "I have the following data:\n\nDate\tRevenue\n2022-01-01\t100\n2022-01-02\t200\n2022-01-03\t300\n\n, could you draw a chart?\n\nI have the following data:\n\nDate\tRevenue\n2023-01-01\t1000\n2023-01-02\t2000\n2023-01-03\t3000\n\n, could you draw another chart?" }
    const messages = [m0, m1]

    const response = await openai.chat.completions.create({
      model: "gpt-4o-2024-08-06",
      messages: messages,
      tools: [draw_chart],
    });
    console.log("response.choices[0].message", response.choices[0].message)
    console.log("response.choices[0].message.content", response.choices[0].message.content)
    console.log("response.choices[0].message.tool_calls", response.choices[0].message.tool_calls)

    const configurations = [];
    for (const tool_call of response.choices[0].message.tool_calls) {
      if (tool_call.function.name === "get_plotly_configuration_json_to_render_a_chart") {
        const arguments = JSON.parse(tool_call.function.arguments).data;
        const messages2 = [
          { role: "system", content: "You are a chart expert." },
          { role: "user", content: "return a Plotly configuration JSON for the data: " + JSON.stringify(arguments) }
        ]
        const response2 = await openai.chat.completions.create({
          model: "gpt-4o-2024-08-06",
          messages: messages2,
          response_format: response_format
        });
        console.log("response2.choices[0].message", response2.choices[0].message)
        console.log("response2.choices[0].message.content", response2.choices[0].message.content)
        configurations.push({
          role: "tool",
          content: JSON.stringify({ plotly_configuration_json: JSON.parse(response2.choices[0].message.content).plotly_configuration_json }),
          tool_call_id: tool_call.id
        })
      };
    }

    console.log("configurations", configurations)
    if (configurations.length > 0) {
      const messages3 = [
        { role: "system", content: "You are a spreadsheet expert. Do NOT mention the term 'Plotly configuration JSON' in the answer because the JSON data will be rendered as charts in the UI. Enclose the Plotly configuration JSON data with <PlotConfiguration> and </PlotConfiguration>." },
        m1,
        response.choices[0].message,
        ...configurations
      ];

      console.log("messages3", messages3)

      const response3 = await openai.chat.completions.create({
        model: "gpt-4o-2024-08-06",
        messages: messages3,
      });
      console.log("response3.choices[0].message", response3.choices[0].message)
      console.log("response3.choices[0].message", response3.choices[0].message.content)
    }
  } catch (error) {
    console.error("An error occurred:", error);
  }  
}
2 Likes

i checked the chatgpt web and the graph is already an image. so perhaps function calling then output image.

but your way might work too. start from function calling, then structured output(JSON), then feed it to your display ui that can read the plotyconfig values to generate the graph dynamically.

2 Likes

Thank you for your help.

Note that the chatbot is supposed to accept other queries than graph drawing as well. Could you tell a little bit more about what tools (functions) you would define?

2 Likes

you can define any tools (e.g. create_dalle_image, draw_graph, create_table_data) then you can format your ui display like this:

[image/graph/table/etc]
[text content]

then your structured output will be like:

{
text_content: "...",
tool: {
     name: "name of tool",
     type: "...", // <-- this will determine what to display either image, graph, table, etc.
     parameters: {...}
}
}
2 Likes

My understanding too is that ChatGPT basically invokes a function call, then creates and executes a Python script on the basis of the data points / information from the conversation to generate the chart (using matplotlib or another library), and eventually returns an image of the chart.

1 Like

Sometime it makes me laugh, how many people really understand AI? Chatgpt is using or might use on of the python package, which is based on its decision but it is not really part of AI. Why you want to about the package which gpt is usng? There are tonnes of out there better. like mermaid etc. You should be only transofrming the data from gpt, which can be feed to package and rest let your system do it better in better way.

1 Like

I understand that we can use strict: true for function calling with structured outputs. And we can use response_format when we “want to indicate a structured schema for use when the model responds to the user, rather than when the model calls a tool.”

However, it seems that what you are suggesting combines a tool_call and a text_content.

I tried the following code

async function runForum() {
  try {
    const openai = new OpenAI({ apiKey: "sk-ThIdpClU..." });

    const tools = [
      {
        type: "function",
        function: {
          name: 'get_plotly_configuration_json',
          description: "Get the plotly configuration json for a chart.",
          parameters: {
            type: 'object',
            properties: {
              data: {
                type: 'array',
                items: {
                  type: 'array',
                  items: {
                    type: 'string'
                  }
                },
                description: 'A two-dimensional array of data'
              },
            },
            required: ["ranges"],
            additionalProperties: false,
          }
        }
      } 
    ]

    const response_format = {
      type: "json_schema",
      json_schema: {
        name: "response",
        strict: true,
        schema: {
          type: "object",
          properties: {
            tool: {
              type: "object",
              properties: {
                name: { type: "string" },
                type: { type: "string" }
              },
              required: ["name", "type"],
              additionalProperties: false
            },
            text_content: {
              type: "string"
            }
          },
          required: ["tool", "text_content"],
          additionalProperties: false
        }
      }
    }

    const messages = [
      { role: "system", content: "You are a spreadsheet expert." },
      { role: "user", content: "I have the following data:\n\nDate\tRevenue\n2022-01-01\t100\n2022-01-02\t200\n2022-01-03\t300\n\n, could you draw a chart?" }
    ]

    const response = await openai.chat.completions.create({
      model: "gpt-4o-2024-08-06",
      messages: messages,
      tools: tools,
      response_format: response_format
    });
    console.log("response.choices[0].message", response.choices[0].message)
    console.log(response.choices[0].message.content)
    console.log("response.choices[0].message.tool_calls", response.choices[0].message.tool_calls)
  } catch (error) {
    console.error("An error occurred:", error);
  }  
}

It returns the follows without text_content

response.choices[0].message {
  role: 'assistant',
  content: null,
  tool_calls: [
    {
      id: 'call_m8134tHaEJwMFB8L5Y9VVoVb',
      type: 'function',
      function: [Object]
    }
  ],
  refusal: null
}
null
response.choices[0].message.tool_calls [
  {
    id: 'call_m8134tHaEJwMFB8L5Y9VVoVb',
    type: 'function',
    function: {
      name: 'get_plotly_configuration_json',
      arguments: '{"data":[["Date","Revenue"],["2022-01-01","100"],["2022-01-02","200"],["2022-01-03","300"]]}'
    }
  }
]

So could you be more precise about your solution? Thank you

I got this. It needs a bit of fixing.

Give me some time…

Hello, please see Edit 1 in my OP.

If you want I can help you design a dynamic chart To create a dynamic chart that is filled from calculations made by ChatGPT’s calculator, and then visualized with custom CSS and properties, we can outline the process with the following steps:

Diagram

Here’s a high-level diagram of how the system works:

graph LR
    A[ChatGPT Calculator] --> B[Calculation Result]
    B --> C[Data Preprocessing]
    C --> D[Send Data to Chart]
    D --> E[Dynamic Chart Render]
    E --> F[Custom CSS Application]

    style A fill:#f96,stroke:#333,stroke-width:2px
    style B fill:#9f6,stroke:#333,stroke-width:2px
    style C fill:#6f9,stroke:#333,stroke-width:2px
    style D fill:#69f,stroke:#333,stroke-width:2px
    style E fill:#f69,stroke:#333,stroke-width:2px
    style F fill:#96f,stroke:#333,stroke-width:2px

Code Outline

  1. Calculation in ChatGPT:

    • Use a Python function or a similar tool to handle the calculations.
    • Example:
      def calculate_data(parameters):
          # Perform necessary calculations
          result = some_calculation_function(parameters)
          return result
      
  2. Data Preprocessing:

    • Format the data from the calculation into a structure suitable for the chart.
    • Example:
      def preprocess_data(calculation_result):
          # Convert the result into the chart data format
          chart_data = {
              "labels": ["Label1", "Label2", "Label3"],
              "datasets": [
                  {
                      "label": "Dataset 1",
                      "data": calculation_result,
                      "backgroundColor": ["rgba(75, 192, 192, 0.2)"],
                      "borderColor": ["rgba(75, 192, 192, 1)"],
                      "borderWidth": 1,
                  }
              ],
          }
          return chart_data
      
  3. Send Data to the Chart:

    • Use JavaScript to dynamically update the chart data.
    • Example:
      function updateChart(chart, newData) {
          chart.data = newData;
          chart.update();
      }
      
  4. Dynamic Chart Render:

    • Use a chart library like Chart.js to render the chart dynamically.
    • Example:
      <canvas id="myChart" width="400" height="400"></canvas>
      <script>
          const ctx = document.getElementById('myChart').getContext('2d');
          const myChart = new Chart(ctx, {
              type: 'bar', // Change to 'line', 'pie', etc. as needed
              data: chartData,
              options: {
                  responsive: true,
                  plugins: {
                      legend: {
                          position: 'top',
                      },
                      title: {
                          display: true,
                          text: 'Dynamic Chart Example'
                      }
                  }
              }
          });
      </script>
      
  5. Custom CSS Application:

    • Apply custom CSS styles to the chart or its container.
    • Example:
      #myChart {
          background-color: #f4f4f4;
          border: 2px solid #333;
          border-radius: 10px;
          padding: 10px;
      }
      
      .chart-container {
          max-width: 600px;
          margin: auto;
          box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
      }
      
  6. Integrate Everything:

    • Connect all the parts so the data calculated by ChatGPT’s calculator is processed, sent to the chart, and displayed with the custom CSS.
    • Example:
      <div class="chart-container">
          <canvas id="myChart"></canvas>
      </div>
      
      <script>
          const calculationResult = {{calculation_result}}; // Replace with the result from the calculator
          const chartData = preprocess_data(calculationResult);
          updateChart(myChart, chartData);
      </script>
      

Custom CSS Properties

The CSS provided can be customized for each use case, with changes to colors, borders, font sizes, or additional styling features like shadows or gradients to suit the specific needs of the project.

Conclusion

This setup allows for a flexible and dynamic chart generation process, where data from calculations can be visualized in a visually appealing way using custom-defined properties.that gets filled from calculations on chat gpts calculator and sent to the chart that draws itself from the data with custom CSS and properties defined by you per use case. To create a dynamic live-updating chart that pulls data from a stock API and displays it in real-time, we’ll use a combination of frontend technologies like HTML, JavaScript (with Chart.js for charting), and CSS for styling. We’ll connect to a stock API (e.g., Alpha Vantage, Yahoo Finance API) to fetch stock prices and update the chart live.

Code Outline and Steps

1Set Up the HTML Structure:

  • Create an HTML file with a canvas element for the chart and a container for styling.

  • Example:

    Live Stock Chart

Custom CSS for Styling:

  • Define styles in a styles.css file to make the chart visually appealing.

  • Example:

    .chart-container {
    max-width: 800px;
    margin: 50px auto;
    padding: 20px;
    background-color: #f9f9f9;
    border: 2px solid #ddd;
    border-radius: 10px;
    box-shadow: 0 0 15px rgba(0, 0, 0, 0.1);
    }

    canvas {
    width: 100% !important;
    height: auto !important;
    }

JavaScript to Fetch Data and Update the Chart:

  • Use JavaScript with Fetch API or Axios to get live data from the stock API.

  • Example app.js:

    const apiKey = ‘YOUR_API_KEY’; // Replace with your API key
    const symbol = ‘AAPL’; // Replace with the stock symbol you want to track
    const apiUrl = https://www.alphavantage.co/query?function=TIME_SERIES_INTRADAY&symbol=${symbol}&interval=1min&apikey=${apiKey};

    const ctx = document.getElementById(‘stockChart’).getContext(‘2d’);
    let stockChart;

    async function fetchStockData() {
    const response = await fetch(apiUrl);
    const data = await response.json();
    return data;
    }

    function processData(stockData) {
    const timeSeries = stockData[‘Time Series (1min)’];
    const labels = ;
    const prices = ;

    for (let time in timeSeries) {
        labels.push(time);
        prices.push(parseFloat(timeSeries[time]['1. open']));
    }
    
    return { labels: labels.reverse(), prices: prices.reverse() };
    

    }

    function createChart(labels, prices) {
    stockChart = new Chart(ctx, {
    type: ‘line’,
    data: {
    labels: labels,
    datasets: [{
    label: Stock Price of ${symbol},
    data: prices,
    fill: false,
    borderColor: ‘rgb(75, 192, 192)’,
    tension: 0.1
    }]
    },
    options: {
    scales: {
    x: {
    display: true,
    title: {
    display: true,
    text: ‘Time’
    }
    },
    y: {
    display: true,
    title: {
    display: true,
    text: ‘Price (USD)’
    }
    }
    }
    }
    });
    }

    async function updateChart() {
    const stockData = await fetchStockData();
    const { labels, prices } = processData(stockData);

    if (stockChart) {
        stockChart.data.labels = labels;
        stockChart.data.datasets[0].data = prices;
        stockChart.update();
    } else {
        createChart(labels, prices);
    }
    

    }

    // Fetch data and update the chart every minute
    setInterval(updateChart, 60000); // 60000 ms = 1 minute
    updateChart(); // Initial call to display data immediately

    
    

API Key and Configuration:

  • Replace YOUR_API_KEY with your actual API key from the stock API provider.
  • Ensure the API you’re using supports real-time or intraday data.

Live Updates:

  • The setInterval(updateChart, 60000); function call ensures the chart is updated every minute. You can adjust the interval as needed based on the API’s rate limits and your requirements.

Deploying and Running:

  • Save the HTML, CSS, and JavaScript files in a directory.
  • Open the HTML file in a browser to see the live stock chart updating in real-time.

This setup gives you a live-updating stock chart using real-time data from a stock API. The chart updates automatically based on the data fetched from the API, providing a dynamic visualization of stock prices. The custom CSS ensures that the chart looks polished and fits within your web design aesthetic.

Better is htmx.

Live Stock Chart with HTMX

from flask import Flask, jsonify
import requests

app = Flask(name)

@app.route(‘/update-stock-data’)
def update_stock_data():
apiKey = ‘YOUR_API_KEY’
symbol = ‘AAPL’
apiUrl = f"https://www.alphavantage.co/query?function=TIME_SERIES_INTRADAY&symbol={symbol}&interval=1min&apikey={apiKey}"
response = requests.get(apiUrl)
data = response.json()

time_series = data['Time Series (1min)']
labels = []
prices = []

for time, info in time_series.items():
    labels.append(time)
    prices.append(float(info['1. open']))

# Reverse the data to show the latest first
return jsonify({'labels': labels[::-1], 'prices': prices[::-1]})

if name == “main”:
app.run(debug=True)

const ctx = document.getElementById(‘stockChart’).getContext(‘2d’);
let stockChart = new Chart(ctx, {
type: ‘line’,
data: {
labels: ,
datasets: [{
label: ‘Stock Price’,
data: ,
fill: false,
borderColor: ‘rgb(75, 192, 192)’,
tension: 0.1
}]
},
options: {
scales: {
x: {
title: {
display: true,
text: ‘Time’
}
},
y: {
title: {
display: true,
text: ‘Price (USD)’
}
}
}
}
});

document.getElementById(‘stockChart’).addEventListener(‘htmx:afterRequest’, (event) => {
const response = event.detail.xhr.response;
const data = JSON.parse(response);

stockChart.data.labels = data.labels;
stockChart.data.datasets[0].data = data.prices;
stockChart.update();

});

.chart-container {
max-width: 800px;
margin: 50px auto;
padding: 20px;
background-color: #f9f9f9;
border: 2px solid #ddd;
border-radius: 10px;
box-shadow: 0 0 15px rgba(0, 0, 0, 0.1);
}

canvas {
width: 100% !important;
height: auto !important;
}

Using HTMX 2.0 makes it easier to update the stock chart without having to manage timing or intervals manually. The backend takes care of fetching the stock data, and HTMX automatically updates the chart every minute. This approach simplifies the whole process, keeping your code clean and efficient while still using the latest tools.

Easy peazy.

1 Like

I made some edits in tool and response schemas:

Tool definition:

{
    "name": "get_plotly_configuration",
    "description": "Retrieve the configuration settings for creating a Plotly chart.",
    "strict": true,
    "parameters": {
        "type": "object",
        "properties": {
            "title": {
                "type": "string",
                "description": "The title of the chart."
            },
            "x_axis_label": {
                "type": "string",
                "description": "The label for the x-axis."
            },
            "y_axis_label": {
                "type": "string",
                "description": "The label for the y-axis."
            },
            "x_axis_data": {
                "type": "array",
                "description": "An array of data points for the x-axis.",
                "items": {
                    "type": "string"
                }
            },
            "y_axis_data": {
                "type": "array",
                "description": "An array of data points for the y-axis.",
                "items": {
                    "type": "number"
                }
            }
        },
        "required": ["title", "x_axis_label", "y_axis_label", "x_axis_data", "y_axis_data"],
        "additionalProperties": false
    }
}

Response Schema:

{
    "name": "responseSchema",
    "strict": true,
    "schema": {
        "type": "object",
        "properties": {
            "text_content": {
                "type": "string",
                "description": "Text response"
            },
            "chart": {
                "type": "object",
                "description": "Ploty configuration data to display chart if there is any.",
                "properties": {
                    "title": {
                        "type": "string",
                        "description": "The title of the chart."
                    },
                    "x_axis_label": {
                        "type": "string",
                        "description": "The label for the x-axis."
                    },
                    "y_axis_label": {
                        "type": "string",
                        "description": "The label for the y-axis."
                    },
                    "x_axis_data": {
                        "type": "array",
                        "description": "An array of data points for the x-axis.",
                        "items": {
                            "type": "string"
                        }
                    },
                    "y_axis_data": {
                        "type": "array",
                        "description": "An array of data points for the y-axis.",
                        "items": {
                            "type": "number"
                        }
                    }
                },
                "required": ["title", "x_axis_label", "y_axis_label", "x_axis_data", "y_axis_data"],
                "additionalProperties": false
            }
        },
        "required": [ "text_content", "chart"],
        "additionalProperties": false
    }
}

Here’s the result

Here we have fixed the output in just the chart format. If we need to support other types, of course, it will need tweaking.

2 Likes

Glad I could help! Am other issues you need clarification on?