zrender
zrender copied to clipboard
fix(shadow): specify the shadow filter units as `userSpaceOnUse`.
- Resolves apache/echarts#15467
Before
After
A simple demo
// use SVG renderer
option = {
xAxis: {
type: 'category',
data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
},
yAxis: {
type: 'value'
},
series: [{
data: [0, 0, 0, 0, 0, 0, 0],
type: 'line',
stack: true,
color: '#7f64ff',
symbol: 'none',
smooth: true,
lineStyle: {
shadowColor: 'red',
shadowBlur: 5,
shadowOffsetX: 0,
shadowOffsetY: 0
}
}]
};
Do we need to change x, y, width, and height after using userSpaceOnUse filterUnits?
Also, this seems to be a bug in chrome. The shadow filter should have no reason to affect the display of the original graphic. I tested it on safari and the line won't disappear.
Also, this seems to be a bug in chrome. The shadow filter should have no reason to affect the display of the original graphic. I tested it on safari and the line won't disappear.
Seems so. It also works in Firefox.
Just one more thing, the shadow seems buggy when hovering.
Two series will use the same shadow filter id when hovering. That is not expected.
option = {
xAxis: {
type: 'category',
data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
},
yAxis: {
type: 'value'
},
series: [{
data: [0, 0, 10, 0, 0, 0, 0],
type: 'line',
stack: true,
color: '#7f64ff',
symbol: 'none',
smooth: true,
lineStyle: {
shadowColor: 'red',
shadowBlur: 5,
shadowOffsetX: 0,
shadowOffsetY: 0
}
}, {
data: [0, 80, 0, 50, 0, 0, 0],
type: 'line',
stack: true,
color: '#7f64ff',
symbol: 'none',
smooth: true,
lineStyle: {
shadowColor: 'blue',
shadowBlur: 5,
shadowOffsetX: 0,
shadowOffsetY: 0
}
}]
};
I opened #822 before seeing this PR...
Perhaps it's better to add this attribute in _getFromPool because this does not need to be updated. Anyway, welcome to close either one...
Also, this seems to be a bug in chrome.
https://bugs.chromium.org/p/chromium/issues/detail?id=1247310#c3
Let's keep the discussion in this PR. The major concern from me is calculating x, y, width, and height after changing to userSpaceOnUse. The unit has been changed, and we need to consider scale to avoid creating a too large clipping region which may lead to performance issues.
Merged the test case from https://github.com/ecomfe/zrender/pull/822
Get this updated slightly. Here are two strategies I can think out.
1. Add a tiny offset to make it not entirely straight. (Ref)
It can make the line displayed but the result is strange and unexpected.
2. Specify especially filterUnits as userSpaceOnUse when the element is Line and is a horizontal or vertical straight line.
It seems to be working for me. But it needs some extra logic to check.
export function getShadowKey(displayable: Displayable) {
const style = displayable.style;
const globalScale = displayable.getGlobalScale();
let isHorizontalVerticalStraightLine = 0;
// FIXME Use `userSpaceOnUse` as the unit when applying filters on a horizontal or vertical straight line
if (displayable.type === 'line') {
const shape = (displayable as Line).shape;
const rad = Math.atan2(shape.y2 - shape.y1, shape.x2 - shape.x1);
isHorizontalVerticalStraightLine = +!(rad % (Math.PI / 2));
}
return [
[
style.shadowColor,
(style.shadowBlur || 0).toFixed(2), // Reduce the precision
(style.shadowOffsetX || 0).toFixed(2),
(style.shadowOffsetY || 0).toFixed(2),
globalScale[0],
globalScale[1],
isHorizontalVerticalStraightLine
].join(','),
isHorizontalVerticalStraightLine
];
}
function setShadow(
el: Displayable,
attrs: SVGVNodeAttrs,
scope: BrushScope
) {
const style = el.style;
if (hasShadow(style)) {
const [shadowKey, isHorizontalVerticalStraightLine] = getShadowKey(el);
const shadowCache = scope.shadowCache;
let shadowId = shadowCache[shadowKey];
if (!shadowId) {
const globalScale = el.getGlobalScale();
const scaleX = globalScale[0];
const scaleY = globalScale[1];
if (!scaleX || !scaleY) {
return;
}
const offsetX = style.shadowOffsetX || 0;
const offsetY = style.shadowOffsetY || 0;
const blur = style.shadowBlur;
const {opacity, color} = normalizeColor(style.shadowColor);
const stdDx = blur / 2 / scaleX;
const stdDy = blur / 2 / scaleY;
const stdDeviation = stdDx + ' ' + stdDy;
// Use a simple prefix to reduce the size
shadowId = scope.zrId + '-s' + scope.shadowIdx++;
const filterAttrs: SVGVNodeAttrs = {
'id': shadowId,
'x': '-100%',
'y': '-100%',
'width': '300%',
'height': '300%'
};
//
isHorizontalVerticalStraightLine && (filterAttrs.filterUnits = 'userSpaceOnUse');
scope.defs[shadowId] = createVNode(
'filter', shadowId, filterAttrs,
[
createVNode('feDropShadow', '', {
'dx': offsetX / scaleX,
'dy': offsetY / scaleY,
'stdDeviation': stdDeviation,
'flood-color': color,
'flood-opacity': opacity
})
]
);
shadowCache[shadowKey] = shadowId;
}
attrs.filter = getIdURL(shadowId);
}
}
| Before | After |
|---|---|
![]() |
![]() |
Here is another article that states the same issue: https://www.amcharts.com/docs/v4/tutorials/fixing-gradients-and-filters-on-straight-lines/
In your second solution. Checking zero area bounding rect without lineWidth will be more accurate than checking type because line chart use an extended path. If we use userSpaceOnUse only on the zero area bounding rect.
I'm not sure but is it possible to use userSpaceOnUse on all cases and consider the scale of element?

