Skip to content

Commit

Permalink
feat(legends): add support for continuous color scale legends
Browse files Browse the repository at this point in the history
  • Loading branch information
plouc committed Jan 12, 2022
1 parent f086791 commit e220345
Show file tree
Hide file tree
Showing 9 changed files with 405 additions and 10 deletions.
4 changes: 3 additions & 1 deletion packages/legends/package.json
Expand Up @@ -22,7 +22,9 @@
"!dist/tsconfig.tsbuildinfo"
],
"devDependencies": {
"@nivo/core": "0.78.0"
"@nivo/colors": "0.78.0",
"@nivo/core": "0.78.0",
"d3-scale": "^3.2.3"
},
"peerDependencies": {
"@nivo/core": "0.78.0",
Expand Down
197 changes: 191 additions & 6 deletions packages/legends/src/compute.ts
@@ -1,4 +1,13 @@
import { BoxLegendSvgProps, LegendAnchor, LegendItemDirection } from './types'
import { scaleLinear } from 'd3-scale'
import { getValueFormatter } from '@nivo/core'
import { computeContinuousColorScaleColorStops } from '@nivo/colors'
import {
BoxLegendSvgProps,
ContinuousColorsLegendProps,
LegendAnchor,
LegendItemDirection,
} from './types'
import { continuousColorsLegendDefaults } from './defaults'

const isObject = <T>(item: unknown): item is T =>
typeof item === 'object' && !Array.isArray(item) && item !== null
Expand Down Expand Up @@ -132,7 +141,7 @@ export const computeItemLayout = ({

labelY = height / 2
labelAlignment = 'central'
if (justify === true) {
if (justify) {
labelX = width
labelAnchor = 'end'
} else {
Expand All @@ -147,7 +156,7 @@ export const computeItemLayout = ({

labelY = height / 2
labelAlignment = 'central'
if (justify === true) {
if (justify) {
labelX = 0
labelAnchor = 'start'
} else {
Expand All @@ -163,7 +172,7 @@ export const computeItemLayout = ({
labelX = width / 2

labelAnchor = 'middle'
if (justify === true) {
if (justify) {
labelY = height
labelAlignment = 'alphabetic'
} else {
Expand All @@ -178,7 +187,7 @@ export const computeItemLayout = ({

labelX = width / 2
labelAnchor = 'middle'
if (justify === true) {
if (justify) {
labelY = 0
labelAlignment = 'text-before-edge'
} else {
Expand All @@ -191,10 +200,186 @@ export const computeItemLayout = ({
return {
symbolX,
symbolY,

labelX,
labelY,
labelAnchor,
labelAlignment,
}
}

export const computeContinuousColorsLegend = ({
// id,
scale,
ticks,
length = continuousColorsLegendDefaults.length,
thickness = continuousColorsLegendDefaults.thickness,
direction = continuousColorsLegendDefaults.direction,
tickPosition = continuousColorsLegendDefaults.tickPosition,
tickSize = continuousColorsLegendDefaults.tickSize,
tickSpacing = continuousColorsLegendDefaults.tickSpacing,
tickOverlap = continuousColorsLegendDefaults.tickOverlap,
tickFormat = continuousColorsLegendDefaults.tickFormat,
title,
titleAlign = continuousColorsLegendDefaults.titleAlign,
titleOffset = continuousColorsLegendDefaults.titleOffset,
}: ContinuousColorsLegendProps) => {
const domain = scale.domain()

const positionScale = scaleLinear().domain(domain)
if (domain.length === 2) {
// sequential, quantize
positionScale.range([0, length])
} else if (domain.length === 3) {
// diverging
positionScale.range([0, length / 2, length])
}

let values: number[]
if ('thresholds' in scale) {
// quantize
values = [domain[0], ...scale.thresholds(), domain[1]]
} else {
// sequential, diverging
values = Array.isArray(ticks) ? ticks : (scale as any).ticks(ticks)
}

const colorStops = computeContinuousColorScaleColorStops(scale, 32)

const formatValue = getValueFormatter(tickFormat)

const computedTicks: {
x1: number
y1: number
x2: number
y2: number
text: string
textX: number
textY: number
textHorizontalAlign: 'start' | 'middle' | 'end'
textVerticalAlign: 'alphabetic' | 'central' | 'hanging'
}[] = []

let titleX: number
let titleY: number
let titleRotation: number
let titleVerticalAlign: 'alphabetic' | 'hanging'

if (direction === 'row') {
let y1: number
let y2: number

let textY: number
const textHorizontalAlign = 'middle'
let textVerticalAlign: 'alphabetic' | 'hanging'

titleRotation = 0
if (titleAlign === 'start') {
titleX = 0
} else if (titleAlign === 'middle') {
titleX = length / 2
} else {
titleX = length
}

if (tickPosition === 'before') {
y1 = -tickSize
y2 = tickOverlap ? thickness : 0

textY = -tickSize - tickSpacing
textVerticalAlign = 'alphabetic'

titleY = thickness + titleOffset
titleVerticalAlign = 'hanging'
} else {
y1 = tickOverlap ? 0 : thickness
y2 = thickness + tickSize

textY = y2 + tickSpacing
textVerticalAlign = 'hanging'

titleY = -titleOffset
titleVerticalAlign = 'alphabetic'
}

values.forEach(value => {
const x = positionScale(value)

computedTicks.push({
x1: x,
y1,
x2: x,
y2,
text: formatValue(value),
textX: x,
textY,
textHorizontalAlign,
textVerticalAlign,
})
})
} else {
let x1: number
let x2: number

let textX: number
let textHorizontalAlign: 'start' | 'end'
const textVerticalAlign = 'central'

titleRotation = -90
if (titleAlign === 'start') {
titleY = length
} else if (titleAlign === 'middle') {
titleY = length / 2
} else {
titleY = 0
}

if (tickPosition === 'before') {
x1 = -tickSize
x2 = tickOverlap ? thickness : 0

textX = x1 - tickSpacing
textHorizontalAlign = 'end'

titleX = thickness + titleOffset
titleVerticalAlign = 'hanging'
} else {
x1 = tickOverlap ? 0 : thickness
x2 = thickness + tickSize

textX = x2 + tickSpacing
textHorizontalAlign = 'start'

titleX = -titleOffset
titleVerticalAlign = 'alphabetic'
}

values.forEach(value => {
const y = positionScale(value)

computedTicks.push({
x1,
y1: y,
x2,
y2: y,
text: formatValue(value),
textX,
textY: y,
textHorizontalAlign,
textVerticalAlign,
})
})
}

return {
colorStops,
ticks: computedTicks,
title: {
text: title,
x: titleX,
y: titleY,
rotation: titleRotation,
horizontalAlign: titleAlign,
verticalAlign: titleVerticalAlign,
},
}
}
25 changes: 25 additions & 0 deletions packages/legends/src/defaults.ts
@@ -0,0 +1,25 @@
import { ContinuousColorsLegendProps } from './types'

export const continuousColorsLegendDefaults: {
length: NonNullable<ContinuousColorsLegendProps['length']>
thickness: NonNullable<ContinuousColorsLegendProps['thickness']>
direction: NonNullable<ContinuousColorsLegendProps['direction']>
tickPosition: NonNullable<ContinuousColorsLegendProps['tickPosition']>
tickSize: NonNullable<ContinuousColorsLegendProps['tickSize']>
tickSpacing: NonNullable<ContinuousColorsLegendProps['tickSpacing']>
tickOverlap: NonNullable<ContinuousColorsLegendProps['tickOverlap']>
tickFormat: NonNullable<ContinuousColorsLegendProps['tickFormat']>
titleAlign: NonNullable<ContinuousColorsLegendProps['titleAlign']>
titleOffset: NonNullable<ContinuousColorsLegendProps['titleOffset']>
} = {
length: 200,
thickness: 16,
direction: 'row',
tickPosition: 'after',
tickSize: 4,
tickSpacing: 3,
tickOverlap: false,
tickFormat: (value: number) => `${value}`,
titleAlign: 'start',
titleOffset: 4,
}
5 changes: 3 additions & 2 deletions packages/legends/src/hooks.ts
Expand Up @@ -34,8 +34,9 @@ export const useQuantizeColorScaleLegendData = ({
color: domainValue,
}
})
if (reverse === true) items.reverse()

if (reverse) items.reverse()

return items
}, [overriddenDomain, scale, reverse])
}, [overriddenDomain, scale, reverse, separator, valueFormat])
}
3 changes: 3 additions & 0 deletions packages/legends/src/index.ts
@@ -1,5 +1,8 @@
export * from './svg'
export * from './canvas'
export * from './defaults'
export * from './hooks'
export * from './props'
export * from './types'
export * from './compute'
export * from './defaults'
47 changes: 47 additions & 0 deletions packages/legends/src/svg/AnchoredContinuousColorsLegendSvg.tsx
@@ -0,0 +1,47 @@
import { AnchoredContinuousColorsLegendProps } from '../types'
import { computePositionFromAnchor } from '../compute'
import { continuousColorsLegendDefaults } from '../defaults'
import { ContinuousColorsLegendSvg } from './ContinuousColorsLegendSvg'

export const AnchoredContinuousColorsLegendSvg = ({
containerWidth,
containerHeight,
anchor,
translateX = 0,
translateY = 0,
length = continuousColorsLegendDefaults.length,
thickness = continuousColorsLegendDefaults.thickness,
direction = continuousColorsLegendDefaults.direction,
...legendProps
}: AnchoredContinuousColorsLegendProps) => {
let width: number
let height: number
if (direction === 'row') {
width = length
height = thickness
} else {
width = thickness
height = length
}

const { x, y } = computePositionFromAnchor({
anchor,
translateX,
translateY,
containerWidth,
containerHeight,
width,
height,
})

return (
<g transform={`translate(${x}, ${y})`}>
<ContinuousColorsLegendSvg
length={length}
thickness={thickness}
direction={direction}
{...legendProps}
/>
</g>
)
}

0 comments on commit e220345

Please sign in to comment.