Vue3+d3-cloud+d3-scale+d3-scale-chromatic实现词云组件

大家好,我是鱼樱!!!

关注公众号【鱼樱AI实验室】持续分享更多前端和AI辅助前端编码新知识~~

写点笔记写点生活~写点经验。

在当前环境下,纯前端开发者可以通过技术深化、横向扩展、切入新兴领域以及产品化思维找到突破口。

著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

1. 组件概述

词云(Word Cloud)是一种视觉上的文字展示方式,常用于直观地表现文本数据中词语的频率或重要性。本组件基于Vue3和TypeScript实现,使用d3-cloud库作为布局引擎,具有高度可定制性和交互性,可应用于各种数据可视化场景。

2. 技术栈

  • 框架: Vue3 + TypeScript
  • 布局引擎: d3-cloud
  • 颜色比例尺: d3-scale
  • 色彩方案: d3-scale-chromatic
  • 构建工具: Vite

3. 词云组件

该项目包含一个功能完整的词云组件,支持以下特性:

  • 根据数据权重自动调整文字大小
  • 支持自定义颜色方案或使用D3内置色彩
  • 可配置文字旋转角度
  • 响应式设计,自适应容器大小
  • 交互反馈,鼠标悬停效果

效果图

参考代码

词云组件

html 复制代码
<template>
  <div class="word-cloud-container" ref="cloudContainer">
    <svg :width="width" :height="height" class="word-cloud-svg">
      <g :transform="`translate(${width / 2}, ${height / 2})`">
        <text
          v-for="(word, index) in words"
          :key="index"
          :font-size="word.size + 'px'"
          :fill="word.color"
          :transform="`translate(${word.x}, ${word.y}) rotate(${word.rotate})`"
          :style="{ fontFamily }"
          text-anchor="middle"
          dominant-baseline="middle"
          class="word-cloud-text"
        >
          {{ word.text }}
        </text>
      </g>
    </svg>
  </div>
</template>

<script setup lang="ts">
import { ref, onMounted, watch, computed } from "vue";
import cloud from "d3-cloud";
import { scaleOrdinal } from "d3-scale";
import * as d3ScaleChromatic from "d3-scale-chromatic";

// 导入自定义类型
import type { Word } from "../types/custom-types";

// 词云词语类型
interface CloudWord {
  text: string;
  size: number;
  value: number;
  x?: number;
  y?: number;
  rotate?: number;
  color?: string;
}

// 定义组件属性
const props = defineProps({
  // 词云数据数组,每项应包含text和value属性
  data: {
    type: Array as () => Array<{ text: string; value: number }>,
    required: true,
    default: () => [],
  },
  // 词云宽度
  width: {
    type: Number,
    default: 500,
  },
  // 词云高度
  height: {
    type: Number,
    default: 400,
  },
  // 字体
  fontFamily: {
    type: String,
    default: "Arial, sans-serif",
  },
  // 最小字号
  minFontSize: {
    type: Number,
    default: 12,
  },
  // 最大字号
  maxFontSize: {
    type: Number,
    default: 60,
  },
  // 旋转角度数组
  rotations: {
    type: Array as () => number[],
    default: () => [0],
  },
  // 自定义颜色数组
  colors: {
    type: Array as () => string[],
    default: null,
  },
  // 颜色主题,从d3-scale-chromatic中选择
  colorScheme: {
    type: String,
    default: "schemeCategory10",
    validator: (value: string) => {
      return Object.keys(d3ScaleChromatic).includes(value);
    },
  },
  // 是否随机布局
  random: {
    type: Boolean,
    default: true,
  },
  // 布局迭代次数
  iterations: {
    type: Number,
    default: 100,
  },
});

// 内部状态
const words = ref<CloudWord[]>([]);
const cloudContainer = ref<HTMLElement | null>(null);

// 计算颜色比例尺
const colorScale = computed(() => {
  if (props.colors && props.colors.length > 0) {
    return scaleOrdinal<string>().range(props.colors);
  } else {
    // 使用d3提供的颜色主题
    const colorRange = (d3ScaleChromatic as any)[props.colorScheme];
    return scaleOrdinal<string>().range(
      typeof colorRange === "function" ? colorRange(10) : colorRange
    );
  }
});

// 生成词云
const generateWordCloud = () => {
  if (!props.data || props.data.length === 0) return;

  // 查找最大和最小值,用于缩放字体大小
  const maxValue = Math.max(...props.data.map((d) => d.value));
  const minValue = Math.min(...props.data.map((d) => d.value));

  // 创建词云布局
  const layout = cloud()
    .size([props.width, props.height])
    .words(props.data.map((d) => ({ text: d.text, size: 0, value: d.value })))
    .padding(5)
    .rotate(
      () => props.rotations[Math.floor(Math.random() * props.rotations.length)]
    )
    .fontSize((d: Word) => {
      // 线性缩放字体大小
      if (maxValue === minValue) return props.minFontSize;
      const size =
        props.minFontSize +
        (((d.value as number) - minValue) / (maxValue - minValue)) *
          (props.maxFontSize - props.minFontSize);
      return size;
    })
    .random(props.random ? Math.random : () => 0.5)
    .spiral("archimedean")
    .on("end", (cloudWords: Word[]) => {
      words.value = cloudWords.map((w, i: number) => ({
        ...w,
        text: w.text,
        size: w.size,
        value: w.value as number,
        x: w.x,
        y: w.y,
        rotate: w.rotate,
        color: colorScale.value(i.toString()),
      }));
    });

  // 启动布局
  layout.start();
};

// 监听数据变化,重新生成词云
watch(
  () => props.data,
  () => {
    generateWordCloud();
  },
  { deep: true }
);

// 监听尺寸变化,重新生成词云
watch([() => props.width, () => props.height], () => {
  generateWordCloud();
});

// 组件挂载时生成词云
onMounted(() => {
  generateWordCloud();
});

// 暴露方法,允许外部手动刷新词云
defineExpose({
  refresh: generateWordCloud,
});
</script>

<script lang="ts">
export default {
  name: "WordCloud",
};
</script>

<style scoped>
.word-cloud-container {
  position: relative;
  display: flex;
  justify-content: center;
  align-items: center;
  overflow: hidden;
}

.word-cloud-svg {
  display: block;
  margin: 0 auto;
}

.word-cloud-text {
  cursor: pointer;
  transition: transform 0.3s ease, opacity 0.3s ease;
}

.word-cloud-text:hover {
  opacity: 0.7;
  transform: scale(1.1);
}
</style>

词云组件使用

html 复制代码
<template>
  <div class="word-cloud-demo">
    <h1 class="title">词云组件示例</h1>

    <div class="control-panel">
      <h3>词云配置</h3>
      <div class="control-group">
        <label>颜色方案:</label>
        <select v-model="colorScheme">
          <option value="schemeCategory10">默认配色</option>
          <option value="schemeAccent">亮色配色</option>
          <option value="schemePaired">成对配色</option>
          <option value="schemeSet3">柔和配色</option>
          <option value="schemeDark2">深色配色</option>
        </select>
      </div>

      <div class="control-group">
        <label>字体旋转:</label>
        <select v-model="rotationMode">
          <option value="horizontal">水平文字</option>
          <option value="mixed">混合旋转</option>
          <option value="vertical">垂直文字</option>
        </select>
      </div>

      <div class="control-group">
        <label>最大字号:</label>
        <input type="range" min="20" max="100" v-model="maxFontSize" />
        <span>{{ maxFontSize }}px</span>
      </div>

      <div class="control-group">
        <label>最小字号:</label>
        <input type="range" min="8" max="30" v-model="minFontSize" />
        <span>{{ minFontSize }}px</span>
      </div>

      <div class="control-group">
        <label>自定义词汇:</label>
        <textarea
          v-model="customWordsText"
          placeholder="输入格式:词汇1:权重1,词汇2:权重2,..."
        ></textarea>
      </div>

      <div class="control-group">
        <button @click="updateWords">更新词云</button>
        <button @click="randomizeWords">随机数据</button>
      </div>
    </div>

    <div class="word-cloud-wrapper">
      <WordCloud
        :data="wordData"
        :width="800"
        :height="500"
        :min-font-size="minFontSize"
        :max-font-size="maxFontSize"
        :rotations="rotationsMap[rotationMode]"
        :color-scheme="colorScheme"
        ref="wordCloudRef"
      />
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref, computed, onMounted } from "vue";
import WordCloud from "../components/WordCloud.vue";

// 旋转模式映射
const rotationsMap: { [key: string]: number[] } = {
  horizontal: [0],
  mixed: [-60, -30, 0, 30, 60],
  vertical: [90, -90],
};

// 词云配置
const colorScheme = ref("schemeCategory10");
const rotationMode = ref("horizontal");
const maxFontSize = ref(60);
const minFontSize = ref(12);
const customWordsText = ref("");
const wordCloudRef = ref<any>(null);

// 示例数据
const defaultWords = [
  { text: "词云", value: 100 },
  { text: "Vue3", value: 85 },
  { text: "TypeScript", value: 80 },
  { text: "可视化", value: 75 },
  { text: "前端开发", value: 70 },
  { text: "组件库", value: 65 },
  { text: "JavaScript", value: 60 },
  { text: "数据展示", value: 55 },
  { text: "D3.js", value: 50 },
  { text: "交互设计", value: 45 },
  { text: "CSS", value: 40 },
  { text: "HTML", value: 35 },
  { text: "响应式", value: 30 },
  { text: "组合式API", value: 25 },
  { text: "状态管理", value: 20 },
  { text: "动画效果", value: 15 },
];

// 当前词云数据
const wordData = ref<Array<{ text: string; value: number }>>(defaultWords);

// 解析自定义词汇文本
const parseCustomWords = () => {
  if (!customWordsText.value.trim()) return [];

  try {
    return customWordsText.value
      .split(",")
      .map((item) => {
        const [text, valueStr] = item.split(":");
        const value = parseInt(valueStr, 10);
        if (text && !isNaN(value)) {
          return { text: text.trim(), value };
        }
        return null;
      })
      .filter((item) => item !== null) as { text: string; value: number }[];
  } catch (e) {
    console.error("解析自定义词汇失败", e);
    return [];
  }
};

// 更新词云数据
const updateWords = () => {
  const customWords = parseCustomWords();
  wordData.value = customWords.length > 0 ? customWords : defaultWords;

  // 手动刷新词云
  setTimeout(() => {
    wordCloudRef.value?.refresh();
  }, 100);
};

// 生成随机数据
const randomizeWords = () => {
  wordData.value = defaultWords.map((word) => ({
    text: word.text,
    value: Math.floor(Math.random() * 100) + 10,
  }));
};

// 组件挂载时初始化
onMounted(() => {
  updateWords();
});
</script>

<style scoped>
.word-cloud-demo {
  padding: 20px;
  max-width: 1200px;
  margin: 0 auto;
}

.title {
  text-align: center;
  margin-bottom: 30px;
  color: #333;
}

.control-panel {
  background-color: #f5f5f5;
  padding: 20px;
  border-radius: 8px;
  margin-bottom: 30px;
  box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
}

.control-group {
  margin-bottom: 15px;
  display: flex;
  align-items: center;
}

.control-group label {
  min-width: 100px;
  margin-right: 10px;
  font-weight: bold;
}

.control-group select,
.control-group input {
  padding: 8px;
  border-radius: 4px;
  border: 1px solid #ddd;
  flex: 1;
  max-width: 300px;
}

.control-group textarea {
  width: 100%;
  height: 80px;
  padding: 8px;
  border-radius: 4px;
  border: 1px solid #ddd;
  margin-top: 5px;
}

.control-group button {
  padding: 8px 16px;
  background-color: #3498db;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
  margin-right: 10px;
  transition: background-color 0.3s;
}

.control-group button:hover {
  background-color: #2980b9;
}

.word-cloud-wrapper {
  background-color: white;
  border-radius: 8px;
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
  padding: 20px;
  min-height: 500px;
  display: flex;
  justify-content: center;
  align-items: center;
}
</style>
ts 复制代码
// 词云词语基本类型
export interface Word {
  text: string;
  size: number;
  value?: number;
  x?: number;
  y?: number;
  rotate?: number;
  [key: string]: any;
}

// 扩展的词云单词类型
export interface CloudWord extends Word {
  value: number;
  color?: string;
} // 词云词语基本类型
export interface Word {
  text: string;
  size: number;
  value?: number;
  x?: number;
  y?: number;
  rotate?: number;
  [key: string]: any;
}

// 扩展的词云单词类型
export interface CloudWord extends Word {
  value: number;
  color?: string;
} 
ts 复制代码
declare module 'd3-cloud' {
  interface Word {
    text: string;
    size: number;
    value?: number;
    x?: number;
    y?: number;
    rotate?: number;
    [key: string]: any;
  }

  interface Cloud {
    size: (size: [number, number]) => Cloud;
    words: (words: Word[]) => Cloud;
    padding: (padding: number) => Cloud;
    rotate: (rotate: ((d: Word) => number)) => Cloud;
    fontSize: (fontSize: ((d: Word) => number)) => Cloud;
    random: (random: (() => number)) => Cloud;
    spiral: (spiral: string) => Cloud;
    on: (type: string, callback: (words: Word[]) => void) => Cloud;
    start: () => void;
  }

  export default function(): Cloud;
}

declare module 'd3-scale' {
  interface ScaleOrdinal<Range> {
    (value: string | number): Range;
    domain: (domain: Array<string | number>) => ScaleOrdinal<Range>;
    range: (range: Range[]) => ScaleOrdinal<Range>;
  }

  export function scaleOrdinal<Range = string>(): ScaleOrdinal<Range>;
}

declare module 'd3-scale-chromatic' {
  // 定义所有颜色方案
  export const schemeCategory10: string[];
  export const schemeAccent: string[];
  export const schemeDark2: string[];
  export const schemePaired: string[];
  export const schemePastel1: string[];
  export const schemePastel2: string[];
  export const schemeSet1: string[];
  export const schemeSet2: string[];
  export const schemeSet3: string[];
  export const schemeTableau10: string[];

  // 其他颜色方案函数
  export function interpolateBlues(t: number): string;
  export function interpolateGreens(t: number): string;
  export function interpolateGreys(t: number): string;
  export function interpolateOranges(t: number): string;
  export function interpolatePurples(t: number): string;
  export function interpolateReds(t: number): string;
} 
相关推荐
dualven_in_csdn1 小时前
搞了两天的win7批处理脚本问题
java·linux·前端
你的人类朋友2 小时前
✍️【Node.js程序员】的数据库【索引优化】指南
前端·javascript·后端
小超爱编程2 小时前
纯前端做图片压缩
开发语言·前端·javascript
银色的白2 小时前
工作记录:人物对话功能开发与集成
vue.js·学习·前端框架
应巅2 小时前
echarts 数据大屏(无UI设计 极简洁版)
前端·ui·echarts
萌萌哒草头将军3 小时前
🚀🚀🚀什么?浏览器也能修改项目源文件了?Chrome 团队开源的超强 Vite 插件!🚀🚀🚀
vue.js·react.js·vite
Jimmy3 小时前
CSS 实现描边文字效果
前端·css·html
islandzzzz4 小时前
HMTL+CSS+JS-新手小白循序渐进案例入门
前端·javascript·css·html
Senar4 小时前
网页中如何判断用户是否处于闲置状态
前端·javascript
很甜的西瓜4 小时前
typescript软渲染实现类似canvas的2d矢量图形引擎
前端·javascript·typescript·图形渲染·canvas