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

axis mark #1197

Merged
merged 156 commits into from
Feb 1, 2023
Merged

axis mark #1197

merged 156 commits into from
Feb 1, 2023

Conversation

mbostock
Copy link
Member

@mbostock mbostock commented Dec 23, 2022

This implements axes as a proper mark, allowing a high degree of custom styling, even data-driven.

  • Channels (e.g., varying color based on value)
  • Grid
  • Left-oriented y-axis
  • Bottom-oriented x-axis
  • Right-oriented y-axis
  • Top-oriented x-axis
  • Smarter default ticks (port autoAxisTicks)
  • Smarter default tick format
  • Tick rotate equivalent
  • Axis insets
  • Axis dx and dy
  • Facet axes 😬
  • A variant of facetAnchor that allows strict placement on the sides (for facet axes)
  • Remove Plot’s built-in axis rendering and instead automatically add axis marks as needed
  • Axis line option
  • y-axis labels
  • x-axis labels
  • fx-axis labels
  • fy-axis labels
  • Implement labelAnchor option
  • Implement labelOffset option
  • Implement tickSpacing option (80px / 35px default)
  • Fix label position for ordinal scales
  • Fix label arrow based on chosen label anchor
  • Fix facet axis label positions
  • Fix named time intervals (polylinear test)
  • Don’t allow tickSize to be specified as a function
  • Implement the both axis option
  • Tests
  • Investigate missing x-axis in “yearly requests” tests
  • Investigate broken metroUnemploymentRidgeline
  • Investigate downloadsOrdinal test
  • Investigate penguinDodgeHexbin test
  • Investigate projectionHeightGeometry test
  • Shorthand x and y option for grid marks
  • Documentation

Nice to haves:

  • Don’t duplicate axis label when there are multiple axes?
  • Pass through the lineWidth option to wrap tick labels?
  • Shorthand for conditional remove-if-the-same-as-previous multi-line tick time formats?
  • Allow the axis tick shape vector option (and others) to be overridden?
  • A lineOffset option for translating tick text relative to the tick anchor?

Fixes #6.
Fixes #7.
Fixes #114 (#385).
Fixes #148.
Fixes #210.
Fixes #375.
Fixes #460.
Fixes #464.
Fixes #522.
Fixes #609.
Fixes #612.
Fixes #631.
Fixes #926.
Fixes #976.
Fixes #985.
Fixes #987.
Fixes #992.
Fixes #1000.
Fixes #1198.
Fixes #1199.

@mbostock mbostock requested a review from Fil December 23, 2022 19:13
@mbostock
Copy link
Member Author

mbostock commented Dec 24, 2022

Since the axes are driven by data, you can now do neat stuff like vary the x-position of the y-axis:

Screenshot 2022-12-24 at 9 19 22 AM

Plot.gridY({x: (y) => AAPL.find((d) => d.Close >= y)?.Date, insetLeft: -10}),
Plot.axisY({x: (y) => AAPL.find((d) => d.Close >= y)?.Date, insetLeft: -10})

@mbostock
Copy link
Member Author

mbostock commented Dec 24, 2022

ggplot2 theme?

Screenshot 2022-12-24 at 11 16 56 AM

Plot.frame({fill: "#eaeaea"}),
Plot.gridY({tickSpacing: 35, stroke: "#fff", strokeOpacity: 1, strokeWidth: 0.5}),
Plot.gridY({tickSpacing: 70, stroke: "#fff", strokeOpacity: 1}),
Plot.axisY({tickSpacing: 70}),
Plot.gridX({tickSpacing: 40, stroke: "#fff", strokeOpacity: 1, strokeWidth: 0.5}),
Plot.gridX({tickSpacing: 80, stroke: "#fff", strokeOpacity: 1}),
Plot.axisX({tickSpacing: 80}),

@mbostock
Copy link
Member Author

We’ll be able to fix #522 with this, too!

Screenshot 2022-12-26 at 5 04 05 PM

README.md Outdated Show resolved Hide resolved
@mbostock
Copy link
Member Author

mbostock commented Jan 30, 2023

I think axis = "both" should only draw one axis label? It’s nice to have the ticks drawn twice but I think you often don’t want a second axis label. (And you can always do that by adding an axis mark yourself, so it’s nice to favor the common one with axis = "both".)

@Fil
Copy link
Contributor

Fil commented Jan 31, 2023

An open question: do we need to support data in the axisX(data, options) syntax, since the tick values can be passed as options.ticks, with—I think?—no loss of generality.

@mbostock
Copy link
Member Author

An open question: do we need to support data in the axisX(data, options) syntax

I chose not to do this. Here are my reasons:

  1. When data is specified up-front, we can compute channels the normal way rather than deferring it to the initializer. This means that specifying the data up-front has different behavior than specifying an array of ticks: in the former case, it can extend the scale domain, whereas in the latter, it can’t (because initializers aren’t allowed to affect the behavior of position scales). I realized I broke this along the way, but I’ve fixed it in 61e1145 and added a test.

Screenshot 2023-01-31 at 8 34 11 AM

Plot.plot({
  marks: [
    Plot.axisY(d3.ticks(0, 200, 10), {anchor: "left"}),
    Plot.lineY(AAPL, {x: "Date", y: "Close"})
  ]
})
  1. Philosophically, I think it’s desirable to support the same syntax with axis marks that we do with other marks, especially when “under the hood” the data associated with an axis mark is this array of tick values. Yes, axis marks are special in that typically the tick values depend on the scale domains, and hence cannot be known until after the other marks are initialized; and because channels depend on data, that means typically all of an axis mark’s channels need to be deferred to an initializer too. But per (1) it’s nice that if the data is specified up-front, we can have the axis mark follow the same code path as a normal mark.*

*The fontVariant and tickFormat also depend on the scale, and so also need to be deferred to the initializer. And likewise axis label orientation. But we can at least make the axis mark behave as much like a normal mark as possible.

@mbostock
Copy link
Member Author

In a case like this

Plot.plot({
  y: {
    grid: true
  },
  marks: [
    Plot.axisY({anchor: "left"}),
    Plot.lineY(AAPL, {x: "Date", y: "Close"})
  ]
})

should we still draw implicit the y-axis grid even though there’s an explicit y-axis? I kind of think that yes, we should. Though that would also mean that the implicit y-axis grid would use the default ticks, which could be different than those chosen by the explicit y-axis. That seems fine, though.

Copy link
Contributor

@Fil Fil left a comment

Choose a reason for hiding this comment

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

Thanks for 61e1145 which clarifies the issue.

We still need to iterate a bit on the documentation, but I'm happy with everything

@mbostock
Copy link
Member Author

Oh, there was another idea I had for shorthand: if the grid option is a number or an interval (or an interval name), it gets promoted to the ticks option of the grid mark; if the grid option is a color, it gets promoted to the stroke option of the grid mark. Then you can say grid: 20 for more grid lines, or grid: "red" for a red grid, etc. Not really needed since you can do the same thing with an explicit mark but it’s always fun to have extra shorthand…

@mbostock
Copy link
Member Author

This seems like a bug: when there’s an explicit x-axis, the default anchor of the implicit fx-axis changes to bottom instead of top, resulting in the two axes overlapping. I’m guessing that the fx-axis should always default to a top anchor.

Screenshot 2023-01-31 at 12 40 19 PM

Plot.plot({
  facet: {marginRight: 80},
  marks: [
    Plot.frame(),
    Plot.dot(penguins, {x: "culmen_length_mm", y: "culmen_depth_mm", fx: "sex", fy: "species"}),
    Plot.axisX({color: "red"})
  ]
})

@Fil
Copy link
Contributor

Fil commented Feb 1, 2023

Should Plot.axisX be wrapped with Plot.marks? Currently we have to write

Plot.marks(Plot.axisX(d3.ticks(0, 1, 10), { stroke: "red" })).plot()

it could be simpler to manipulate as:

Plot.axisX(d3.ticks(0, 1, 10), { stroke: "red" }).plot()

@mbostock
Copy link
Member Author

mbostock commented Feb 1, 2023

Yes, it should use Plot.marks. That’s an oversight on my part.

@mbostock mbostock merged commit 1da65ff into main Feb 1, 2023
@mbostock mbostock deleted the mbostock/axis-mark branch February 1, 2023 18:19
@yurivish
Copy link
Contributor

yurivish commented Feb 2, 2023

tiny note: i just noticed that the README says "The mark.facetAnchor option controls TK. " which was introduced in this PR. I'd fix it myself but am not sure what it should say...

@mbostock
Copy link
Member Author

mbostock commented Feb 2, 2023

Yes, the documentation is still being written. Thanks!

chaichontat pushed a commit to chaichontat/plot that referenced this pull request Jan 14, 2024
* axis mark

* deferred channels

* vector tick

* tickPadding

* grid

* grid

* no rounded stroke

* fix half-pixel offset

* more conditional axes

* grid mark

* DRY tick

* tick[XY]

* explicit ticks

* axisTick[XY]

* axis mark data

* axis insets

* tickFormat

* better default tick format

* simpler inferTickFormat

* auto ticks; expose scale.interval

* anchor

* manual axis label, for now

* tickRotate

* fix crash in sparse excludeIndex

* facetAnchor; fix mark-level missing facet

* simpler facet sorting

* fix observablehq#522; better facetAnchor

* fix observablehq#1198; better empty facets

* fix for ordinal scales

* soft hyphen

* test label wrapping

* facet grid

* better facet grid

* better facet grid, again

* better grid

* better facet axes; fix font variant

* extract facetSkip

* empty facets

* destructure options

* remove redundant defaults

* fix tickPadding, tickRotate

* better grid defaults

* more empty facets

* simpler facet warning

* decoration

* better empty facets

* better facet sort

* extract facetAnchor

* extract more facet logic

* const facets

* unify code

* more facet anchors

* invert facetAnchor

* checkpoint axis label

* checkpoint axis mark integration

* fix minimum offset margin

* fix for null marks and scales

* better implicit axes

* denote side-effect

* better axis options

* implicit facet axes

* more implicit axis options

* inferAxes

* delete old axes

* minimize diff

* fix default margins

* remove mark.decoration

* facet margin options

* move side effect

* inheritScaleLabels

* remove comment

* better ARIA labels; restore prior axis order

* regenerate tests; smarter partial implicit axes

* more smarter partial implicit axes

* axis line

* text stroke options

* don’t inline text elements

* do inline title elements

* axis label

* fix axes for interval scales

* gridDasharray

* better implicit axis

* x-axis label

* another fancy axis example

* polish example

* fancy multi-line time ticks

* another fancy axis example

* remove secondary grid shorthand

* tweak y-axis label position

* first stab at documentation

* the grid doesn't follow anchor

* better deferred channels

* better explicit built-in axes

* fix metroUnemploymentRidgeline test

* use insetTop for better semantics

* fix label option inheritance

* axis = both

* fix comment

* a few fixes to axis label position

* labelAnchor

* smarter default labelAnchor

* fix googleTrendsRidgeline

* fix longLabels

* non-fix for named time intervals

* fix projectionHeightGeometry

* non-fix for x-axis label position

* better axis label position

* inset axis label

* fix observablehq#375; respect facet margins

* fix industryUnemploymentTrack

* restore grid in penguinCulmen

* facet margin collapse

* better facet margin collapse

* drop comment

* labelOffset

* fix auto labels and arrows

* named exports for tests

* enable facet axis labels

* add non-faceted athletesSample test

* better facet axis label position

* cleaner

* enforce constant tick options

* shorter athletesSportWeight

* remove dead code

* checkpoint cross-facet marks

* checkpoint supermarks

* super option

* better super marks

* drop unused mid facet anchors

* disallow superposition, for now

* facet = super

* minimize diff

* fix axis label position, finally?

* better hexbin tests

* polish tests

* fix named time intervals for ticks

* grid xy shorthand

* consistent scale checks

* better axis options

* tickSpacing option

* propagate tickSpacing option

* fix off-by-one with interval ticks

* typo (thanks, @yurivish!)

* only one label for both axes

* a bit of documentation for axisX

* given data, don’t defer channels

* reduce aaplCloseDataTicks test

* implicit grid with explicit axis

* more grid shorthand

* Update README

* fix default facet axis anchor

* axis plot shorthand

* Update README

---------

Co-authored-by: Philippe Rivière <fil@rezo.net>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment