Skip to content
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

Composable Axis #1640

Open
VisualMelon opened this issue Aug 13, 2020 · 26 comments
Open

Composable Axis #1640

VisualMelon opened this issue Aug 13, 2020 · 26 comments
Labels

Comments

@VisualMelon
Copy link
Contributor

VisualMelon commented Aug 13, 2020

Feature description

We have axes that override formatting (Time), that override the transforms (Logarithmic), that override the Tick and Label placement (Category), and all these features are difficult to combine - despite dealing with independent concerns - because they are based on inheritance and internal implementations. I propose we design a composable axis class that allows you to 'plug in' the separate behaviours.

Obvious candidate extension points (with no real organisation) would be

  • Measuring and Rendering (e.g. polar/XY, margin behaviour, label positioning)
  • Axis domains & scales e.g. linear, log, log log (LogLogAxis #1173), discontinuities (Removing gaps for irregular time data on DateTimeAxis #83)
    • The Transform methods depend on the type of plot (polar/non-polar) and domain & scale
  • Axis tick/label locating and formatting (which can be made much more flexible than it is already, to hopefully simplify the rendering and reduce the duplication between e.g. minor/major/extra grid lines), e.g. defaults for each of the scales (linear, log, etc.), special ones for Time/Discontinuous axes, ones that deal with unusual periods (e.g. radians)
  • Automatic Minimum-Maximum finding (discussion in Display minimum/maximum axis labels #156)

Additionally, by formalising these boundaries and providing nice implementations, they can be recomposed in exciting ways e.g. to provide axes with multiple 'banks' of ticks (#1357, #1336, #1629), or (if they are ever supported) non-XY/polar (e.g. triangle #1136, or even 3D (would of course need dedicated front-end support)) plots, would minimise duplication.

This is something I've tried a number of times in the past (thinking it could go in oxy-contrib), but I've always come a gutser because it would involve duplicated the work done in HorizontalAndVerticalAxisRenderer, and my astounding laziness prevents me from trying stuff without a clear design goal, so I just end up fudging whatever I actually need at the time.

Some random notes :

  • discontinuous axes are a real technical concern that probably shouldn't be discussed here, but should be considered if we want to have them in the future, because they may influence the API for axis transformations
    • indeed, funny axes are already a problem, e.g. when drawing line annotations it is hard coded to handle logarithmic axes 'properly': it would be nice to generalise that code
  • the current design is easy for us to maintain because it is all hidden away, but it's difficult to produce good-quality extensions from consuming assemblies; this would involve designing interfaces and that comes with the effort of maintaining them over time
  • interrogability of the axis must be a consideration if we want to enable user extensions like Editable minimum and maximum values of axes #1651

Anyhow, I thought it worth putting an issue together where we can put ideas, because I'm certainly never going to get around to it otherwise, though it's something on which I'd be keen to work.

@Jonarw
Copy link
Member

Jonarw commented Aug 14, 2020

Sounds interesting! I agree that the axes could use could use more flexibility.
At the moment I have a hard time imagining how this would look exactly without going into a lot of detail, but you have clearly given this some thought and the idea definitely sounds promising. I'd be happy to review anything you can come up with in this regard.

@VisualMelon
Copy link
Contributor Author

Linking #1255 : inexact maths will always be troublesome, but something to keep in mind.

@VisualMelon
Copy link
Contributor Author

Linking #1721 : this is a good use-case for 'inline-side' bands.

@karuzzo
Copy link

karuzzo commented Dec 12, 2020

how are you progressing in the composite axis design? I am really looking forward to the opportunity to draw financial charts without gaps.

@VisualMelon
Copy link
Contributor Author

@karuzzo not looked at it for a while. I rushed some notes a while back, but got caught up in designing a new layout system (https://github.com/VisualMelon/oxyplot/blob/ComposableAxis/Source/OxyPlot/Axes/ComposableAxis/Composable%20Axis.pptx) and have been focussed on other things lately.

My conclusion for making axis less painful was to define a new space Interaction Space which sits between the Dataspace and Screen Space that only the axis need know about, and that just provides a way to provide 'linear' behaviour to a non-linear or non-continuous data-space (can't use screen space, because that depends on layout).

However, there is a whole set of suffering to be had in making things render properly on such axes, because the assumption of linear interpolation doesn't work on a non-linear/discontinuous axis: we need to permit series to query where discontinuities occur so that it they can render accordingly (in the same way that Axis.IsLogarithmic changes the behaviour of e.g. LineAnnotation: we need to implement a way of performing non-linear/discontinuous interpolation in an IAxis interface and update all the series to support it: not as painful as it sounds: points will be as ever, and we can change any code that draws lines to use a common InterpolateLine(Axis x, Axis y, List<DataPoint> dataPoints, List<ScreenPoint> screenPoints) (screenPoints si the output buffer) which deals with the new interface. (note: this should also be generic for perf reasons)

The new interface for interpolation will probably need to look something like

public interface IAxisScreenMappingThing
{
    bool IsNonLinear { get; } // for fast-pathing
    bool IsDiscontinuous { get; } // not sure this helps, but why not
    void LocateDiscontenuities(TData start, TData end, IList<Discontinuity> discontinuities); // adds to an (ordered) list of discontinuities, which are a start/end pair (start can equal end)
    TSpace Transform(TData value); // Data->Screen mapping
    TData InverseTransform(TSpace value); // Screen->Data mapping: may need to be expanded to deal 

    // maybe additional stuff that computes gradients where cheap
}

The DrawLineOnAxes method would start by locating any discontinuities. Then it would go through the list of data-points, and reject any segment (i.e. DataPoint pair) that intersects a discontinuity (replace them with an invalid ScreenPoint instead of interpolating into the output buffer). Segments that are not rejected need to to be interpolated (if either axis is non-linear) using something like https://github.com/VisualMelon/oxyplot/blob/vectorfieldseries/Source/OxyPlot/Series/VectorSeries.cs#L318.

Note: this interface would be provided by a generic struct that wraps a mapping from Data to Interaction space and view information (i.e. Offset and Scale)

It would probably be good for me to implement all this without the layout mechanism soon, because the layout mechanism is going to be a nightmare and the whole point is that its composable, so if I can't implement this independently then I'm doomed fail whatever I do.

@VisualMelon
Copy link
Contributor Author

@Jonarw @objorke how would we feel about trying to make axis types statically typed for 3.0? That is, LinearAxis would implement IAxis<double>, and LineSeries would become LineSeries<XData, YData> (DataSample<XData, YData> would replace DataPoint). (existing types can be recast as double versions of new interfaces for compatibility).

The idea is that the 'type gap' would be closed when axes are assigned to series (by Key), so there wouldn't be any additional complexity from the user, just the joy of being able to define an IAxis<DateTime> and Series<DateTime, double>. If a series asks to use an axis of the wrong time, it's a run-time failure.

The benefit would be that users can use 'proper' types instead of turning everything into double. This will help with

  • Tick formatting and generation
  • Avoiding APIs like DateTimeAxis.ToDouble and its inverse
  • Avoiding plotting nonsense: it won't be 'acceptable' to stuff everything into a double an allow any operation you like on it (types need a mapping to doubles for interaction and plotting, but this will know if it is discontinuous and will only provide an 'interpolate' operation, not arbitrary arithmetic: all complexity needed for other components)

Below is mindless commentary, mostly for my benefit.

I may end up butchering everything, but I'm going to try and get this working in my Composable Axis branch, because a lot of the work to provide powerful axes requires strongly typed axes. The big issue I'm facing (or rather avoiding) at the moment is that I need to replace the Axis abstraction with IAxis, and that will mean effort, but I can't yet see any reason this couldn't be done without breaking everything.

If we are very lucky, the anguish associated with resolving all the types at run-time will mean we don't suffer hideous performance from the abstractions. Instead, we will have hideous types like TypedXYRenderHelper: because all the providers can be structs, the JIT should do a good job of producing efficient code (potentially faster that what we have now, but obviously that's wishful thinking) as there shouldn't be any virtual calls in any tight loops: instead, there will be additional overhead in initialising such render helpers (which can exist for 1, 2, 3, etc. axis (e.g. X,(Y),(Color)))

Other notes:

  • Most of the work to support this is needed if we want to allow efficient implementation of non-linear/non-continuous axes
  • There is a lot of hideous code involved in this, but it should all be at our end: it should be easy for a consumer to compose their own Axis<MyOwnType> with its own transforms by just providing the relevant providers (e.g. log axis just swaps out the IDataTransformation)
  • No reflection required; just needs casts to e.g. IAxis<XData> (throws if incorrect type) after resolving axis keys
  • LineSeries smoothing will break-down with this sort of API, because it is performed in Data-space at the moment, and the splines can't operate on things like DateTime. I'm opposed to smoothing on the grounds it reduces interpretability, but this is obviously a concern. Would it make sense to move smoothing to screen-space?
  • Not fully thought out color axes yet, but I think they can use the same set of providers, because they just map a value to a number and then map a number to a color, or a range to a color (it wraps the other functionality in the color mapping).
  • Can keep DataPoint if we use an IDataSample<XData,YData> abstraction (IDataSampleProvider for extra generality, much like the ItemMapping current... only hopefully faster because you can provide a struct: this is probably what I'll end up implementing for user-facing stuff, but it will boil everything down to DataSample for processing (for a few reasons)).

@VisualMelon
Copy link
Contributor Author

Looking into replacing Axis as the lowest Axis abstraction (i.e. what PlotModel records and the interaction logic looks at), I am rapidly coming to the conclusion that the interaction space should be exposed (though perhaps through an IInteractiveAxis interface so that it can be hidden from most usage). It will greatly simplify - and I suppose I shouldn't be surprised by this - interaction logic. Indeed, on reflection, this has to be done, because you can't interact in data space if it is discontinuous or bounded.

With that out out of the way, the number of places that axis type parameters will need to be 'passed' should go right down... we won't even need it for series (but it would be nice to keep the option open, as it may help with performance (though, I think performance is pretty much limited by rendering).

I'll shelve my work for the moment, and go from the other end (i.e. start by working out the Axis abstraction, now that I know what Axes themselves can look like).

@karuzzo
Copy link

karuzzo commented Dec 17, 2020

Thanks for your hard work!

@VisualMelon
Copy link
Contributor Author

Linking #1612: GetValue is documented as "Gets the value from an axis coordinate, converts from a coordinate" and is needed for e.g. DateTime axes so that things can use the real value. By working in the 'value' space from the outset, and converting to an explicit 'interaction' space, this sort of method will become less important (it will, of course, still exist, as a conversion from interaction to data space).

@karuzzo
Copy link

karuzzo commented Dec 29, 2020

Just look at this (OxyPlot + SkiaSharp) https://youtu.be/6y4X1a494kM?t=114
your library is great, it only lacks the ability to add exclusion gaps to the axes

@VisualMelon
Copy link
Contributor Author

VisualMelon commented Dec 30, 2020

Thanks for pinging this. I've since changed the plan for handling discontinuities. I still need to work out how to fit my plan into the existing library, but for the moment I'm looking at how painful it will be to implement a series using typed axes (including all the power of discontinuities, etc.) because it's easier.

Note to self: may need to know where discontinuities are precisely in order to render them... but that's probably a different type of discontinuity that should be customisable within the axis, not the data provision. Note: can probably do that quite nicely by wrapping the data-provider with a known-type data-provider that supports flexible gaps (and make this known to the axis renderer).

@karuzzo
Copy link

karuzzo commented Dec 30, 2020

The library should not have a mechanism for self-determination of gaps. But it would be great if the DateTime axis had a method for adding a break of the form axis.AddGap (double, double)

@VisualMelon
Copy link
Contributor Author

My ComposableAxis branch has predictably become a nightmare, but I'm getting to the point where I should be able to start putting typed axes on a plot. (then I need to sort out typed series and typed annotations). Remaining tasks:

  • Implement some rendering: can worry about the abstraction later
  • Implement some tick finding: again, can worry about abstraction later: need to test things like discontinuities now to see if the stronger typing is going to achieve anything
  • Patch all the interaction logic so that it will work with an AxisBase (can't assign non Axis axes yet... which is a big deal)
  • Must not forget color axes: I expect I'll use separate XY and V providers for e.g. scatter series so that I can reuse the existing XY helpers without any effort. It's a tad unpleasant splitting it... so I might make the API combined, and just use wrappers to decompose it. (Wrappers can be provided to the user to compose them as well, so everybody is happy)

Not really looked into the Series abstraction yet... but I'm hoping it will be easier than Axis because they aren't interrogated much:

  • Tracker asks them stuff, but the output is fairly free-form
  • Interaction logic hardly affects them, because it all goes through axes
  • They need to be told to update things at various times, but that's pretty mundane
  • They need to tell their Axes their min/max: should be easy enough as long as I continue to keep typed and un-typed axes seperate

Some notes on things that are producing lots of overhead though and aren't totally on point but I need to remember:

  • Axis.Render takes an int as the pass: this is virtually meaningless, I've changed it to an enum AxisRenderPass
  • There is no static separation between Data and Screen space values when not in a DataPoint or ScreenPoint: with the necessity of introducing Interaction space, I've added InteractionReal and ScreenReal as 1-D versions, and I'm strongly regretting not having explicit absolute and relatives variants.
  • DataPoint is replaced by DataSample<XData, YData> in typed series, though it can be be used as a source of DataSample<double, double> without any trouble, which is basically my long term plan for a measure of compatibility (that is, provide Series, but it's just an extension of LineSeries<DataPoint, DataPointXYSampleProvider, double, double>: not sure what to do about points/actual points... I guess I just tie ActualPoints to Samples and move on?)
  • Most of the interaction code assumes a 'class' Axis (as opposed to AxisBase)
  • I really must move lots of stuff into IAxis from AxisBase: IAxis<TData> cannot work without it

I am totally convinced of the value of the data/interaction/screen space separation, and once you have that, you might as well make everything typed...

Other:

  • Looks like the WPF performance project is behind again
  • VS seems to have issues with deciding whether it can infer generic parameters or in dual-targeting assemblies... I can't think why, it should be a language feature, but whatever
  • The generic extension min/max methods are really annoying, I should make them non-extension and fully qualify on usage

@VisualMelon
Copy link
Contributor Author

Finally got a plot on the screen:
image

Changes:

  • I dealt with the Series abstraction by not changing anything: new series can inherit series directly without too much trouble it seems. Obviously this can be tightened later as necessary.
  • I went nuts and had a go at banding: the conclusion is that it can't work as designed, because it requires knowing the zero-crossing position.

Next tasks:
0. I was working on a TickBand, looking to implement a TickRenderHelper for X/Y pairs, when I realised that I don't have a sensible X/Y pair abstraction yet, and really rather need one, so that's the next task. It would be really nice if I can make that a struct, but it's compositional, so that might be a problem.

  1. Finish implementing the basic TickBand
  2. Fix band margins and layout. I should look at the existing code to see what it does: fundamentally the problem is choosing margins for the inline stuff, and I think it just assigns margins as normal, ignoring the waste when the inline stuff is far from the edge. Note: still need to move all the layout code into Render. Won't touch Near/Far bands for now, because no-one will ever use them and time can be better spent elsewhere.
  3. Once I have ticks on the screen (1) and in the right place (2), I need to look at interaction (previous post has notes); currently attempting to interact with a typed axis results in a crash. It's worth putting effort into that next because it means sorting out abstractions, and will make it easier to test other things.
  4. Fix the CI build: don't want to keep running if I've broken something

Other:

  • The generics are pretty frightening... I should make a dependency chart some time so that they are less confusing.

@VisualMelon
Copy link
Contributor Author

VisualMelon commented Feb 1, 2021

Progress:

  • Wrote a truly awful TickBand, which has revealed some issues with its design (notes below)
  • Implemented plot interactions. This required changing interaction code to work in screen space: we do not need to expose interaction space in the axes. This involved adding new APIs to axes, but their implementations (in Axis) are trivial.
  • Fixed reversed axes... which ended up being a case of just removing code that was trying to detect reversed axes, so that's good.

Next Tasks:

  1. Fix CI before this gets any worse. No doubt all the XAML stuff in contrib will be broken as well, but that's not a problem at this easy stage
  2. Try to implement typed annotations: should be easy, but means pealing apart Annotation: it does too much that the moment: requiring an implementation of IXYPlotElement is excessive and not compatible with typed annotations.
  • This is mostly so that the MarginsAndPaddingAsymmetrical example will work, because it will be useful for testing). (Might need to ask @Jonarw about the rationales for IXYPlotElement... it may suffice to put everything in Annotation into XYAnnotation, but obviously that's not a given)
  • Also, a typed LineAnnotation would be good for testing the interpolation stuff
  • Also, it's the last (existing) abstraction that needs addressing (I hope), so that'll be a good milestone
  1. Implement a DateTime provider
  2. Implement a discontinuous provider, and then test everything against it (especially all the interpolation stuff)
  3. Implement another type of series, to get a feel for how painful that will be
  4. Start porting more examples, so that we can start to make everything 'feature complete' and put all the various properties in the right place before I produce too many types of things.

Tick band redesign:

  • Must take ITickLocator into consideration, since it needs to know how far apart ticks will be in order to do its job
  • Not a priority at the moment
  • Basically, it won't cope with zero-crossings or multiple tiers, and doesn't make it easy to measure things
  • The solution is to consume BandLocation in ITickRenderHelper<TData>: that gives it vectors it needs
    • Note: because the axis itself produces screen-space with an offset, we need to provide an additional offset to offset the vectors: this might seem needlessly unpleasant of Horizontal/Vertical axes, but should reduce the number of IsHorizontal checks, and means that the same code can be used for magnitude/color axes, with all the bodging acknowledged and addressed in the same place.
  • SpacingOptions is a disaster as well: you can't have a minimum interval on e.g. a log axis, and we don't want to bodge everything by using interaction space (which nothing except the axis interaction code needs to understand): should focus on implementing the 'default' behaviour of spacing by interval size. Oh, and Add MinimumMajorIntervalCount and MaximumMajorIntervalCount Axis Properties (#24) #1712 if possible, because I want that feature.

Since all the work is around typing, I'm considering putting all this in a separate OxyPlot.Typed namespace, so that it has some prospect of being merged (and so tested) before I have to re-implement literally everything (though the composable nature of it means it's only the rendering which is a pain for X/Y stuff; not thought about polar plots yet... but since when has anyone wanted to put anything other than linear axes on a polar plot?)

@VisualMelon
Copy link
Contributor Author

I don't think the CI failures are related to the new stuff: https://github.com/VisualMelon/oxyplot/runs/1806133454?check_suite_focus=true

Also, I was able to rebase the Composable Axis branch (forked from 2.1preview1) without any conflicts, so that's reassuring.

@VisualMelon
Copy link
Contributor Author

Thinking about it... why not expose interaction space to everyone? Series (e.g. LineSeries) could project their samples into interaction space before doing anything else, (including information on breaks/discontinuities), then rendering would just be a linear transformation. This would produce the same render-time performance for any type of axis, as e.g. logs would only be computed once.

Other benefits:

  • much simpler rendering routes: won't need half a dozen generic parameters, so stack traces will be more legible; discontinuities will already be checked, so that can determine the 'breaks' in method calls rather than needing logic in each method to check for it on a per-point basis
  • much simpler transformation routes: don't need to collate lots of generics for X/Y transforms: the interesting bit can be done independently for each axis with just 2 virtual calls (one for each pass), and we just need to something to combine the (non-generic) ViewInfo for a pair into a linear transform (note: if we passed such information to the render context, it could be used for efficient caching on vertex buffers in a GPU implementation, as the same set of verticies would be rendered each time, just with a different transformation matrix sitting in front of them).
  • issues with data will throw in a sensible place, instead of during rendering depending on what's being rendered

This is a ludicrously appealing prospect... which I shall ignore for the moment. It should be easy to implement this alongside what I already have later.

That said... this would be much closer to the idea of a pre-transform on axis: it could be easy to bolt on-top of existing stuff by just attaching the pre-transform for to the axis, and looking for it in the series (rather than re-fabbing everything): redefine interaction space as data-space, and data-space as pre-transform-space. This wouldn't be as semantically nice, but it would be much easier. It also has nothing to do with composition: it might be easier to separate the need for a composable axis from the desire for typed everything.

@VisualMelon
Copy link
Contributor Author

Instead of doing anything useful today, I looked into the performance of the new LineSeries: it was very disappointing. After making everything readonly and in-lining a couple of things in ExtractNextContinuousLineSegment, it is generally as fast as the old one now. This method takes about 20ns to process each DataPoint in the best case of a pair of Linear axes (i.e. there is no 'extra' overhead in performing transformations or discontinuity checks) on my not-slow machine. This is probably good enough, but it's a bit disappointing (though it does suggest the CLR is doing a good job already on the LineSeries version with all its possibly-virtual calls).

@VisualMelon
Copy link
Contributor Author

Progress:

  • Added AnnotationBase, which was no trouble at all (consumed by PlotModel, DelegateAnnotation, and the unfinished XYAnnotation (which will be a base class for typed annotations)
  • Made typed version of LineAnnotation and FunctionAnnotation and fixed the interpolation code so that it doesn't crash on vertical/horizontal lines
  • Implemented a simple 'Weekday' data transformation, which maps DateTimes, but with large gaps. I'm not sure what is wrong with it, but it isn't behaving quite right (may just be issues with ExtractNextContinuousSegment) and reveals more issue with axis rendering.
  • Made the existing axes implement IAxis<double>: linear H/V seem to work; not tested anything else. (This means that double series/annotations can use them rather than new offerings
    • doing the opposite (classic series on new axes) will be more effort: basically, I need to stuff everything they need which is currently in AxisBase into IAxis<TData>
  • Scrounged a few more milli-seconds in the 1million point spiral example by caching stuff in locals, and replacing more properties with readonly fields (no idea why this helps when everything is readonly struct already, but oh well); the current speed seems to be ~25% of what is possible without any abstractions. May need to give FindMinMax similar treatment, because they will be heavily loaded for changing graphs
  • Added BandPosition.None to the enum, to be used for independent grid-line rendering; nothing implemented yet
  • (hopefully) fixed all the margin code in the new

Notes on non-obvious deficiencies in the new LineSeries:

  • doesn't support axis filter values (not the clip bounds, it does support those: suggests we should probably feed it a custom range to ExtractNextContinuousLineSegment which is he intersection of the filter and clip bounds) (perf note: this shouldn't make it any slower, but doesn't make it look less favourable when compared to the existing line series which does do these checks, and does mean that we need to expose this information somewhere).
  • similarly, it doesn't support per-series filter functions: may be best to wrap all these up into an ISampleFilter interface and pass that around instead of using meaningful methods like WithinClipBounds: no, because the Axis-filtering is in data space (i.e. compares TData, and sample filtering will be in sample space (i.e. filters TSample).
  • UpdateMinMax similarly doesn't not perform necessary filtering
  • no discontinuity check between calls to ExtractNextContinuousLineSegment
  • There's a bug in ExtractNextContinuousLineSegment, such that it loses the 'previous' off-screen points (see margins example). It certainly looks the last point if the previous point was off-screen, possible in other cases.

Other notes:

  • contrary to my expectation, x==x does seem run faster than double.IsNaN in practice (see DaraPoint.IsDefined) under .NET Core 3.1

New TODO list:

  1. Fix ExtractNextContinousSegment, and work out why the Weekday example has dodgyness at the close times. (Interested to get feedback from @karuzzo if happy to test it, though probably after I've fixed the other issues with axes).
  2. Implement Tracker support in LineSeries, because it's really annoying when I click on them and they don't respond
  3. Implement another couple of types of series, to get a feel for how painful that will be. Candidates: Candle-stick (to go with financial week theme), Area series (on top of XYSampleSeries, not LineSeries: I've never liked that in the classic ones), Scatter (force me to sort out the Value abstraction)
  4. Fix axes (somewhere above), because they are insanely ugly, and it's starting to get to me
  5. Focus on composable axes, because that was the original idea

@VisualMelon
Copy link
Contributor Author

Progress:

  • Significantly patched up ExtractNextContinuousLineSegment, and added filters to XYSampleSeries<...> and HorizontalVerticalAxis<...>
  • (Hopefully) fixed the monotonicity checks, which were completely broken (and the cause of one of the many bugs I thought were due to ExtractNextContinuousLineSegment)
  • Made LinearDoubleTickLocator use the same methods as existing axes for consistency
  • Rudimentary tracker support in XYSampleSeries
    • Note: reveals more assumptions about the axis API: see notes in GetNearestPoint

TODO:

  • Implement typed scatter series, because it should be painless wrt. rendering, and will help with ColorAxis<...> and other XYV design problems I have been ignoring, which are probably the priority for the composable axis idea
  • More axis fixes (going for near-feature parity in linear axes ATM, i.e. we should be able to reproduce exactly the same figures, just everything is typed and swappable)
    • Rendering and Measuring should be along their BandLocation (important for basic color axis implementation)
      • Don't forget Gridlines (note: these need to share ticks with TickBands: resurrect the ITicks idea?
    • AxisDistance should be considered in margins
  • One those foundational things are done, I shall try to make the typed CandleStickSeries, and put together a nice looking financial example, a discontinuous axis and all the hideousness that goes with it.
    • Alternative approach would be to focus on getting non-typed series onto (typed) composable axes (i.e. inflate the IPrettyAxis interface so that series can look for an IAxis<double> instead of an AxisBase): maybe the existing candle-stick will be OK with using a discontinuous axis, and save me a whole load of typing?

@VisualMelon
Copy link
Contributor Author

VisualMelon commented Feb 7, 2021

Progress:

  • Lots of axis fixes
  • Implemented ScatterSeries and ColorAxis, though neither are very efficient, and the color-axis rendering is pretty dodgy
    • Could hand the tick locating for color axis to color axis itself... it knows the palette... (general architecture of allowing arbitrary ranges is nice, but really needs to be in concern with the actual mapping (e.g. ColorRangeAxis))

image

Problems I am ignoring for the moment or that I might get around to during the week:

  • Scatter series API is pretty scary
  • No gridlines
  • Zero-crossing is unimplemented/broken everywhere
  • Had a bash at swapping AxisBase or IAxis<double> in the classic series: main issue is with the Transform methods... could try throwing XYRenderHelper at the problem... (maybe better perf (though I expect not), among other benefits, and can do this one series at a time for testing)
  • Tick rendering still uses wrong normals: just need to get around to refitting them a bit
  • Concept of default bands needs addressing (make them properties and concat as necessary?)
    • At a higher level, need suitable default tick locators for e.g. log axes
  • Log tick locator needs to be refitted
  • SpacingOptions should be drawn from Axis where possible (which is why it was an interface originally, but I've scrapped that abstraction for now)
  • Clip testing should probably be in screen space: given this, we can probably remove WithinClipBounds completely, and do the clip testing manually in e.g. NextScreenSegment: all inlined double comparisons. The irritation with this, is that we have to transform everything... but then, pre-transforming everything (in Update) may well be the long term plan, so I should stop worrying about it.

Hopefully I'll find more time for this next weekend, when I'll focus on trying to make one nice example with lots of shiny new features.

I think I need to sort out some nice wrapper APIs before getting feedback on this stuff; however, I don't want to try for drop-in-replacement before discussing the architecture and general approach (and whether any of this is desirable).

Last thought: I was thinking about a Samples or SampleSource abstraction earlier. By abstracting the source away it could:

  • be easy to implement data-bindings (which have been often requested in the past)
  • reduce redundancy within series (e.g. area series just has 2 sources)
  • allow the sample provider better control on whether they are e.g. evaluated each frame, sampled each frame, or pre-sampled when possible

@VisualMelon
Copy link
Contributor Author

VisualMelon commented Feb 9, 2021

Surprise first pass at Candle Stick on discontinuous axis, based on an existing Candle Stick example.

image

Data is the same as in the existing Candle Stick examples, just with Sunday, Saturday, and 20:00 to 08:00 removed (hence the big jumps) You can see that each day cycled back to 08:00 instead of reaching 20:00, and that it skips June 19 and 20, which are a weekend.

Example code is here: https://github.com/VisualMelon/oxyplot/blob/d7eab0f2fc4f2ff8cbce2f8a1755481f812183d0/Source/Examples/ExampleLibrary/Axes/ComposableAxisExamples.cs#L802

The bits that are 'unique' to the example are the use of the WeekdayTransform on the X axis, and the filtering (which involved removing any samples from the data which the WeekdayTransform considers to be in a discontinuous region.

Other notes:

  • The various interfaces for interrogating axes seem to be slowly growing.
  • Axes should probably be responsible for creating axis helpers. This could simply their creation considerably (e.g. ValueHelper doesn't need to be a consumer (and currently it consumes too much)) and be easier to match to the existing behaviour of asking an axis to transform a point relative to a second axis (instead, you ask an axis to provide a render helper relative to another axis).
    • Upon further reflection, I think this is the way to go, but I should start by re-implementing the Transform methods from existing axes:
      • the principle is the same, they just don't put everything in a helper, so it should help inform the new interface
      • it is the last big thing whereof I'm aware that stops me putting old series on new axes (which would take us much closer to the goal of a useable composable axes, and means I should be able to make old series typed very quickly)
  • Added an event just for the candle-stick example: probably should not have done, but I couldn't think of a better way of achieving what it demanded (relevant to recent comment in Handle mouse events on PlotController instead of PlotModel (and elements) #111, though I can't seem to load that Issue ATM)

@karuzzo
Copy link

karuzzo commented Feb 9, 2021

you are wonderful! tomorrow I'll study your example

@VisualMelon
Copy link
Contributor Author

VisualMelon commented Feb 9, 2021

@karuzzo any and all feedback welcome, especially if it's along the lines of "you've totally missed the point".

The bit that enables discontinuous axes to work is the IDataTransformation<TData, TDataProvider> abstraction (which is what WeekdayTransformation implements): it maps the 'data space' information (e.g. a DateTime) to a linear and continuous 'interaction space' (doubles), which undergoes a linear projection into screen space (mediated by ViewInfo and some other stuff that is pretty far removed). This mapping (Transform) must be monotonic for any hope of sanity, but doesn't need to continuous. The data transformation also allows things to query whether a discontinuity exists between two points in data space, which is necessary for e.g. LineSeries on a discontinuous axis (not tested yet... yes it is...).

The DataProvider is just a type that helps perform basic operations on a data-type, specifically comparisons and interpolations. Notably it doesn't provide addition, because you can't e.g. add a DateTime to a DateTime.

I don't anticipate much of what I've been doing lately to end up in the final library: it's just too complicated/bity. I would be very interested to hear ideas about which bits are important, so that we can try to work them in without creating too many compatibility headaches.

@VisualMelon
Copy link
Contributor Author

VisualMelon commented Feb 13, 2021

Progress:

  • Managed to (mostly) get (most) existing series and annotations onto new axes (not tested, surely some bugs in there, but the current architecture seems to work)
  • Unfortunately, the performance was poor. I believe this is because the JIT is generating lots of defensive copies. I reclaimed the performance back by relaxing some readonlyness: I don't think this should have changed anything, so I'm just assuming the JIT was feeling lazy beforehand. The original LineSeries now performs slightly better than it did before the refit.
  • The performance of the new (typed) LineSeries has regressed somewhat, because I fixed the bounds checking. It is slightly slower than the old one, but the checks help significantly when there are points to clip.

Notes:

  • Transform consumers should be provided at the XYAxisTransformation level.
    • Would be nice if the axes provided the transform and not the helper: the point of the helper is to make common operations fast. This doesn't seem to be necessary, and it effectively by-passes the XYAxisTransformation abstraction.
    • Helper is all independent of old series stuff: we can field the new axes without the new series.
  • Annotations that don't need axes will be broken, and a couple of series are also broken on new axes (but should work on old)
  • Polar plots are broken

TODO:

  • Main thing is fixing Polar plots, because I need to know whether the current architecture will support them.
  • Must work out the easiest way to make existing series/annotations support discontinuous axes. Some relevant series:
    • LineSeries (use NextContinuous rather than NextContiguous)
    • AreaSeries (can't really support discontinuities, will be the case for a number of series, actually)
    • PathAnnotation (use new Interpolate method)
    • HistogramSeries (should probably throw if you try to render an item across a discontinuity, should be restricted to linear plots anyway)
  • Check color-axis abstraction: can't remember if typed/untyped are interop-able yet

@VisualMelon
Copy link
Contributor Author

VisualMelon commented Mar 13, 2021

Having stopped looking at this regularly for a bit, I think I need to start focus on writing code that can actually be used in the library.

I think the place to start is by fixing on a new axis abstraction (because everything else depends on them; everything else is pretty much stand-alone otherwise). I was going to write lots of stuff about it here... but then I looked at what I already have, and I'm pretty happy with it. See

One thing that is missing is an explicit provision for operating in interaction space: that is, currently the design expects you to feed data-space stuff to the render helper, but there are benefits to being about to perform 'pre transforms', taking data-space to interaction space and then rendering all that:

  • Data -> Interaction space is per axis
  • Interaction -> Screen is per pair of axes

Providing such a capability means that we would have 2 ways of implementing typed axes: one using 'pre-transforms', and one not. It also means we could fairly easily retro-fit existing series, but changing the rendering logic to work in interaction space, and just put a mapping infront of this.

Notes:

  • Note sure what to do about DataSample<X,Y>... might rename DataPoint<X,Y>, or see if it isn't necessary at this level of abstraction (note: could provide an IPoint<X,Y> abstraction which DataPoint can implement: should test that in the existing implementation to check perf doesn't change)
  • There is a git commit saying that I hate color axes... but I can't remember why. IColorAxis<V> seems fine... there were some issues with need axes to stuff in Axis, but I think my complaints were with what is in the API, rather than what isn't in the API
  • Do we want InteractionReal and ScreenReal? I like them because they make things slightly less terrible, but they do create lots of boring changes. (Is it possible to keep some of the joy by using an implicit conversion from double? still means you have to access stuff with .Value)
  • Not all series are 'working' at the moment in my branch, but I think this is a case of not calling VerifyAxes enough (the method which checks that a series has an appropriate pair of axes and gets the IXYHelper for them)
  • I'm not sure if there is any point in keeping IXYHelper: it can probably just be IXYRenderHelper; anything that doesn't involve rendering can just use a per-axis IAxisTransformation (as opposed to IAxisScreenTransformation)
  • The perf for existing series seems to be improved (in the extreme cases where perf actually matters) by the IXYRenderHelper abstraction (with a bit of encouragement in the form of making some specific things non-readonly which really should be readonly).
  • There are LOADS of generics, but the CLR seems to be so good at its job these days that we shouldn't need to expose this to users: I think we should keep it in the 'back-end', but we can provide non-generic top-level classes that mostly just take interfaces (i.e. Thing<X> : Thing<X, IProvider, ISampler, IFilter, IEtc>)
  • In the long run, I think an OxyPlot.Series.Typed namespace would be nice for typed series

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

3 participants