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

Tick Marks layout #8

Merged
merged 30 commits into from
Jul 12, 2023
Merged

Tick Marks layout #8

merged 30 commits into from
Jul 12, 2023

Conversation

danielost
Copy link
Collaborator

Worked on the issue:

The following PR contains the following features:

  • Major tick marks layout
  • Grid layout

This is what a plot looks like now:

image

The same plot, but with a grid:

image

Next steps:

  • Minor ticks layout
  • Colour picking
  • Font picking
  • Fixing label location (right now label position is not relative to the tick, so it breaks when a label is a big number). Will use Landmark to fix this
  • Using manual ticks (specified by the user)

Copy link
Contributor

@noelwelsh noelwelsh left a comment

Choose a reason for hiding this comment

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

I think the code needs a bit of cleanup.

I also think having all these code within Plot will soon become unmanagable, but that is a refactoring that can be done later.

@@ -26,14 +26,16 @@ import doodle.syntax.all.*
*/
final case class Plot[
A,
Alg <: Layout & Shape
Alg <: Layout & Shape & Style & Text & doodle.algebra.Transform & Path
Copy link
Contributor

@noelwelsh noelwelsh Jul 6, 2023

Choose a reason for hiding this comment

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

I think this is incorrect. This is a constraint on layers, and this seems an overly restrictive to require that they implement these algebras. (I'm not sure why the original constraint was Layout & Shape. I don't think it needs to be anything other than doodle.algebra.Algebra.) This constraint should be on draw, I think.

}

def draw(width: Int, height: Int): Picture[Alg, Unit] = {
val allData = layers.flatMap(_.data.foldLeft(List.empty[A])(_ :+ _))
Copy link
Contributor

Choose a reason for hiding this comment

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

This is only to calculate the enclosing bounding box, right? It's quite inefficient to do all this copying.

I think this will do:

layers.foldLeft(BoundingBox.empty){ (accum, layer) => 
  accum.on(layer.data.boundingBox)
}

xTicksSequence,
yTicksSequence
)
return withTitles(plotWithTicksAndAxesAndGrid)
Copy link
Contributor

Choose a reason for hiding this comment

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

Don't use return. Rewrite to an expression.

* coordinate of a tick - to place the tick on a graph. The second one is the
* original coordinate - to give the tick a label with its coordinate.
*/
private def xTicksToSequence(
Copy link
Contributor

Choose a reason for hiding this comment

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

Too much repetition in this code. There shouldn't be repeated code for the x- and y-axis. You can abstract over x- and y-coordinates using a function. E.g.

val asX: Double => Point = x => Point(x, 0)
val asY: Double => Point = y => Point(0, y)

plot
.on(
OpenPath.empty
.moveTo(tick._1.x, yTicksMapped.min - 10)
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm a bit not-fan of using these methods on tuple. I'd rather destructure with an irrefutable match like so:

val (screen, _) = tick

.toList
}

/** Converts `Ticks` to a list of tuples. The first element is the mapped
Copy link
Contributor

Choose a reason for hiding this comment

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

Need to be clearer on what these coordinates mean. I think of screen vs data coordinates.

Screen coordinates are the coordinates of the graph rendered on the screen. Data coordinates are the values in the data.

Consider using opaque types to distinguish these at the type level, as I think it will be very easy to get them confused in code.

@danielost danielost added the enhancement New feature or request label Jul 7, 2023
@danielost danielost linked an issue Jul 7, 2023 that may be closed by this pull request
@danielost danielost requested a review from noelwelsh July 8, 2023 18:38
Copy link
Contributor

@noelwelsh noelwelsh left a comment

Choose a reason for hiding this comment

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

A few more changes here, I think.


def draw(width: Int, height: Int): PlotPicture = {
val dataBoundingBox = layers.foldLeft(BoundingBox.empty) { (bb, layer) =>
bb.on(layer.data.boundingBox(layers.head.toPoint))
Copy link
Contributor

Choose a reason for hiding this comment

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

layers.head.toPoint is wrong. Different layers could have different toPoint functions. This only compiles due to the overly restrictive type of layers (should probably be Layer[?, Alg] not Layer[A, Alg])

Suggested change
bb.on(layer.data.boundingBox(layers.head.toPoint))
bb.on(layer.data.boundingBox(layer.toPoint))

It might be worth adding a boundingBox method to Layer.


import doodle.core.Point

object Coordinate {
Copy link
Contributor

Choose a reason for hiding this comment

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

This wrapper object is not needed, if I'm correctly reading the documentation on opaque types.

@@ -20,20 +20,24 @@ import chartreuse.layout.ScatterPlot
import doodle.algebra.*
import doodle.core.*
import doodle.syntax.all.*
import chartreuse.Coordinate._
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
import chartreuse.Coordinate._
import chartreuse.Coordinate.*

_ is Scala 2 syntax and will be removed in the future.

.path
)
.on(
text(((dataCoordinate.x * 1000).round / 1000.0).toString)
Copy link
Contributor

Choose a reason for hiding this comment

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

plot
.on(
OpenPath.empty
.moveTo(xTicksMapped.min - 10, screenCoordinate.y)
Copy link
Contributor

Choose a reason for hiding this comment

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

Ad-hoc magic numbers need explanation. I assume this is a temporary decision that will be replaced by style options later.

)
}

private def withYTicks(
Copy link
Contributor

Choose a reason for hiding this comment

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

If this method only returned the PlotPicture that represents the ticks, instead of adding it to the plot, there would be more possibilities for composition in the use above.

plot: PlotPicture,
yTicksMapped: Ticks
): PlotPicture = {
xTicksSequence
Copy link
Contributor

Choose a reason for hiding this comment

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

Repetition of this code and withYTicks is somewhat annoying. It would be nice to remove it.

val plotWithTicksAndAxes =
withAxes(plotWithXAndYTicks, xTicksMapped, yTicksMapped)

if (grid) {
Copy link
Contributor

Choose a reason for hiding this comment

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

If all the with methods (withGrid, withXTicks`) just returned the picture instead of adding it to the plot, you could write something like

xTicks
  .on(yTicks)
  .on(axes)
  .on(allLayers)
  .on(if grid then theGrid else empty[Shape])

I.e. emphasize composition.

@danielost
Copy link
Collaborator Author

build task fails because of NumberFormat.

Same issue is described here:
https://stackoverflow.com/questions/40165940/referring-to-a-non-existent-class-java-io-file

We probably need to find a replacement for NumberFormat.

@danielost
Copy link
Collaborator Author

I think it is not necessary to add the whole Plot implementation for JS and JVM and it must be fixed. But I just want to make sure everything else is OK.

Copy link
Contributor

@noelwelsh noelwelsh left a comment

Choose a reason for hiding this comment

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

LGTM!

@danielost
Copy link
Collaborator Author

Thanks!

@danielost danielost merged commit a8d5bcd into creativescala:main Jul 12, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
Development

Successfully merging this pull request may close these issues.

Add plot abstraction
2 participants