diff --git a/ReportEngine/renderers/chart_to_svg.py b/ReportEngine/renderers/chart_to_svg.py index b892aa4..94592aa 100644 --- a/ReportEngine/renderers/chart_to_svg.py +++ b/ReportEngine/renderers/chart_to_svg.py @@ -167,6 +167,11 @@ class ChartToSVGConverter: return None # 根据图表类型调用相应的渲染方法 + if 'wordcloud' in str(chart_type).lower(): + # 词云由专用渲染逻辑处理,这里跳过SVG转换以避免告警 + logger.debug("检测到词云图表,跳过chart_to_svg转换") + return None + render_method = getattr(self, f'_render_{chart_type}', None) if not render_method: logger.warning(f"不支持的图表类型: {chart_type}") diff --git a/ReportEngine/renderers/html_renderer.py b/ReportEngine/renderers/html_renderer.py index 4c21712..2376eb6 100644 --- a/ReportEngine/renderers/html_renderer.py +++ b/ReportEngine/renderers/html_renderer.py @@ -1628,57 +1628,61 @@ class HTMLRenderer: if is_chart: self.chart_validation_stats['total'] += 1 - # 如果此前已记录失败,直接使用占位提示,避免重复修复 - has_failed, cached_reason = self._has_chart_failure(block) - if has_failed: - self._record_chart_failure_stat(cache_key) - reason = cached_reason or "LLM返回的图表信息格式有误,无法正常显示" - return self._render_chart_error_placeholder(display_title, reason, widget_id) - - # 验证图表数据 - validation_result = self.chart_validator.validate(block) - - if not validation_result.is_valid: - logger.warning( - f"图表 {block.get('widgetId', 'unknown')} 验证失败: {validation_result.errors}" - ) - - # 尝试修复 - repair_result = self.chart_repairer.repair(block, validation_result) - - if repair_result.success and repair_result.repaired_block: - # 修复成功,使用修复后的数据 - block = repair_result.repaired_block - logger.info( - f"图表 {block.get('widgetId', 'unknown')} 修复成功 " - f"(方法: {repair_result.method}): {repair_result.changes}" - ) - - # 更新统计 - if repair_result.method == 'local': - self.chart_validation_stats['repaired_locally'] += 1 - elif repair_result.method == 'api': - self.chart_validation_stats['repaired_api'] += 1 - else: - # 修复失败,记录失败并输出占位提示 - fail_reason = self._format_chart_error_reason(validation_result) - block["_chart_renderable"] = False - block["_chart_error_reason"] = fail_reason - self._note_chart_failure(cache_key, fail_reason) - self._record_chart_failure_stat(cache_key) - logger.warning( - f"图表 {block.get('widgetId', 'unknown')} 修复失败,已跳过渲染: {fail_reason}" - ) - return self._render_chart_error_placeholder(display_title, fail_reason, widget_id) - else: - # 验证通过 + # 词云使用专用渲染逻辑,不按Chart.js规则验证,直接跳过防止误判 + if is_wordcloud: self.chart_validation_stats['valid'] += 1 - if validation_result.warnings: - logger.info( - f"图表 {block.get('widgetId', 'unknown')} 验证通过," - f"但有警告: {validation_result.warnings}" + else: + # 如果此前已记录失败,直接使用占位提示,避免重复修复 + has_failed, cached_reason = self._has_chart_failure(block) + if has_failed: + self._record_chart_failure_stat(cache_key) + reason = cached_reason or "LLM返回的图表信息格式有误,无法正常显示" + return self._render_chart_error_placeholder(display_title, reason, widget_id) + + # 验证图表数据 + validation_result = self.chart_validator.validate(block) + + if not validation_result.is_valid: + logger.warning( + f"图表 {block.get('widgetId', 'unknown')} 验证失败: {validation_result.errors}" ) + # 尝试修复 + repair_result = self.chart_repairer.repair(block, validation_result) + + if repair_result.success and repair_result.repaired_block: + # 修复成功,使用修复后的数据 + block = repair_result.repaired_block + logger.info( + f"图表 {block.get('widgetId', 'unknown')} 修复成功 " + f"(方法: {repair_result.method}): {repair_result.changes}" + ) + + # 更新统计 + if repair_result.method == 'local': + self.chart_validation_stats['repaired_locally'] += 1 + elif repair_result.method == 'api': + self.chart_validation_stats['repaired_api'] += 1 + else: + # 修复失败,记录失败并输出占位提示 + fail_reason = self._format_chart_error_reason(validation_result) + block["_chart_renderable"] = False + block["_chart_error_reason"] = fail_reason + self._note_chart_failure(cache_key, fail_reason) + self._record_chart_failure_stat(cache_key) + logger.warning( + f"图表 {block.get('widgetId', 'unknown')} 修复失败,已跳过渲染: {fail_reason}" + ) + return self._render_chart_error_placeholder(display_title, fail_reason, widget_id) + else: + # 验证通过 + self.chart_validation_stats['valid'] += 1 + if validation_result.warnings: + logger.info( + f"图表 {block.get('widgetId', 'unknown')} 验证通过," + f"但有警告: {validation_result.warnings}" + ) + # 渲染图表HTML self.chart_counter += 1 canvas_id = f"chart-{self.chart_counter}" @@ -1700,7 +1704,7 @@ class HTMLRenderer: title = props.get("title") title_html = f'
{self._escape_html(title)}
' if title else "" fallback_html = ( - self._render_wordcloud_fallback(props, block.get("widgetId")) + self._render_wordcloud_fallback(props, block.get("widgetId"), block.get("data")) if is_wordcloud else self._render_widget_fallback(normalized_data, block.get("widgetId")) ) @@ -1750,20 +1754,57 @@ class HTMLRenderer: """ return table_html - def _render_wordcloud_fallback(self, props: Dict[str, Any] | None, widget_id: str | None = None) -> str: + def _render_wordcloud_fallback( + self, + props: Dict[str, Any] | None, + widget_id: str | None = None, + block_data: Any | None = None, + ) -> str: """为词云提供表格兜底,避免WordCloud渲染失败后页面空白""" - words = [] - if isinstance(props, dict): - raw = props.get("data") + def _collect_items(raw: Any) -> list[dict]: + collected: list[dict] = [] if isinstance(raw, list): for item in raw: - if not isinstance(item, dict): - continue - text = item.get("word") or item.get("text") or item.get("label") - weight = item.get("weight") - category = item.get("category") or "" - if text: - words.append({"word": str(text), "weight": weight, "category": str(category)}) + if isinstance(item, dict): + text = item.get("word") or item.get("text") or item.get("label") + weight = item.get("weight") + category = item.get("category") or "" + if text: + collected.append({"word": str(text), "weight": weight, "category": str(category)}) + elif isinstance(item, (list, tuple)) and item: + text = item[0] + weight = item[1] if len(item) > 1 else None + category = item[2] if len(item) > 2 else "" + if text: + collected.append({"word": str(text), "weight": weight, "category": str(category)}) + elif isinstance(item, str): + collected.append({"word": item, "weight": 1.0, "category": ""}) + elif isinstance(raw, dict): + if not {"labels", "datasets"}.intersection(raw.keys()): + for text, weight in raw.items(): + collected.append({"word": str(text), "weight": weight, "category": ""}) + return collected + + words: list[dict] = [] + seen: set[str] = set() + candidates = [] + if isinstance(props, dict): + for key in ("data", "items", "words"): + if key in props: + candidates.append(props[key]) + candidates.append((props or {}).get("sourceData")) + + # 允许使用block.data兜底,避免缺失props时出现空白 + if block_data is not None: + candidates.append(block_data) + + for raw in candidates: + for item in _collect_items(raw): + key = f"{item['word']}::{item.get('category','')}" + if key in seen: + continue + seen.add(key) + words.append(item) if not words: return "" @@ -2712,11 +2753,11 @@ table th {{ line-height: 1.6; }} .chart-card.wordcloud-card .chart-container {{ - min-height: 260px; + min-height: 180px; }} .chart-container {{ position: relative; - min-height: 320px; + min-height: 220px; }} .chart-fallback {{ display: none; @@ -2743,6 +2784,31 @@ table th {{ .chart-fallback th {{ background: rgba(0,0,0,0.04); }} +.wordcloud-fallback .wordcloud-badges {{ + display: flex; + flex-wrap: wrap; + gap: 6px; + margin-top: 6px; +}} +.wordcloud-badge {{ + display: inline-flex; + align-items: center; + gap: 4px; + padding: 4px 8px; + border-radius: 999px; + border: 1px solid rgba(74, 144, 226, 0.35); + color: var(--text-color); + background: linear-gradient(135deg, rgba(74, 144, 226, 0.14) 0%, rgba(74, 144, 226, 0.24) 100%); + box-shadow: 0 4px 10px rgba(15, 23, 42, 0.06); +}} +.dark-mode .wordcloud-badge {{ + box-shadow: 0 6px 16px rgba(0, 0, 0, 0.35); +}} +.wordcloud-badge small {{ + color: var(--secondary-color); + font-weight: 600; + font-size: 0.75rem; +}} .chart-note {{ margin-top: 8px; font-size: 0.85rem; @@ -3039,6 +3105,45 @@ function liftDarkColor(color) { return normalized; } +function mixColors(colorA, colorB, amount) { + const rgbA = rgbFromColor(colorA); + const rgbB = rgbFromColor(colorB); + if (!rgbA && !rgbB) return colorA || colorB; + if (!rgbA) return colorB; + if (!rgbB) return colorA; + const t = Math.min(1, Math.max(0, amount || 0)); + const mixed = rgbA.map((v, idx) => Math.round(v * (1 - t) + rgbB[idx] * t)); + return `rgb(${mixed[0]}, ${mixed[1]}, ${mixed[2]})`; +} + +function pickComputedColor(keys, fallback, styles) { + const styleRef = styles || getComputedStyle(document.body); + for (const key of keys) { + const val = styleRef.getPropertyValue(key); + if (val && val.trim()) { + const normalized = normalizeColorToken(val.trim()); + if (normalized) return normalized; + } + } + return fallback; +} + +function resolveWordcloudTheme() { + const styles = getComputedStyle(document.body); + const isDark = document.body.classList.contains('dark-mode'); + const text = pickComputedColor(['--text-color'], isDark ? '#e5e7eb' : '#111827', styles); + const secondary = pickComputedColor(['--secondary-color', '--color-text-secondary'], isDark ? '#cbd5e1' : '#475569', styles); + const accent = liftDarkColor( + pickComputedColor(['--primary-color', '--color-accent', '--re-accent-color'], '#4A90E2', styles) + ); + const cardBg = pickComputedColor( + ['--card-bg', '--paper-bg', '--bg', '--bg-color', '--background', '--page-bg'], + isDark ? '#0f172a' : '#ffffff', + styles + ); + return { text, secondary, accent, cardBg, isDark }; +} + function normalizeDatasetColors(payload, chartType) { const changes = []; const data = payload && payload.data; @@ -3246,29 +3351,79 @@ function isWordCloudWidget(payload) { return typeof type === 'string' && type.toLowerCase().includes('wordcloud'); } +function hashString(str) { + let h = 0; + if (!str) return h; + for (let i = 0; i < str.length; i++) { + h = (h << 5) - h + str.charCodeAt(i); + h |= 0; + } + return h; +} + function normalizeWordcloudItems(payload) { - const source = payload && payload.props && payload.props.data; - if (!Array.isArray(source)) return []; - return source.map(item => { - if (!item || typeof item !== 'object') return null; - const word = item.word || item.text || item.label; - if (!word) return null; - const rawWeight = item.weight; - let weight = 0; - if (typeof rawWeight === 'number' && !Number.isNaN(rawWeight)) { - weight = rawWeight; - } else if (typeof rawWeight === 'string') { - const parsed = parseFloat(rawWeight); - weight = Number.isNaN(parsed) ? 0 : parsed; + const sources = []; + const props = payload && payload.props; + const dataField = payload && payload.data; + if (props) { + ['data', 'items', 'words', 'sourceData'].forEach(key => { + if (props[key]) sources.push(props[key]); + }); + } + if (dataField) { + sources.push(dataField); + } + + const seen = new Map(); + const pushItem = (word, weight, category) => { + if (!word) return; + let numeric = 1; + if (typeof weight === 'number' && Number.isFinite(weight)) { + numeric = weight; + } else if (typeof weight === 'string') { + const parsed = parseFloat(weight); + numeric = Number.isFinite(parsed) ? parsed : 1; } - const category = (item.category || '').toString().toLowerCase(); - return { word: String(word), weight, category }; - }).filter(Boolean); + if (!(numeric > 0)) numeric = 1; + const cat = (category || '').toString().toLowerCase(); + const key = `${word}__${cat}`; + const existing = seen.get(key); + const payloadItem = { word: String(word), weight: numeric, category: cat }; + if (!existing || numeric > existing.weight) { + seen.set(key, payloadItem); + } + }; + + const consume = (raw) => { + if (!raw) return; + if (Array.isArray(raw)) { + raw.forEach(item => { + if (!item) return; + if (Array.isArray(item)) { + pushItem(item[0], item[1], item[2]); + } else if (typeof item === 'object') { + pushItem(item.word || item.text || item.label, item.weight, item.category); + } else if (typeof item === 'string') { + pushItem(item, 1, ''); + } + }); + } else if (typeof raw === 'object') { + Object.entries(raw).forEach(([word, weight]) => pushItem(word, weight, '')); + } + }; + + sources.forEach(consume); + + const items = Array.from(seen.values()); + items.sort((a, b) => (b.weight || 0) - (a.weight || 0)); + return items.slice(0, 150); } function wordcloudColor(category) { const key = typeof category === 'string' ? category.toLowerCase() : ''; - return WORDCLOUD_CATEGORY_COLORS[key] || '#334155'; + const palette = resolveWordcloudTheme(); + const base = WORDCLOUD_CATEGORY_COLORS[key] || palette.accent || palette.secondary || '#334155'; + return liftDarkColor(base); } function renderWordCloudFallback(canvas, items, reason) { @@ -3282,26 +3437,44 @@ function renderWordCloudFallback(canvas, items, reason) { } else { canvas.style.display = 'none'; } - let fallback = card.querySelector('.chart-fallback'); + let fallback = card.querySelector('.chart-fallback[data-dynamic="true"]'); + if (!fallback) { + fallback = card.querySelector('.chart-fallback'); + } if (!fallback) { fallback = document.createElement('div'); - fallback.className = 'chart-fallback wordcloud-fallback'; - fallback.setAttribute('data-dynamic', 'true'); card.appendChild(fallback); } + fallback.className = 'chart-fallback wordcloud-fallback'; + fallback.setAttribute('data-dynamic', 'true'); fallback.style.display = 'block'; + fallback.innerHTML = ''; card.setAttribute('data-chart-state', 'fallback'); - if (reason) { - let notice = fallback.querySelector('.chart-fallback__notice'); - if (!notice) { - notice = document.createElement('p'); - notice.className = 'chart-fallback__notice'; - fallback.insertBefore(notice, fallback.firstChild || null); + const buildBadge = (item, maxWeight) => { + const badge = document.createElement('span'); + badge.className = 'wordcloud-badge'; + const clampedWeight = Math.max(0.5, (item.weight || 1)); + const normalized = Math.min(1, clampedWeight / (maxWeight || 1)); + const fontSize = 0.85 + normalized * 0.9; + badge.style.fontSize = `${fontSize}rem`; + badge.style.background = `linear-gradient(135deg, ${lightenColor(wordcloudColor(item.category), 0.05)} 0%, ${lightenColor(wordcloudColor(item.category), 0.15)} 100%)`; + badge.style.borderColor = lightenColor(wordcloudColor(item.category), 0.25); + badge.textContent = item.word; + if (item.weight !== undefined && item.weight !== null) { + const meta = document.createElement('small'); + meta.textContent = item.weight >= 0 && item.weight <= 1.5 + ? `${(item.weight * 100).toFixed(0)}%` + : item.weight.toFixed(1).replace(/\.0+$/, '').replace(/0+$/, '').replace(/\.$/, ''); + badge.appendChild(meta); } - notice.textContent = `词云未能渲染${reason ? `(${reason})` : ''},已展示数据表。`; - } - if (fallback.querySelector('table')) { - return; + return badge; + }; + + if (reason) { + const notice = document.createElement('p'); + notice.className = 'chart-fallback__notice'; + notice.textContent = `词云未能渲染${reason ? `(${reason})` : ''},已展示关键词列表。`; + fallback.appendChild(notice); } if (!items || !items.length) { const empty = document.createElement('p'); @@ -3309,39 +3482,13 @@ function renderWordCloudFallback(canvas, items, reason) { fallback.appendChild(empty); return; } - const table = document.createElement('table'); - const thead = document.createElement('thead'); - const headRow = document.createElement('tr'); - ['关键词', '权重', '类别'].forEach(text => { - const th = document.createElement('th'); - th.textContent = text; - headRow.appendChild(th); - }); - thead.appendChild(headRow); - table.appendChild(thead); - const tbody = document.createElement('tbody'); + const badges = document.createElement('div'); + badges.className = 'wordcloud-badges'; + const maxWeight = items.reduce((max, item) => Math.max(max, item.weight || 0), 1); items.forEach(item => { - const row = document.createElement('tr'); - const wordCell = document.createElement('td'); - wordCell.textContent = item.word; - const weightCell = document.createElement('td'); - if (typeof item.weight === 'number' && !Number.isNaN(item.weight)) { - weightCell.textContent = item.weight >= 0 && item.weight <= 1.5 - ? `${(item.weight * 100).toFixed(1)}%` - : item.weight.toFixed(2).replace(/\.0+$/, '').replace(/0+$/, '').replace(/\.$/, ''); - } else { - weightCell.textContent = item.weight !== undefined && item.weight !== null ? String(item.weight) : '—'; - } - const categoryCell = document.createElement('td'); - categoryCell.textContent = item.category || '—'; - categoryCell.style.color = wordcloudColor(item.category); - row.appendChild(wordCell); - row.appendChild(weightCell); - row.appendChild(categoryCell); - tbody.appendChild(row); + badges.appendChild(buildBadge(item, maxWeight)); }); - table.appendChild(tbody); - fallback.appendChild(table); + fallback.appendChild(badges); } function renderWordCloud(canvas, payload, skipRegistry) { @@ -3358,38 +3505,82 @@ function renderWordCloud(canvas, payload, skipRegistry) { renderWordCloudFallback(canvas, items, '词云依赖未加载'); return; } + const theme = resolveWordcloudTheme(); const dpr = Math.max(1, window.devicePixelRatio || 1); - const width = Math.max(240, (container ? container.clientWidth : canvas.clientWidth || canvas.width || 320)); - const height = Math.max(180, Math.round(width * 0.62)); + const width = Math.max(260, (container ? container.clientWidth : canvas.clientWidth || canvas.width || 320)); + const height = Math.max(120, Math.round(width / 5)); // 5:1 宽高比 canvas.width = Math.round(width * dpr); canvas.height = Math.round(height * dpr); canvas.style.width = `${width}px`; canvas.style.height = `${height}px`; + canvas.style.backgroundColor = 'transparent'; + + const resolveBgColor = () => { + const cardEl = card || container || document.body; + const style = getComputedStyle(cardEl); + const tokens = ['--card-bg', '--panel-bg', '--paper-bg', '--bg', '--background', '--page-bg']; + for (const key of tokens) { + const val = style.getPropertyValue(key); + if (val && val.trim() && val.trim() !== 'transparent') return val.trim(); + } + if (style.backgroundColor && style.backgroundColor !== 'rgba(0, 0, 0, 0)') return style.backgroundColor; + const bodyStyle = getComputedStyle(document.body); + for (const key of tokens) { + const val = bodyStyle.getPropertyValue(key); + if (val && val.trim() && val.trim() !== 'transparent') return val.trim(); + } + if (bodyStyle.backgroundColor && bodyStyle.backgroundColor !== 'rgba(0, 0, 0, 0)') { + return bodyStyle.backgroundColor; + } + return 'transparent'; + }; + const bgColor = resolveBgColor() || theme.cardBg || 'transparent'; const maxWeight = items.reduce((max, item) => Math.max(max, item.weight || 0), 0) || 1; + const weightLookup = new Map(); + const categoryLookup = new Map(); + items.forEach(it => { + weightLookup.set(it.word, it.weight || 1); + categoryLookup.set(it.word, it.category || ''); + }); const list = items.map(item => [item.word, item.weight && item.weight > 0 ? item.weight : 1]); try { WordCloud(canvas, { list, - gridSize: Math.max(8, Math.floor(Math.sqrt(canvas.width * canvas.height) / 80)), + gridSize: Math.max(3, Math.floor(Math.sqrt(canvas.width * canvas.height) / 170)), weightFactor: (val) => { const normalized = Math.max(0, val) / maxWeight; - const size = 16 + normalized * 32; + const cap = Math.min(width, height); + const base = Math.max(9, cap / 5.5); + const size = base * (0.8 + normalized * 1.3); return size * dpr; }, color: (word) => { - const found = items.find(entry => entry.word === word); - return lightenColor(wordcloudColor(found && found.category), 0.05); + const w = weightLookup.get(word) || 1; + const ratio = Math.max(0, Math.min(1, w / (maxWeight || 1))); + const category = categoryLookup.get(word) || ''; + const base = wordcloudColor(category); + const target = theme.isDark ? '#ffffff' : (theme.text || '#111827'); + const mixAmount = theme.isDark + ? 0.28 + (1 - ratio) * 0.22 + : 0.12 + (1 - ratio) * 0.35; + const mixed = mixColors(base, target, mixAmount); + return ensureAlpha(mixed || base, theme.isDark ? 0.95 : 1); }, - rotateRatio: 0.15, + rotateRatio: 0, + rotationSteps: 0, shuffle: false, shrinkToFit: true, drawOutOfBound: false, - backgroundColor: getComputedStyle(document.body).getPropertyValue('--card-bg').trim() || '#fff' + shape: 'square', + ellipticity: 0.45, + clearCanvas: true, + backgroundColor: bgColor }); if (container) { container.style.display = ''; container.style.minHeight = `${height}px`; + container.style.background = 'transparent'; } const fallback = card && card.querySelector('.chart-fallback'); if (fallback) { @@ -3870,11 +4061,19 @@ function exportPdf() { } document.addEventListener('DOMContentLoaded', () => { + const rerenderWordclouds = debounce(() => { + wordCloudRegistry.forEach(fn => { + if (typeof fn === 'function') { + fn(); + } + }); + }, 260); const themeBtn = document.getElementById('theme-toggle'); if (themeBtn) { themeBtn.addEventListener('click', () => { document.body.classList.toggle('dark-mode'); chartRegistry.forEach(applyChartTheme); + rerenderWordclouds(); }); } const printBtn = document.getElementById('print-btn'); @@ -3885,13 +4084,6 @@ document.addEventListener('DOMContentLoaded', () => { if (exportBtn) { exportBtn.addEventListener('click', exportPdf); } - const rerenderWordclouds = debounce(() => { - wordCloudRegistry.forEach(fn => { - if (typeof fn === 'function') { - fn(); - } - }); - }, 260); window.addEventListener('resize', rerenderWordclouds); hydrateCharts(); }); diff --git a/ReportEngine/renderers/pdf_renderer.py b/ReportEngine/renderers/pdf_renderer.py index bde61fb..699125e 100644 --- a/ReportEngine/renderers/pdf_renderer.py +++ b/ReportEngine/renderers/pdf_renderer.py @@ -331,6 +331,13 @@ class PDFRenderer: # 只处理chart.js类型的widget if widget_id and widget_type.startswith('chart.js'): + widget_type_lower = widget_type.lower() + props = block.get('props') + props_type = str(props.get('type') or '').lower() if isinstance(props, dict) else '' + if 'wordcloud' in widget_type_lower or 'wordcloud' in props_type: + logger.debug(f"检测到词云 {widget_id},跳过SVG转换并使用图片注入流程") + continue + failed, fail_reason = self.html_renderer._has_chart_failure(block) if block.get("_chart_renderable") is False or failed: logger.debug( @@ -392,7 +399,13 @@ class PDFRenderer: widget_id = block.get('widgetId') widget_type = block.get('widgetType', '') - if widget_id and isinstance(widget_type, str) and 'wordcloud' in widget_type.lower(): + props = block.get('props') + props_type = str(props.get('type') or '') if isinstance(props, dict) else '' + is_wordcloud = ( + isinstance(widget_type, str) and 'wordcloud' in widget_type.lower() + ) or ('wordcloud' in props_type.lower()) + + if widget_id and is_wordcloud: try: data_uri = self._generate_wordcloud_image(block) if data_uri: @@ -464,12 +477,14 @@ class PDFRenderer: font_path = str(self._get_font_path()) wc = WordCloud( - width=900, - height=520, + width=1000, + height=360, background_color="white", font_path=font_path, - prefer_horizontal=0.9, + prefer_horizontal=0.98, random_state=42, + max_words=180, + collocations=False, ) wc.generate_from_frequencies(frequencies)