-
-
Notifications
You must be signed in to change notification settings - Fork 1.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[charts] Support rounded corners on BarChart
#12834
[charts] Support rounded corners on BarChart
#12834
Conversation
Deploy preview: https://deploy-preview-12834--material-ui-x.netlify.app/ Updated pages: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice CSS property 🎉
When adding demos in the docs, we should probably link to the material design docs for educational purpose :)
The current strategy works for single bars, but not staked ones. For that you will need the information "is the bar the top one"
Otherwise, it will look like Nivo bar chart
The only solution I can think of is to go in the useAggregatedData
and while looping on stacking group keep in memory the max/min coordinate of the group. And then maybe instead of applying the clip-path to the rectangle apply it to a group that contains all the stacked rectangles, such that having a rectangle with height=0
do not cause issue
Dividing the solution
We could go with two steps:
- Document your solution for rounding bar charts. It's only one line, but it solves probably 80% of the usecases
slotProps={{ bar: { clipPath: `inset(0px round 5px 5px 0px 0px)` } }}
- Introduce this notion of border radius that solves most of the edge cases described. For this one, I assume the
borderRadius
props should be moved to theBarPlot
level instead of theBarElement
.
Note
Surprisingly, chartjs when with full rounded rectangles
https://www.chartjs.org/docs/latest/samples/bar/border-radius.html
Echarts need to manually defines which rectangle is the end one.
@alexfauquette can you take a look at the code again? Documentation is still missing, but I want to be sure I'm on the right path before documenting the behaviours. It took me a while to figure out how the mask could work with the animation, but the trick was to animate everything together 😅, there are some unnecessary masks, but I don't think they should be a problem. There were two options I could go for in terms of masking
I chose to mask the entire column as I assume it gives more flexibility to the radius, as you can really make the columns round, although you probably shouldn't. If we put the mask only on the last item, if the item is very small or non-existant it would probably break and we would need more logic to figure everything out. Only drawbacks I see is that we have to use one mask for every segment, rather than one for the entire column, but it works and animation is aligned. Another "drawback" but this would be in any solution, is that if the "column" is smaller than the chosen radius, it will "clamp" the radius to the height of the column, which might look odd, but there is no other way around it. |
cc10152
to
102fddb
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks quite good.
For now, all the bars are listed without structure, because it would require to create sub-components to have multiple useTransition
For mask we could rely on the id
from SVG with the following structure:
// Assuming series a, b, c, d
// stacked together [a,b] and [c, d]
// With two x categories
// The clip paths of the stacked groups for first x value
<clipPath id="stack1-index0">
<clipPath id="stack1-index1">
// The clip paths of the stacked groups for second x value
<clipPath id="stack2-index0">
<clipPath id="stack2-index1">
...
// The rectangles for the first x value
<rect seriesId="a" clipId="stack1-index0">
<rect seriesId="b" clipId="stack1-index0">
<rect seriesId="c" clipId="stack2-index0">
<rect seriesId="d" clipId="stack2-index0">
// The rectangles for the second x value
<rect seriesId="a" clipId="stack1-index1">
<rect seriesId="b" clipId="stack1-index1">
<rect seriesId="c" clipId="stack2-index1">
<rect seriesId="d" clipId="stack2-index1">
if (!masks[result.maskId]) { | ||
masks[result.maskId] = { | ||
id: result.maskId, | ||
width: 0, | ||
height: 0, | ||
hasNegative: false, | ||
hasPositive: false, | ||
layout: result.layout, | ||
xOrigin: xScale(0)!, | ||
yOrigin: yScale(0)!, | ||
x: 0, | ||
y: 0, | ||
}; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why not directly puting the correct data?
x :Math.min(!mask.x ? Infinity : mask.x, result.x);
...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Mostly so I don't repeat logic. All other values can change based on different reasons, so I would have to duplicate the logic.
This way if the object doesn't exist, I create the object in a step with default values, then in a second step I update the values based on the current iteration. If the object already exists, I just update the values based on the current iteration.
I find it simpler to organise my thoughts that way.
{transition((style, { seriesId, dataIndex, color, highlightScope, maskId }) => { | ||
return ( | ||
<BarGroup | ||
key={`${seriesId}-${dataIndex}`} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A bit weird to have as many mask as bar. Would it make sens to define a <BarGroupClipPath clipingId={maskId} style={ ... } />
that defines the mask at put an id (will need to add the chartId to avoid conflicts)
Render those compoennts in their dedicated loop. And keep our current BarElement with clipPath={`url(#${maskId})`}
Bonus point for not rendering the BarGroupClipPath
if borderRadius is undefined or is set to 0 :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My initial idea was to render all the masks, and only animate those. But it didn't seem visually correct, as the masks would "slide over" stacks, while currently stacks grow individually.
Another try was to have the statics masks, but allowing the stacks to grow. This solved the previous problem, but the stacks were "flat" (no border radius), while they were growing, which looked odd.
In the end I had to animate both the mask and the stack to have the behaviour I was looking for, but then I didn't know how to align the animations to play at exactly the same time, while adding/removing stacks also looked odd because each of the animations reacted differently.
My current solution simply directly animates them all in the same useTransition
, with the sideeffect that we have as many masks as stack items, as you pointed out.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I figured out I could simply use a new useTransition
with the mask data 🙃, this way we can remove unneeded masks in when stacking
I can just use |
Co-authored-by: Alexandre Fauquette <45398769+alexfauquette@users.noreply.github.com> Signed-off-by: Jose C Quintas Jr <juniorquintas@gmail.com>
Co-authored-by: Alexandre Fauquette <45398769+alexfauquette@users.noreply.github.com> Signed-off-by: Jose C Quintas Jr <juniorquintas@gmail.com>
5fa957a
to
17885c5
Compare
@noraleonte @LukasTy I believe I addressed most if not all of @alexfauquette points. So please review when possible. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I loved how easy it is to apply a border-radius.
Can it support different values on different corners?
Co-authored-by: José Rodolfo Freitas <joserodolfo.freitas@gmail.com> Signed-off-by: Jose C Quintas Jr <juniorquintas@gmail.com>
It is possible to implement, but I didn't, as I couldn't see a specific use-case for it. |
Regarding use cases, I think it's mainly about the design of your charts. If it's a quick win, it'd be interesting if the
Great work, btw! 👌 |
Yeah, but then, how does the overriding of the current behaviour work? Like, if we use your example on the docs and suppose the array always overrides the default behaviour, we would get the following corners. It could allow for other options however, like Or |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is looking so nice 🎉
Great work! I really enjoy how simple the dx is 🔥 Left a few nitpicks, but otherwise LGTM 🍰
Can't wait for this to get merged 🫢
@joserodolfofreitas I don't really see a use case for different |
Co-authored-by: Nora <72460825+noraleonte@users.noreply.github.com> Signed-off-by: Jose C Quintas Jr <juniorquintas@gmail.com>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks great!
Good job! 👏 💯
Leaving some nitpick comments. 😉
BarChart
Co-authored-by: Lukas <llukas.tyla@gmail.com> Signed-off-by: Jose C Quintas Jr <juniorquintas@gmail.com>
@LukasTy @noraleonte that for the good inputs. Edit: nvm I can accept them myself, thanks @noraleonte |
Sorry, I missed the thread here. Recharts solution is overall worst than ours, but they cover this aspect, and maybe we can follow the same behavior.
We should aim to be the most customizable as possible and dismantle any myth that we are not. |
@joserodolfofreitas I don't see many issues in implementing it, and it can be done without any breaking change. My point in this #12834 (comment) is more that currently the border radius manages Negative & Positive values differently, eg: positive vertical values have no border radius on the bottom corners. If we allow for setting border radius for every corner, just like the |
Alright, I created a new issue to host the discussion around a potential solution. |
Signed-off-by: Jose C Quintas Jr <juniorquintas@gmail.com> Co-authored-by: Alexandre Fauquette <45398769+alexfauquette@users.noreply.github.com> Co-authored-by: José Rodolfo Freitas <joserodolfo.freitas@gmail.com> Co-authored-by: Nora <72460825+noraleonte@users.noreply.github.com> Co-authored-by: Lukas <llukas.tyla@gmail.com>
Signed-off-by: Jose C Quintas Jr <juniorquintas@gmail.com> Co-authored-by: Alexandre Fauquette <45398769+alexfauquette@users.noreply.github.com> Co-authored-by: José Rodolfo Freitas <joserodolfo.freitas@gmail.com> Co-authored-by: Nora <72460825+noraleonte@users.noreply.github.com> Co-authored-by: Lukas <llukas.tyla@gmail.com>
Initial Proposal
We can use
clip-path: inset()
to easily make round cornersResult https://codesandbox.io/p/sandbox/mui-mui-x-x-data-grid-forked-jt6jxh?file=%2Fsrc%2Fdemo.tsx%3A7%2C6
Docs
https://deploy-preview-12834--material-ui-x.netlify.app/x/react-charts/bars/#border-radius
resolves #12220
resolves #12947
Todo