之前在jupyter中尝试使用typescript基于plotly.js画图,但没有成功。

https://blog.csdn.net/liliang199/article/details/157908193

这里进一步借助于Deepseek实现这一目标,讲过生成-精简-多轮纠错,Deepseek圆满完成任务。

所用示例由Deepseek模拟生成。

1 第一轮-生成

1.1 提示词

提示词示例如下

帮我生成一个在typescript环境下使用plotly.js画图的项目,包括所有配置文件,包括运行方法,以一到两个具体画图的示例。

1.2 输出 

输出如下所示,可见deepseek生成了完整的项目目录,以及对应ts文件、配置问价内容。

# TypeScript + Plotly.js 图表项目

我将为您创建一个完整的 TypeScript 项目,使用 Plotly.js 绘制图表。这个项目包含所有必要的配置文件和两个具体的图表示例。

## 项目结构

```
typescript-plotly-project/
├── src/
│   ├── index.html
│   ├── index.ts
│   ├── styles.css
│   └── charts.ts
├── package.json
├── tsconfig.json
├── webpack.config.js
└── README.md
```

## 文件内容

### 1. `package.json`

```json
{
  "name": "typescript-plotly-project",
  "version": "1.0.0",
  "description": "TypeScript project with Plotly.js for data visualization",
  "main": "dist/bundle.js",
  "scripts": {
    "start": "webpack serve --mode development --open",
    "build": "webpack --mode production",
    "dev": "webpack serve --mode development",
    "type-check": "tsc --noEmit"
  },
  "keywords": [
    "typescript",
    "plotly",
    "visualization",
    "charts"
  ],
  "author": "Your Name",
  "license": "MIT",
  "devDependencies": {
    "@types/plotly.js": "^2.12.18",
    "css-loader": "^6.10.0",
    "html-webpack-plugin": "^5.6.0",
    "style-loader": "^3.3.4",
    "ts-loader": "^9.5.1",
    "typescript": "^5.4.5",
    "webpack": "^5.91.0",
    "webpack-cli": "^5.1.4",
    "webpack-dev-server": "^5.0.4"
  },
  "dependencies": {
    "plotly.js-dist-min": "^2.27.0"
  }
}
```

### 2. `tsconfig.json`

```json
{
  "compilerOptions": {
    "target": "ES2020",
    "module": "ESNext",
    "lib": ["ES2020", "DOM", "DOM.Iterable"],
    "allowJs": true,
    "skipLibCheck": true,
    "esModuleInterop": true,
    "allowSyntheticDefaultImports": true,
    "strict": true,
    "forceConsistentCasingInFileNames": true,
    "moduleResolution": "node",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "noEmit": false,
    "outDir": "./dist",
    "sourceMap": true,
    "declaration": false
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist"]
}
```

### 3. `webpack.config.js`

```javascript
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = (env, argv) => {
  const isProduction = argv.mode === 'production';
  
  return {
    entry: './src/index.ts',
    output: {
      filename: 'bundle.js',
      path: path.resolve(__dirname, 'dist'),
      clean: true
    },
    devtool: isProduction ? 'source-map' : 'inline-source-map',
    devServer: {
      static: './dist',
      hot: true,
      open: true,
      port: 3000
    },
    module: {
      rules: [
        {
          test: /\.ts$/,
          use: 'ts-loader',
          exclude: /node_modules/
        },
        {
          test: /\.css$/,
          use: ['style-loader', 'css-loader']
        }
      ]
    },
    resolve: {
      extensions: ['.ts', '.js']
    },
    plugins: [
      new HtmlWebpackPlugin({
        template: './src/index.html',
        title: 'TypeScript + Plotly.js Charts'
      })
    ]
  };
};
```

### 4. `src/index.html`

```html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>TypeScript + Plotly.js Charts</title>
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
</head>
<body>
    <div class="container">
        <header>
            <h1><i class="fas fa-chart-line"></i> TypeScript + Plotly.js 图表示例</h1>
            <p class="subtitle">使用 TypeScript 和 Plotly.js 创建交互式数据可视化</p>
        </header>
        
        <main>
            <div class="controls">
                <button id="randomize-btn" class="btn">
                    <i class="fas fa-random"></i> 随机生成数据
                </button>
                <button id="theme-toggle" class="btn">
                    <i class="fas fa-moon"></i> 切换主题
                </button>
            </div>
            
            <div class="charts-grid">
                <div class="chart-container">
                    <h2><i class="fas fa-chart-bar"></i> 示例 1: 3D 曲面图</h2>
                    <div id="surface-plot" class="chart"></div>
                    <p class="chart-description">
                        这个 3D 曲面图展示了数学函数 z = sin(x) + cos(y) 的图形。
                        您可以使用鼠标旋转和缩放视图。
                    </p>
                </div>
                
                <div class="chart-container">
                    <h2><i class="fas fa-chart-pie"></i> 示例 2: 交互式散点图</h2>
                    <div id="scatter-plot" class="chart"></div>
                    <p class="chart-description">
                        这个散点图展示了三个不同类别(A、B、C)的随机数据分布。
                        点击图例可以显示/隐藏不同的类别。
                    </p>
                </div>
            </div>
            
            <div class="info-section">
                <h3><i class="fas fa-info-circle"></i> 项目说明</h3>
                <div class="info-grid">
                    <div class="info-card">
                        <h4><i class="fas fa-cogs"></i> 技术栈</h4>
                        <ul>
                            <li>TypeScript</li>
                            <li>Plotly.js</li>
                            <li>Webpack</li>
                            <li>Webpack Dev Server</li>
                        </ul>
                    </div>
                    <div class="info-card">
                        <h4><i class="fas fa-mouse-pointer"></i> 交互功能</h4>
                        <ul>
                            <li>点击图例切换显示/隐藏数据</li>
                            <li>鼠标悬停查看数据点详情</li>
                            <li>3D 图表支持旋转和缩放</li>
                            <li>主题切换(亮色/暗色)</li>
                        </ul>
                    </div>
                </div>
            </div>
        </main>
        
        <footer>
            <p>TypeScript + Plotly.js 图表项目 &copy; 2023</p>
            <p class="hint">提示:点击上方按钮可以随机生成新数据或切换主题</p>
        </footer>
    </div>
</body>
</html>

2 第二轮-精简

上述ts代码其实有一些语法错误,而且文件之间引用错误,可能画图需求太多导致。

这里我们尝试让deepseek只生成一个ts文件,不需要画太多图,仅画一个图即可。

2.2 提示词

提示词示例如下

请重写测试例子,画一个图即可,不需要画多个图,不需要index.ts和charts.ts两个,仅需要一个文件index.ts。

2.2 输出

Deepseek输出如下所示,本次仅包括一个index.ts代码,仅画一个图。

然而,vscode验证显示,index.ts有语法错误,所以在Deepseek的辅助下继续优化。

# 简化版 TypeScript + Plotly.js 图表项目

我将为您创建一个简化的 TypeScript 项目,仅包含一个图表示例,并将所有代码合并到一个 `index.ts` 文件中。

## 项目结构

```
typescript-plotly-simple/
├── src/
│   ├── index.html
│   ├── index.ts
│   └── styles.css
├── package.json
├── tsconfig.json
├── webpack.config.js
└── README.md
```

## 文件内容

### 1. `package.json`

```json
{
  "name": "typescript-plotly-simple",
  "version": "1.0.0",
  "description": "Simple TypeScript project with Plotly.js for data visualization",
  "main": "dist/bundle.js",
  "scripts": {
    "start": "webpack serve --mode development --open",
    "build": "webpack --mode production",
    "dev": "webpack serve --mode development",
    "type-check": "tsc --noEmit"
  },
  "keywords": ["typescript", "plotly", "charts"],
  "author": "Your Name",
  "license": "MIT",
  "devDependencies": {
    "@types/plotly.js": "^2.12.18",
    "css-loader": "^6.10.0",
    "html-webpack-plugin": "^5.6.0",
    "style-loader": "^3.3.4",
    "ts-loader": "^9.5.1",
    "typescript": "^5.4.5",
    "webpack": "^5.91.0",
    "webpack-cli": "^5.1.4",
    "webpack-dev-server": "^5.0.4"
  },
  "dependencies": {
    "plotly.js-dist-min": "^2.27.0"
  }
}
```

### 2. `tsconfig.json`

```json
{
  "compilerOptions": {
    "target": "ES2020",
    "module": "ESNext",
    "lib": ["ES2020", "DOM", "DOM.Iterable"],
    "allowJs": true,
    "skipLibCheck": true,
    "esModuleInterop": true,
    "allowSyntheticDefaultImports": true,
    "strict": true,
    "forceConsistentCasingInFileNames": true,
    "moduleResolution": "node",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "noEmit": false,
    "outDir": "./dist",
    "sourceMap": true,
    "declaration": false
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist"]
}
```

### 3. `webpack.config.js`

```javascript
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = (env, argv) => {
  const isProduction = argv.mode === 'production';
  
  return {
    entry: './src/index.ts',
    output: {
      filename: 'bundle.js',
      path: path.resolve(__dirname, 'dist'),
      clean: true
    },
    devtool: isProduction ? 'source-map' : 'inline-source-map',
    devServer: {
      static: './dist',
      hot: true,
      open: true,
      port: 3000
    },
    module: {
      rules: [
        {
          test: /\.ts$/,
          use: 'ts-loader',
          exclude: /node_modules/
        },
        {
          test: /\.css$/,
          use: ['style-loader', 'css-loader']
        }
      ]
    },
    resolve: {
      extensions: ['.ts', '.js']
    },
    plugins: [
      new HtmlWebpackPlugin({
        template: './src/index.html',
        title: 'TypeScript + Plotly.js 图表示例'
      })
    ]
  };
};
```

### 4. `src/index.html`

```html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Plotly.js + TypeScript 图表示例</title>
</head>
<body>
    <div class="container">
        <header>
            <h1>📊 Plotly.js + TypeScript 图表示例</h1>
            <p class="subtitle">使用 TypeScript 和 Plotly.js 创建交互式数据可视化</p>
        </header>
        
        <div class="controls">
            <button id="update-btn" class="btn">🔄 更新图表数据</button>
            <button id="theme-btn" class="btn">🌙 切换主题</button>
        </div>
        
        <main>
            <div class="chart-container">
                <h2>📈 股票价格趋势图(模拟数据)</h2>
                <div id="chart" class="chart"></div>
                <div class="chart-info">
                    <p>这是一个使用 TypeScript 和 Plotly.js 创建的交互式图表示例。</p>
                    <ul>
                        <li>🖱️ 鼠标悬停查看数据点详情</li>
                        <li>📏 使用工具栏缩放、平移图表</li>
                        <li>📊 点击图例可显示/隐藏数据系列</li>
                        <li>🎨 支持亮色/暗色主题切换</li>
                    </ul>
                </div>
            </div>
            
            <div class="data-info">
                <h3>📋 数据说明</h3>
                <div class="data-grid">
                    <div class="data-card">
                        <h4>📅 时间范围</h4>
                        <p>2023年1月 - 2023年12月</p>
                    </div>
                    <div class="data-card">
                        <h4>📊 数据系列</h4>
                        <p>3个模拟股票数据系列</p>
                    </div>
                    <div class="data-card">
                        <h4>🔄 交互功能</h4>
                        <p>可点击按钮更新随机数据</p>
                    </div>
                </div>
            </div>
        </main>
        
        <footer>
            <p>使用 TypeScript + Plotly.js 创建 | 示例项目</p>
        </footer>
    </div>
</body>
</html>

3 第三轮-纠错

3.1 提示词

错误示例如下,直接将错误示例输入Deepseek,没有加额外提示词。

Argument of type '{ x: string[]; y: number[]; name: string; type: "scatter"; mode: string; line: { color: string; width: number; }; marker: { size: number; opacity: number; }; hovertemplate: string; }[]' is not assignable to parameter of type 'Data[]'.
  Type '{ x: string[]; y: number[]; name: string; type: "scatter"; mode: string; line: { color: string; width: number; }; marker: { size: number; opacity: number; }; hovertemplate: string; }' is not assignable to type 'Data'.
    Type '{ x: string[]; y: number[]; name: string; type: "scatter"; mode: string; line: { color: string; width: number; }; marker: { size: number; opacity: number; }; hovertemplate: string; }' is not assignable to type 'Partial<PlotData>'.
      Types of property 'mode' are incompatible.
        Type 'string' is not assignable to type '"number" | "lines+markers" | "text" | "delta" | "gauge" | "lines" | "markers" | "text+markers" | "text+lines" | "text+lines+markers" | "none" | "number+delta" | "gauge+number" | "gauge+number+delta" | "gauge+delta" | undefined'.ts(2345)

3.2 输出

Deepseek依据错误信息对index.ts进行纠错,输出如下

这是一个 TypeScript 类型错误。问题在于 `mode` 属性的类型定义不匹配。我们需要明确指定 `mode` 的类型为 Plotly 接受的特定字符串字面量。下面是修复后的 `index.ts` 文件:

```typescript
import './styles.css';
import Plotly from 'plotly.js-dist-min';

// 定义数据接口
interface StockData {
  name: string;
  color: string;
  prices: number[];
  dates: string[];
}

// 定义 Plotly 数据接口
interface PlotlyTrace {
  x: string[];
  y: number[];
  name: string;
  type: 'scatter';
  mode: 'lines' | 'markers' | 'lines+markers' | 'text' | 'text+lines' | 'text+markers' | 'text+lines+markers' | 'none';
  line: {
    color: string;
    width: number;
  };
  marker: {
    size: number;
    opacity: number;
  };
  hovertemplate: string;
}

// 全局变量
let chartData: StockData[] = [];
let isDarkTheme = false;

// 生成随机股票数据
function generateStockData(): StockData[] {
  const companies = [
    { name: '科技公司A', color: '#4285f4' },
    { name: '金融公司B', color: '#34a853' },
    { name: '消费品C', color: '#ea4335' }
  ];
  
  const startDate = new Date(2023, 0, 1); // 2023年1月1日
  const months = 12;
  
  return companies.map(company => {
    const prices: number[] = [];
    const dates: string[] = [];
    
    // 起始价格
    let price = 100 + Math.random() * 50;
    
    for (let i = 0; i < months; i++) {
      // 添加随机波动
      const change = (Math.random() - 0.5) * 20;
      price += change;
      
      // 确保价格为正数
      price = Math.max(price, 50);
      
      prices.push(Number(price.toFixed(2)));
      
      // 生成日期
      const currentDate = new Date(startDate);
      currentDate.setMonth(startDate.getMonth() + i);
      const month = currentDate.getMonth() + 1;
      const year = currentDate.getFullYear();
      dates.push(`${year}-${month.toString().padStart(2, '0')}`);
    }
    
    return {
      name: company.name,
      color: company.color,
      prices,
      dates
    };
  });
}

// 创建图表
function createChart(): void {
  if (chartData.length === 0) {
    chartData = generateStockData();
  }
  
  // 准备Plotly数据
  const plotlyData: PlotlyTrace[] = chartData.map(stock => ({
    x: stock.dates,
    y: stock.prices,
    name: stock.name,
    type: 'scatter',
    mode: 'lines+markers' as const,
    line: {
      color: stock.color,
      width: 3
    },
    marker: {
      size: 8,
      opacity: 0.8
    },
    hovertemplate: `
      <b>${stock.name}</b><br>
      日期: %{x}<br>
      价格: $%{y:.2f}<br>
      <extra></extra>
    `
  }));
  
  // 准备布局
  const isDark = document.body.classList.contains('dark-theme');
  const bgColor = isDark ? '#292a2d' : '#ffffff';
  const textColor = isDark ? '#e8eaed' : '#202124';
  const gridColor = isDark ? '#5f6368' : '#dadce0';
  
  const layout = {
    title: {
      text: '股票价格趋势 (2023年)',
      font: {
        size: 22,
        color: textColor
      }
    },
    xaxis: {
      title: {
        text: '月份',
        font: {
          size: 14,
          color: textColor
        }
      },
      tickfont: {
        color: textColor
      },
      gridcolor: gridColor,
      showgrid: true,
      zeroline: false
    },
    yaxis: {
      title: {
        text: '价格 ($)',
        font: {
          size: 14,
          color: textColor
        }
      },
      tickfont: {
        color: textColor
      },
      gridcolor: gridColor,
      showgrid: true,
      zeroline: false
    },
    plot_bgcolor: bgColor,
    paper_bgcolor: bgColor,
    font: {
      color: textColor
    },
    margin: {
      l: 60,
      r: 30,
      b: 60,
      t: 80
    },
    legend: {
      x: 0.01,
      y: 0.99,
      bgcolor: 'rgba(255, 255, 255, 0.05)',
      bordercolor: gridColor,
      borderwidth: 1,
      font: {
        color: textColor
      }
    },
    hovermode: 'closest',
    showlegend: true,
    autosize: true
  };
  
  // 配置选项
  const config = {
    responsive: true,
    displayModeBar: true,
    displaylogo: false,
    modeBarButtonsToRemove: ['lasso2d', 'select2d'],
    toImageButtonOptions: {
      format: 'png',
      filename: 'stock-chart',
      height: 600,
      width: 1000,
      scale: 2
    }
  };
  
  // 绘制图表
  Plotly.newPlot('chart', plotlyData, layout, config);
}

// 更新图表数据
function updateChart(): void {
  chartData = generateStockData();
  createChart();
  
  // 显示更新提示
  const message = document.createElement('div');
  message.textContent = '图表数据已更新!';
  message.style.cssText = `
    position: fixed;
    top: 20px;
    right: 20px;
    background-color: var(--secondary-color);
    color: white;
    padding: 12px 20px;
    border-radius: 8px;
    z-index: 1000;
    animation: fadeInOut 2.5s ease-in-out;
  `;
  
  document.body.appendChild(message);
  
  // 添加动画样式
  if (!document.querySelector('#fade-animation')) {
    const style = document.createElement('style');
    style.id = 'fade-animation';
    style.textContent = `
      @keyframes fadeInOut {
        0% { opacity: 0; transform: translateY(-20px); }
        20% { opacity: 1; transform: translateY(0); }
        80% { opacity: 1; transform: translateY(0); }
        100% { opacity: 0; transform: translateY(-20px); }
      }
    `;
    document.head.appendChild(style);
  }
  
  setTimeout(() => {
    if (message.parentNode) {
      message.parentNode.removeChild(message);
    }
  }, 2500);
}

// 切换主题
function toggleTheme(): void {
  isDarkTheme = !isDarkTheme;
  
  if (isDarkTheme) {
    document.body.classList.add('dark-theme');
    (document.querySelector('#theme-btn') as HTMLElement)!.innerHTML = '☀️ 浅色主题';
  } else {
    document.body.classList.remove('dark-theme');
    (document.querySelector('#theme-btn') as HTMLElement)!.innerHTML = '🌙 深色主题';
  }
  
  // 重新创建图表以应用新主题
  createChart();
}

// 初始化页面
function init(): void {
  // 创建初始图表
  createChart();
  
  // 添加按钮事件监听器
  document.getElementById('update-btn')!.addEventListener('click', updateChart);
  document.getElementById('theme-btn')!.addEventListener('click', toggleTheme);
  
  // 检查是否偏好暗色主题
  const prefersDark = window.matchMedia('(prefers-color-scheme: dark)');
  isDarkTheme = prefersDark.matches;
  
  if (isDarkTheme) {
    document.body.classList.add('dark-theme');
    (document.querySelector('#theme-btn') as HTMLElement)!.innerHTML = '☀️ 浅色主题';
  }
  
  // 监听系统主题变化
  prefersDark.addEventListener('change', (e) => {
    isDarkTheme = e.matches;
    if (isDarkTheme) {
      document.body.classList.add('dark-theme');
      (document.querySelector('#theme-btn') as HTMLElement)!.innerHTML = '☀️ 浅色主题';
    } else {
      document.body.classList.remove('dark-theme');
      (document.querySelector('#theme-btn') as HTMLElement)!.innerHTML = '🌙 深色主题';
    }
    createChart();
  });
  
  // 窗口大小变化时重新调整图表
  window.addEventListener('resize', () => {
    const chartElement = document.getElementById('chart');
    if (chartElement) {
      Plotly.Plots.resize(chartElement);
    }
  });
  
  // 添加一些交互效果
  const buttons = document.querySelectorAll('.btn');
  buttons.forEach(button => {
    button.addEventListener('mousedown', function(this: HTMLElement) {
      this.style.transform = 'scale(0.95)';
    });
    
    button.addEventListener('mouseup', function(this: HTMLElement) {
      this.style.transform = '';
    });
    
    button.addEventListener('mouseleave', function(this: HTMLElement) {
      this.style.transform = '';
    });
  });
  
  console.log('图表应用已初始化完成!');
}

// 当DOM加载完成后初始化应用
document.addEventListener('DOMContentLoaded', init);
```

主要修复了以下内容:

1. 创建了 `PlotlyTrace` 接口,明确指定了 `mode` 属性的类型为 Plotly 接受的特定字符串字面量联合类型
2. 将 `plotlyData` 的类型明确指定为 `PlotlyTrace[]`
3. 使用 `as const` 断言来确保 `mode: 'lines+markers'` 被视为字面量类型而不是字符串类型
4. 添加了一些类型断言来确保 TypeScript 编译器不会报错

现在 TypeScript 应该能够正确识别 `mode` 属性的类型,并且能够将数据正确传递给 Plotly.js 的 `newPlot` 函数。

4 第四轮-继续纠错

4.1 提示词

经过上一轮纠错后,依然有错误信息,如下所示,直接作为提示词输入Deepseek。

Argument of type '{ title: { text: string; font: { size: number; color: string; }; }; xaxis: { title: { text: string; font: { size: number; color: string; }; }; tickfont: { color: string; }; gridcolor: string; showgrid: boolean; zeroline: boolean; }; ... 8 more ...; autosize: boolean; }' is not assignable to parameter of type 'Partial<Layout>'.
  Types of property 'hovermode' are incompatible.
    Type 'string' is not assignable to type 'false | "closest" | "x" | "y" | "x unified" | "y unified" | undefined'.ts(2345)

4.2 输出

以下是Deepseek依据错误信息,进一步对index.ts纠错结果,包括index.ts文件本身。

这是另一个 TypeScript 类型错误。问题在于 `hovermode` 属性的类型定义不匹配。我们需要明确指定 `hovermode` 的类型为 Plotly 接受的特定字符串字面量。下面是修复后的 `index.ts` 文件:

```typescript
import './styles.css';
import Plotly from 'plotly.js-dist-min';

// 定义数据接口
interface StockData {
  name: string;
  color: string;
  prices: number[];
  dates: string[];
}

// 定义 Plotly 数据接口
interface PlotlyTrace {
  x: string[];
  y: number[];
  name: string;
  type: 'scatter';
  mode: 'lines' | 'markers' | 'lines+markers' | 'text' | 'text+lines' | 'text+markers' | 'text+lines+markers' | 'none';
  line: {
    color: string;
    width: number;
  };
  marker: {
    size: number;
    opacity: number;
  };
  hovertemplate: string;
}

// 定义 Plotly 布局接口
interface PlotlyLayout {
  title: {
    text: string;
    font: {
      size: number;
      color: string;
    };
  };
  xaxis: {
    title: {
      text: string;
      font: {
        size: number;
        color: string;
      };
    };
    tickfont: {
      color: string;
    };
    gridcolor: string;
    showgrid: boolean;
    zeroline: boolean;
  };
  yaxis: {
    title: {
      text: string;
      font: {
        size: number;
        color: string;
      };
    };
    tickfont: {
      color: string;
    };
    gridcolor: string;
    showgrid: boolean;
    zeroline: boolean;
  };
  plot_bgcolor: string;
  paper_bgcolor: string;
  font: {
    color: string;
  };
  margin: {
    l: number;
    r: number;
    b: number;
    t: number;
  };
  legend: {
    x: number;
    y: number;
    bgcolor: string;
    bordercolor: string;
    borderwidth: number;
    font: {
      color: string;
    };
  };
  hovermode: 'closest' | 'x' | 'y' | 'x unified' | 'y unified' | false;
  showlegend: boolean;
  autosize: boolean;
}

// 全局变量
let chartData: StockData[] = [];
let isDarkTheme = false;

// 生成随机股票数据
function generateStockData(): StockData[] {
  const companies = [
    { name: '科技公司A', color: '#4285f4' },
    { name: '金融公司B', color: '#34a853' },
    { name: '消费品C', color: '#ea4335' }
  ];
  
  const startDate = new Date(2023, 0, 1); // 2023年1月1日
  const months = 12;
  
  return companies.map(company => {
    const prices: number[] = [];
    const dates: string[] = [];
    
    // 起始价格
    let price = 100 + Math.random() * 50;
    
    for (let i = 0; i < months; i++) {
      // 添加随机波动
      const change = (Math.random() - 0.5) * 20;
      price += change;
      
      // 确保价格为正数
      price = Math.max(price, 50);
      
      prices.push(Number(price.toFixed(2)));
      
      // 生成日期
      const currentDate = new Date(startDate);
      currentDate.setMonth(startDate.getMonth() + i);
      const month = currentDate.getMonth() + 1;
      const year = currentDate.getFullYear();
      dates.push(`${year}-${month.toString().padStart(2, '0')}`);
    }
    
    return {
      name: company.name,
      color: company.color,
      prices,
      dates
    };
  });
}

// 创建图表
function createChart(): void {
  if (chartData.length === 0) {
    chartData = generateStockData();
  }
  
  // 准备Plotly数据
  const plotlyData: PlotlyTrace[] = chartData.map(stock => ({
    x: stock.dates,
    y: stock.prices,
    name: stock.name,
    type: 'scatter',
    mode: 'lines+markers' as const,
    line: {
      color: stock.color,
      width: 3
    },
    marker: {
      size: 8,
      opacity: 0.8
    },
    hovertemplate: `
      <b>${stock.name}</b><br>
      日期: %{x}<br>
      价格: $%{y:.2f}<br>
      <extra></extra>
    `
  }));
  
  // 准备布局
  const isDark = document.body.classList.contains('dark-theme');
  const bgColor = isDark ? '#292a2d' : '#ffffff';
  const textColor = isDark ? '#e8eaed' : '#202124';
  const gridColor = isDark ? '#5f6368' : '#dadce0';
  
  const layout: Partial<Plotly.Layout> & { hovermode: 'closest' | 'x' | 'y' | 'x unified' | 'y unified' | false } = {
    title: {
      text: '股票价格趋势 (2023年)',
      font: {
        size: 22,
        color: textColor
      }
    },
    xaxis: {
      title: {
        text: '月份',
        font: {
          size: 14,
          color: textColor
        }
      },
      tickfont: {
        color: textColor
      },
      gridcolor: gridColor,
      showgrid: true,
      zeroline: false
    },
    yaxis: {
      title: {
        text: '价格 ($)',
        font: {
          size: 14,
          color: textColor
        }
      },
      tickfont: {
        color: textColor
      },
      gridcolor: gridColor,
      showgrid: true,
      zeroline: false
    },
    plot_bgcolor: bgColor,
    paper_bgcolor: bgColor,
    font: {
      color: textColor
    },
    margin: {
      l: 60,
      r: 30,
      b: 60,
      t: 80
    },
    legend: {
      x: 0.01,
      y: 0.99,
      bgcolor: 'rgba(255, 255, 255, 0.05)',
      bordercolor: gridColor,
      borderwidth: 1,
      font: {
        color: textColor
      }
    },
    hovermode: 'closest' as const,
    showlegend: true,
    autosize: true
  };
  
  // 配置选项
  const config = {
    responsive: true,
    displayModeBar: true,
    displaylogo: false,
    modeBarButtonsToRemove: ['lasso2d', 'select2d'],
    toImageButtonOptions: {
      format: 'png',
      filename: 'stock-chart',
      height: 600,
      width: 1000,
      scale: 2
    }
  };
  
  // 绘制图表
  Plotly.newPlot('chart', plotlyData as any, layout, config);
}

// 更新图表数据
function updateChart(): void {
  chartData = generateStockData();
  createChart();
  
  // 显示更新提示
  const message = document.createElement('div');
  message.textContent = '图表数据已更新!';
  message.style.cssText = `
    position: fixed;
    top: 20px;
    right: 20px;
    background-color: var(--secondary-color);
    color: white;
    padding: 12px 20px;
    border-radius: 8px;
    z-index: 1000;
    animation: fadeInOut 2.5s ease-in-out;
  `;
  
  document.body.appendChild(message);
  
  // 添加动画样式
  if (!document.querySelector('#fade-animation')) {
    const style = document.createElement('style');
    style.id = 'fade-animation';
    style.textContent = `
      @keyframes fadeInOut {
        0% { opacity: 0; transform: translateY(-20px); }
        20% { opacity: 1; transform: translateY(0); }
        80% { opacity: 1; transform: translateY(0); }
        100% { opacity: 0; transform: translateY(-20px); }
      }
    `;
    document.head.appendChild(style);
  }
  
  setTimeout(() => {
    if (message.parentNode) {
      message.parentNode.removeChild(message);
    }
  }, 2500);
}

// 切换主题
function toggleTheme(): void {
  isDarkTheme = !isDarkTheme;
  
  if (isDarkTheme) {
    document.body.classList.add('dark-theme');
    (document.querySelector('#theme-btn') as HTMLElement)!.innerHTML = '☀️ 浅色主题';
  } else {
    document.body.classList.remove('dark-theme');
    (document.querySelector('#theme-btn') as HTMLElement)!.innerHTML = '🌙 深色主题';
  }
  
  // 重新创建图表以应用新主题
  createChart();
}

// 初始化页面
function init(): void {
  // 创建初始图表
  createChart();
  
  // 添加按钮事件监听器
  document.getElementById('update-btn')!.addEventListener('click', updateChart);
  document.getElementById('theme-btn')!.addEventListener('click', toggleTheme);
  
  // 检查是否偏好暗色主题
  const prefersDark = window.matchMedia('(prefers-color-scheme: dark)');
  isDarkTheme = prefersDark.matches;
  
  if (isDarkTheme) {
    document.body.classList.add('dark-theme');
    (document.querySelector('#theme-btn') as HTMLElement)!.innerHTML = '☀️ 浅色主题';
  }
  
  // 监听系统主题变化
  prefersDark.addEventListener('change', (e) => {
    isDarkTheme = e.matches;
    if (isDarkTheme) {
      document.body.classList.add('dark-theme');
      (document.querySelector('#theme-btn') as HTMLElement)!.innerHTML = '☀️ 浅色主题';
    } else {
      document.body.classList.remove('dark-theme');
      (document.querySelector('#theme-btn') as HTMLElement)!.innerHTML = '🌙 深色主题';
    }
    createChart();
  });
  
  // 窗口大小变化时重新调整图表
  window.addEventListener('resize', () => {
    const chartElement = document.getElementById('chart');
    if (chartElement) {
      Plotly.Plots.resize(chartElement);
    }
  });
  
  // 添加一些交互效果
  const buttons = document.querySelectorAll('.btn');
  buttons.forEach(button => {
    button.addEventListener('mousedown', function(this: HTMLElement) {
      this.style.transform = 'scale(0.95)';
    });
    
    button.addEventListener('mouseup', function(this: HTMLElement) {
      this.style.transform = '';
    });
    
    button.addEventListener('mouseleave', function(this: HTMLElement) {
      this.style.transform = '';
    });
  });
  
  console.log('图表应用已初始化完成!');
}

// 当DOM加载完成后初始化应用
document.addEventListener('DOMContentLoaded', init);
```

主要修复了以下内容:

1. 创建了 `PlotlyLayout` 接口,明确指定了 `hovermode` 属性的类型为 Plotly 接受的特定字符串字面量联合类型
2. 将 `layout` 的类型声明为 `Partial<Plotly.Layout> & { hovermode: 'closest' | 'x' | 'y' | 'x unified' | 'y unified' | false }`,这样既可以使用 Plotly 的类型定义,又可以明确指定 `hovermode` 的类型
3. 使用 `as const` 断言来确保 `hovermode: 'closest'` 被视为字面量类型而不是字符串类型
4. 在调用 `Plotly.newPlot` 时,将 `plotlyData` 转换为 `any` 类型,以绕过 TypeScript 的严格类型检查(这是一种简化处理方式)

现在 TypeScript 应该能够正确识别 `hovermode` 属性的类型,并且能够将布局正确传递给 Plotly.js 的 `newPlot` 函数。

5 第五轮-继续纠错

5.1 提示词 

然而,依然还有错误信息,那就进一步纠错,提示词输入如下所示。

Argument of type '{ responsive: boolean; displayModeBar: boolean; displaylogo: boolean; modeBarButtonsToRemove: string[]; toImageButtonOptions: { format: string; filename: string; height: number; width: number; scale: number; }; }' is not assignable to parameter of type 'Partial<Config>'.
  Types of property 'modeBarButtonsToRemove' are incompatible.
    Type 'string[]' is not assignable to type 'ModeBarDefaultButtons[]'.
      Type 'string' is not assignable to type 'ModeBarDefaultButtons'.ts(2345)

5.2 输出

Deepseek依据错误信息对index.ts进一步纠错,输入如下,此时,vscode不在提示错误信息。

这是另一个 TypeScript 类型错误。问题在于 `modeBarButtonsToRemove` 属性的类型定义不匹配。我们需要明确指定 `modeBarButtonsToRemove` 的类型为 Plotly 接受的特定字符串字面量数组。下面是修复后的 `index.ts` 文件:

```typescript
import './styles.css';
import Plotly from 'plotly.js-dist-min';

// 定义数据接口
interface StockData {
  name: string;
  color: string;
  prices: number[];
  dates: string[];
}

// 定义 Plotly 数据接口
interface PlotlyTrace {
  x: string[];
  y: number[];
  name: string;
  type: 'scatter';
  mode: 'lines' | 'markers' | 'lines+markers' | 'text' | 'text+lines' | 'text+markers' | 'text+lines+markers' | 'none';
  line: {
    color: string;
    width: number;
  };
  marker: {
    size: number;
    opacity: number;
  };
  hovertemplate: string;
}

// 全局变量
let chartData: StockData[] = [];
let isDarkTheme = false;

// 生成随机股票数据
function generateStockData(): StockData[] {
  const companies = [
    { name: '科技公司A', color: '#4285f4' },
    { name: '金融公司B', color: '#34a853' },
    { name: '消费品C', color: '#ea4335' }
  ];
  
  const startDate = new Date(2023, 0, 1); // 2023年1月1日
  const months = 12;
  
  return companies.map(company => {
    const prices: number[] = [];
    const dates: string[] = [];
    
    // 起始价格
    let price = 100 + Math.random() * 50;
    
    for (let i = 0; i < months; i++) {
      // 添加随机波动
      const change = (Math.random() - 0.5) * 20;
      price += change;
      
      // 确保价格为正数
      price = Math.max(price, 50);
      
      prices.push(Number(price.toFixed(2)));
      
      // 生成日期
      const currentDate = new Date(startDate);
      currentDate.setMonth(startDate.getMonth() + i);
      const month = currentDate.getMonth() + 1;
      const year = currentDate.getFullYear();
      dates.push(`${year}-${month.toString().padStart(2, '0')}`);
    }
    
    return {
      name: company.name,
      color: company.color,
      prices,
      dates
    };
  });
}

// 创建图表
function createChart(): void {
  if (chartData.length === 0) {
    chartData = generateStockData();
  }
  
  // 准备Plotly数据
  const plotlyData: PlotlyTrace[] = chartData.map(stock => ({
    x: stock.dates,
    y: stock.prices,
    name: stock.name,
    type: 'scatter',
    mode: 'lines+markers' as const,
    line: {
      color: stock.color,
      width: 3
    },
    marker: {
      size: 8,
      opacity: 0.8
    },
    hovertemplate: `
      <b>${stock.name}</b><br>
      日期: %{x}<br>
      价格: $%{y:.2f}<br>
      <extra></extra>
    `
  }));
  
  // 准备布局
  const isDark = document.body.classList.contains('dark-theme');
  const bgColor = isDark ? '#292a2d' : '#ffffff';
  const textColor = isDark ? '#e8eaed' : '#202124';
  const gridColor = isDark ? '#5f6368' : '#dadce0';
  
  const layout: Partial<Plotly.Layout> & { hovermode: 'closest' | 'x' | 'y' | 'x unified' | 'y unified' | false } = {
    title: {
      text: '股票价格趋势 (2023年)',
      font: {
        size: 22,
        color: textColor
      }
    },
    xaxis: {
      title: {
        text: '月份',
        font: {
          size: 14,
          color: textColor
        }
      },
      tickfont: {
        color: textColor
      },
      gridcolor: gridColor,
      showgrid: true,
      zeroline: false
    },
    yaxis: {
      title: {
        text: '价格 ($)',
        font: {
          size: 14,
          color: textColor
        }
      },
      tickfont: {
        color: textColor
      },
      gridcolor: gridColor,
      showgrid: true,
      zeroline: false
    },
    plot_bgcolor: bgColor,
    paper_bgcolor: bgColor,
    font: {
      color: textColor
    },
    margin: {
      l: 60,
      r: 30,
      b: 60,
      t: 80
    },
    legend: {
      x: 0.01,
      y: 0.99,
      bgcolor: 'rgba(255, 255, 255, 0.05)',
      bordercolor: gridColor,
      borderwidth: 1,
      font: {
        color: textColor
      }
    },
    hovermode: 'closest' as const,
    showlegend: true,
    autosize: true
  };
  
  // 配置选项 - 使用更简单的配置避免类型问题
  const config: Partial<Plotly.Config> = {
    responsive: true,
    displayModeBar: true,
    displaylogo: false,
    // 使用类型断言来避免复杂的类型检查
    modeBarButtonsToRemove: ['lasso2d', 'select2d'] as any,
    toImageButtonOptions: {
      format: 'png',
      filename: 'stock-chart',
      height: 600,
      width: 1000,
      scale: 2
    }
  };
  
  // 绘制图表 - 使用类型断言来避免复杂的类型检查
  Plotly.newPlot('chart', plotlyData as any, layout as any, config);
}

// 更新图表数据
function updateChart(): void {
  chartData = generateStockData();
  createChart();
  
  // 显示更新提示
  const message = document.createElement('div');
  message.textContent = '图表数据已更新!';
  message.style.cssText = `
    position: fixed;
    top: 20px;
    right: 20px;
    background-color: var(--secondary-color);
    color: white;
    padding: 12px 20px;
    border-radius: 8px;
    z-index: 1000;
    animation: fadeInOut 2.5s ease-in-out;
  `;
  
  document.body.appendChild(message);
  
  // 添加动画样式
  if (!document.querySelector('#fade-animation')) {
    const style = document.createElement('style');
    style.id = 'fade-animation';
    style.textContent = `
      @keyframes fadeInOut {
        0% { opacity: 0; transform: translateY(-20px); }
        20% { opacity: 1; transform: translateY(0); }
        80% { opacity: 1; transform: translateY(0); }
        100% { opacity: 0; transform: translateY(-20px); }
      }
    `;
    document.head.appendChild(style);
  }
  
  setTimeout(() => {
    if (message.parentNode) {
      message.parentNode.removeChild(message);
    }
  }, 2500);
}

// 切换主题
function toggleTheme(): void {
  isDarkTheme = !isDarkTheme;
  
  if (isDarkTheme) {
    document.body.classList.add('dark-theme');
    (document.querySelector('#theme-btn') as HTMLElement)!.innerHTML = '☀️ 浅色主题';
  } else {
    document.body.classList.remove('dark-theme');
    (document.querySelector('#theme-btn') as HTMLElement)!.innerHTML = '🌙 深色主题';
  }
  
  // 重新创建图表以应用新主题
  createChart();
}

// 初始化页面
function init(): void {
  // 创建初始图表
  createChart();
  
  // 添加按钮事件监听器
  document.getElementById('update-btn')!.addEventListener('click', updateChart);
  document.getElementById('theme-btn')!.addEventListener('click', toggleTheme);
  
  // 检查是否偏好暗色主题
  const prefersDark = window.matchMedia('(prefers-color-scheme: dark)');
  isDarkTheme = prefersDark.matches;
  
  if (isDarkTheme) {
    document.body.classList.add('dark-theme');
    (document.querySelector('#theme-btn') as HTMLElement)!.innerHTML = '☀️ 浅色主题';
  }
  
  // 监听系统主题变化
  prefersDark.addEventListener('change', (e) => {
    isDarkTheme = e.matches;
    if (isDarkTheme) {
      document.body.classList.add('dark-theme');
      (document.querySelector('#theme-btn') as HTMLElement)!.innerHTML = '☀️ 浅色主题';
    } else {
      document.body.classList.remove('dark-theme');
      (document.querySelector('#theme-btn') as HTMLElement)!.innerHTML = '🌙 深色主题';
    }
    createChart();
  });
  
  // 窗口大小变化时重新调整图表
  window.addEventListener('resize', () => {
    const chartElement = document.getElementById('chart');
    if (chartElement) {
      Plotly.Plots.resize(chartElement);
    }
  });
  
  // 添加一些交互效果
  const buttons = document.querySelectorAll('.btn');
  buttons.forEach(button => {
    button.addEventListener('mousedown', function(this: HTMLElement) {
      this.style.transform = 'scale(0.95)';
    });
    
    button.addEventListener('mouseup', function(this: HTMLElement) {
      this.style.transform = '';
    });
    
    button.addEventListener('mouseleave', function(this: HTMLElement) {
      this.style.transform = '';
    });
  });
  
  console.log('图表应用已初始化完成!');
}

// 当DOM加载完成后初始化应用
document.addEventListener('DOMContentLoaded', init);
```

主要修复了以下内容:

1. 将 `config` 对象明确指定为 `Partial<Plotly.Config>` 类型
2. 对 `modeBarButtonsToRemove` 属性使用 `as any` 类型断言,绕过 TypeScript 的严格类型检查
3. 在调用 `Plotly.newPlot` 时,将 `plotlyData` 和 `layout` 转换为 `any` 类型,以简化类型问题

这些修复使用了类型断言 (`as any`) 来避免与 Plotly.js 复杂类型定义的不匹配。虽然这不是最理想的类型安全解决方案,但它允许我们继续使用简化的项目结构,而不必处理 Plotly.js 复杂且可能不完整的 TypeScript 类型定义。

如果你希望保持更高的类型安全性,可以安装完整的 `plotly.js` 库(而不是 `plotly.js-dist-min`),并使用其完整的类型定义。但这样会增加包大小。

现在,TypeScript 应该能够编译代码,并且图表应该能正常运行。

6 最后-运行验证

6.1 验证指令

依据生成README.md等文件给出的指示,在vscode的term窗口运行如下指令。

npm run dev

6.2 系统输出

此时,系统自主启动一个网页,示例plotly.js的绘图效果。

而且可以更新数据和更改主题颜色,示例如下

reference

---

如何在jupyter中使用typescript基于plotly.js画图

https://blog.csdn.net/liliang199/article/details/157908193

Logo

欢迎加入DeepSeek 技术社区。在这里,你可以找到志同道合的朋友,共同探索AI技术的奥秘。

更多推荐