In my last WPF article I spewed about how nice it was. It’s great, but as I explained. A graph is nasty.
Now, anyone who has used GDI knows that, as its all vectors, a line is not just a line, it is a rectangle. Scale this 'line' up, and the rectangle gets bigger. That’s cool - its fine even, you can just set the line width to Width = PreferredWidth / ScaleFactor. No biggy. However, what happens if you want to scale unevenly across the X and Y? You will quickly find that lines of different angles become skewed as shown below.
Example 1 - Skewing Line
So, how do I scale a line without it skewing?
This is an issue that vexed me. It potentially meant that each time the scale factor changed, I was required to recalculate the positions of each point on each trace. With a graph with limitless datasets and data ranges, this could pose a huge performance issue when expanding window size, decreasing frame rates and reducing all the advantages of WPF to squat - I would be back to square one.
I scoured the intertubes and found some nice charting examples that redrew on scale to show me how bad my graph was going to fail. I also found a lot of forum posts asking how to scale a line without it expanding or skewing. After all - this should be possible. You know what kids? It is possible. I am going to tell you how.
NB. I saw a lot of examples quoting shullbit about using Viewboxs to wrap the Canvas you are drawing to - that is not correct. If you read anything about setting the Path's Stretch property to Fill, this is also inappropriate - although in the design you will see pleasing results at first (the technicalities of this I will not be entering into).
See, the issue is not the transforming. That happens automatically through the Transform object. As with any matrix challenge, the issue is, where you are applying the Transform changes? The solution came to me while studying WPF's limitations. While support for custom pixel shading is in place, vertex shading is not. You cannot push algorithmic point transformations to the graphics card (this is what makes the discussed example slow). So what? I am transforming my points anyway right? Wrong. Any transforms I apply to my Traces will be unconditional and global - meaning all points use the same transformation and the 'slow' calculation is done once.
All (quite literally) of the examples I saw, quite rightly drew their Traces by nesting PathFigure objects within PathGeometry, which is turn is wrapped in an instance of Path. Path is derived from UI element, which is derived from the base Visual. This means path is the object we see on screen - it handles the drawing. Because the Path object treats all its children as the same object, it is largely the same speed regardless of the line length. It also creates the nicest code and tidiest logical tree - there is no excuse not to use Path.
As you will see in the above example, if I apply my transform to my Path object, the line skews when you scale asymmetrically. This is because the vector 'rectangle' representation of the line is calculated before the layout pass, and before the transform is applied. The big and largely undocumented advantage of the Path object is that PathGeometry has a Transform object of its own. Apply the transformation here, instead of on the Path object and the problem goes away. This is due to the raw geometry points being transformed, rather than the post-draw vector rectangle. See the following example.
Example 2 - Implemented Solution
Hope this helps.
No comments:
Post a Comment