Repository navigation
Add an opt-in polar grid to the 1.x branch - #373
Conversation
mauriciopoppe
left a comment
There was a problem hiding this comment.
Please change the base branch to 1.x, the reason is that this is a feature so I'd need to create the branch release-1.26.
|
|
||
| [`Check the available options in the docs`](https://mauriciopoppe.github.io/function-plot/docs/functions/default-1.html) | ||
|
|
||
| Polar functions use Cartesian coordinates by default. To display a polar grid, |
There was a problem hiding this comment.
All the examples are in site/js/site.js, please move this example alongside a comment above it, the dev server running in localhost:8080 should display it.
| throw Error('axis type ' + axis.type + ' unsupported') | ||
| })(this.options.yAxis)) | ||
|
|
||
| if (this.options.coordinateSystem === 'polar') { |
There was a problem hiding this comment.
It feels a little weird to have two locations where xDomain & yDomain are set (the block above this one and this one).
I'm thinking we could create a function that returns the xDomain and yDomain, and within that function call getPolarDomains if needed or fallback to the other cartesian domain logic.
function getDomains(
options: FunctionPlotOptions,
width: number,
height: number
): {
xDomain: number[]
yDomain: number[]
} {
const computeYDomainSize = (xDomain: number[]) => {
const xSize = xDomain[1] - xDomain[0]
return (height * xSize) / width
}
const radius =
options.coordinateSystem === 'polar'
? options.polar?.radiusDomain?.[1]
: undefined
const radiusLimit = radius === undefined ? undefined : radius * 1.2
const xDomain = (() => {
if (options.xAxis.domain) {
return options.xAxis.domain
}
if (options.xAxis.type === 'log') {
return [1, 10]
}
if (options.xAxis.type === 'linear') {
return radiusLimit === undefined
? [-6, 6]
: [-radiusLimit, radiusLimit]
}
throw new Error(`axis type ${options.xAxis.type} unsupported`)
})()
const yDomain = (() => {
if (options.yAxis.domain) {
return options.yAxis.domain
}
if (options.yAxis.type === 'log') {
return [1, 10]
}
if (options.yAxis.type === 'linear') {
if (radiusLimit !== undefined) {
return [-radiusLimit, radiusLimit]
}
const ySize = computeYDomainSize(xDomain)
return [-ySize / 2, ySize / 2]
}
throw new Error(`axis type ${options.yAxis.type} unsupported`)
})()
if (options.coordinateSystem === 'polar') {
return getPolarDomains(xDomain, yDomain, width, height)
}
return { xDomain, yDomain }
}
Caller:
const { xDomain, yDomain } = getDomains(
this.options,
this.meta.width,
this.meta.height
)
this.meta.xDomain = xDomain
this.meta.yDomain = yDomain
| .selectAll('text.x.axis-label') | ||
| .data(function (d: FunctionPlotOptions) { | ||
| return [d.xAxis.label].filter(Boolean) | ||
| return d.coordinateSystem === 'polar' ? [] : [d.xAxis.label].filter(Boolean) |
There was a problem hiding this comment.
self.isPolarCoordinateSystem() instead (where self = outer scope).
| .selectAll('text.y.axis-label') | ||
| .data(function (d: FunctionPlotOptions) { | ||
| return [d.yAxis.label].filter(Boolean) | ||
| return d.coordinateSystem === 'polar' ? [] : [d.yAxis.label].filter(Boolean) |
There was a problem hiding this comment.
self.isPolarCoordinateSystem() instead (where self = outer scope).
| .attr('stroke', 'black') | ||
| .attr('opacity', 0.2) | ||
| yOrigin.merge(yOriginEnter).attr('d', this.line) | ||
| yOrigin.exit().remove() |
| const polar = options.polar || {} | ||
| const xScale = meta.xScale | ||
| const yScale = meta.yScale | ||
| const visibleDomain = getPolarRadiusDomain(xScale.domain(), yScale.domain()) |
There was a problem hiding this comment.
Some of these variables are used within functions e.g. radius is only used by getRayGeometry, please limit the scope of a variable to the smallest scope possible, review this for all the consts you defined here.
| const TWO_PI = 2 * Math.PI | ||
| const DEFAULT_POLAR_ANGLE_UNIT = 'radians' | ||
|
|
||
| export function validatePolarOptions(options: FunctionPlotOptions) { |
There was a problem hiding this comment.
Please add a docstring in all the functions, if possible add some explanations with ascii graphs about the math.
| position?: 'sticky' | 'left' | 'bottom' | ||
| } | ||
|
|
||
| export interface PolarOptions { |
There was a problem hiding this comment.
API wise looks clean, I explored the following but I think a similar version it would be part of the v2 (main) branch instead.
interface AxisOptions {
domain?: [number, number]
label?: string
}
interface CartesianAxisOptions extends AxisOptions {
type?: 'linear' | 'log'
invert?: boolean
position?: 'sticky' | 'left' | 'bottom'
}
interface PolarRadiusAxisOptions extends AxisOptions {
ticks?: number | number[]
tickFormat?: (radius: number) => string
}
interface PolarAngleAxisOptions extends AxisOptions {
ticks?: number | number[]
unit?: 'radians' | 'degrees'
tickFormat?: (angle: number) => string
labels?: boolean
}
interface PolarOptions {
grid?: boolean
radiusAxis?: PolarRadiusAxisOptions
angleAxis?: PolarAngleAxisOptions
}
| @@ -0,0 +1,187 @@ | |||
| const puppeteer = require('puppeteer') | |||
There was a problem hiding this comment.
Please add a test to test/e2e/graphs.test.js too to see a screenshot
| await browser?.close() | ||
| }) | ||
|
|
||
| it('renders a true polar grid while preserving Cartesian defaults', async () => { |
There was a problem hiding this comment.
These are good e2e tests, in this codebase I just did screenshot tests and that was enough but checking dom properties seems like a good idea.
|
Thank you very much for the detailed review and helpful suggestions. |
|
|
0fb9e4e to
a0619c7
Compare
|
The missing snapshot baseline is now included. I've also improved the polar grid labels and clipping, and fixed zoom continuity across resizing and coordinate-system switches, with regression tests. |
| // update the range only but not the domain, the domain is going to be updated | ||
| self.meta.zoomBehavior.xScale.range(self.meta.xScale.range()) | ||
| self.meta.zoomBehavior.yScale.range(self.meta.yScale.range()) | ||
| if (this.isPolarCoordinateSystem() || this.getEmitInstance().wasPolarCoordinateSystem) { |
There was a problem hiding this comment.
In the v2 version I'm thinking about removing the update hook which means that I might not carry over this change, the reason is that I dislike the current way of updating things e.g.
// initial render
const opts = {}
functionPlot(opts)
// update requires setting properties on the object sent.
opts.foo = bar
functionPlot(opts)I imagine that this update might be for that type of update from cartesian to polar?
In any case handling the update seems good to me.
| */ | ||
| private linkedGraphs: Array<Chart> | ||
| private line: Line<[number, number]> | ||
| // Keep the rendered mode separately because callers can mutate and reuse options. |
There was a problem hiding this comment.
A chart holds the metadata for the axis under the options, maybe this property should be a hidden property there and not in the chart. Can this be derived instead?
|
Thanks for the suggestion. I'll move the last rendered coordinate-system state into an internal property on options. My concern with deriving the previous mode is that the existing state doesn't provide a reliable source: options.coordinateSystem may already be changed, and the layout and scales have been updated by the time the zoom baseline is rebuilt. Grid presence isn't reliable either, since polar.grid can be disabled. |
|
To clarify the cause with a concrete example: for a 550 × 550 chart without a title, polar mode uses the full 550 × 550 plot area because its margins are zero. Cartesian mode reserves 40px on the left, 20px on the right, and 20px on both the top and bottom, leaving a 490 × 510 plot area. |
|
Thank you for your contribution @wwwaker! I'll publish a 1.26 release soon. |



What this changes
polar.angularLabels: true), using radians by default.Open design question
When angular labels are enabled, the current implementation selects a

visible radial ring independently for each angle. After panning or zooming,
labels may therefore appear on different rings.
I think we should select one shared ring, favor an outer ring with enough
visible space for labels, and hide labels that do not fit (I learn it from Desmos). I would appreciate
your thoughts on that rule.
Validation
Fixes #371