This is an automated email from the ASF dual-hosted git repository. ovilia pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/echarts-theme-builder.git
commit 4f42db8bd809482d3c38b3bf28962d64837b5494 Author: Ovilia <[email protected]> AuthorDate: Tue Aug 26 15:55:21 2025 +0800 feat: preview --- src/App.vue | 78 +++-- src/components/ChartPreviewPanel.vue | 144 +++++++++ src/components/HelloWorld.vue | 41 --- src/composables/useLocalization.ts | 69 ++++ src/i18n.ts | 50 +++ src/locales/en.json | 43 +++ src/locales/zh.json | 43 +++ src/main.ts | 6 +- src/stores/theme.ts | 233 ++++++++++++++ src/style.css | 78 +---- src/types/theme.ts | 104 ++++++ src/utils/chartConfigs.ts | 609 +++++++++++++++++++++++++++++++++++ src/utils/chartOptions.ts | 328 +++++++++++++++++++ 13 files changed, 1690 insertions(+), 136 deletions(-) diff --git a/src/App.vue b/src/App.vue index 58b0f21..d5cc463 100644 --- a/src/App.vue +++ b/src/App.vue @@ -1,30 +1,72 @@ <script setup lang="ts"> -import HelloWorld from './components/HelloWorld.vue' +// Simple fixed sidebar layout without responsive design +import ChartPreviewPanel from './components/ChartPreviewPanel.vue' </script> <template> - <div> - <a href="https://vite.dev" target="_blank"> - <img src="/vite.svg" class="logo" alt="Vite logo" /> - </a> - <a href="https://vuejs.org/" target="_blank"> - <img src="./assets/vue.svg" class="logo vue" alt="Vue logo" /> - </a> + <div id="theme-builder"> + <div class="container-fluid" id="content"> + <van-row class="row-container" :gutter="0"> + <!-- Left panel: Theme configuration - Fixed width --> + <van-col span="6" class="theme-config"> + <!-- Theme configuration panel will be implemented here --> + <div class="placeholder"> + Theme Configuration Panel + </div> + </van-col> + + <!-- Right panel: Chart preview - Remaining width --> + <van-col span="18" class="chart-container"> + <ChartPreviewPanel /> + </van-col> + </van-row> + </div> </div> - <HelloWorld msg="Vite + Vue" /> </template> <style scoped> -.logo { - height: 6em; - padding: 1.5em; - will-change: filter; - transition: filter 300ms; +#theme-builder { + width: 100%; + height: 100vh; } -.logo:hover { - filter: drop-shadow(0 0 2em #646cffaa); + +.container-fluid { + height: 100%; + padding: 0; + width: 100%; } -.logo.vue:hover { - filter: drop-shadow(0 0 2em #42b883aa); + +.row-container { + height: 100%; + display: flex !important; + flex-direction: row !important; +} + +.theme-config { + height: 100vh; + overflow-y: auto; + background-color: #f8f9fa; + border-right: 1px solid #dee2e6; + padding: 20px; + box-sizing: border-box; + flex: 0 0 25%; /* Fixed 25% width */ +} + +.chart-container { + height: 100vh; + overflow: hidden; + background-color: #ffffff; + padding: 20px; + box-sizing: border-box; + flex: 1; /* Take remaining space */ +} + +.placeholder { + padding: 20px; + text-align: center; + color: #6c757d; + border: 2px dashed #dee2e6; + border-radius: 4px; + font-size: 16px; } </style> diff --git a/src/components/ChartPreviewPanel.vue b/src/components/ChartPreviewPanel.vue new file mode 100644 index 0000000..2c2da56 --- /dev/null +++ b/src/components/ChartPreviewPanel.vue @@ -0,0 +1,144 @@ +<template> + <div class="chart-preview"> + <div class="preview-header"> + <h3>Chart Preview</h3> + </div> + + <div class="charts-grid"> + <div + v-for="(config, index) in displayedCharts" + :key="config.type + index" + class="chart-item" + > + <div + :ref="el => setChartRef(el, index)" + class="chart-container" + ></div> + </div> + </div> + </div> +</template> + +<script setup lang="ts"> +import { ref, onMounted, onUnmounted, nextTick } from 'vue' +import * as echarts from 'echarts' +import { getChartConfigs } from '../utils/chartConfigs' +import type { ECharts } from 'echarts' + +const chartInstances = ref<ECharts[]>([]) +const chartRefs = ref<(HTMLElement | null)[]>([]) + +// Always display all charts +const displayedCharts = ref(getChartConfigs(4)) + +// Set chart ref +function setChartRef(el: any, index: number) { + if (el) { + chartRefs.value[index] = el as HTMLElement + } +} + +// Initialize all charts +function initializeCharts() { + // Dispose existing charts + chartInstances.value.forEach(chart => chart.dispose()) + chartInstances.value = [] + + // Create new chart instances + displayedCharts.value.forEach((config, index) => { + const container = chartRefs.value[index] + if (container) { + const chart = echarts.init(container) + chart.setOption(config.option) + chartInstances.value.push(chart) + } + }) +} + +// Resize charts when window resizes +function handleResize() { + chartInstances.value.forEach(chart => chart.resize()) +} + +onMounted(() => { + nextTick(() => { + initializeCharts() + }) + window.addEventListener('resize', handleResize) +}) + +onUnmounted(() => { + chartInstances.value.forEach(chart => chart.dispose()) + window.removeEventListener('resize', handleResize) +}) +</script> + +<style scoped> +.chart-preview { + height: 100%; + display: flex; + flex-direction: column; +} + +.preview-header { + display: flex; + justify-content: flex-start; + align-items: center; + margin-bottom: 20px; + padding-bottom: 15px; + border-bottom: 1px solid #e9ecef; +} + +.preview-header h3 { + margin: 0; + color: #333; + font-size: 24px; +} + +.charts-grid { + flex: 1; + display: grid; + grid-template-columns: repeat(auto-fit, minmax(400px, 1fr)); + gap: 20px; + overflow-y: auto; + padding-right: 8px; +} + +.chart-item { + background: #fff; + border: 1px solid #e9ecef; + border-radius: 8px; + padding: 16px; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); + transition: box-shadow 0.2s ease; +} + +.chart-item:hover { + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); +} + +.chart-container { + width: 100%; + height: 320px; + border-radius: 4px; +} + +/* Responsive adjustments */ +@media (max-width: 1400px) { + .charts-grid { + grid-template-columns: repeat(auto-fit, minmax(350px, 1fr)); + } +} + +@media (max-width: 1200px) { + .charts-grid { + grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); + } +} + +@media (max-width: 768px) { + .charts-grid { + grid-template-columns: 1fr; + } +} +</style> diff --git a/src/components/HelloWorld.vue b/src/components/HelloWorld.vue deleted file mode 100644 index b58e52b..0000000 --- a/src/components/HelloWorld.vue +++ /dev/null @@ -1,41 +0,0 @@ -<script setup lang="ts"> -import { ref } from 'vue' - -defineProps<{ msg: string }>() - -const count = ref(0) -</script> - -<template> - <h1>{{ msg }}</h1> - - <div class="card"> - <button type="button" @click="count++">count is {{ count }}</button> - <p> - Edit - <code>components/HelloWorld.vue</code> to test HMR - </p> - </div> - - <p> - Check out - <a href="https://vuejs.org/guide/quick-start.html#local" target="_blank" - >create-vue</a - >, the official Vue + Vite starter - </p> - <p> - Learn more about IDE Support for Vue in the - <a - href="https://vuejs.org/guide/scaling-up/tooling.html#ide-support" - target="_blank" - >Vue Docs Scaling up Guide</a - >. - </p> - <p class="read-the-docs">Click on the Vite and Vue logos to learn more</p> -</template> - -<style scoped> -.read-the-docs { - color: #888; -} -</style> diff --git a/src/composables/useLocalization.ts b/src/composables/useLocalization.ts new file mode 100644 index 0000000..106834b --- /dev/null +++ b/src/composables/useLocalization.ts @@ -0,0 +1,69 @@ +import { useI18n } from 'vue-i18n' +import { setLocale, getCurrentLocale, getAvailableLocales } from '../i18n' + +/** + * Localization composable + * Provides common i18n functionality + */ +export function useLocalization() { + const { t, locale } = useI18n() + + /** + * Switch language + * @param newLocale New language code + */ + const switchLanguage = (newLocale: string) => { + setLocale(newLocale) + } + + /** + * Get current language + */ + const currentLanguage = getCurrentLocale() + + /** + * Get available languages list + */ + const availableLanguages = getAvailableLocales() + + /** + * Check if current language matches specified language + * @param lang Language code + */ + const isLanguage = (lang: string) => { + return getCurrentLocale() === lang + } + + /** + * Get formatted date text + * @param date Date object + */ + const formatDate = (date: Date) => { + const isZh = isLanguage('zh') + return date.toLocaleDateString(isZh ? 'zh-CN' : 'en-US', { + year: 'numeric', + month: 'long', + day: 'numeric' + }) + } + + /** + * Get formatted number text + * @param num Number + */ + const formatNumber = (num: number) => { + const isZh = isLanguage('zh') + return num.toLocaleString(isZh ? 'zh-CN' : 'en-US') + } + + return { + t, + locale, + switchLanguage, + currentLanguage, + availableLanguages, + isLanguage, + formatDate, + formatNumber + } +} diff --git a/src/i18n.ts b/src/i18n.ts new file mode 100644 index 0000000..ed8b5d4 --- /dev/null +++ b/src/i18n.ts @@ -0,0 +1,50 @@ +import { createI18n } from 'vue-i18n' +import en from './locales/en.json' +import zh from './locales/zh.json' + +// 从 localStorage 获取保存的语言设置,默认为中文 +const getStoredLanguage = (): string => { + const stored = localStorage.getItem('echarts-theme-builder-locale') + if (stored && ['en', 'zh'].includes(stored)) { + return stored + } + // 根据浏览器语言自动选择 + const browserLang = navigator.language.toLowerCase() + if (browserLang.startsWith('zh')) { + return 'zh' + } + return 'en' +} + +const i18n = createI18n({ + legacy: false, // 使用 Composition API 模式 + locale: getStoredLanguage(), + fallbackLocale: 'en', + messages: { + en, + zh + } +}) + +// 保存语言设置到 localStorage +export const setLocale = (locale: string) => { + if (['en', 'zh'].includes(locale)) { + i18n.global.locale.value = locale as 'en' | 'zh' + localStorage.setItem('echarts-theme-builder-locale', locale) + } +} + +// 获取当前语言 +export const getCurrentLocale = () => { + return i18n.global.locale.value +} + +// 获取可用语言列表 +export const getAvailableLocales = () => { + return [ + { code: 'en', name: 'English' }, + { code: 'zh', name: '中文' } + ] +} + +export default i18n diff --git a/src/locales/en.json b/src/locales/en.json new file mode 100644 index 0000000..c70a602 --- /dev/null +++ b/src/locales/en.json @@ -0,0 +1,43 @@ +{ + "common": { + "title": "ECharts Theme Builder", + "welcome": "Welcome to ECharts Theme Builder", + "language": "Language", + "theme": "Theme", + "preview": "Preview", + "download": "Download", + "reset": "Reset" + }, + "themeBuilder": { + "title": "Theme Editor", + "reset": "Reset", + "export": "Export", + "preDefinedThemes": "Predefined Themes", + "basicSettings": "Basic Settings", + "themeName": "Theme Name", + "themeNamePlaceholder": "Enter theme name", + "backgroundColor": "Background Color", + "titleColor": "Title Color", + "subtitleColor": "Subtitle Color", + "textColor": "Text Color", + "colorPalette": "Color Palette", + "legend": "Legend", + "legendTextColor": "Legend Text Color", + "line": "Line", + "lineWidth": "Line Width", + "symbolSize": "Symbol Size", + "symbol": "Symbol Type", + "lineSmooth": "Smooth Line", + "toolbox": "Toolbox", + "toolboxColor": "Toolbox Color", + "toolboxEmphasisColor": "Toolbox Emphasis Color" + }, + "chartPreview": { + "title": "Chart Preview", + "themeCode": "Theme Code", + "copy": "Copy", + "close": "Close", + "showCode": "Show Code", + "hideCode": "Hide Code" + } +} diff --git a/src/locales/zh.json b/src/locales/zh.json new file mode 100644 index 0000000..c70a602 --- /dev/null +++ b/src/locales/zh.json @@ -0,0 +1,43 @@ +{ + "common": { + "title": "ECharts Theme Builder", + "welcome": "Welcome to ECharts Theme Builder", + "language": "Language", + "theme": "Theme", + "preview": "Preview", + "download": "Download", + "reset": "Reset" + }, + "themeBuilder": { + "title": "Theme Editor", + "reset": "Reset", + "export": "Export", + "preDefinedThemes": "Predefined Themes", + "basicSettings": "Basic Settings", + "themeName": "Theme Name", + "themeNamePlaceholder": "Enter theme name", + "backgroundColor": "Background Color", + "titleColor": "Title Color", + "subtitleColor": "Subtitle Color", + "textColor": "Text Color", + "colorPalette": "Color Palette", + "legend": "Legend", + "legendTextColor": "Legend Text Color", + "line": "Line", + "lineWidth": "Line Width", + "symbolSize": "Symbol Size", + "symbol": "Symbol Type", + "lineSmooth": "Smooth Line", + "toolbox": "Toolbox", + "toolboxColor": "Toolbox Color", + "toolboxEmphasisColor": "Toolbox Emphasis Color" + }, + "chartPreview": { + "title": "Chart Preview", + "themeCode": "Theme Code", + "copy": "Copy", + "close": "Close", + "showCode": "Show Code", + "hideCode": "Hide Code" + } +} diff --git a/src/main.ts b/src/main.ts index 2425c0f..a10b54a 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,5 +1,9 @@ import { createApp } from 'vue' +import { Col, Row } from 'vant' import './style.css' import App from './App.vue' -createApp(App).mount('#app') +const app = createApp(App) +app.use(Col) +app.use(Row) +app.mount('#app') diff --git a/src/stores/theme.ts b/src/stores/theme.ts new file mode 100644 index 0000000..f5a1412 --- /dev/null +++ b/src/stores/theme.ts @@ -0,0 +1,233 @@ +import { ref, reactive } from 'vue' +import type { ThemeData, PreDefinedTheme } from '../types/theme' + +// 预定义主题 +export const PRE_DEFINED_THEMES: PreDefinedTheme[] = [ + { + name: 'vintage', + background: '#fef8ef', + theme: [ + '#d87c7c', '#919e8b', '#d7ab82', '#6e7074', '#61a0a8', + '#efa18d', '#787464', '#cc7e63', '#724e58', '#4b565b' + ] + }, + { + name: 'dark', + background: '#333', + theme: [ + '#dd6b66', '#759aa0', '#e69d87', '#8dc1a9', '#ea7e53', + '#eedd78', '#73a373', '#73b9bc', '#7289ab', '#91ca8c', + '#f49f42' + ] + }, + { + name: 'westeros', + background: 'transparent', + theme: [ + '#516b91', '#59c4e6', '#edafda', '#93b7e3', '#a5e7f0', + '#cbb0e3' + ] + }, + { + name: 'essos', + background: 'rgba(242,234,191,0.15)', + theme: [ + '#893448', '#d95850', '#eb8146', '#ffb248', '#f2d643', + '#ebdba4' + ] + }, + { + name: 'wonderland', + background: 'transparent', + theme: [ + '#4ea397', '#22c3aa', '#7bd9a5', '#d0648a', '#f58db2', + '#f2b3c9' + ] + }, + { + name: 'walden', + background: 'rgba(252,252,252,0)', + theme: [ + '#3fb1e3', '#6be6c1', '#626c91', '#a0a7e6', '#c4ebad', + '#96dee8' + ] + }, + { + name: 'chalk', + background: '#293441', + theme: [ + '#fc97af', '#87f7cf', '#f7f494', '#72ccff', '#f7c5a0', + '#d4a4eb', '#d2f5a6', '#76f2f2' + ] + }, + { + name: 'infographic', + background: 'transparent', + theme: [ + '#C1232B', '#27727B', '#FCCE10', '#E87C25', '#B5C334', + '#FE8463', '#9BCA63', '#FAD860', '#F3A43B', '#60C0DD', + '#D7504B', '#C6E579', '#F4E001', '#F0805A', '#26C0C0' + ] + }, + { + name: 'macarons', + background: 'transparent', + theme: [ + '#2ec7c9', '#b6a2de', '#5ab1ef', '#ffb980', '#d87a80', + '#8d98b3', '#e5cf0d', '#97b552', '#95706d', '#dc69aa', + '#07a2a4', '#9a7fd1', '#588dd5', '#f5994e', '#c05050', + '#59678c', '#c9ab00', '#7eb00a', '#6f5553', '#c14089' + ] + }, + { + name: 'roma', + background: 'transparent', + theme: [ + '#E01F54', '#001852', '#f5e8c8', '#b8d2c7', '#c6b38e', + '#a4d8c2', '#f3d999', '#d3758f', '#dcc392', '#2e4783', + '#82b6e9', '#ff6347', '#a092f1', '#0a915d', '#eaf889', + '#6699FF', '#ff6666', '#3cb371', '#d5b158', '#38b6b6' + ] + }, + { + name: 'shine', + background: 'transparent', + theme: [ + '#c12e34', '#e6b600', '#0098d9', '#2b821d', '#005eaa', + '#339ca8', '#cda819', '#32a487' + ] + }, + { + name: 'purple-passion', + background: 'rgba(91,92,110,1)', + theme: [ + '#8a7ca8', '#e098c7', '#8fd3e8', '#71669e', '#cc70af', + '#7cb4cc' + ] + } +] + +// 默认主题配置 +const createDefaultAxes = () => { + const types = ['all', 'category', 'value', 'log', 'time'] + const names = ['通用', '类目', '数值', '对数', '时间'] + return types.map((type, i) => ({ + type, + name: names[i] + '坐标轴', + axisLineShow: type !== 'value' && type !== 'log', + axisLineColor: '#6E7079', + axisTickShow: type !== 'value' && type !== 'log', + axisTickColor: '#6E7079', + axisLabelShow: true, + axisLabelColor: '#6E7079', + splitLineShow: type !== 'category' && type !== 'time', + splitLineColor: ['#E0E6F1'], + splitAreaShow: false, + splitAreaColor: ['rgba(250,250,250,0.2)', 'rgba(210,219,238,0.2)'] + })) +} + +export const createDefaultTheme = (): ThemeData => { + const axes = createDefaultAxes() + return { + seriesCnt: 3, + backgroundColor: 'rgba(0, 0, 0, 0)', + titleColor: '#464646', + subtitleColor: '#6E7079', + textColorShow: false, + textColor: '#333', + markTextColor: '#eee', + color: [ + '#5470c6', + '#91cc75', + '#fac858', + '#ee6666', + '#73c0de', + '#3ba272', + '#fc8452', + '#9a60b4', + '#ea7ccc' + ], + borderColor: '#ccc', + borderWidth: 0, + visualMapColor: ['#bf444c', '#d88273', '#f6efa6'], + legendTextColor: '#333', + kColor: '#eb5454', + kColor0: '#47b262', + kBorderColor: '#eb5454', + kBorderColor0: '#47b262', + kBorderWidth: 1, + lineWidth: 2, + symbolSize: 4, + symbol: 'emptyCircle', + symbolBorderWidth: 1, + lineSmooth: false, + graphLineWidth: 1, + graphLineColor: '#aaa', + mapLabelColor: '#000', + mapLabelColorE: 'rgb(100,0,0)', + mapBorderColor: '#444', + mapBorderColorE: '#444', + mapBorderWidth: 0.5, + mapBorderWidthE: 1, + mapAreaColor: '#eee', + mapAreaColorE: 'rgba(255,215,0,0.8)', + axes, + axisSeperateSetting: true, + axis: [axes[0]], + toolboxColor: '#999', + toolboxEmphasisColor: '#666', + tooltipAxisColor: '#ccc', + tooltipAxisWidth: 1, + timelineLineColor: '#DAE1F5', + timelineLineWidth: 2, + timelineItemColor: '#A4B1D7', + timelineItemColorE: '#FFF', + timelineCheckColor: '#316bf3', + timelineCheckBorderColor: '#fff', + timelineItemBorderWidth: 1, + timelineControlColor: '#A4B1D7', + timelineControlBorderColor: '#A4B1D7', + timelineControlBorderWidth: 1, + timelineLabelColor: '#A4B1D7' + } +} + +// 全局状态管理 +export const useThemeStore = () => { + const theme = reactive<ThemeData>(createDefaultTheme()) + const themeName = ref('customized') + const isPauseChartUpdating = ref(false) + const chartDisplay = reactive({ + background: '#fff', + title: '#000' + }) + + const resetTheme = () => { + Object.assign(theme, createDefaultTheme()) + themeName.value = 'customized' + } + + const loadPreDefinedTheme = (preTheme: PreDefinedTheme) => { + theme.backgroundColor = preTheme.background + theme.color = [...preTheme.theme] + themeName.value = preTheme.name + } + + const exportTheme = () => { + const exportData = { ...theme } + // 删除重复的 axis 选项,因为它已经包含在 theme.axes 中 + const { axis, ...cleanedData } = exportData + return cleanedData + } + + return { + theme, + themeName, + isPauseChartUpdating, + chartDisplay, + resetTheme, + loadPreDefinedTheme, + exportTheme + } +} diff --git a/src/style.css b/src/style.css index f691315..1b18993 100644 --- a/src/style.css +++ b/src/style.css @@ -1,79 +1,5 @@ -:root { - font-family: system-ui, Avenir, Helvetica, Arial, sans-serif; - line-height: 1.5; - font-weight: 400; - - color-scheme: light dark; - color: rgba(255, 255, 255, 0.87); - background-color: #242424; - - font-synthesis: none; - text-rendering: optimizeLegibility; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; -} - -a { - font-weight: 500; - color: #646cff; - text-decoration: inherit; -} -a:hover { - color: #535bf2; -} - body { + width: 100%; + height: 100%; margin: 0; - display: flex; - place-items: center; - min-width: 320px; - min-height: 100vh; -} - -h1 { - font-size: 3.2em; - line-height: 1.1; -} - -button { - border-radius: 8px; - border: 1px solid transparent; - padding: 0.6em 1.2em; - font-size: 1em; - font-weight: 500; - font-family: inherit; - background-color: #1a1a1a; - cursor: pointer; - transition: border-color 0.25s; -} -button:hover { - border-color: #646cff; -} -button:focus, -button:focus-visible { - outline: 4px auto -webkit-focus-ring-color; -} - -.card { - padding: 2em; -} - -#app { - max-width: 1280px; - margin: 0 auto; - padding: 2rem; - text-align: center; -} - -@media (prefers-color-scheme: light) { - :root { - color: #213547; - background-color: #ffffff; - } - a:hover { - color: #747bff; - } - button { - background-color: #f9f9f9; - } } diff --git a/src/types/theme.ts b/src/types/theme.ts new file mode 100644 index 0000000..fe65437 --- /dev/null +++ b/src/types/theme.ts @@ -0,0 +1,104 @@ +// ECharts 主题类型定义 +export interface ThemeAxis { + type: string + name: string + axisLineShow: boolean + axisLineColor: string + axisTickShow: boolean + axisTickColor: string + axisLabelShow: boolean + axisLabelColor: string + splitLineShow: boolean + splitLineColor: string[] + splitAreaShow: boolean + splitAreaColor: string[] +} + +export interface ThemeData { + // 基础配置 + seriesCnt: number + backgroundColor: string + titleColor: string + subtitleColor: string + textColorShow: boolean + textColor: string + markTextColor: string + + // 主色板 + color: string[] + + // 边框 + borderColor: string + borderWidth: number + + // 视觉映射 + visualMapColor: string[] + + // 图例 + legendTextColor: string + + // K线图 + kColor: string + kColor0: string + kBorderColor: string + kBorderColor0: string + kBorderWidth: number + + // 线条 + lineWidth: number + symbolSize: number + symbol: string + symbolBorderWidth: number + lineSmooth: boolean + + // 关系图 + graphLineWidth: number + graphLineColor: string + + // 地图 + mapLabelColor: string + mapLabelColorE: string + mapBorderColor: string + mapBorderColorE: string + mapBorderWidth: number + mapBorderWidthE: number + mapAreaColor: string + mapAreaColorE: string + + // 坐标轴 + axes: ThemeAxis[] + axisSeperateSetting: boolean + axis: ThemeAxis[] | null + + // 工具箱 + toolboxColor: string + toolboxEmphasisColor: string + + // 提示框 + tooltipAxisColor: string + tooltipAxisWidth: number + + // 时间轴 + timelineLineColor: string + timelineLineWidth: number + timelineItemColor: string + timelineItemColorE: string + timelineCheckColor: string + timelineCheckBorderColor: string + timelineItemBorderWidth: number + timelineControlColor: string + timelineControlBorderColor: string + timelineControlBorderWidth: number + timelineLabelColor: string +} + +export interface PreDefinedTheme { + name: string + background: string + theme: string[] +} + +export interface ChartDisplay { + background: string + title: string +} diff --git a/src/utils/chartConfigs.ts b/src/utils/chartConfigs.ts new file mode 100644 index 0000000..7ce05c3 --- /dev/null +++ b/src/utils/chartConfigs.ts @@ -0,0 +1,609 @@ +import type { EChartsOption } from 'echarts' + +// Chart configuration interface +interface ChartConfig { + title: string + subtitle?: string + type: string + option: EChartsOption +} + +// Generate random data helper functions +function getRandomValue(max: number = 1000, min: number = 100): number { + return Math.floor(Math.random() * (max - min) + min) +} + +function getRandomArray(length: number, max: number = 1000, min: number = 100): number[] { + return Array.from({ length }, () => getRandomValue(max, min)) +} + +// Chart configurations +export function getChartConfigs(seriesCnt: number = 4): ChartConfig[] { + const axisCat = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'] + const legendData = Array.from({ length: seriesCnt }, (_, i) => `Series ${i + 1}`) + + const commonGrid = { + left: 60, + right: 20, + top: 60, + bottom: 50 + } + + const commonTooltip = { + trigger: 'axis' as const + } + + const commonToolbox = { + feature: { + restore: { show: true }, + saveAsImage: { show: true }, + dataView: { show: true }, + dataZoom: { show: true } + } + } + + const configs: ChartConfig[] = [ + // Line Chart + { + title: 'Line Chart', + subtitle: 'Basic line chart example', + type: 'line', + option: { + title: { + text: 'Line Chart', + subtext: 'Basic line chart example' + }, + legend: { + data: legendData, + right: 0 + }, + grid: commonGrid, + tooltip: commonTooltip, + toolbox: commonToolbox, + xAxis: { + type: 'category', + data: axisCat + }, + yAxis: { + type: 'value' + }, + series: legendData.map(name => ({ + name, + type: 'line' as const, + data: getRandomArray(axisCat.length, 800, 200), + markPoint: { + data: [{ name: 'Max', type: 'max' }] + } + })) + } + }, + + // Stacked Area Chart + { + title: 'Stacked Area Chart', + subtitle: 'Stacked area chart example', + type: 'area', + option: { + title: { + text: 'Stacked Area Chart', + subtext: 'Stacked area chart example' + }, + legend: { + data: legendData, + right: 0 + }, + grid: commonGrid, + tooltip: commonTooltip, + toolbox: commonToolbox, + xAxis: { + type: 'category', + data: axisCat, + boundaryGap: false + }, + yAxis: { + type: 'value' + }, + series: legendData.map(name => ({ + name, + type: 'line' as const, + data: getRandomArray(axisCat.length, 800, 200), + areaStyle: {}, + stack: 'total' + })) + } + }, + + // Bar Chart + { + title: 'Bar Chart', + subtitle: 'Basic bar chart example', + type: 'bar', + option: { + title: { + text: 'Bar Chart', + subtext: 'Basic bar chart example' + }, + legend: { + data: legendData, + right: 0 + }, + grid: commonGrid, + tooltip: commonTooltip, + toolbox: commonToolbox, + xAxis: { + type: 'category', + data: axisCat + }, + yAxis: { + type: 'value' + }, + series: legendData.map(name => ({ + name, + type: 'bar' as const, + data: getRandomArray(axisCat.length, 800, 200), + markPoint: { + data: [{ name: 'Max', type: 'max' }] + } + })) + } + }, + + // Stacked Bar Chart + { + title: 'Stacked Bar Chart', + subtitle: 'Stacked bar chart example', + type: 'stackedBar', + option: { + title: { + text: 'Stacked Bar Chart', + subtext: 'Stacked bar chart example' + }, + legend: { + data: legendData, + right: 0 + }, + grid: commonGrid, + tooltip: commonTooltip, + toolbox: commonToolbox, + xAxis: { + type: 'category', + data: axisCat + }, + yAxis: { + type: 'value' + }, + series: legendData.map(name => ({ + name, + type: 'bar' as const, + data: getRandomArray(axisCat.length, 800, 200), + stack: 'total' + })) + } + }, + + // Scatter Chart + { + title: 'Scatter Chart', + subtitle: 'Basic scatter chart example', + type: 'scatter', + option: { + title: { + text: 'Scatter Chart', + subtext: 'Basic scatter chart example' + }, + legend: { + data: legendData, + right: 0 + }, + grid: commonGrid, + tooltip: { + trigger: 'item' as const + }, + toolbox: commonToolbox, + xAxis: { + type: 'value' + }, + yAxis: { + type: 'value' + }, + series: legendData.map(name => ({ + name, + type: 'scatter' as const, + data: Array.from({ length: 32 }, () => [ + getRandomValue(600, 100), + getRandomValue(600, 100) + ]) + })) + } + }, + + // Pie Chart + { + title: 'Pie Chart', + subtitle: 'Basic pie chart example', + type: 'pie', + option: { + title: { + text: 'Pie Chart', + subtext: 'Basic pie chart example' + }, + legend: { + data: legendData, + right: 0 + }, + tooltip: { + trigger: 'item', + formatter: '{a} <br/>{b}: {c} ({d}%)' + }, + toolbox: commonToolbox, + series: [{ + name: 'Data', + type: 'pie' as const, + radius: '50%', + data: legendData.map(name => ({ + name, + value: getRandomValue(800, 200) + })), + emphasis: { + itemStyle: { + shadowBlur: 10, + shadowOffsetX: 0, + shadowColor: 'rgba(0, 0, 0, 0.5)' + } + } + }] + } + }, + + // Radar Chart + { + title: 'Radar Chart', + subtitle: 'Basic radar chart example', + type: 'radar', + option: { + title: { + text: 'Radar Chart', + subtext: 'Basic radar chart example' + }, + legend: { + data: legendData, + right: 0 + }, + tooltip: commonTooltip, + toolbox: commonToolbox, + radar: { + indicator: axisCat.map(name => ({ name, max: 1000 })), + center: ['50%', '60%'] + }, + series: legendData.map(name => ({ + name, + type: 'radar' as const, + data: [{ + value: getRandomArray(axisCat.length, 800, 200), + name + }] + })) + } + }, + + // Candlestick Chart (K-Line) + { + title: 'Candlestick Chart', + subtitle: 'K-line chart with data zoom', + type: 'candlestick', + option: { + title: { + text: 'Candlestick Chart', + subtext: 'K-line chart with data zoom' + }, + grid: { + left: 60, + right: 20, + top: 40, + bottom: 70 + }, + tooltip: { + trigger: 'axis' as const + }, + toolbox: { + show: true, + feature: { + dataZoom: { show: true }, + dataView: { show: true }, + restore: { show: true } + } + }, + dataZoom: { + show: true, + realtime: true, + start: 50, + end: 100 + }, + xAxis: { + type: 'category', + data: Array.from({ length: 20 }, (_, i) => `Day ${i + 1}`) + }, + yAxis: { + type: 'value', + scale: true + }, + series: [{ + name: 'Stock Price', + type: 'candlestick' as const, + data: Array.from({ length: 20 }, () => { + const open = getRandomValue(2400, 2200) + const close = getRandomValue(2400, 2200) + const low = Math.min(open, close) - getRandomValue(50, 0) + const high = Math.max(open, close) + getRandomValue(50, 0) + return [open, close, low, high] + }) + }] + } + }, + + // Heatmap + { + title: 'Heatmap', + subtitle: 'Basic heatmap example', + type: 'heatmap', + option: { + title: { + text: 'Heatmap', + subtext: 'Basic heatmap example' + }, + grid: { + left: 90, + right: 20, + top: 40, + bottom: 40 + }, + tooltip: { + trigger: 'item' as const + }, + toolbox: commonToolbox, + xAxis: { + type: 'category', + data: ['12a', '1a', '2a', '3a', '4a', '5a', '6a', '7a', '8a', '9a', '10a', '11a'] + }, + yAxis: { + type: 'category', + data: ['Sat', 'Fri', 'Thu', 'Wed', 'Tue', 'Mon', 'Sun'] + }, + visualMap: { + min: 1, + max: 10, + calculable: true + }, + series: [{ + name: 'Heat', + type: 'heatmap' as const, + data: Array.from({ length: 84 }, (_, i) => { + const x = i % 12 + const y = Math.floor(i / 12) + return [x, y, getRandomValue(10, 1)] + }), + label: { + show: true + } + }] + } + }, + + // Treemap + { + title: 'Treemap', + subtitle: 'Basic treemap example', + type: 'treemap', + option: { + title: { + text: 'Treemap', + subtext: 'Basic treemap example' + }, + tooltip: { + trigger: 'item' as const, + formatter: '{b}: {c}' + }, + toolbox: commonToolbox, + series: [{ + type: 'treemap' as const, + data: [ + { + name: 'Category A', + value: getRandomValue(1000, 500), + children: [ + { name: 'A1', value: getRandomValue(300, 100) }, + { name: 'A2', value: getRandomValue(300, 100) }, + { name: 'A3', value: getRandomValue(300, 100) } + ] + }, + { + name: 'Category B', + value: getRandomValue(1000, 500), + children: [ + { name: 'B1', value: getRandomValue(300, 100) }, + { name: 'B2', value: getRandomValue(300, 100) } + ] + }, + { + name: 'Category C', + value: getRandomValue(1000, 500), + children: [ + { name: 'C1', value: getRandomValue(300, 100) }, + { name: 'C2', value: getRandomValue(300, 100) }, + { name: 'C3', value: getRandomValue(300, 100) }, + { name: 'C4', value: getRandomValue(300, 100) } + ] + } + ] + }] + } + }, + + // Graph/Network Chart + { + title: 'Graph Chart', + subtitle: 'Network graph example', + type: 'graph', + option: { + title: { + text: 'Graph Chart', + subtext: 'Network graph example' + }, + tooltip: { + trigger: 'item' as const + }, + toolbox: commonToolbox, + legend: { + data: ['Category 0', 'Category 1', 'Category 2', 'Category 3'] + }, + series: [{ + type: 'graph' as const, + layout: 'force', + roam: true, + label: { + show: true, + fontSize: 12 + }, + force: { + repulsion: 400, + edgeLength: 150 + }, + categories: [ + { name: 'Category 0' }, + { name: 'Category 1' }, + { name: 'Category 2' }, + { name: 'Category 3' } + ], + data: [ + { id: '0', name: 'Node 1', symbolSize: 30, value: 30, category: 0 }, + { id: '1', name: 'Node 2', symbolSize: 25, value: 25, category: 1 }, + { id: '2', name: 'Node 3', symbolSize: 35, value: 35, category: 0 }, + { id: '3', name: 'Node 4', symbolSize: 20, value: 20, category: 2 }, + { id: '4', name: 'Node 5', symbolSize: 40, value: 40, category: 1 }, + { id: '5', name: 'Node 6', symbolSize: 28, value: 28, category: 3 }, + { id: '6', name: 'Node 7', symbolSize: 32, value: 32, category: 2 }, + { id: '7', name: 'Node 8', symbolSize: 22, value: 22, category: 0 }, + { id: '8', name: 'Node 9', symbolSize: 38, value: 38, category: 3 }, + { id: '9', name: 'Node 10', symbolSize: 26, value: 26, category: 1 } + ], + links: [ + { source: '0', target: '1' }, + { source: '0', target: '2' }, + { source: '1', target: '3' }, + { source: '2', target: '4' }, + { source: '3', target: '5' }, + { source: '4', target: '6' }, + { source: '5', target: '7' }, + { source: '6', target: '8' }, + { source: '7', target: '9' }, + { source: '8', target: '0' }, + { source: '9', target: '2' }, + { source: '1', target: '4' }, + { source: '3', target: '7' }, + { source: '5', target: '9' } + ], + lineStyle: { + curveness: 0.3 + } + }] + } + }, + + // Timeline Chart + { + title: 'Timeline Chart', + subtitle: 'Timeline with multiple series', + type: 'timeline', + option: { + baseOption: { + timeline: { + axisType: 'category', + autoPlay: false, + data: ['2020', '2021', '2022', '2023'], + label: { + formatter: (value: string | number) => value.toString() + } + }, + title: { + text: 'Timeline Chart', + subtext: 'Timeline with multiple series' + }, + tooltip: { + trigger: 'axis' as const + }, + legend: { + data: ['Primary', 'Secondary', 'Tertiary'], + right: 0 + }, + grid: { + top: 80, + bottom: 100, + left: 60, + right: 20 + }, + xAxis: { + type: 'category', + data: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun'], + splitLine: { show: false } + }, + yAxis: { + type: 'value', + name: 'Value' + }, + series: [ + { name: 'Primary', type: 'bar' as const }, + { name: 'Secondary', type: 'bar' as const }, + { name: 'Tertiary', type: 'bar' as const } + ] + }, + options: [ + { + title: { text: 'Timeline Chart - 2020' }, + series: [ + { data: getRandomArray(6, 800, 400) }, + { data: getRandomArray(6, 600, 300) }, + { data: getRandomArray(6, 400, 200) } + ] + }, + { + title: { text: 'Timeline Chart - 2021' }, + series: [ + { data: getRandomArray(6, 900, 500) }, + { data: getRandomArray(6, 700, 400) }, + { data: getRandomArray(6, 500, 300) } + ] + }, + { + title: { text: 'Timeline Chart - 2022' }, + series: [ + { data: getRandomArray(6, 1000, 600) }, + { data: getRandomArray(6, 800, 500) }, + { data: getRandomArray(6, 600, 400) } + ] + }, + { + title: { text: 'Timeline Chart - 2023' }, + series: [ + { data: getRandomArray(6, 1100, 700) }, + { data: getRandomArray(6, 900, 600) }, + { data: getRandomArray(6, 700, 500) } + ] + } + ] + } + } + ] + + return configs +} + +// Get specific chart by type +export function getChartByType(type: string, seriesCnt: number = 4): ChartConfig | undefined { + const configs = getChartConfigs(seriesCnt) + return configs.find(config => config.type === type) +} + +// Get all available chart types +export function getAvailableChartTypes(): string[] { + return ['line', 'area', 'bar', 'stackedBar', 'scatter', 'pie', 'radar', 'candlestick', 'heatmap', 'treemap', 'graph', 'timeline'] +} diff --git a/src/utils/chartOptions.ts b/src/utils/chartOptions.ts new file mode 100644 index 0000000..427d9bd --- /dev/null +++ b/src/utils/chartOptions.ts @@ -0,0 +1,328 @@ +import type { ThemeData } from '../types/theme' + +export interface ChartOption { + title?: any + legend?: any + tooltip?: any + xAxis?: any + yAxis?: any + series?: any[] + toolbox?: any + grid?: any + [key: string]: any +} + +export const generateChartOptions = (theme: ThemeData) => { + const groupCnt = theme.seriesCnt + const axisCat = ['周一', '周二', '周三', '周四', '周五', '周六', '周日'] + const dataLength = axisCat.length + + const getLegend = () => { + const data = [] + for (let i = 0; i < groupCnt; i++) { + data.push('第' + (i + 1) + '组') + } + return data + } + + const getSeriesRandomValue = (typeName: string) => { + const data = [] + const dlen = typeName === 'scatter' ? 32 : dataLength + + for (let i = 0; i < groupCnt; i++) { + const group = [] + for (let j = 0; j < dlen; j++) { + let v: any + if (typeName === 'scatter') { + v = [ + Math.floor((Math.random() * 600 + 400) * (groupCnt - i) / groupCnt), + Math.floor((Math.random() * 600 + 400) * (groupCnt - i) / groupCnt) + ] + } else { + v = Math.floor((Math.random() * 600 + 400) * (groupCnt - i) / groupCnt) + } + group.push(v) + } + + data.push({ + type: typeName, + data: typeName === 'radar' ? [group] : group, + name: '第' + (i + 1) + '组', + markPoint: ['line', 'bar', 'scatter'].includes(typeName) ? { + data: [{ + name: '最高', + type: 'max' + }] + } : undefined + }) + } + return data + } + + const getSeriesRandomStack = (typeName: string) => { + const data = getSeriesRandomValue(typeName) + data.forEach((item: any) => { + item.areaStyle = { normal: {} } + item.stack = 'total' + }) + return data + } + + const getSeriesRandomGroup = (typeName: string) => { + const data = [] + for (let i = 0; i < groupCnt; i++) { + data.push({ + name: getLegend()[i], + value: Math.floor((Math.random() * 800 + 200) * (groupCnt - i) / groupCnt) + }) + } + return { + type: typeName, + data: data + } + } + + const getIndicator = () => { + return axisCat.map(name => ({ + name, + max: 1000 + })) + } + + // 基础配置 + const baseOptions = { + title: { + text: '示例图表', + textStyle: { + color: theme.titleColor + } + }, + legend: { + data: getLegend(), + right: 20, + textStyle: { + color: theme.legendTextColor + } + }, + tooltip: { + trigger: 'axis', + axisPointer: { + lineStyle: { + color: theme.tooltipAxisColor, + width: theme.tooltipAxisWidth + } + } + }, + toolbox: { + feature: { + restore: { show: true }, + saveAsImage: { show: true } + }, + iconStyle: { + normal: { + borderColor: theme.toolboxColor + }, + emphasis: { + borderColor: theme.toolboxEmphasisColor + } + } + }, + grid: { + left: '3%', + right: '4%', + bottom: '3%', + containLabel: true + } + } + + // 图表配置集合 + const chartOptions: { [key: string]: ChartOption } = { + // 柱状图 + bar: { + ...baseOptions, + xAxis: { + type: 'category', + data: axisCat, + axisLine: { + show: theme.axes[1].axisLineShow, + lineStyle: { color: theme.axes[1].axisLineColor } + }, + axisTick: { + show: theme.axes[1].axisTickShow, + lineStyle: { color: theme.axes[1].axisTickColor } + }, + axisLabel: { + show: theme.axes[1].axisLabelShow, + color: theme.axes[1].axisLabelColor + } + }, + yAxis: { + type: 'value', + axisLine: { + show: theme.axes[2].axisLineShow, + lineStyle: { color: theme.axes[2].axisLineColor } + }, + axisTick: { + show: theme.axes[2].axisTickShow, + lineStyle: { color: theme.axes[2].axisTickColor } + }, + axisLabel: { + show: theme.axes[2].axisLabelShow, + color: theme.axes[2].axisLabelColor + }, + splitLine: { + show: theme.axes[2].splitLineShow, + lineStyle: { color: theme.axes[2].splitLineColor } + } + }, + series: getSeriesRandomValue('bar') + }, + + // 折线图 + line: { + ...baseOptions, + xAxis: { + type: 'category', + data: axisCat, + axisLine: { + show: theme.axes[1].axisLineShow, + lineStyle: { color: theme.axes[1].axisLineColor } + }, + axisTick: { + show: theme.axes[1].axisTickShow, + lineStyle: { color: theme.axes[1].axisTickColor } + }, + axisLabel: { + show: theme.axes[1].axisLabelShow, + color: theme.axes[1].axisLabelColor + } + }, + yAxis: { + type: 'value', + axisLine: { + show: theme.axes[2].axisLineShow, + lineStyle: { color: theme.axes[2].axisLineColor } + }, + axisTick: { + show: theme.axes[2].axisTickShow, + lineStyle: { color: theme.axes[2].axisTickColor } + }, + axisLabel: { + show: theme.axes[2].axisLabelShow, + color: theme.axes[2].axisLabelColor + }, + splitLine: { + show: theme.axes[2].splitLineShow, + lineStyle: { color: theme.axes[2].splitLineColor } + } + }, + series: getSeriesRandomValue('line') + }, + + // 饼图 + pie: { + ...baseOptions, + tooltip: { + trigger: 'item', + formatter: '{a} <br/>{b}: {c} ({d}%)' + }, + series: [getSeriesRandomGroup('pie')] + }, + + // 散点图 + scatter: { + ...baseOptions, + xAxis: { + type: 'value', + axisLine: { + show: theme.axes[2].axisLineShow, + lineStyle: { color: theme.axes[2].axisLineColor } + }, + axisTick: { + show: theme.axes[2].axisTickShow, + lineStyle: { color: theme.axes[2].axisTickColor } + }, + axisLabel: { + show: theme.axes[2].axisLabelShow, + color: theme.axes[2].axisLabelColor + }, + splitLine: { + show: theme.axes[2].splitLineShow, + lineStyle: { color: theme.axes[2].splitLineColor } + } + }, + yAxis: { + type: 'value', + axisLine: { + show: theme.axes[2].axisLineShow, + lineStyle: { color: theme.axes[2].axisLineColor } + }, + axisTick: { + show: theme.axes[2].axisTickShow, + lineStyle: { color: theme.axes[2].axisTickColor } + }, + axisLabel: { + show: theme.axes[2].axisLabelShow, + color: theme.axes[2].axisLabelColor + }, + splitLine: { + show: theme.axes[2].splitLineShow, + lineStyle: { color: theme.axes[2].splitLineColor } + } + }, + series: getSeriesRandomValue('scatter') + }, + + // 雷达图 + radar: { + ...baseOptions, + radar: { + indicator: getIndicator() + }, + series: getSeriesRandomValue('radar') + }, + + // 面积图 + area: { + ...baseOptions, + xAxis: { + type: 'category', + data: axisCat, + axisLine: { + show: theme.axes[1].axisLineShow, + lineStyle: { color: theme.axes[1].axisLineColor } + }, + axisTick: { + show: theme.axes[1].axisTickShow, + lineStyle: { color: theme.axes[1].axisTickColor } + }, + axisLabel: { + show: theme.axes[1].axisLabelShow, + color: theme.axes[1].axisLabelColor + } + }, + yAxis: { + type: 'value', + axisLine: { + show: theme.axes[2].axisLineShow, + lineStyle: { color: theme.axes[2].axisLineColor } + }, + axisTick: { + show: theme.axes[2].axisTickShow, + lineStyle: { color: theme.axes[2].axisTickColor } + }, + axisLabel: { + show: theme.axes[2].axisLabelShow, + color: theme.axes[2].axisLabelColor + }, + splitLine: { + show: theme.axes[2].splitLineShow, + lineStyle: { color: theme.axes[2].splitLineColor } + } + }, + series: getSeriesRandomStack('line') + } + } + + return chartOptions +} --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
