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

Replace Point type with Pair<T,U> #31

Merged
merged 5 commits into from
Jul 13, 2019
Merged

Replace Point type with Pair<T,U> #31

merged 5 commits into from
Jul 13, 2019

Conversation

KarthikRIyer
Copy link
Owner

#27
I have changed Point to Pair<T,U>. This removes the unwanted xString and yString variables.
I've added some specific comments with the code below.

If this seems ok, could you please review #30 ? I'd liketo get that merged before this one so that I can convert the currently implemented Histogram to Pair<>. Also that PR has some fixes for segmentation fault errors.

}

public func drawSolidRect(topLeftPoint p1: Point, topRightPoint p2: Point, bottomRightPoint p3: Point, bottomLeftPoint p4: Point, fillColor: Color = Color.white, hatchPattern: BarGraphSeriesOptions.Hatching, isOriginShifted: Bool) {
public func drawSolidRect(topLeftPoint p1: Pair<FloatConvertible,FloatConvertible>,
Copy link
Collaborator

Choose a reason for hiding this comment

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

While there's utility in making data points in a series generic, I don't know that there's as much to be gained by having things like plot borders be generic. Would there ever be cases where people would need precision beyond Float or need to plot values that would overflow standard types?

I wonder if this could be made easier to work with by having Point be a special case of Pair with two float values. Point still makes sense as a name for a type that defines physical points (upper-left, upper-right coordinates, etc.).

Copy link
Owner Author

Choose a reason for hiding this comment

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

Yes, we wont need precision beyond Float for drawing. I am using Pair<FloatConvertible,FloatConvertible> everywhere in all the plots. Even while calculations are being done I'm just temporarily casting them to Float. This is why I've used FloatConvertible here.

@@ -1,10 +1,10 @@
public class Axis{
public class Axis<T,U>{
public var scaleX: Float = 1
public var scaleY: Float = 1
Copy link
Collaborator

Choose a reason for hiding this comment

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

For scaleX and scaleY, should they be generic in the types of the Series? Type T for x, type U for y.

If that's the case, you won't be able to assign a default value. That would then imply that these should be optional, with nil as a default value, so that they can be ignored when no scaling is desired.

Copy link
Owner Author

Choose a reason for hiding this comment

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

I don't think scaleX and scaleY need to be generics. scales used everywhere are always Floats.

Copy link
Collaborator

Choose a reason for hiding this comment

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

OK, worst case, if you do need this to be generic at some point you should be able to go back and change this then.

@KarthikRIyer
Copy link
Owner Author

Sorry for resolving the above review> I clicked it by mistake.

init(_ other: Float)
init(_ other: Double)
// init(_ other: Int)
init(_ x: FloatConvertible)
Copy link
Owner Author

Choose a reason for hiding this comment

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

Here, if I uncomment line 4 to allow taking in Int too, I am unable to typecast a Float to an Int anywhere else, because it isn't able to find the right initializer for it. This can be fixed I think by adding a parameter label here like:
init(fromOther x: FloatConvertible).
But we'll have to write that everywhere.
Thoughts?

Copy link
Collaborator

Choose a reason for hiding this comment

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

You really don't want to initialize FloatConvertible directly as a type. As I commented elsewhere, I think you should be able to remove all of these initializers and focus instead on an initializer to Float that takes in a FloatConvertible, with Int conforming to that type via a simple method.

@@ -29,90 +34,71 @@ public class BarGraph: Plot {
public var scaleY: Float = 1
public var scaleX: Float = 1
public var plotMarkers: PlotMarkers = PlotMarkers()
public var series: Series = Series()
public var stackSeries = [Series]()
public var series: Series<LosslessStringConvertible,FloatConvertible> = Series<LosslessStringConvertible,FloatConvertible>()
Copy link
Owner Author

Choose a reason for hiding this comment

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

I have used LosslessStringConvertible instead of CustomStringConvertible.

// init(_ other: Int)
init(_ x: FloatConvertible)

func _asOther<T:FloatConvertible>() -> T
Copy link
Collaborator

Choose a reason for hiding this comment

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

This protocol seems overly complicated in its construction. What you really want is to use this to be able to convert other types to Float. You want it to be as simple as Float(value) producing a Float.

For that, you don't need the initializers here. Instead, I'd define an extension on Float that provides an init like the following:

extension Float {
	    public init(_ x:FloatConvertible) {self = x.toFloat()}
}

and I'd remove the other initializers here. You're not directly initializing a FloatConvertible type, you simply want to make sure that you can filter for types that can be used with Float(value).

I'm using toFloat() as a method name here, because you don't want to be using an underscore for a public method name. The signature should be something like the following:

func toFloat() -> Float

for index in 1..<p.count {
if (p[index].x > max) {
max = p[index].x
public func getMaxX<T>(pairs : [Pair<FloatConvertible,T>]) -> Float {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Should getMaxX be generic about the X input type? If that's the case, you'll need to change the definition to be T, U where T: FloatConvertible and return type T.

Also, is getMaxX the best name for this? maxX would seem to be simpler, or even argmax as used by numpy. Could this be defined as an extension to Array, with an appropriate Element type?

Copy link
Owner Author

Choose a reason for hiding this comment

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

@BradLarson we cannot use extensions with the array element as generic types yet.
This is what I tried:

extension Array where Element:Pair<FloatConvertible,U> {
    var maxX: Float {
        var max = Float(self[0].x)
        for index in 1..<self.count {
            if (Float(self[index].x) > max) {
                max = Float(self[index].x)
            }
        }
        return max
    }
    var minX: Float {
        var min = Float(self[0].x)
        for index in 1..<self.count {
            if (Float(self[index].x) < min) {
                min = Float(self[index].x)
            }
        }
        return min
    }
}

But this doesn't work. So I'm letting it be a function for now.

Copy link
Owner Author

@KarthikRIyer KarthikRIyer Jul 7, 2019

Choose a reason for hiding this comment

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

@BradLarson also, will there be a problem with type erasure? I don't think that X needs to be a generic...
Let's take LineChart as an example.
We have `var primaryAxis = Axis<FloatConvertible,FloatConvertible>()
This makes the series inside axis as Series<FloatConvertible,FloatConvertible>
and the values inside the series also as Pair<FloatConvertible,FloatConvertible>

Now if I change the definition to T,U with T as FloatConvertible, It gives me this error:
error: protocol type 'FloatConvertible' cannot conform to 'FloatConvertible' because only concrete types can conform to protocols

Now I think, to get past this I'll need to make the LineChart class a generic. This doesn't seem like a very nice idea, or is it?

Copy link
Collaborator

Choose a reason for hiding this comment

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

Don't know if it would help, but here's another take on maxX, relying instead on Comparable to avoid casting to Float for every value, while preserving type information:

public func maxX<T, U>(_ values: [Pair<T, U>]) -> T where T: Comparable {
    var max = values[0].x
    for index in 1..<values.count {
        if (values[index].x > max) {
            max = values[index].x
        }
    }
    return max
}

I could see an argument for making a plot type generic about the series values it would pass in, allowing for a preservation of types for X and Y values. Some of this might require experimentation, creating cases in your sample applications where you try to pass in various data types and simulate common user interactions with the plots to see what produces the easiest API to use and understand. That's the goal, make this as flexible as possible while hiding as much complexity from end users (developers using this API).

public func rotatePoint(point: Point, center: Point, angleDegrees: Float) -> Point{
public func rotatePoint(point: Pair<FloatConvertible,FloatConvertible>,
center: Pair<FloatConvertible,FloatConvertible>,
angleDegrees: Float) -> Pair<FloatConvertible,FloatConvertible>{
Copy link
Collaborator

Choose a reason for hiding this comment

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

When you're defining Pair in this manner, I believe you're erasing the type. I think instead you'll want to use Pair<T, U> where T: FloatConvertible, U: FloatConvertible. That preserves the type, but makes sure they conform to the protocol, where right now you're using the protocol as the type.

}
}

let zeroPair = Pair<FloatConvertible,FloatConvertible>(0.0, 0.0)
Copy link
Collaborator

Choose a reason for hiding this comment

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

Can this be done as a static let, such as the previous Point.zero? I believe you should be able to use that with a Pair<Float, Float> type in the static let.

Copy link
Owner Author

Choose a reason for hiding this comment

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

I tried this. But am getting this error:
static stored properties not supported in generic types

Copy link
Collaborator

Choose a reason for hiding this comment

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

Hmm, guess that won't work then. Think I know why that's happening. Oh, well.

public var topRight : Point = Point.zero
public var bottomLeft : Point = Point.zero
public var bottomRight : Point = Point.zero
public var topLeft : Pair<FloatConvertible,FloatConvertible> = zeroPair
Copy link
Collaborator

@BradLarson BradLarson Jul 5, 2019

Choose a reason for hiding this comment

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

You shouldn't need to specify the Pair type, inference should take care of that for you. I believe you should be able to just use Pair as a type, without the constraints.

Again, see my earlier comment about how Point as a special case of Pair might make sense from a readability perspective on plot borders, etc.

Copy link
Owner Author

Choose a reason for hiding this comment

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

You shouldn't need to specify the Pair type, inference should take care of that for you. I believe you should be able to just use Pair as a type, without the constraints.

Yes. I'm sorry. I overlooked this.

public init(frameWidth : Float = 1000,
frameHeight : Float = 660,
subWidth sW : Float = 1000,
subHeight sH : Float = 660) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

As a stylistic note, it's general convention to have no spaces before a colon in function parameters, and one space afterward. (frameWidth: Float). Google's Swift style guide (which you don't have to adhere to, but is one example) has a section on that here:

https://google.github.io/swift/#horizontal-whitespace

public var yMarkers = [Point]()
public var xMarkersTextLocation = [Point]()
public var yMarkersTextLocation = [Point]()
public var xMarkers = [Pair<FloatConvertible,FloatConvertible>]()
Copy link
Collaborator

Choose a reason for hiding this comment

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

Another place where a specialized Point type might make this a little simpler. Also, you're again treating FloatConvertible as a type, where you'd probably mean Float directly.

public var barGraphSeriesOptions = BarGraphSeriesOptions()
public var scatterPlotSeriesOptions = ScatterPlotSeriesOptions()
public var points = [Point]()
public var scaledPoints = [Point]()
public var pairs = [Pair<T,U>]()
Copy link
Collaborator

Choose a reason for hiding this comment

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

While Pair is a good generic name for a type, I'm not sure that pairs as a variable name here is as descriptive as it could be. Maybe values would be better.

points = p

public init(pairs : [Pair<T,U>],
label l: String,
Copy link
Collaborator

@BradLarson BradLarson Jul 5, 2019

Choose a reason for hiding this comment

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

I'd just use label as the parameter name, and drop the l. You can always use self.label = label to differentiate between local parameters and class properties. There are other examples of this below.

endColor : Color = Color.lightBlue,
hatchPattern: BarGraphSeriesOptions.Hatching = .none,
scatterPattern: ScatterPlotSeriesOptions.ScatterPattern = .circle){
self.pairs = pairs
Copy link
Collaborator

Choose a reason for hiding this comment

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

Thinking about this further, I don't think pairs (or values or whatever) should be exposed directly within Series. Instead, Series should act as a collection that wraps this. For example, count should be defined directly on Series so that it returns pairs.count, which would simplify code in a few places.

Likewise, you should define a custom subscript operator on Series that indexes into pairs. Finally, per my earlier comments on getMaxX, maybe you just have a maxX() function on Series itself and save having to call into the pairs property externally.

Copy link
Owner Author

Choose a reason for hiding this comment

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

Yes! This is a great idea. It would make it more convenient to use the Series type.

Copy link
Owner Author

@KarthikRIyer KarthikRIyer Jul 6, 2019

Choose a reason for hiding this comment

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

@BradLarson
I think letting it be exposed along with adding the subscript operator might be a good idea? We might need to make a copy of just the values sometime and not the entire series variable...
Also we might need to use the maxX function elsewhere. Like for histogram I have a separate HistogramSeries Type. Although I amn't using the maxX function there but we might need such functionality in the future.

Copy link
Collaborator

Choose a reason for hiding this comment

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

You could try both, and see what makes sense. I've generally wrapped values like this so that this implementation detail isn't exposed, but see what works best for your architecture.

About HistogramSeries, is there a different way to architect that so that you could use a more generic Series instead of having a special type for that? Could a histogram process a Series into a more specific internal representation? How do other frameworks handle this, do people provide a more standard set of datapoints and have the histogram plot process them, or do they provide histogram-specific data?

Copy link
Owner Author

Choose a reason for hiding this comment

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

The kind of data just a little different. Instead of having X and Y values, we need the user to give just an array of data and the number of bins it needs to be divided into. I wrote a different Series type because we don't need the X vales here. I'll try if I can use the normal Series type here.

…rator to Series, simplify FloatConvertible protocol
@KarthikRIyer
Copy link
Owner Author

I've made the changes apart from some queries which I've specified above. The thing left to be done is include Histogram. I'll do that by tomorrow.

var maximumYSecondary: Float = 0
var minimumXSecondary: Float = 0
var minimumYSecondary: Float = 0
var maximumXSecondary = T(0)
Copy link
Owner Author

Choose a reason for hiding this comment

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

@BradLarson I had to add those extra initializers in FloatConvertible.swift to do this.

@KarthikRIyer
Copy link
Owner Author

@BradLarson I thought it would be best to converts all the plots to generics. So I did that.
Also I have kept the HistogramSeries as a separate type because I would have had to add a lot of unnecessary stuff to the normal series to get it working and it would have got messy. But I moved all the calculation part to the Histogram class. Now the HistogramSeries type just stores the required variables and does no calculation.

@KarthikRIyer KarthikRIyer requested a review from BradLarson July 12, 2019 17:57
Copy link
Collaborator

@BradLarson BradLarson left a comment

Choose a reason for hiding this comment

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

This seems like a sensible design, let's see how it works as features are added and people use it in different cases.

@KarthikRIyer
Copy link
Owner Author

@BradLarson I've created files for the random values. Merging this now.

@KarthikRIyer KarthikRIyer merged commit 0d7e3ba into master Jul 13, 2019
@KarthikRIyer KarthikRIyer deleted the data_input branch January 11, 2020 07:23
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.

2 participants