Skip to content

Add custom label formatter support for crosshair#1173

Merged
swharden merged 6 commits intoScottPlot:masterfrom
Maoyao233:master
Aug 6, 2021
Merged

Add custom label formatter support for crosshair#1173
swharden merged 6 commits intoScottPlot:masterfrom
Maoyao233:master

Conversation

@Maoyao233
Copy link
Contributor

New Contributors:
please review CONTRIBUTING.md

Purpose:
#1172

New Functionality:

 var ch = plt.AddCrosshair(20, 0);

// use the custom formatter for crosshair labels
ch.ManualStringFormatterX = customTickFormatter;
ch.ManualStringFormatterY = customTickFormatter;

Copy link
Member

@bclehmann bclehmann left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good, just a few minor things. They're not about your code so much as maintaining a consistent API, as anyone who uses this feature probably is also using the custom formatter on the axis as well. So those features should probably expose a similar API.

Maybe even the crosshair label formatter should use the axis formatter? I don't know how easy that would be though, especially now that we have support for multiple axes. It would also introduce a lot of coupling.

/// <summary>
/// If defined, this function will be used to generate labels from X position, value of StringFormatX and IsDateTimeX will be ignored
/// </summary>
public Func<double, string> ManualStringFormatterX = null;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think TickLabelFormat is ignored if it's a datetime axis, so this introduces an inconsistency.

I think it's probably better that it isn't ignored for datetime axes, but that change would mean changing the axis code as well.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think TickLabelFormat is ignored if it's a datetime axis

Maybe there should be a NumericFormatter and DateTimeFormatter?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's fine for the user to figure that out themselves, as they should know whether or not it's a DateTime axis.

Although I suppose we could make it easier for them by having DateTimeFormatter take in a DateTime instead of a double.

gfx.DrawLine(pen, dims.DataOffsetX, yPixel, dims.DataOffsetX + dims.DataWidth, yPixel);

string yLabel = IsDateTimeY ? DateTime.FromOADate(Y).ToString(StringFormatY) : Y.ToString(StringFormatY);
string yLabel = ManualStringFormatterY != null ? ManualStringFormatterY(Y) : IsDateTimeY ? DateTime.FromOADate(Y).ToString(StringFormatY) : Y.ToString(StringFormatY);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This ternary within a ternary is pretty unwieldly.

Copy link
Contributor Author

@Maoyao233 Maoyao233 Aug 3, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks. I'll avoid using it in future.

<Version>4.1.16</Version>
<AssemblyVersion>4.1.16.0</AssemblyVersion>
<FileVersion>4.1.16.0</FileVersion>
<Version>4.1.17</Version>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think Scott will bump the version himself once this is merged and any other PRs have been merged in.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed! This can be left as 4.1.16 for now 👍

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't intentionally change the version number, probably because I'm not familiar with ScottPlot's automated build system. I also think this small change should not change the version number.


// use the custom formatter for crosshair labels
ch.ManualStringFormatterX = customTickFormatter;
ch.ManualStringFormatterY = customTickFormatter;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again on the consistency argument, it's a little weird that for one of them you pass in a formatter delegate to a function and for the other you set a property.

I think that setting a property is probably the better API, but if we want consistency that would mean changing the axis code.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The consistency argument is also present in the original API. I think changing the axis code is a good way.

@bclehmann
Copy link
Member

Also, it looks like you made this PR from your master branch, instead of a new branch. That's not a problem for us but it could make it more difficult for you to fetch new commits that get merged in.

@swharden
Copy link
Member

swharden commented Aug 3, 2021

Hi @Maoyao233, thank you for this PR! I really like the idea of exposing custom formatters in more places throughout ScottPlot, and this is a great improvement.

Thinking out loud, an alternative to allowing formatters to be null we could always use these formatters and pre-populate them with generic formatters created in the constructor. This would eliminate the need for those ternary null checks.

I think separate functions would be helpful, but I recognize this is super verbose:

  • XNumericFormatter
  • YNumericFormatter
  • XDateTimeFormatter
  • YDateTimeFormatter

Maybe this is an indication this class should be refactored as two smaller objects (because this isn't the only duplication of X and Y properties). Alternatively I wonder if these advanced label features should be added to AxisLine, then Crosshair could contain two of those. We can consider that type of refactoring in a future PR. For now this PR is going in a good direction, and I'm happy to merge it in (with its cookbook recipe) after @Maoyao233 has a chance to review some of the code comments. Thanks @bclehmann for your input!

@bclehmann
Copy link
Member

I think separate functions would be helpful, but I recognize this is super verbose:
.
XNumericFormatter
YNumericFormatter
XDateTimeFormatter
YDateTimeFormatter

One could allow the user to pass in one function (or class) which would override one or several of these? It could be done with the decorator pattern. There is a way to do this where they pass in a function, where they're passed an object, and setting fields of that object sets the formatter. The formatter should probably be allowed to be null just so it's easy for them to reset it.

Both of these are departures from the rest of the ScottPlot API however.

@swharden
Copy link
Member

swharden commented Aug 3, 2021

@Maoyao233 thanks again for this PR and your feedback! When you're done making changes to this PR let me know and I'll make a few small changes of my own then merge this in 👍

One could allow the user to pass in one function (or class) which would override one or several of these? It could be done with the decorator pattern.

Both of these are departures from the rest of the ScottPlot API however.

@bclehmann this is a good observation! This trend could be taken to its logical conclusion by having a fully-abstracted "configuration object" or "styling object" that could be instantiated and modified by the user and passed into plots or plottables. This would help separate code for configuration, styling, and rendering. If ScottPlot were a private library that only I used, I would prefer this. Since it's a public library and I strive to make it quick and easy to learn for new users, I find myself preferring the simplicity of having users assign public fields.

@Maoyao233
Copy link
Contributor Author

@swharden I have finished my work on this PR.

I think TickLabelFormat is ignored if it's a datetime axis, so this introduces an inconsistency.

Again on the consistency argument, it's a little weird that for one of them you pass in a formatter delegate to a function and for the other you set a property.

I ended up using different formatter for numeric and datetime to avoid the first problem, and I think changing the axis code to keep consistency is valuable.

Thanks again to @swharden and @bclehmann. I've learnt a lot.

@bclehmann
Copy link
Member

I wonder if we should have a demo as well, for the datetime axes? Perhaps that should wait for when axes get custom datetime formatters.

@swharden swharden merged commit a7c20bc into ScottPlot:master Aug 6, 2021
@swharden
Copy link
Member

swharden commented Aug 6, 2021

Thanks @Maoyao233 this looks fantastic! I also appreciate your input along the way @bclehmann

I'll be refactoring the Crosshair module shortly. I intend to add support for position labels to axis lines, and when I do that I'll be sure to support this type of custom formatter support as well. I suspect the Crosshair will get refactored to contain one HorizontalLine and one VerticalLine, but we'll see. Progress there will be tracked in #1122

swharden added a commit that referenced this pull request Aug 7, 2021
swharden added a commit that referenced this pull request Aug 8, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants