[Bug] point mark keeps hover-state size after updateState() during hover (state animation not reverted)
I maintainer di solito rispondono entro 1 giorno
Nessuno ha ancora preso questa issue.
Valutazione
- Difficoltà
- 4/5
- Tempo stimato
- 3-5 giorni
- Idoneità per principianti
- 68/100
- Tipo di issue
- Bug
- Chiarezza
- Abbastanza chiara
- Stato di attività
- Attiva
- Stack tecnologico
- typescript
- Ambito
- data-visualization, frontend
Direzione di ricerca
Inizia riproducendo la specifica VChart fornita e tracciando updateState() mentre è in esecuzione l'animazione dello stato di un punto, confrontando i casi con animazione abilitata e disabilitata. La correzione è completa quando le animazioni dello stato interrotte riallineano gli attributi del punto allo stile inattivo dopo che il puntatore si allontana, inclusi gli stili con valori callback, senza introdurre regressioni nei casi di durata forniti.
Scritto dal modello di indicizzazione a partire dal testo della issue.
Descrizione
Version
@visactor/[email protected]
Description
When a series declares a state animation on the point mark (animationState.point), and a mark's state is changed via updateState() while that mark is in the hover state, the point permanently keeps the hover state's interpolated attribute values after the pointer leaves.
The state itself is removed correctly — graphic.currentStates no longer contains hover — but graphic.attribute.size stays at the hover value instead of returning to the idle value. So this is not a stale-pixel / dirty-rect issue: forcing a full stage.render() does not clear it, because the wrong value is genuinely in the scene graph.
Points that are invisible at rest (idle size resolves to 0) therefore stay permanently visible after the user has moved the mouse away.
The bug only occurs when instance-level animation is enabled. With { animation: false } the same interaction is clean, because state animations never run.
Reproduction
const spec = {
type: 'common',
seriesField: 'group',
animation: true,
hover: { dimension: {} },
data: [{ id: 'allData', values: [
{ group: 'cost', index: 0, value: 400119 }, { group: 'cost', index: 1, value: 425357 },
{ group: 'cost', index: 2, value: 339819 }, { group: 'cost', index: 3, value: 430718 },
{ group: 'revenue', index: 0, value: 553817 }, { group: 'revenue', index: 1, value: 493905 },
{ group: 'revenue', index: 2, value: 446204 }, { group: 'revenue', index: 3, value: 573945 },
] }],
series: [{
type: 'line',
dataId: 'allData',
xField: 'index',
yField: 'value',
seriesField: 'group',
hover: { dimension: {} },
animation: true,
// (1) state animation on the point mark
animationState: { point: { duration: 1000 }, line: false, area: false },
point: {
// (2) idle size resolves to 0 -> points are invisible at rest
style: { size: () => 0, fill: '#fff', lineWidth: () => 1 },
state: {
dimension_hover: { globalZIndex: 400, size: () => 8, lineWidth: () => 2, fill: '#fff' },
hover: { size: () => 10, lineWidth: () => 2, fill: '#fff' },
// (3) custom states toggled through updateState()
sel_series: { outerBorder: { stroke: '#4e83fd', lineWidth: 1, distance: 3 } },
sel_point: { outerBorder: { stroke: '#4e83fd', lineWidth: 1, distance: 3 } },
},
},
}],
axes: [{ orient: 'left', type: 'linear' }, { orient: 'bottom', type: 'band' }],
};
const vchart = new VChart(spec, { dom: CONTAINER_ID, animation: true });
vchart.renderSync();
// (4) toggle custom states while the pointer is over a point.
// Both keys are always passed: one filter matches, the other never matches,
// i.e. every call simultaneously adds one state and removes another.
const NEVER = () => false;
let hasSelection = false;
vchart.on('dblclick', e => {
const datum = Array.isArray(e?.datum) ? e.datum[0] : e?.datum;
if (!datum?.group) return;
let seriesFilter = NEVER;
let pointFilter = NEVER;
if (hasSelection) {
pointFilter = d => d?.group === datum.group && d?.index === datum.index;
} else {
seriesFilter = d => d?.group === datum.group;
}
hasSelection = true;
vchart.updateState({ sel_series: { filter: seriesFilter }, sel_point: { filter: pointFilter } });
});
Steps
- Hover a data point of series
costand double-click it. - Hover a data point of series
revenueand double-click it. - Repeat once more on two further points (4 double-clicks total, alternating series).
- Move the mouse completely off the chart and wait.
Expected — every point returns to its idle size 0 and becomes invisible again.
Actual — the points touched in steps 1–3 stay at size: 10 (the hover value) and remain visible, although their currentStates no longer contains hover.
Inspecting the scene graph after step 4:
// walk the stage and collect the point symbols
[
{ size: 0, states: [] },
{ size: 10, states: ['selected'] }, // <-- leaked, should be 0
{ size: 10, states: ['selected'] }, // <-- leaked, should be 0
{ size: 0, states: [] },
{ size: 0, states: [] },
{ size: 10, states: ['selected'] }, // <-- leaked, should be 0
{ size: 10, states: ['sel_point', 'selected'] }, // <-- leaked, should be 0
{ size: 0, states: [] },
]
Measurements
Driven with Playwright, counting points whose resolved attribute.size > 0 after the pointer has left, 4 runs per cell:
instance animation |
animationState.point |
leaked points per run |
|---|---|---|
true |
{ duration: 1000 } |
4, 4, 4, 4 |
false |
{ duration: 1000 } |
0, 0, 0, 0 |
true |
{ duration: 300 } |
0, 1, 0, 1 |
true |
{ duration: 1 } |
0, 0, 0, 1 |
true |
absent | 0, 0, 0, 0 |
The leak rate scales with the state animation's duration, which suggests the interpolated value is committed when a state animation is interrupted by the next state change and never reconciled against the mark's declared idle style.
Two further observations that may help narrow it down:
- Hovering points without calling
updateState()never leaks — theupdateState()call during hover is required. - With static values (
size: 0/size: 10instead of callbacks) the leak was not observed. It reproduces with callback-valued styles.
Workaround
Removing animationState from the series spec entirely avoids the problem. Setting animationState.point = false also works. Note that a very short duration ({ duration: 1 }) does not reliably avoid it — it only narrows the race window.
- Lingua principale
- TypeScript
- Stelle
- 1.9k
- Fork
- 221
- Merge medio
- 2g 1h
- PR unite (30g)
- 17
Preparare l'ambiente
- Nessun Dockerfile né file Docker Compose
- Ha un modello di pull request
- Leggi la guida per i contributori
Come iniziare
- Leggi tutta la issue e poi la guida ai contributi del progetto.
- Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
- Fai un fork del repository e lavora su un branch.
- Apri una pull request che faccia riferimento al numero della issue.
Altre issue di VisActor/VChart
-
Difficoltà 5/5 Più di una settimana Idoneità per principianti 35/100
VisActor/VChart#4648 · 1 commento ·
I maintainer di solito rispondono entro 1 giorno
-
[Bug] 布局会在图里筛选掉全部数据项后出现错乱Apertabug
Difficoltà 3/5 1-2 giorni Idoneità per principianti 55/100
VisActor/VChart#4634 · 2 commenti ·
I maintainer di solito rispondono entro 1 giorno
-
[Bug] bar、line等图表随图例切换而残留Forse di nuovo libera @skie1997 l’ha presa 98 giorni fa e non c’è nessuna pull request aperta. Apertabug
VisActor/VChart#4622 · 1 commento · 1 assegnatario ·
I maintainer di solito rispondono entro 1 giorno
-
[Feature] 支持线性 x 轴场景下的自动柱宽策略Apertaenhancement vchart
Difficoltà 4/5 3-5 giorni Idoneità per principianti 52/100
VisActor/VChart#4615 · 1 commento ·
I maintainer di solito rispondono entro 1 giorno
-
[Bug] 瀑布图 负值柱子方向异常Apertabug
Difficoltà 3/5 1-2 giorni Idoneità per principianti 52/100
VisActor/VChart#4612 · 1 commento ·
I maintainer di solito rispondono entro 1 giorno
Tutte le issue di VisActor/VChart
Issue simili
-
[BUG] rag: sentence content can cross the reported sentence boundariesForse già presa @LJK-ikun l’ha presa oggi. Apertabug
Difficoltà 2/5 1-3 ore Idoneità per principianti 75/100
I maintainer di solito rispondono entro 1 giorno
-
bug needs-triage
Difficoltà 2/5 1-3 ore Idoneità per principianti 70/100
I maintainer di solito rispondono entro 1 giorno
-
README: the Resource URI Schemas example's file://documents/* pattern only matches that literal URIAperta
Difficoltà 1/5 Meno di un'ora Idoneità per principianti 88/100
platformatic/mcp#361 ·
I maintainer di solito rispondono entro 1 giorno
-
frontend
Difficoltà 2/5 1-3 ore Idoneità per principianti 78/100
I maintainer di solito rispondono entro 1 giorno
-
Push: sem service worker ou sem VAPID, o interruptor de Notificações fica ligado sem avisar nadaAperta
Difficoltà 2/5 1-3 ore Idoneità per principianti 76/100
melgarafael/DeskcommCRM#3164 ·
I maintainer di solito rispondono entro 1 giorno