Skip to content

Commit

Permalink
feat(scales): move ticks logic to the scales package
Browse files Browse the repository at this point in the history
  • Loading branch information
plouc committed Sep 11, 2021
1 parent 490b761 commit 801c767
Show file tree
Hide file tree
Showing 15 changed files with 384 additions and 394 deletions.
2 changes: 0 additions & 2 deletions packages/axes/package.json
Expand Up @@ -30,13 +30,11 @@
"@nivo/scales": "0.73.0",
"@react-spring/web": "9.2.4",
"d3-format": "^1.4.4",
"d3-time": "^1.0.11",
"d3-time-format": "^3.0.0"
},
"devDependencies": {
"@nivo/core": "0.73.0",
"@types/d3-format": "^1.4.1",
"@types/d3-time": "^1.1.1",
"@types/d3-time-format": "^2.3.1"
},
"peerDependencies": {
Expand Down
18 changes: 6 additions & 12 deletions packages/axes/src/canvas.ts
@@ -1,16 +1,10 @@
import { degreesToRadians, CompleteTheme } from '@nivo/core'
import { ScaleValue, AnyScale, TicksSpec } from '@nivo/scales'
import { computeCartesianTicks, getFormatter, computeGridLines } from './compute'
import { positions } from './props'
import {
AxisValue,
TicksSpec,
AnyScale,
AxisLegendPosition,
CanvasAxisProp,
ValueFormatter,
} from './types'

export const renderAxisToCanvas = <Value extends AxisValue>(
import { AxisLegendPosition, CanvasAxisProp, ValueFormatter } from './types'

export const renderAxisToCanvas = <Value extends ScaleValue>(
ctx: CanvasRenderingContext2D,
{
axis,
Expand Down Expand Up @@ -163,7 +157,7 @@ export const renderAxisToCanvas = <Value extends AxisValue>(
ctx.restore()
}

export const renderAxesToCanvas = <X extends AxisValue, Y extends AxisValue>(
export const renderAxesToCanvas = <X extends ScaleValue, Y extends ScaleValue>(
ctx: CanvasRenderingContext2D,
{
xScale,
Expand Down Expand Up @@ -217,7 +211,7 @@ export const renderAxesToCanvas = <X extends AxisValue, Y extends AxisValue>(
})
}

export const renderGridLinesToCanvas = <Value extends AxisValue>(
export const renderGridLinesToCanvas = <Value extends ScaleValue>(
ctx: CanvasRenderingContext2D,
{
width,
Expand Down
5 changes: 3 additions & 2 deletions packages/axes/src/components/Axes.tsx
@@ -1,10 +1,11 @@
import { memo } from 'react'
import { ScaleValue, AnyScale } from '@nivo/scales'
import { Axis } from './Axis'
import { positions } from '../props'
import { AnyScale, AxisProps, AxisValue } from '../types'
import { AxisProps } from '../types'

export const Axes = memo(
<X extends AxisValue, Y extends AxisValue>({
<X extends ScaleValue, Y extends ScaleValue>({
xScale,
yScale,
width,
Expand Down
5 changes: 3 additions & 2 deletions packages/axes/src/components/Axis.tsx
Expand Up @@ -2,11 +2,12 @@ import { useMemo, memo } from 'react'
import * as React from 'react'
import { useSpring, useTransition, animated } from '@react-spring/web'
import { useTheme, useMotionConfig } from '@nivo/core'
import { ScaleValue, AnyScale } from '@nivo/scales'
import { computeCartesianTicks, getFormatter } from '../compute'
import { AxisTick } from './AxisTick'
import { AnyScale, AxisProps, AxisValue } from '../types'
import { AxisProps } from '../types'

const Axis = <Value extends AxisValue>({
const Axis = <Value extends ScaleValue>({
axis,
scale,
x = 0,
Expand Down
5 changes: 3 additions & 2 deletions packages/axes/src/components/AxisTick.tsx
Expand Up @@ -2,9 +2,10 @@ import { useMemo, memo } from 'react'
import * as React from 'react'
import { animated } from '@react-spring/web'
import { useTheme } from '@nivo/core'
import { AxisTickProps, AxisValue } from '../types'
import { ScaleValue } from '@nivo/scales'
import { AxisTickProps } from '../types'

const AxisTick = <Value extends AxisValue>({
const AxisTick = <Value extends ScaleValue>({
value: _value,
format,
lineX,
Expand Down
4 changes: 2 additions & 2 deletions packages/axes/src/components/Grid.tsx
@@ -1,10 +1,10 @@
import { useMemo, memo } from 'react'
import { ScaleValue, AnyScale, TicksSpec } from '@nivo/scales'
import { GridLines } from './GridLines'
import { computeGridLines } from '../compute'
import { AnyScale, AxisValue, TicksSpec } from '../types'

export const Grid = memo(
<X extends AxisValue, Y extends AxisValue>({
<X extends ScaleValue, Y extends ScaleValue>({
width,
height,
xScale,
Expand Down
167 changes: 8 additions & 159 deletions packages/axes/src/compute.ts
@@ -1,164 +1,13 @@
import {
CountableTimeInterval,
timeMillisecond,
utcMillisecond,
timeSecond,
utcSecond,
timeMinute,
utcMinute,
timeHour,
utcHour,
timeWeek,
utcWeek,
timeSunday,
utcSunday,
timeMonday,
utcMonday,
timeTuesday,
utcTuesday,
timeWednesday,
utcWednesday,
timeThursday,
utcThursday,
timeFriday,
utcFriday,
timeSaturday,
utcSaturday,
timeMonth,
utcMonth,
timeYear,
utcYear,
timeInterval,
} from 'd3-time'
import { timeFormat } from 'd3-time-format'
import { format as d3Format } from 'd3-format'
// @ts-ignore
import { textPropsByEngine } from '@nivo/core'
import {
AxisValue,
Point,
TicksSpec,
AnyScale,
ScaleWithBandwidth,
ValueFormatter,
Line,
} from './types'

export const centerScale = <Value>(scale: ScaleWithBandwidth) => {
const bandwidth = scale.bandwidth()

if (bandwidth === 0) return scale

let offset = bandwidth / 2
if (scale.round()) {
offset = Math.round(offset)
}

return <T extends Value>(d: T) => (scale(d) ?? 0) + offset
}

const timeDay = timeInterval(
date => date.setHours(0, 0, 0, 0),
(date, step) => date.setDate(date.getDate() + step),
(start, end) => (end.getTime() - start.getTime()) / 864e5,
date => Math.floor(date.getTime() / 864e5)
)

const utcDay = timeInterval(
date => date.setUTCHours(0, 0, 0, 0),
(date, step) => date.setUTCDate(date.getUTCDate() + step),
(start, end) => (end.getTime() - start.getTime()) / 864e5,
date => Math.floor(date.getTime() / 864e5)
)

const timeByType: Record<string, [CountableTimeInterval, CountableTimeInterval]> = {
millisecond: [timeMillisecond, utcMillisecond],
second: [timeSecond, utcSecond],
minute: [timeMinute, utcMinute],
hour: [timeHour, utcHour],
day: [timeDay, utcDay],
week: [timeWeek, utcWeek],
sunday: [timeSunday, utcSunday],
monday: [timeMonday, utcMonday],
tuesday: [timeTuesday, utcTuesday],
wednesday: [timeWednesday, utcWednesday],
thursday: [timeThursday, utcThursday],
friday: [timeFriday, utcFriday],
saturday: [timeSaturday, utcSaturday],
month: [timeMonth, utcMonth],
year: [timeYear, utcYear],
}

const timeTypes = Object.keys(timeByType)
const timeIntervalRegexp = new RegExp(`^every\\s*(\\d+)?\\s*(${timeTypes.join('|')})s?$`, 'i')

const isInteger = (value: unknown): value is number =>
typeof value === 'number' && isFinite(value) && Math.floor(value) === value
import { ScaleValue, AnyScale, TicksSpec, getScaleTicks, centerScale } from '@nivo/scales'
import { Point, ValueFormatter, Line } from './types'

const isArray = <T>(value: unknown): value is T[] => Array.isArray(value)

export const getScaleTicks = <Value extends AxisValue>(
scale: AnyScale,
spec?: TicksSpec<Value>
) => {
// specific values
if (Array.isArray(spec)) {
return spec
}

if (typeof spec === 'string' && 'useUTC' in scale) {
// time interval
const matches = spec.match(timeIntervalRegexp)

if (matches) {
const [, amount, type] = matches
// UTC is used as it's more predictible
// however local time could be used too
// let's see how it fits users' requirements
const timeType = timeByType[type][scale.useUTC ? 1 : 0]

if (type === 'day') {
const [start, originalStop] = scale.domain()
const stop = new Date(originalStop)

// Set range to include last day in the domain since `interval.range` function is exclusive stop
stop.setDate(stop.getDate() + 1)

return timeType.every(Number(amount ?? 1))?.range(start, stop) ?? []
}

if (amount === undefined) {
return scale.ticks(timeType)
}

const interval = timeType.every(Number(amount))

if (interval) {
return scale.ticks(interval)
}
}

throw new Error(`Invalid tickValues: ${spec}`)
}

// continuous scales
if ('ticks' in scale) {
// default behaviour
if (spec === undefined) {
return scale.ticks()
}

// specific tick count
if (isInteger(spec)) {
return scale.ticks(spec)
}
}

// non linear scale default
return scale.domain()
}

export const computeCartesianTicks = <Value extends AxisValue>({
export const computeCartesianTicks = <Value extends ScaleValue>({
axis,
scale,
ticksPosition,
Expand All @@ -177,7 +26,7 @@ export const computeCartesianTicks = <Value extends AxisValue>({
tickRotation: number
engine?: 'svg' | 'canvas'
}) => {
const values = getScaleTicks(scale, tickValues)
const values = getScaleTicks<Value>(scale, tickValues)

const textProps = textPropsByEngine[engine]

Expand Down Expand Up @@ -245,7 +94,7 @@ export const computeCartesianTicks = <Value extends AxisValue>({
}
}

export const getFormatter = <Value extends AxisValue>(
export const getFormatter = <Value extends ScaleValue>(
format: string | ValueFormatter<Value> | undefined,
scale: AnyScale
): ValueFormatter<Value> | undefined => {
Expand All @@ -254,13 +103,13 @@ export const getFormatter = <Value extends AxisValue>(
if (scale.type === 'time') {
const formatter = timeFormat(format)

return (d => formatter(d instanceof Date ? d : new Date(d))) as ValueFormatter<Value>
return ((d: any) => formatter(d instanceof Date ? d : new Date(d))) as ValueFormatter<Value>
}

return (d3Format(format) as unknown) as ValueFormatter<Value>
}

export const computeGridLines = <Value extends AxisValue>({
export const computeGridLines = <Value extends ScaleValue>({
width,
height,
scale,
Expand All @@ -274,7 +123,7 @@ export const computeGridLines = <Value extends AxisValue>({
values?: TicksSpec<Value>
}) => {
const lineValues = isArray<number>(_values) ? _values : undefined
const values = lineValues || getScaleTicks(scale, _values)
const values = lineValues || getScaleTicks<Value>(scale, _values)
const position = 'bandwidth' in scale ? centerScale(scale) : scale

const lines: Line[] =
Expand Down
31 changes: 6 additions & 25 deletions packages/axes/src/types.ts
@@ -1,9 +1,7 @@
import * as React from 'react'
import { Scale, ScaleBand, ScalePoint } from '@nivo/scales'
import { ScaleValue, TicksSpec } from '@nivo/scales'
import { SpringValues } from '@react-spring/web'

export type AxisValue = string | number | Date

export type GridValuesBuilder<T> = T extends number
? number[]
: T extends string
Expand All @@ -12,34 +10,18 @@ export type GridValuesBuilder<T> = T extends number
? Date[]
: never

export type GridValues<T extends AxisValue> = number | GridValuesBuilder<T>
export type GridValues<T extends ScaleValue> = number | GridValuesBuilder<T>

export type Point = {
x: number
y: number
}

export type ScaleWithBandwidth = ScaleBand<any> | ScalePoint<any>

export type AnyScale = Scale<any, any>

export type TicksSpec<Value extends AxisValue> =
// exact number of ticks, please note that
// depending on the current range of values,
// you might not get this exact count
| number
// string is used for Date based scales,
// it can express a time interval,
// for example: every 2 weeks
| string
// override scale ticks with custom explicit values
| Value[]

export type AxisLegendPosition = 'start' | 'middle' | 'end'

export type ValueFormatter<Value extends AxisValue> = (value: Value) => Value | string
export type ValueFormatter<Value extends ScaleValue> = (value: Value) => Value | string

export interface AxisProps<Value extends AxisValue = any> {
export interface AxisProps<Value extends ScaleValue = any> {
ticksPosition?: 'before' | 'after'
tickValues?: TicksSpec<Value>
tickSize?: number
Expand All @@ -53,12 +35,11 @@ export interface AxisProps<Value extends AxisValue = any> {
ariaHidden?: boolean
}

export interface CanvasAxisProp<Value extends string | number | Date>
extends Omit<AxisProps<Value>, 'legend'> {
export interface CanvasAxisProp<Value extends ScaleValue> extends Omit<AxisProps<Value>, 'legend'> {
legend?: string
}

export interface AxisTickProps<Value extends AxisValue> {
export interface AxisTickProps<Value extends ScaleValue> {
tickIndex: number
value: Value
format?: ValueFormatter<Value>
Expand Down

0 comments on commit 801c767

Please sign in to comment.