Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"changes": [
{
"packageName": "@visactor/vchart-extension",
"comment": "fix: 修复注册 3D 扩展后二维图表被误判为 3D,导致带缩放自定义图元在动画和悬停时闪烁的问题",
"type": "patch"
}
],
"packageName": "@visactor/vchart-extension"
}
122 changes: 122 additions & 0 deletions docs/assets/examples/en/lieflat-charts/aggregate-sankey.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
---
category: examples
group: lieflat charts
title: G22 · Aggregate Sankey
keywords: lieflat,G22,editorial,theme
order: 55
cover: /vchart/preview/lieflat-charts-aggregate-sankey.png
---

# G22 · Aggregate Sankey

Channels pour into plans · ribbon width = accounts.

A VChart implementation of G22 from [Lieflat Charts](https://github.com/larashero3-dotcom/lieflat-charts). All values are deterministic, simulated demo data.

## Key options

- Native entrance animation is enabled by default; animated stories start their player automatically.
- Native VChart series, axes and data binding.
- Background, text, data colors and tooltips follow the active VChart theme; categorical and sequential palettes are separate.

## Demo

```javascript livedemo
// 文档站提供 VCHART_MODULE / VisUtil;项目中分别从 @visactor/vchart / @visactor/vutils 导入。
const { getActualColor, getDataScheme, computeActualDataScheme } = VCHART_MODULE;
// 布局数据只保存颜色角色,实际颜色从当前 VChart 主题读取。
const ink = 'primaryFontColor';
const paper = 'backgroundColor';
const muted = 'secondaryFontColor';
const grid = 'axisGridColor';
const shades = Array.from({ length: 6 }, (_, i) => 'category' + i);
const sequential = Array.from({ length: 6 }, (_, i) => 'intensity' + i);
let colorScheme;
let colors;
const themeColor = (role, ctx) => {
const nextScheme = ctx.vchart.getChart().getColorScheme();
// 每次切换主题只解析一次,图元编码与 hover 复用结果。
if (nextScheme !== colorScheme) {
colorScheme = nextScheme;
colors = Object.fromEntries(
[ink, paper, muted, grid, 'borderColor', 'popupBackgroundColor', 'disableFontColor'].map(key => [
key,
getActualColor({ type: 'palette', key }, colorScheme)
])
);
const dataColors = computeActualDataScheme(getDataScheme(colorScheme), shades);
shades.forEach((key, i) => {
colors[key] = dataColors[i % dataColors.length];
});
// 数值色阶保持单向强弱,不把分类色板误用为热力图渐变。
const ramp = VisUtil.ColorUtil.interpolateRgb(
VisUtil.Color.parseColorString(colors.category0),
VisUtil.Color.parseColorString(colors[paper])
);
sequential.forEach((key, i) => {
colors[key] = ramp(i * 0.14).toString();
});
}
return colors[role] ?? role;
};
const paint = role => (_, ctx) => themeColor(role, ctx);
const font = 'Inter, Arial, PingFang SC, sans-serif';
// 确定性演示数据:刷新后保持一致,所有数值均为模拟数据。
const sample = (i, seed = 1) => ((Math.sin(i * 127.1 + seed * 311.7) * 43758.5453) % 1) + 1;
const noise = (i, seed = 1) => sample(i, seed) % 1;
const axis = orient => ({
orient,
domainLine: { visible: false },
tick: { visible: false },
grid: { visible: orient === 'left', style: { lineDash: [2, 4] } },
label: { style: { fontSize: 10, fontFamily: font } }
});
const base = {
padding: { top: 24, right: 30, bottom: 30, left: 30 },
title: {
text: 'Aggregate Sankey',
subtext: 'Channels pour into plans · ribbon width = accounts',
textStyle: { fontFamily: font, fontSize: 19 },
subtextStyle: { fontFamily: font, fontSize: 11 },
padding: { bottom: 28 }
},
animationAppear: { duration: 900, easing: 'quartOut' },
legends: { visible: false }
};

const nodes = ['Search', 'Social', 'Partners', 'Referral', 'Free', 'Pro', 'Team'].map(name => ({ name }));
const links = [
[0, 4, 180],
[0, 5, 120],
[0, 6, 60],
[1, 4, 140],
[1, 5, 55],
[1, 6, 25],
[2, 4, 60],
[2, 5, 85],
[2, 6, 105],
[3, 4, 90],
[3, 5, 65],
[3, 6, 40]
].map(([source, target, value]) => ({ source, target, value }));
const spec = {
animation: true,
...base,
type: 'sankey',
data: [{ id: 'flow', values: [{ nodes, links }] }],
categoryField: 'name',
valueField: 'value',
nodeAlign: 'justify',
nodeWidth: 8,
nodeGap: 18,
node: { style: { fill: paint(shades[0]) } },
link: { style: { fillOpacity: 0.35 }, state: { hover: { fillOpacity: 0.8 } } },
label: { visible: true, style: { fill: paint(ink), fontSize: 12 } }
};
const vchart = new VChart(spec, { dom: CONTAINER_ID });
vchart.renderSync();

// 通过 vchart.setCurrentTheme('已注册的主题名') 切换,无需重新计算业务数据。
// 仅用于控制台调试。
window['vchart'] = vchart;
```
242 changes: 242 additions & 0 deletions docs/assets/examples/en/lieflat-charts/arc-matrix.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,242 @@
---
category: examples
group: lieflat charts
title: L4 · Arc matrix
keywords: lieflat,L4,editorial,theme
order: 20
cover: /vchart/preview/lieflat-charts-arc-matrix.png
---

# L4 · Arc matrix

Eight products in twelve cities · bubble area = paying accounts.

A VChart implementation of L4 from [Lieflat Charts](https://github.com/larashero3-dotcom/lieflat-charts). All values are deterministic, simulated demo data.

## Key options

- Entrance animation is enabled by default; custom graphics appear in sequence after their text labels.
- Data-bound `customMark` layers preserve the individual units.
- Uniform scaling preserves geometry; hover a mark to inspect its record.
- Background, text, data colors and tooltips follow the active VChart theme; categorical and sequential palettes are separate.

## Demo

```javascript livedemo
// 文档站提供 VCHART_MODULE / VisUtil;项目中分别从 @visactor/vchart / @visactor/vutils 导入。
const { getActualColor, getDataScheme, computeActualDataScheme } = VCHART_MODULE;
// 布局数据只保存颜色角色,实际颜色从当前 VChart 主题读取。
const ink = 'primaryFontColor';
const paper = 'backgroundColor';
const muted = 'secondaryFontColor';
const grid = 'axisGridColor';
const shades = Array.from({ length: 6 }, (_, i) => 'category' + i);
const sequential = Array.from({ length: 6 }, (_, i) => 'intensity' + i);
let colorScheme;
let colors;
const themeColor = (role, ctx) => {
const nextScheme = ctx.vchart.getChart().getColorScheme();
// 每次切换主题只解析一次,图元编码与 hover 复用结果。
if (nextScheme !== colorScheme) {
colorScheme = nextScheme;
colors = Object.fromEntries(
[ink, paper, muted, grid, 'borderColor', 'popupBackgroundColor', 'disableFontColor'].map(key => [
key,
getActualColor({ type: 'palette', key }, colorScheme)
])
);
const dataColors = computeActualDataScheme(getDataScheme(colorScheme), shades);
shades.forEach((key, i) => {
colors[key] = dataColors[i % dataColors.length];
});
// 数值色阶保持单向强弱,不把分类色板误用为热力图渐变。
const ramp = VisUtil.ColorUtil.interpolateRgb(
VisUtil.Color.parseColorString(colors.category0),
VisUtil.Color.parseColorString(colors[paper])
);
sequential.forEach((key, i) => {
colors[key] = ramp(i * 0.14).toString();
});
}
return colors[role] ?? role;
};
const paint = role => (_, ctx) => themeColor(role, ctx);
const font = 'Inter, Arial, PingFang SC, sans-serif';
// 确定性演示数据:刷新后保持一致,所有数值均为模拟数据。
const sample = (i, seed = 1) => ((Math.sin(i * 127.1 + seed * 311.7) * 43758.5453) % 1) + 1;
const noise = (i, seed = 1) => sample(i, seed) % 1;
const layers = {};
// 同类图元共用一个数据集和 mark,避免为每条记录创建独立组件。
const put = (type, attrs, detail = '') => {
(layers[type] || (layers[type] = [])).push({
...(type === 'text' ? { angle: 0, textBaseline: 'middle', fontWeight: 400 } : {}),
...attrs,
...(type === 'text'
? { fill: shades.includes(attrs.fill) ? ink : attrs.fill }
: {
...(attrs.fill === ink ? { fill: shades[0] } : {}),
...(attrs.stroke === ink ? { stroke: shades[0] } : {})
}),
key: layers[type].length,
detail
});
};
const dot = (x, y, radius = 3, fill = ink, detail = '', stroke = fill) =>
put('symbol', { x, y, size: radius * 2, symbolType: 'circle', fill, stroke, lineWidth: 1 }, detail);
const text = (x, y, value, fontSize = 10, fill = muted, textAlign = 'center', fontWeight = 400) =>
put('text', {
x,
y,
text: String(value),
fontSize,
fill,
textAlign,
fontWeight,
fontFamily: font,
textBaseline: 'middle'
});
const path = (value, stroke = ink, lineWidth = 1, fill = false, detail = '') =>
put('path', { path: value, stroke, lineWidth, fill }, detail);

const products = ['Editor', 'Boards', 'Docs', 'Flows', 'Chat', 'Vault', 'Pages', 'Sync'];
const cities = ['SF', 'NYC', 'LON', 'BER', 'TOK', 'SYD', 'SIN', 'PAR', 'AMS', 'TOR', 'SEO', 'SAO'];
products.forEach((name, row) => {
const points = cities.map((city, col) => [115 + col * 42, 105 + row * 30 + 20 * Math.sin((col / 11) * Math.PI)]);
path(points.map((p, i) => (i ? 'L' : 'M') + p.join(',')).join(' '), grid, 0.7);
text(98, points[0][1], name, 10, muted, 'right');
cities.forEach((city, col) => {
const value = Math.round(noise(col, row + 11) * 100);
dot(
...points[col],
Math.sqrt(value) * 0.85,
row % 2 ? muted : ink,
name + ' / ' + city + ': ' + value + ' accounts'
);
});
});
cities.forEach((city, i) => text(115 + i * 42, 375, city, 9));

text(30, 28, 'Arc matrix', 19, ink, 'left', 700);
text(30, 52, 'Eight products in twelve cities · bubble area = paying accounts', 10, muted, 'left');
text(30, 408, 'L4 / ARC MATRIX / SIMULATED DATA', 9, muted, 'left');
const data = Object.entries(layers).map(([type, values]) => ({ id: type, values }));
const children = ['rule', 'arc', 'rect', 'path', 'symbol', 'text']
.filter(type => layers[type])
.map(type => {
const values = layers[type];
const keys = [...new Set(values.flatMap(Object.keys))].filter(
key => !['key', 'detail', 'nodeId', 'source', 'target', 'routeId', 'route', 'stage', 'node'].includes(key)
);
return {
type,
id: 'lieflat-' + type,
dataId: type,
dataKey: 'key',
...(type === 'text' ? { interactive: false } : {}),
style: {
...Object.fromEntries(
keys.map(key => [
key,
key === 'fill' || key === 'stroke' ? (datum, ctx) => themeColor(datum[key], ctx) : datum => datum[key]
])
),
// 装饰图元不参与拾取;扩大细线的命中范围,不改变实际线宽。
pickable: datum => Boolean(datum.detail),
pickStrokeBuffer: type === 'rule' ? 6 : type === 'path' || type === 'symbol' ? 4 : 0
},
animation: true,
// 文字先出现;图形在 450ms 内依次开始,整段入场约 1.1s。
animationAppear: {
type: 'fadeIn',
duration: type === 'text' ? 450 : 650,
delay: type === 'text' ? 0 : datum => (datum.key / Math.max(1, values.length - 1)) * 450,
easing: 'cubicOut'
}
};
});
// 整组等比缩放,保持点阵、圆形和文字的比例;不依赖固定容器宽度。
const scale = (_, ctx) => {
const { width, height } = ctx.vchart.getChart().getCanvasRect();
return Math.min(width / 640, height / 440);
};
const spec = {
animation: true,
type: 'common',
padding: 0,
data,
series: [],
customMark: [
{
type: 'group',
style: {
x: (_, ctx) => (ctx.vchart.getChart().getCanvasRect().width - 640 * scale(_, ctx)) / 2,
y: (_, ctx) => (ctx.vchart.getChart().getCanvasRect().height - 440 * scale(_, ctx)) / 2,
scaleX: scale,
scaleY: scale
},
children
}
],
// customMark 没有 series,使用下方 TooltipHandler 处理图元事件与提示生命周期。
tooltip: { trigger: [] }
};

const vchart = new VChart(spec, { dom: CONTAINER_ID });
vchart.renderSync();
// 通过标准 TooltipHandler 托管浮层,release 时自动移除。
const container = vchart.getContainer();
const tooltipElement = document.createElement('div');
tooltipElement.setAttribute('role', 'tooltip');
tooltipElement.className = 'lieflat-tooltip';
tooltipElement.style.cssText =
'position:absolute;visibility:hidden;pointer-events:none;z-index:10;box-sizing:border-box;' +
'max-width:min(280px, calc(100% - 16px));padding:8px 11px;border:1px solid;' +
'border-radius:4px;font:12px/1.5 ' +
font;
const updateTooltipTheme = () => {
const ctx = { vchart };
tooltipElement.style.background = themeColor('popupBackgroundColor', ctx);
tooltipElement.style.color = themeColor(ink, ctx);
tooltipElement.style.borderColor = themeColor('borderColor', ctx);
};
updateTooltipTheme();
vchart.on('afterRender', updateTooltipTheme);
container.style.position = 'relative';
container.appendChild(tooltipElement);
let tooltipWidth = 0;
let tooltipHeight = 0;
const tooltipHandler = {
showTooltip: (activeType, data, { event }) => {
const detail = data[0].datum[0].detail;
if (tooltipElement.textContent !== detail || tooltipElement.style.visibility === 'hidden') {
tooltipElement.textContent = detail;
tooltipWidth = tooltipElement.offsetWidth;
tooltipHeight = tooltipElement.offsetHeight;
}
const { width, height } = vchart.getChart().getCanvasRect();
const x = Math.max(8, Math.min(event.canvasX + 12, width - tooltipWidth - 8));
const y = Math.max(8, Math.min(event.canvasY + 12, height - tooltipHeight - 8));
tooltipElement.style.left = x + 'px';
tooltipElement.style.top = y + 'px';
tooltipElement.style.visibility = 'visible';
},
hideTooltip: () => {
tooltipElement.style.visibility = 'hidden';
},
isTooltipShown: () => tooltipElement.style.visibility === 'visible',
release: () => tooltipElement.remove()
};
vchart.setTooltipHandler(tooltipHandler);
// 使用 pointermove 的最终命中结果,避免相邻图元的 out/over 导致提示反复隐藏。
vchart.on('pointermove', params => {
if (params.datum && params.datum.detail) {
tooltipHandler.showTooltip('mark', [{ datum: [params.datum] }], params);
} else {
tooltipHandler.hideTooltip();
}
});
vchart.on('pointerleave', { source: 'chart' }, tooltipHandler.hideTooltip);
// 通过 vchart.setCurrentTheme('已注册的主题名') 切换,无需重新计算业务数据。
// 仅用于控制台调试。
window['vchart'] = vchart;
```
Loading
Loading