diff --git a/packages/icicles/LICENSE.md b/packages/icicles/LICENSE.md new file mode 100644 index 000000000..faa45389e --- /dev/null +++ b/packages/icicles/LICENSE.md @@ -0,0 +1,19 @@ +Copyright (c) Raphaël Benitte + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/packages/icicles/README.md b/packages/icicles/README.md new file mode 100644 index 000000000..82132a0d9 --- /dev/null +++ b/packages/icicles/README.md @@ -0,0 +1,12 @@ +nivo + +# `@nivo/icicles` + +[![version](https://img.shields.io/npm/v/@nivo/icicles?style=for-the-badge)](https://www.npmjs.com/package/@nivo/icicles) +[![downloads](https://img.shields.io/npm/dm/@nivo/icicles?style=for-the-badge)](https://www.npmjs.com/package/@nivo/icicles) + +## Icicles + +[documentation](http://nivo.rocks/icicles/) + +![Icicles](https://raw.githubusercontent.com/plouc/nivo/master/website/src/assets/captures/icicles.png) diff --git a/packages/icicles/package.json b/packages/icicles/package.json new file mode 100644 index 000000000..7595f752c --- /dev/null +++ b/packages/icicles/package.json @@ -0,0 +1,53 @@ +{ + "name": "@nivo/icicles", + "version": "0.79.1", + "license": "MIT", + "author": { + "name": "Raphaël Benitte", + "url": "https://github.com/plouc" + }, + "contributors": [ + "Lilian Saget-Lethias ", + "Mehdi Louraoui " + ], + "repository": { + "type": "git", + "url": "https://github.com/plouc/nivo.git", + "directory": "packages/icicles" + }, + "keywords": [ + "nivo", + "dataviz", + "react", + "d3", + "charts", + "icicles-chart" + ], + "main": "./dist/nivo-icicles.cjs.js", + "module": "./dist/nivo-icicles.es.js", + "typings": "./dist/types/index.d.ts", + "files": [ + "README.md", + "LICENSE.md", + "dist/", + "!dist/tsconfig.tsbuildinfo" + ], + "dependencies": { + "@nivo/rects": "0.79.1", + "@nivo/colors": "0.79.1", + "@nivo/tooltip": "0.79.0", + "d3-hierarchy": "^1.1.8", + "lodash": "^4.17.21" + }, + "devDependencies": { + "@nivo/core": "0.79.0", + "@types/d3-hierarchy": "^1.1.7" + }, + "peerDependencies": { + "@nivo/core": "0.79.0", + "react": ">= 16.14.0 < 18.0.0" + }, + "publishConfig": { + "access": "public" + } +} \ No newline at end of file diff --git a/packages/icicles/src/Icicles.tsx b/packages/icicles/src/Icicles.tsx new file mode 100644 index 000000000..0983d9004 --- /dev/null +++ b/packages/icicles/src/Icicles.tsx @@ -0,0 +1,173 @@ +import { InheritedColorConfig } from '@nivo/colors' +import { + // @ts-ignore -- internal function + bindDefs, + Container, + SvgWrapper, + useDimensions, +} from '@nivo/core' +import { Fragment, ReactNode, createElement, useMemo } from 'react' +import { Rects } from './Rects' +import { useIcicles, useIciclesLayerContext } from './hooks' +import { RectLabelsLayer } from '@nivo/rects' +import { defaultProps } from './props' +import { IciclesSvgProps, IciclesLayerId, ComputedDatum } from './types' + +type InnerIciclesProps = Partial< + Omit< + IciclesSvgProps, + 'data' | 'width' | 'height' | 'isInteractive' | 'animate' | 'motionConfig' + > +> & + Pick, 'data' | 'width' | 'height' | 'isInteractive'> + +const InnerIcicles = ({ + data, + id = defaultProps.id, + value = defaultProps.value, + valueFormat, + layers = ['rects', 'rectLabels'], + colors = defaultProps.colors, + colorBy = defaultProps.colorBy, + inheritColorFromParent = defaultProps.inheritColorFromParent, + childColor = defaultProps.childColor as InheritedColorConfig>, + borderWidth = defaultProps.borderWidth, + borderColor = defaultProps.borderColor, + margin: partialMargin, + width, + height, + enableRectLabels = defaultProps.enableRectLabels, + rectLabelsTextColor = defaultProps.rectLabelsTextColor, + defs = defaultProps.defs, + fill = defaultProps.fill, + isInteractive = defaultProps.isInteractive, + onClick, + onMouseEnter, + onMouseLeave, + onMouseMove, + onWheel, + onContextMenu, + tooltip = defaultProps.tooltip, + role = defaultProps.role, + rectLabel = defaultProps.rectLabel, + rectLabelsComponent, + rectLabelsSkipLength = defaultProps.rectLabelsSkipLength, + rectLabelsSkipPercentage = defaultProps.rectLabelsSkipPercentage, + direction = defaultProps.direction, + rectLabelsOffset = defaultProps.rectLabelsOffset, +}: InnerIciclesProps) => { + const { margin, outerHeight, outerWidth } = useDimensions(width, height, partialMargin) + + const { nodes, baseOffsetLeft, baseOffsetTop } = useIcicles({ + data, + id, + value, + valueFormat, + colors, + colorBy, + inheritColorFromParent, + childColor, + height: outerHeight, + width: outerWidth, + direction, + }) + + const boundDefs = bindDefs(defs, nodes, fill, { + dataKey: '.', + colorKey: 'color', + targetKey: 'fill', + }) + + const layerById: Record = { + rects: null, + rectLabels: null, + } + + if (layers.includes('rects')) { + layerById.rects = ( + + key="rects" + data={nodes} + borderWidth={borderWidth} + borderColor={borderColor} + isInteractive={isInteractive} + tooltip={tooltip} + onClick={onClick} + onMouseEnter={onMouseEnter} + onMouseLeave={onMouseLeave} + onMouseMove={onMouseMove} + onWheel={onWheel} + onContextMenu={onContextMenu} + /> + ) + } + + const filteredData = useMemo( + () => + nodes.filter(datum => { + return ( + datum.rect.percentage >= rectLabelsSkipPercentage && + datum.rect[['left', 'right'].includes(direction) ? 'height' : 'width'] >= + rectLabelsSkipLength + ) + }), + [nodes, rectLabelsSkipPercentage, rectLabelsSkipLength, direction] + ) + + if (enableRectLabels && layers.includes('rectLabels')) { + layerById.rectLabels = ( + > + key="rectLabels" + data={filteredData} + label={rectLabel} + textColor={rectLabelsTextColor} + component={rectLabelsComponent} + offset={rectLabelsOffset} + baseOffsetLeft={baseOffsetLeft} + baseOffsetTop={baseOffsetTop} + /> + ) + } + + const layerContext = useIciclesLayerContext({ + nodes, + baseOffsetLeft, + baseOffsetTop, + }) + + return ( + + {layers?.map((layer, i) => { + if (layerById[layer as IciclesLayerId] !== undefined) { + return layerById[layer as IciclesLayerId] + } + + if (typeof layer === 'function') { + return {createElement(layer, layerContext)} + } + + return null + })} + + ) +} + +export const Icicles = ({ + isInteractive = defaultProps.isInteractive, + animate = defaultProps.animate, + motionConfig = defaultProps.motionConfig, + theme, + renderWrapper, + ...otherProps +}: Partial, 'data' | 'width' | 'height'>> & + Pick, 'data' | 'width' | 'height'>) => ( + + isInteractive={isInteractive} {...otherProps} /> + +) diff --git a/packages/icicles/src/IciclesTooltip.tsx b/packages/icicles/src/IciclesTooltip.tsx new file mode 100644 index 000000000..a22b2ea4f --- /dev/null +++ b/packages/icicles/src/IciclesTooltip.tsx @@ -0,0 +1,10 @@ +import { BasicTooltip } from '@nivo/tooltip' +import { ComputedDatum } from './types' + +export const IciclesTooltip = ({ + id, + formattedValue, + color, +}: ComputedDatum) => ( + +) diff --git a/packages/icicles/src/Rects.tsx b/packages/icicles/src/Rects.tsx new file mode 100644 index 000000000..1f1c03f54 --- /dev/null +++ b/packages/icicles/src/Rects.tsx @@ -0,0 +1,100 @@ +import { useTooltip } from '@nivo/tooltip' +import { createElement, useMemo } from 'react' +import * as React from 'react' +import { RectsLayer } from '@nivo/rects' +import { IciclesCommonProps, ComputedDatum, MouseHandlers } from './types' + +export interface RectsProps { + borderColor: IciclesCommonProps['borderColor'] + borderWidth: IciclesCommonProps['borderWidth'] + data: ComputedDatum[] + isInteractive: IciclesCommonProps['isInteractive'] + onClick?: MouseHandlers['onClick'] + onMouseEnter?: MouseHandlers['onMouseEnter'] + onMouseLeave?: MouseHandlers['onMouseLeave'] + onMouseMove?: MouseHandlers['onMouseMove'] + onWheel?: MouseHandlers['onWheel'] + onContextMenu?: MouseHandlers['onContextMenu'] + tooltip: IciclesCommonProps['tooltip'] +} + +export const Rects = ({ + data, + borderWidth, + borderColor, + isInteractive, + onClick, + onMouseEnter, + onMouseMove, + onMouseLeave, + onWheel, + onContextMenu, + tooltip, +}: RectsProps) => { + const { showTooltipFromEvent, hideTooltip } = useTooltip() + + const handleClick = useMemo(() => { + if (!isInteractive) return undefined + + return (datum: ComputedDatum, event: React.MouseEvent) => { + onClick?.(datum, event) + } + }, [isInteractive, onClick]) + + const handleMouseEnter = useMemo(() => { + if (!isInteractive) return undefined + + return (datum: ComputedDatum, event: React.MouseEvent) => { + showTooltipFromEvent(createElement(tooltip, datum), event) + onMouseEnter?.(datum, event) + } + }, [isInteractive, showTooltipFromEvent, tooltip, onMouseEnter]) + + const handleMouseMove = useMemo(() => { + if (!isInteractive) return undefined + + return (datum: ComputedDatum, event: React.MouseEvent) => { + showTooltipFromEvent(createElement(tooltip, datum), event) + onMouseMove?.(datum, event) + } + }, [isInteractive, showTooltipFromEvent, tooltip, onMouseMove]) + + const handleMouseLeave = useMemo(() => { + if (!isInteractive) return undefined + + return (datum: ComputedDatum, event: React.MouseEvent) => { + hideTooltip() + onMouseLeave?.(datum, event) + } + }, [isInteractive, hideTooltip, onMouseLeave]) + + const handleWheel = useMemo(() => { + if (!isInteractive) return undefined + + return (datum: ComputedDatum, event: React.WheelEvent) => { + onWheel?.(datum, event) + } + }, [isInteractive, onWheel]) + + const handleContextMenu = useMemo(() => { + if (!isInteractive) return undefined + + return (datum: ComputedDatum, event: React.MouseEvent) => { + onContextMenu?.(datum, event) + } + }, [isInteractive, onContextMenu]) + + return ( + > + data={data} + borderWidth={borderWidth} + borderColor={borderColor} + onClick={handleClick} + onMouseEnter={handleMouseEnter} + onMouseMove={handleMouseMove} + onMouseLeave={handleMouseLeave} + onWheel={handleWheel} + onContextMenu={handleContextMenu} + /> + ) +} diff --git a/packages/icicles/src/ResponsiveIcicles.tsx b/packages/icicles/src/ResponsiveIcicles.tsx new file mode 100644 index 000000000..de1d0972f --- /dev/null +++ b/packages/icicles/src/ResponsiveIcicles.tsx @@ -0,0 +1,16 @@ +import { ResponsiveWrapper } from '@nivo/core' +import { Icicles } from './Icicles' +import { IciclesSvgProps } from './types' + +type ResponsiveIciclesProps = Partial< + Omit, 'data' | 'width' | 'height'> +> & + Pick, 'data'> + +export const ResponsiveIcicles = (props: ResponsiveIciclesProps) => ( + + {({ width, height }: { height: number; width: number }) => ( + width={width} height={height} {...props} /> + )} + +) diff --git a/packages/icicles/src/hooks.ts b/packages/icicles/src/hooks.ts new file mode 100644 index 000000000..b7de16cae --- /dev/null +++ b/packages/icicles/src/hooks.ts @@ -0,0 +1,217 @@ +import { useOrdinalColorScale, useInheritedColor, InheritedColorConfig } from '@nivo/colors' +import { usePropertyAccessor, useTheme, useValueFormatter } from '@nivo/core' +import { + partition as d3Partition, + hierarchy as d3Hierarchy, + HierarchyRectangularNode, +} from 'd3-hierarchy' +import cloneDeep from 'lodash/cloneDeep' +import sortBy from 'lodash/sortBy' +import { useMemo } from 'react' +import { Rect } from '@nivo/rects' +import { defaultProps } from './props' +import { + DataProps, + DatumId, + IciclesCommonProps, + ComputedDatum, + IciclesCustomLayerProps, +} from './types' + +const computeLength = (a: number, b: number) => b - a - Math.min(1, (b - a) / 2) + +const widthHeight = (d: HierarchyRectangularNode) => ({ + topBottom: () => ({ + height: computeLength(d.y0, d.y1), + width: computeLength(d.x0, d.x1), + }), + leftRight: () => ({ + height: computeLength(d.x0, d.x1), + width: computeLength(d.y0, d.y1), + }), +}) + +export const useIcicles = ({ + data, + id = defaultProps.id, + value = defaultProps.value, + valueFormat, + colors = defaultProps.colors, + colorBy = defaultProps.colorBy, + inheritColorFromParent = defaultProps.inheritColorFromParent, + childColor = defaultProps.childColor as InheritedColorConfig>, + width, + height, + direction, +}: { + childColor?: IciclesCommonProps['childColor'] + colorBy?: IciclesCommonProps['colorBy'] + colors?: IciclesCommonProps['colors'] + data: DataProps['data'] + direction: IciclesCommonProps['direction'] + height: IciclesCommonProps['height'] + id?: DataProps['id'] + inheritColorFromParent?: IciclesCommonProps['inheritColorFromParent'] + value?: DataProps['value'] + valueFormat?: DataProps['valueFormat'] + width: IciclesCommonProps['width'] +}) => { + const theme = useTheme() + const getColor = useOrdinalColorScale, 'color' | 'fill'>>( + colors, + colorBy + ) + const getChildColor = useInheritedColor>(childColor, theme) + + const isLeftRight = direction === 'left' || direction === 'right' + + const getId = usePropertyAccessor(id) + const getValue = usePropertyAccessor(value) + const formatValue = useValueFormatter(valueFormat) + + const { + nodes, + baseOffsetTop, + baseOffsetLeft, + }: { + baseOffsetLeft: number + baseOffsetTop: number + nodes: ComputedDatum[] + } = useMemo(() => { + // d3 mutates the data for performance reasons, + // however it does not work well with reactive programming, + // this ensures that we don't mutate the input data + const clonedData = cloneDeep(data) + + const hierarchy = d3Hierarchy(clonedData) + .sum(getValue) + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + .sort((a, b) => b.height - a.height || b.value! - a.value!) + + const partition = d3Partition().size( + isLeftRight ? [height, width] : [width, height] + ) + const descendants = partition(hierarchy).descendants() + + const total = hierarchy.value ?? 0 + + // It's important to sort node by depth, + // it ensures that we assign a parent node + // which has already been computed, because parent nodes + // are going to be computed first + const sortedNodes = sortBy(descendants, 'depth') + + const rootRect = { + ...widthHeight(sortedNodes[0])[isLeftRight ? 'leftRight' : 'topBottom'](), + } + + // we pre compute offsets relative from container + // and from root node. + // it will be used to later compute nodes and text positions + const baseOffsetLeft = direction === 'left' ? width : 0 + const baseOffsetTop = direction === 'top' ? height : 0 + const rectOffsetLeft = direction === 'left' ? baseOffsetLeft - rootRect.width : 0 + const rectOffsetTop = direction === 'top' ? baseOffsetTop - rootRect.height : 0 + + return { + baseOffsetLeft, + baseOffsetTop, + nodes: sortedNodes.reduce[]>((acc, descendant) => { + const id = getId(descendant.data) + // d3 hierarchy node value is optional by default as it depends on + // a call to `count()` or `sum()`, and we previously called `sum()`, + // d3 typings could be improved and make it non optional when calling + // one of those. + + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const value = descendant.value! + const percentage = (100 * value) / total + const path = descendant.ancestors()?.map(ancestor => getId(ancestor.data)) + + const descendantRect = + widthHeight(descendant)[isLeftRight ? 'leftRight' : 'topBottom']() + + // if we switch direction, we need to switch point values + const x0 = isLeftRight ? descendant.y0 : descendant.x0, + x1 = isLeftRight ? descendant.y1 : descendant.x1, + y0 = isLeftRight ? descendant.x0 : descendant.y0, + y1 = isLeftRight ? descendant.x1 : descendant.y1 + + const transformX = Math.abs(rectOffsetLeft - x0) + const transformY = Math.abs(rectOffsetTop - y0) + + const rect: Rect = { + ...descendantRect, + transformX, + transformY, + x0, + x1, + y0, + y1, + percentage, + } + + let parent: ComputedDatum | undefined + if (descendant.parent) { + // as the parent is defined by the input data, and we sorted the data + // by `depth`, we can safely assume it's defined. + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + parent = acc.find(node => node.id === getId(descendant.parent!.data)) + } + + const normalizedNode: ComputedDatum = { + id, + path, + value, + percentage, + rect, + formattedValue: valueFormat ? formatValue(value) : `${percentage.toFixed(2)}%`, + color: '', + data: descendant.data, + depth: descendant.depth, + height: descendant.height, + } + + if (inheritColorFromParent && parent && normalizedNode.depth > 1) { + normalizedNode.color = getChildColor(parent, normalizedNode) + } else { + normalizedNode.color = getColor(normalizedNode) + } + + return [...acc, normalizedNode] + }, []), + } + }, [ + data, + getValue, + getId, + valueFormat, + formatValue, + getColor, + inheritColorFromParent, + getChildColor, + width, + height, + direction, + isLeftRight, + ]) + + return { nodes, baseOffsetLeft, baseOffsetTop } +} + +/** + * Memoize the context to pass to custom layers. + */ +export const useIciclesLayerContext = ({ + nodes, + baseOffsetLeft, + baseOffsetTop, +}: IciclesCustomLayerProps): IciclesCustomLayerProps => + useMemo( + () => ({ + nodes, + baseOffsetLeft, + baseOffsetTop, + }), + [nodes, baseOffsetLeft, baseOffsetTop] + ) diff --git a/packages/icicles/src/index.ts b/packages/icicles/src/index.ts new file mode 100644 index 000000000..75ad484b4 --- /dev/null +++ b/packages/icicles/src/index.ts @@ -0,0 +1,6 @@ +export * from './Icicles' +export * from './Rects' +export * from './ResponsiveIcicles' +export * from './IciclesTooltip' +export * from './types' +export * from './props' diff --git a/packages/icicles/src/props.ts b/packages/icicles/src/props.ts new file mode 100644 index 000000000..fd847949f --- /dev/null +++ b/packages/icicles/src/props.ts @@ -0,0 +1,30 @@ +import { IciclesTooltip } from './IciclesTooltip' +import { IciclesSvgProps } from './types' + +const _defaultProps: Partial> = { + id: 'id', + value: 'value', + layers: ['rects', 'rectLabels'], + colors: { scheme: 'nivo' }, + colorBy: 'id', + inheritColorFromParent: true, + childColor: { from: 'color' }, + borderWidth: 1, + borderColor: { from: 'color', modifiers: [['darker', 0.6]] }, + enableRectLabels: false, + rectLabel: 'formattedValue', + rectLabelsTextColor: { theme: 'labels.text.fill' }, + animate: true, + motionConfig: 'gentle', + isInteractive: true, + defs: [], + fill: [], + tooltip: IciclesTooltip, + role: 'img', + direction: 'bottom', + rectLabelsSkipLength: 0, + rectLabelsSkipPercentage: 0, + rectLabelsOffset: 1, +} + +export const defaultProps = _defaultProps as IciclesSvgProps diff --git a/packages/icicles/src/types.ts b/packages/icicles/src/types.ts new file mode 100644 index 000000000..165615a0c --- /dev/null +++ b/packages/icicles/src/types.ts @@ -0,0 +1,109 @@ +import { OrdinalColorScaleConfig, InheritedColorConfig } from '@nivo/colors' +import { + Theme, + Box, + ValueFormat, + SvgDefsAndFill, + ModernMotionProps, + PropertyAccessor, +} from '@nivo/core' +import { type Rect, RectLabelsProps } from '@nivo/rects' + +export type DatumId = string | number + +export type IciclesLayerId = 'rects' | 'rectLabels' + +export interface IciclesCustomLayerProps { + nodes: ComputedDatum[] + baseOffsetLeft: number + baseOffsetTop: number +} + +export type IciclesCustomLayer = React.FC> + +export type IciclesLayer = IciclesLayerId | IciclesCustomLayer + +export interface DataProps { + data: RawDatum + id?: PropertyAccessor + value?: PropertyAccessor + valueFormat?: ValueFormat +} + +export interface ChildrenDatum { + children?: Array> +} + +export interface ComputedDatum { + color: string + // contains the raw node's data + data: RawDatum + depth: number + // defined when using patterns or gradients + fill?: string + formattedValue: string + height: number + id: DatumId + parent?: ComputedDatum + // contain own id plus all ancestor ids + path: DatumId[] + percentage: number + rect: Rect + value: number +} + +export type IciclesDirection = 'top' | 'right' | 'bottom' | 'left' + +export type IciclesCommonProps = { + animate: boolean + borderColor: InheritedColorConfig> + borderWidth: number + // used if `inheritColorFromParent` is `true` + childColor: InheritedColorConfig> + colorBy: 'id' | 'depth' + colors: OrdinalColorScaleConfig, 'color' | 'fill'>> + data: RawDatum + direction: IciclesDirection + enableRectLabels: boolean + height: number + id: PropertyAccessor + inheritColorFromParent: boolean + isInteractive: boolean + layers: IciclesLayer[] + margin?: Box + motionConfig: ModernMotionProps['motionConfig'] + rectLabelsOffset: number + rectLabelsSkipLength: number + rectLabelsSkipPercentage: number + rectLabelsTextColor: InheritedColorConfig + renderWrapper: boolean + role: string + theme: Theme + tooltip: (props: ComputedDatum) => JSX.Element + value: PropertyAccessor + valueFormat?: ValueFormat + width: number +} & RectLabelsProps> + +export type MouseHandler = ( + datum: ComputedDatum, + event: React.MouseEvent +) => void + +export type WheelHandler = ( + datum: ComputedDatum, + event: React.WheelEvent +) => void + +export type MouseHandlers = Partial<{ + onClick: MouseHandler + onMouseEnter: MouseHandler + onMouseLeave: MouseHandler + onMouseMove: MouseHandler + onWheel: WheelHandler + onContextMenu: MouseHandler +}> + +export type IciclesSvgProps = IciclesCommonProps & + SvgDefsAndFill & + MouseHandlers diff --git a/packages/icicles/stories/icicles.stories.tsx b/packages/icicles/stories/icicles.stories.tsx new file mode 100644 index 000000000..65b530197 --- /dev/null +++ b/packages/icicles/stories/icicles.stories.tsx @@ -0,0 +1,235 @@ +/* eslint-disable @typescript-eslint/ban-ts-comment */ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { useState } from 'react' +import { storiesOf } from '@storybook/react' +import { action } from '@storybook/addon-actions' +import { withKnobs, boolean, select } from '@storybook/addon-knobs' +// @ts-ignore +import { linearGradientDef, patternDotsDef, useTheme } from '@nivo/core' +// @ts-ignore +import { generateLibTree } from '@nivo/generators' +import { colorSchemes } from '@nivo/colors' +// @ts-ignore +import { Icicles, IciclesComputedDatum, IciclesDirection } from '../src' + +interface RawDatum { + name: string + loc: number +} + +const commonProperties = { + width: 900, + height: 500, + data: generateLibTree() as any, + id: 'name', + value: 'loc', +} + +const stories = storiesOf('Icicles', module) + +stories.addDecorator(withKnobs) + +stories.add('default', () => ) + +stories.add('with child color modifier', () => ( + + {...commonProperties} + childColor={{ from: 'color', modifiers: [['brighter', 0.13]] }} + /> +)) + +stories.add('with child colors independent of parent', () => ( + {...commonProperties} inheritColorFromParent={false} /> +)) + +const customPalette = ['#ffd700', '#ffb14e', '#fa8775', '#ea5f94', '#cd34b5', '#9d02d7', '#0000ff'] + +stories.add('with custom colors', () => ( + {...commonProperties} colors={customPalette} /> +)) + +stories.add('with custom child colors', () => ( + + {...commonProperties} + childColor={(parent, child) => { + // @ts-expect-error + return child.data.color + }} + /> +)) + +stories.add('with formatted tooltip value', () => ( + {...commonProperties} valueFormat=" >-$,.2f" /> +)) + +const CustomTooltip = ({ id, value, color }: IciclesComputedDatum) => { + const theme = useTheme() + + return ( + + {id}: {value} + + ) +} + +stories.add('custom tooltip', () => ( + + {...commonProperties} + tooltip={CustomTooltip} + theme={{ + tooltip: { + container: { + background: '#333', + }, + }, + }} + /> +)) + +stories.add('enter/leave (check actions)', () => ( + + {...commonProperties} + onMouseEnter={action('onMouseEnter')} + onMouseLeave={action('onMouseLeave')} + /> +)) + +stories.add('wheel/contextmenu (check actions)', () => ( + + {...commonProperties} + onWheel={action('onWheel')} + onContextMenu={action('onContextMenu')} + /> +)) + +stories.add('patterns & gradients', () => ( + + {...commonProperties} + defs={[ + linearGradientDef('gradient', [ + { offset: 0, color: '#ffffff' }, + { offset: 15, color: 'inherit' }, + { offset: 100, color: 'inherit' }, + ]), + patternDotsDef('pattern', { + background: 'inherit', + color: '#ffffff', + size: 2, + padding: 3, + stagger: true, + }), + ]} + fill={[ + { + match: node => + ['viz', 'text', 'utils'].includes( + (node as unknown as IciclesComputedDatum).id as string + ), + id: 'gradient', + }, + { + match: node => + ['set', 'generators', 'misc'].includes( + (node as unknown as IciclesComputedDatum).id as string + ), + id: 'pattern', + }, + ]} + /> +)) + +const flatten = (data: T[]): T[] => + data.reduce((acc, item) => { + if (item.children) { + return [...acc, item, ...flatten(item.children)] + } + + return [...acc, item] + }, []) + +const findObject = (data: T[], name: string): T | undefined => + data.find(searchedName => searchedName.name === name) + +const drillDownColors = colorSchemes.brown_blueGreen[7] +const drillDownColorMap = { + viz: drillDownColors[0], + colors: drillDownColors[1], + utils: drillDownColors[2], + generators: drillDownColors[3], + set: drillDownColors[4], + text: drillDownColors[5], + misc: drillDownColors[6], +} +const getDrillDownColor = (node: Omit, 'color' | 'fill'>) => { + const category = [...node.path].reverse()[1] as keyof typeof drillDownColorMap + + return drillDownColorMap[category] +} + +stories.add( + 'children drill down', + () => { + const [data, setData] = useState(commonProperties.data) + + return ( + <> + + + {...commonProperties} + colors={getDrillDownColor} + inheritColorFromParent={false} + borderWidth={1} + borderColor={{ + from: 'color', + modifiers: [['darker', 0.6]], + }} + animate={boolean('animate', true)} + motionConfig={select( + 'motion config', + ['default', 'gentle', 'wobbly', 'stiff', 'slow', 'molasses'], + 'gentle' + )} + enableRectLabels + rectLabelsTextColor={{ + from: 'color', + modifiers: [['darker', 3]], + }} + data={data} + onClick={clickedData => { + const foundObject = findObject( + flatten(data.children) as any, + clickedData.id as string + ) + if (foundObject && (foundObject as any).children) { + setData(foundObject) + } + }} + /> + + ) + }, + { + info: { + text: ` + You can drill down into individual children by clicking on them + `, + }, + } +) + +stories.add('change direction', () => ( + + {...commonProperties} + direction={select( + 'direction', + ['top', 'right', 'bottom', 'left'], + 'bottom' + )} + animate={boolean('animate', true)} + motionConfig={select( + 'motion config', + ['default', 'gentle', 'wobbly', 'stiff', 'slow', 'molasses'], + 'gentle' + )} + /> +)) diff --git a/packages/icicles/tests/.eslintrc.yml b/packages/icicles/tests/.eslintrc.yml new file mode 100644 index 000000000..2f8de9aea --- /dev/null +++ b/packages/icicles/tests/.eslintrc.yml @@ -0,0 +1,2 @@ +env: + jest: true diff --git a/packages/icicles/tests/Icicles.test.tsx b/packages/icicles/tests/Icicles.test.tsx new file mode 100644 index 000000000..e8dab93a8 --- /dev/null +++ b/packages/icicles/tests/Icicles.test.tsx @@ -0,0 +1,654 @@ +/* eslint-disable @typescript-eslint/ban-ts-comment */ +import { mount } from 'enzyme' +// @ts-ignore +import { linearGradientDef, patternDotsDef } from '@nivo/core' +// @ts-ignore +import { Icicles } from '../src' +import { RectLabelsLayer, RectShape, RectLabelComponent as RectLabel } from '@nivo/rects' + +interface CustomSampleData { + name: string + color: string + attributes?: { + volume: number + } + children?: CustomSampleData[] +} + +const sampleData = { + id: 'root', + color: '#abcdef', + children: [ + { + id: 'A', + color: '#ff5500', + children: [ + { + id: 'A-1', + color: '#bbddcc', + children: [ + { + id: 'A-1-I', + value: 60, + color: '#ffaacc', + }, + ], + }, + { + id: 'A-2', + value: 50, + color: '#cc99dd', + }, + ], + }, + { + id: 'B', + value: 20, + color: '#ffdd00', + }, + ], +} + +const sampleDataWithCustomProps = { + name: 'root', + color: '#abcdef', + children: [ + { + name: 'A', + color: '#ff5500', + children: [ + { + name: 'A-1', + color: '#bbddcc', + children: [ + { + name: 'A-1-I', + attributes: { + volume: 60, + }, + color: '#ffaacc', + }, + ], + }, + { + name: 'A-2', + attributes: { + volume: 50, + }, + color: '#cc99dd', + }, + ], + }, + { + name: 'B', + attributes: { + volume: 20, + }, + color: '#ffdd00', + }, + ], +} + +describe('Icicles', () => { + describe('data', () => { + it('should use default id and value properties', () => { + const wrapper = mount() + + const rects = wrapper.find(RectShape) + expect(rects).toHaveLength(6) + + expect(rects.at(0).prop('datum').id).toEqual('root') + + expect(rects.at(1).prop('datum').id).toEqual('A') + expect(rects.at(1).prop('datum').value).toEqual(110) + + expect(rects.at(2).prop('datum').id).toEqual('B') + expect(rects.at(2).prop('datum').value).toEqual(20) + + expect(rects.at(3).prop('datum').id).toEqual('A-1') + expect(rects.at(3).prop('datum').value).toEqual(60) + + expect(rects.at(4).prop('datum').id).toEqual('A-2') + expect(rects.at(4).prop('datum').value).toEqual(50) + + expect(rects.at(5).prop('datum').id).toEqual('A-1-I') + expect(rects.at(5).prop('datum').value).toEqual(60) + }) + + it('should use custom id and value accessors expressed as path', () => { + const wrapper = mount( + + ) + + const rects = wrapper.find(RectShape) + expect(rects).toHaveLength(6) + + expect(rects.at(0).prop('datum').id).toEqual('root') + + expect(rects.at(1).prop('datum').id).toEqual('A') + expect(rects.at(1).prop('datum').value).toEqual(110) + + expect(rects.at(2).prop('datum').id).toEqual('B') + expect(rects.at(2).prop('datum').value).toEqual(20) + + expect(rects.at(3).prop('datum').id).toEqual('A-1') + expect(rects.at(3).prop('datum').value).toEqual(60) + + expect(rects.at(4).prop('datum').id).toEqual('A-2') + expect(rects.at(4).prop('datum').value).toEqual(50) + + expect(rects.at(5).prop('datum').id).toEqual('A-1-I') + expect(rects.at(5).prop('datum').value).toEqual(60) + }) + + it('should use custom id and value accessors expressed as functions', () => { + const wrapper = mount( + + width={400} + height={400} + data={sampleDataWithCustomProps} + id={d => d.name} + value={d => d.attributes?.volume ?? 0} + /> + ) + + const rects = wrapper.find(RectShape) + expect(rects).toHaveLength(6) + + expect(rects.at(0).prop('datum').id).toEqual('root') + + expect(rects.at(1).prop('datum').id).toEqual('A') + expect(rects.at(1).prop('datum').value).toEqual(110) + + expect(rects.at(2).prop('datum').id).toEqual('B') + expect(rects.at(2).prop('datum').value).toEqual(20) + + expect(rects.at(3).prop('datum').id).toEqual('A-1') + expect(rects.at(3).prop('datum').value).toEqual(60) + + expect(rects.at(4).prop('datum').id).toEqual('A-2') + expect(rects.at(4).prop('datum').value).toEqual(50) + + expect(rects.at(5).prop('datum').id).toEqual('A-1-I') + expect(rects.at(5).prop('datum').value).toEqual(60) + }) + }) + + describe('colors', () => { + it('should use colors from scheme', () => { + const wrapper = mount( + + ) + + const rects = wrapper.find(RectShape) + expect(rects).toHaveLength(6) + + expect(rects.at(0).prop('datum').id).toEqual('root') + expect(rects.at(0).prop('datum').color).toEqual('#7fc97f') + + expect(rects.at(1).prop('datum').id).toEqual('A') + expect(rects.at(1).prop('datum').color).toEqual('#beaed4') + + expect(rects.at(2).prop('datum').id).toEqual('B') + expect(rects.at(2).prop('datum').color).toEqual('#fdc086') + + expect(rects.at(3).prop('datum').id).toEqual('A-1') + expect(rects.at(3).prop('datum').color).toEqual('#beaed4') + + expect(rects.at(4).prop('datum').id).toEqual('A-2') + expect(rects.at(4).prop('datum').color).toEqual('#beaed4') + + expect(rects.at(5).prop('datum').id).toEqual('A-1-I') + expect(rects.at(5).prop('datum').color).toEqual('#beaed4') + }) + + it('should allow to use colors from data using a path', () => { + const wrapper = mount( + + ) + + const rects = wrapper.find(RectShape) + expect(rects).toHaveLength(6) + + expect(rects.at(0).prop('datum').id).toEqual('root') + expect(rects.at(0).prop('datum').color).toEqual('#abcdef') + + expect(rects.at(1).prop('datum').id).toEqual('A') + expect(rects.at(1).prop('datum').color).toEqual('#ff5500') + + expect(rects.at(2).prop('datum').id).toEqual('B') + expect(rects.at(2).prop('datum').color).toEqual('#ffdd00') + + expect(rects.at(3).prop('datum').id).toEqual('A-1') + expect(rects.at(3).prop('datum').color).toEqual('#ff5500') + + expect(rects.at(4).prop('datum').id).toEqual('A-2') + expect(rects.at(4).prop('datum').color).toEqual('#ff5500') + + expect(rects.at(5).prop('datum').id).toEqual('A-1-I') + expect(rects.at(5).prop('datum').color).toEqual('#ff5500') + }) + + it('should allow to use colors from data using a function', () => { + const wrapper = mount( + d.data.color} + inheritColorFromParent + /> + ) + + const rects = wrapper.find(RectShape) + expect(rects).toHaveLength(6) + + expect(rects.at(0).prop('datum').id).toEqual('root') + expect(rects.at(0).prop('datum').color).toEqual('#abcdef') + + expect(rects.at(1).prop('datum').id).toEqual('A') + expect(rects.at(1).prop('datum').color).toEqual('#ff5500') + + expect(rects.at(2).prop('datum').id).toEqual('B') + expect(rects.at(2).prop('datum').color).toEqual('#ffdd00') + + expect(rects.at(3).prop('datum').id).toEqual('A-1') + expect(rects.at(3).prop('datum').color).toEqual('#ff5500') + + expect(rects.at(4).prop('datum').id).toEqual('A-2') + expect(rects.at(4).prop('datum').color).toEqual('#ff5500') + + expect(rects.at(5).prop('datum').id).toEqual('A-1-I') + expect(rects.at(5).prop('datum').color).toEqual('#ff5500') + }) + + it('should allow to define colors according to depth', () => { + const wrapper = mount( + + ) + + const rects = wrapper.find(RectShape) + expect(rects).toHaveLength(6) + + expect(rects.at(0).prop('datum').id).toEqual('root') + expect(rects.at(0).prop('datum').color).toEqual('#e8c1a0') + + expect(rects.at(1).prop('datum').id).toEqual('A') + expect(rects.at(1).prop('datum').color).toEqual('#f47560') + + expect(rects.at(2).prop('datum').id).toEqual('B') + expect(rects.at(2).prop('datum').color).toEqual('#f47560') + + expect(rects.at(3).prop('datum').id).toEqual('A-1') + expect(rects.at(3).prop('datum').color).toEqual('#f1e15b') + + expect(rects.at(4).prop('datum').id).toEqual('A-2') + expect(rects.at(4).prop('datum').color).toEqual('#f1e15b') + + expect(rects.at(5).prop('datum').id).toEqual('A-1-I') + expect(rects.at(5).prop('datum').color).toEqual('#e8a838') + }) + }) + + describe('patterns & gradients', () => { + xit('should support patterns', () => { + const wrapper = mount( + + ) + + const rects = wrapper.find(RectShape) + expect(rects).toHaveLength(6) + + expect(rects.at(0).prop('datum').id).toEqual('root') + expect(rects.at(1).prop('datum').id).toEqual('A') + expect(rects.at(1).prop('datum').fill).toEqual('url(#pattern.bg.#e8c1a0)') + + const svg = wrapper.find('SvgWrapper') + expect(svg.at(0).prop('defs')).toEqual([ + { + background: 'inherit', + color: '#ffffff', + id: 'pattern', + padding: 4, + size: 1, + stagger: true, + type: 'patternDots', + }, + { + background: '#e8c1a0', + color: '#ffffff', + id: 'pattern.bg.#e8c1a0', + padding: 4, + size: 1, + stagger: true, + type: 'patternDots', + }, + ]) + }) + + xit('should support gradients', () => { + const wrapper = mount( + + ) + + const rects = wrapper.find(RectShape) + expect(rects).toHaveLength(6) + + expect(rects.at(0).prop('datum').id).toEqual('root') + expect(rects.at(1).prop('datum').id).toEqual('A') + expect(rects.at(1).prop('datum').fill).toEqual('url(#gradient.1.#e8c1a0.2.#e8c1a0)') + + const svg = wrapper.find('SvgWrapper') + expect(svg.at(0).prop('defs')).toEqual([ + { + colors: [ + { color: '#ffffff', offset: 0 }, + { color: 'inherit', offset: 15 }, + { color: 'inherit', offset: 100 }, + ], + id: 'gradient', + type: 'linearGradient', + }, + { + colors: [ + { color: '#ffffff', offset: 0 }, + { color: '#e8c1a0', offset: 15 }, + { color: '#e8c1a0', offset: 100 }, + ], + id: 'gradient.1.#e8c1a0.2.#e8c1a0', + type: 'linearGradient', + }, + ]) + }) + }) + + describe('slice labels', () => { + it('should be disabled by default', () => { + const wrapper = mount() + expect(wrapper.find(RectLabelsLayer).exists()).toBeFalsy() + }) + + it('should render labels when enabled', () => { + const wrapper = mount( + + ) + + const labels = wrapper.find(RectLabel) + expect(labels).toHaveLength(6) + + expect(labels.at(0).text()).toEqual('100.00%') + expect(labels.at(1).text()).toEqual('84.62%') + expect(labels.at(2).text()).toEqual('15.38%') + expect(labels.at(3).text()).toEqual('46.15%') + expect(labels.at(4).text()).toEqual('38.46%') + expect(labels.at(5).text()).toEqual('46.15%') + }) + + it('should use formattedValue', () => { + const wrapper = mount( + + ) + + const labels = wrapper.find(RectLabel) + expect(labels).toHaveLength(6) + + expect(labels.at(1).prop('datum').formattedValue).toEqual('$110.00') + expect(labels.at(1).find('text').text()).toEqual('$110.00') + + expect(labels.at(2).prop('datum').formattedValue).toEqual('$20.00') + expect(labels.at(2).find('text').text()).toEqual('$20.00') + + expect(labels.at(3).prop('datum').formattedValue).toEqual('$60.00') + expect(labels.at(3).find('text').text()).toEqual('$60.00') + + expect(labels.at(4).prop('datum').formattedValue).toEqual('$50.00') + expect(labels.at(4).find('text').text()).toEqual('$50.00') + + expect(labels.at(5).prop('datum').formattedValue).toEqual('$60.00') + expect(labels.at(5).find('text').text()).toEqual('$60.00') + }) + + it('should allow to change the label accessor using a path', () => { + const wrapper = mount( + + ) + + const labels = wrapper.find(RectLabel) + expect(labels).toHaveLength(6) + + expect(labels.at(0).text()).toEqual('root') + expect(labels.at(1).text()).toEqual('A') + expect(labels.at(2).text()).toEqual('B') + expect(labels.at(3).text()).toEqual('A-1') + expect(labels.at(4).text()).toEqual('A-2') + expect(labels.at(5).text()).toEqual('A-1-I') + }) + + it('should allow to change the label accessor using a function', () => { + const wrapper = mount( + `${datum.id} - ${datum.value}`} + /> + ) + + const labels = wrapper.find(RectLabel) + expect(labels).toHaveLength(6) + + expect(labels.at(1).find('text').text()).toEqual('A - 110') + expect(labels.at(2).find('text').text()).toEqual('B - 20') + }) + }) + + describe('interactivity', () => { + it('should support onClick handler', () => { + const onClick = jest.fn() + const wrapper = mount( + + ) + + wrapper.find(RectShape).at(0).simulate('click') + + expect(onClick).toHaveBeenCalledTimes(1) + const [datum] = onClick.mock.calls[0] + expect(datum.id).toEqual('root') + }) + + it('should support onMouseEnter handler', () => { + const onMouseEnter = jest.fn() + const wrapper = mount( + + ) + + wrapper.find(RectShape).at(1).simulate('mouseenter') + + expect(onMouseEnter).toHaveBeenCalledTimes(1) + const [datum] = onMouseEnter.mock.calls[0] + expect(datum.id).toEqual('A') + }) + + it('should support onMouseMove handler', () => { + const onMouseMove = jest.fn() + const wrapper = mount( + + ) + + wrapper.find(RectShape).at(2).simulate('mousemove') + + expect(onMouseMove).toHaveBeenCalledTimes(1) + const [datum] = onMouseMove.mock.calls[0] + expect(datum.id).toEqual('B') + }) + + it('should support onMouseLeave handler', () => { + const onMouseLeave = jest.fn() + const wrapper = mount( + + ) + + wrapper.find(RectShape).at(0).simulate('mouseleave') + + expect(onMouseLeave).toHaveBeenCalledTimes(1) + const [datum] = onMouseLeave.mock.calls[0] + expect(datum.id).toEqual('root') + }) + + it('should allow to completely disable interactivity', () => { + const onClick = jest.fn() + const onMouseEnter = jest.fn() + const onMouseMove = jest.fn() + const onMouseLeave = jest.fn() + + const wrapper = mount( + + ) + + wrapper.find(RectShape).at(0).simulate('click') + wrapper.find(RectShape).at(0).simulate('mouseenter') + wrapper.find(RectShape).at(0).simulate('mousemove') + wrapper.find(RectShape).at(0).simulate('mouseleave') + + expect(onClick).not.toHaveBeenCalled() + expect(onMouseEnter).not.toHaveBeenCalled() + expect(onMouseMove).not.toHaveBeenCalled() + expect(onMouseLeave).not.toHaveBeenCalled() + + wrapper.find(RectShape).forEach(rect => { + const shape = rect.find('rect') + expect(shape.prop('onClick')).toBeUndefined() + expect(shape.prop('onMouseEnter')).toBeUndefined() + expect(shape.prop('onMouseMove')).toBeUndefined() + expect(shape.prop('onMouseLeave')).toBeUndefined() + }) + }) + }) + + describe('tooltip', () => { + it('should render a tooltip when hovering a slice', () => { + const wrapper = mount() + + expect(wrapper.find('IciclesTooltip').exists()).toBeFalsy() + + wrapper.find(RectShape).at(2).simulate('mouseenter') + + const tooltip = wrapper.find('IciclesTooltip') + expect(tooltip.exists()).toBeTruthy() + expect(tooltip.text()).toEqual('B: 15.38%') + }) + + it('should allow to override the default tooltip', () => { + const CustomTooltip = ({ id }) => {id} + const wrapper = mount( + + ) + + wrapper.find(RectShape).at(2).simulate('mouseenter') + + const tooltip = wrapper.find(CustomTooltip) + expect(tooltip.exists()).toBeTruthy() + expect(tooltip.text()).toEqual('B') + }) + }) + + describe('layers', () => { + it('should support disabling a layer', () => { + const wrapper = mount() + expect(wrapper.find(RectShape)).toHaveLength(6) // root included + + wrapper.setProps({ layers: ['rectLabels'] }) + expect(wrapper.find(RectShape)).toHaveLength(0) + }) + + it('should support adding a custom layer', () => { + const CustomLayer = () => null + + const wrapper = mount( + + ) + + const customLayer = wrapper.find(CustomLayer) + + // root included + expect(customLayer.prop('nodes')).toHaveLength(6) + }) + }) +}) diff --git a/packages/icicles/tsconfig.json b/packages/icicles/tsconfig.json new file mode 100644 index 000000000..855b4b2b7 --- /dev/null +++ b/packages/icicles/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.types.json", + "compilerOptions": { + "outDir": "./dist/types", + "rootDir": "./src" + }, + "include": ["src/**/*"] +} diff --git a/packages/rects/LICENSE.md b/packages/rects/LICENSE.md new file mode 100644 index 000000000..faa45389e --- /dev/null +++ b/packages/rects/LICENSE.md @@ -0,0 +1,19 @@ +Copyright (c) Raphaël Benitte + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/packages/rects/README.md b/packages/rects/README.md new file mode 100644 index 000000000..d07d0c3d0 --- /dev/null +++ b/packages/rects/README.md @@ -0,0 +1,9 @@ +nivo + +# `@nivo/rects` + +[![version](https://img.shields.io/npm/v/@nivo/rects?style=for-the-badge)](https://www.npmjs.com/package/@nivo/rects) +[![downloads](https://img.shields.io/npm/dm/@nivo/rects?style=for-the-badge)](https://www.npmjs.com/package/@nivo/rects) + +This package is used internally by nivo packages dealing with rects +such as `@nivo/icicles`. diff --git a/packages/rects/package.json b/packages/rects/package.json new file mode 100644 index 000000000..618da04be --- /dev/null +++ b/packages/rects/package.json @@ -0,0 +1,48 @@ +{ + "name": "@nivo/rects", + "version": "0.79.1", + "license": "MIT", + "author": { + "name": "Raphaël Benitte", + "url": "https://github.com/plouc" + }, + "contributors": [ + "Lilian Saget-Lethias ", + "Mehdi Louraoui " + ], + "repository": { + "type": "git", + "url": "https://github.com/plouc/nivo.git", + "directory": "packages/rects" + }, + "keywords": [ + "nivo", + "dataviz", + "react", + "d3", + "rects" + ], + "main": "./dist/nivo-rects.cjs.js", + "module": "./dist/nivo-rects.es.js", + "typings": "./dist/types/index.d.ts", + "files": [ + "README.md", + "LICENSE.md", + "dist/", + "!dist/tsconfig.tsbuildinfo" + ], + "dependencies": { + "@nivo/colors": "0.79.1", + "@react-spring/web": "9.3.1" + }, + "devDependencies": { + "@nivo/core": "0.79.0" + }, + "peerDependencies": { + "@nivo/core": "0.79.0", + "react": ">= 16.14.0 < 18.0.0" + }, + "publishConfig": { + "access": "public" + } +} \ No newline at end of file diff --git a/packages/rects/src/RectShape.tsx b/packages/rects/src/RectShape.tsx new file mode 100644 index 000000000..0ff272230 --- /dev/null +++ b/packages/rects/src/RectShape.tsx @@ -0,0 +1,89 @@ +import { SpringValue, animated, Interpolation } from '@react-spring/web' +import { MouseEvent, useCallback, WheelEvent } from 'react' +import { DatumWithRectAndColor } from './types' + +export type RectMouseHandler = ( + datum: TDatum, + event: MouseEvent +) => void + +export type RectWheelHandler = ( + datum: TDatum, + event: WheelEvent +) => void + +export interface RectShapeProps { + datum: TDatum + onClick?: RectMouseHandler + onMouseEnter?: RectMouseHandler + onMouseLeave?: RectMouseHandler + onMouseMove?: RectMouseHandler + onWheel?: RectWheelHandler + onContextMenu?: RectMouseHandler + style: { + borderColor: SpringValue + borderWidth: number + color: SpringValue + height: number + opacity: SpringValue + transform: Interpolation + width: number + } +} + +/** + * A simple rect component to be used typically with an `RectsLayer`. + * + * Please note that the component accepts `SpringValue`s instead of + * regular values to support animations. + */ +export const RectShape = ({ + datum, + style, + onClick, + onMouseEnter, + onMouseLeave, + onMouseMove, + onWheel, + onContextMenu, +}: RectShapeProps) => { + const handleClick = useCallback(event => onClick?.(datum, event), [onClick, datum]) + + const handleMouseEnter = useCallback( + event => onMouseEnter?.(datum, event), + [onMouseEnter, datum] + ) + + const handleMouseMove = useCallback(event => onMouseMove?.(datum, event), [onMouseMove, datum]) + + const handleMouseLeave = useCallback( + event => onMouseLeave?.(datum, event), + [onMouseLeave, datum] + ) + + const handleWheel = useCallback(event => onWheel?.(datum, event), [onWheel, datum]) + + const handleContextMenu = useCallback( + event => onContextMenu?.(datum, event), + [onContextMenu, datum] + ) + + return ( + + ) +} diff --git a/packages/rects/src/RectsLayer.tsx b/packages/rects/src/RectsLayer.tsx new file mode 100644 index 000000000..3fc2c1d23 --- /dev/null +++ b/packages/rects/src/RectsLayer.tsx @@ -0,0 +1,93 @@ +import { InheritedColorConfig, useInheritedColor } from '@nivo/colors' +import { useTheme } from '@nivo/core' +import { createElement } from 'react' +import { RectMouseHandler, RectShape, RectShapeProps, RectWheelHandler } from './RectShape' +import { DatumWithRectAndColor } from './types' +import { useRectsTransition } from './useRectsTransition' + +export type RectComponent = ( + props: RectShapeProps +) => JSX.Element + +export interface RectsLayerProps { + borderColor: InheritedColorConfig + borderWidth: number + component?: RectComponent + data: TDatum[] + onClick?: RectMouseHandler + onMouseEnter?: RectMouseHandler + onMouseLeave?: RectMouseHandler + onMouseMove?: RectMouseHandler + onWheel?: RectWheelHandler + onContextMenu?: RectMouseHandler +} + +export const RectsLayer = ({ + onMouseMove, + onMouseLeave, + onMouseEnter, + onClick, + onWheel, + onContextMenu, + borderWidth, + data, + borderColor, + component = RectShape, +}: RectsLayerProps) => { + const theme = useTheme() + const getBorderColor = useInheritedColor(borderColor, theme) + + const { transition, interpolate } = useRectsTransition< + TDatum, + { + borderColor: string + color: string + opacity: number + } + >(data, { + enter: datum => ({ + opacity: 0, + color: datum.color, + borderColor: getBorderColor(datum), + }), + update: datum => ({ + opacity: 1, + color: datum.color, + borderColor: getBorderColor(datum), + }), + leave: datum => ({ + opacity: 0, + color: datum.color, + borderColor: getBorderColor(datum), + }), + }) + + const Rect: RectComponent = component + + return ( + + {transition((transitionProps, datum) => { + return createElement(Rect, { + key: datum.id, + datum, + style: { + ...transitionProps, + borderWidth, + transform: interpolate( + transitionProps.transformX, + transitionProps.transformY + ), + width: datum.rect.width, + height: datum.rect.height, + }, + onClick, + onMouseEnter, + onMouseMove, + onMouseLeave, + onWheel, + onContextMenu, + }) + })} + + ) +} diff --git a/packages/rects/src/centers.ts b/packages/rects/src/centers.ts new file mode 100644 index 000000000..c1eac8fd7 --- /dev/null +++ b/packages/rects/src/centers.ts @@ -0,0 +1,57 @@ +import { useMotionConfig } from '@nivo/core' +import { SpringValue, to, useTransition } from '@react-spring/web' +import { DatumWithRect } from './types' +import { TransitionExtra, useRectExtraTransition } from './useRectsTransition' + +export const interpolateRectCenter = + (offset: number, baseOffsetLeft: number, baseOffsetTop: number) => + ( + x0Value: SpringValue, + y0Value: SpringValue, + widthValue: SpringValue, + heightValue: SpringValue + ) => + to( + [x0Value, y0Value, widthValue, heightValue], + (x0, y0, width, height) => + `translate(${Math.abs(baseOffsetLeft - (x0 + width / 2))}, ${Math.abs( + baseOffsetTop - (y0 + height / 2) * offset + )})` + ) + +export const useRectCentersTransition = ( + data: TDatum[], + offset = 1, + baseOffsetLeft = 0, + baseOffsetTop = 0, + extra?: TransitionExtra +) => { + const { animate, config: springConfig } = useMotionConfig() + + const phases = useRectExtraTransition(extra) + + const transition = useTransition< + TDatum, + { + height: number + progress: number + width: number + x0: number + y0: number + } & TExtraProps + >(data, { + keys: datum => datum.id, + initial: phases.update, + from: phases.enter, + enter: phases.update, + update: phases.update, + leave: phases.leave, + config: springConfig, + immediate: !animate, + }) + + return { + transition, + interpolate: interpolateRectCenter(offset, baseOffsetLeft, baseOffsetTop), + } +} diff --git a/packages/rects/src/index.ts b/packages/rects/src/index.ts new file mode 100644 index 000000000..fcd952634 --- /dev/null +++ b/packages/rects/src/index.ts @@ -0,0 +1,6 @@ +export * from './rect_labels' +export * from './RectShape' +export * from './RectsLayer' +export * from './types' +export * from './useRectsTransition' +export * from './centers' diff --git a/packages/rects/src/rect_labels/RectLabel.tsx b/packages/rects/src/rect_labels/RectLabel.tsx new file mode 100644 index 000000000..252007ed0 --- /dev/null +++ b/packages/rects/src/rect_labels/RectLabel.tsx @@ -0,0 +1,40 @@ +import { useTheme } from '@nivo/core' +import { animated, Interpolation, SpringValue } from '@react-spring/web' +import { CSSProperties } from 'react' +import { DatumWithRectAndColor } from '../types' + +const staticStyle: CSSProperties = { + pointerEvents: 'none', +} + +export interface RectLabelProps { + datum: TDatum + label: string + style: { + progress: SpringValue + textColor: string + transform: Interpolation + } +} + +export const RectLabel = ({ + label, + style, +}: RectLabelProps) => { + const theme = useTheme() + + return ( + + + {label} + + + ) +} diff --git a/packages/rects/src/rect_labels/RectLabelsLayer.tsx b/packages/rects/src/rect_labels/RectLabelsLayer.tsx new file mode 100644 index 000000000..e1b190c35 --- /dev/null +++ b/packages/rects/src/rect_labels/RectLabelsLayer.tsx @@ -0,0 +1,66 @@ +import { useInheritedColor } from '@nivo/colors' +import { PropertyAccessor, usePropertyAccessor, useTheme } from '@nivo/core' +import { createElement } from 'react' +import { useRectCentersTransition } from '../centers' +import { DatumWithRectAndColor } from '../types' +import { RectLabel, RectLabelProps } from './RectLabel' +import { RectLabelsProps } from './props' + +export type RectLabelComponent = ( + props: RectLabelProps +) => JSX.Element + +export interface RectLabelsLayerProps { + baseOffsetLeft: number + baseOffsetTop: number + component?: RectLabelsProps['rectLabelsComponent'] + data: TDatum[] + label: PropertyAccessor + offset: RectLabelsProps['rectLabelsOffset'] + textColor: RectLabelsProps['rectLabelsTextColor'] +} + +export const RectLabelsLayer = ({ + data, + label: labelAccessor, + textColor, + offset, + baseOffsetLeft, + baseOffsetTop, + component = RectLabel, +}: RectLabelsLayerProps) => { + const getLabel = usePropertyAccessor(labelAccessor) + const theme = useTheme() + const getTextColor = useInheritedColor(textColor, theme) + + const { transition, interpolate } = useRectCentersTransition( + data, + offset, + baseOffsetLeft, + baseOffsetTop + ) + + const Label: RectLabelComponent = component + + return ( + + {transition((transitionProps, datum) => { + return createElement(Label, { + key: datum.id, + datum, + label: getLabel(datum), + style: { + ...transitionProps, + textColor: getTextColor(datum), + transform: interpolate( + transitionProps.x0, + transitionProps.y0, + transitionProps.width, + transitionProps.height + ), + }, + }) + })} + + ) +} diff --git a/packages/rects/src/rect_labels/index.ts b/packages/rects/src/rect_labels/index.ts new file mode 100644 index 000000000..115a446fb --- /dev/null +++ b/packages/rects/src/rect_labels/index.ts @@ -0,0 +1,3 @@ +export { RectLabel as RectLabelComponent } from './RectLabel' +export * from './RectLabelsLayer' +export * from './props' diff --git a/packages/rects/src/rect_labels/props.ts b/packages/rects/src/rect_labels/props.ts new file mode 100644 index 000000000..bf592c985 --- /dev/null +++ b/packages/rects/src/rect_labels/props.ts @@ -0,0 +1,13 @@ +import { InheritedColorConfig } from '@nivo/colors' +import { PropertyAccessor } from '@nivo/core' +import { DatumWithRectAndColor } from '../types' +import { RectLabelComponent } from './RectLabelsLayer' + +export interface RectLabelsProps { + rectLabel: PropertyAccessor + rectLabelsComponent: RectLabelComponent + rectLabelsOffset: number + rectLabelsSkipLength: number + rectLabelsSkipPercentage: number + rectLabelsTextColor: InheritedColorConfig +} diff --git a/packages/rects/src/types.ts b/packages/rects/src/types.ts new file mode 100644 index 000000000..d6506d814 --- /dev/null +++ b/packages/rects/src/types.ts @@ -0,0 +1,23 @@ +export interface Rect { + height: number + /** size in percentage from the root node */ + percentage: number + transformX: number + transformY: number + width: number + x0: number + x1: number + y0: number + y1: number +} + +export interface DatumWithRect { + id: string | number + rect: Rect +} + +export interface DatumWithRectAndColor extends DatumWithRect { + color: string + /** When using patterns/gradients */ + fill?: string +} diff --git a/packages/rects/src/useRectsTransition.ts b/packages/rects/src/useRectsTransition.ts new file mode 100644 index 000000000..0c6f6401a --- /dev/null +++ b/packages/rects/src/useRectsTransition.ts @@ -0,0 +1,83 @@ +import { useMotionConfig } from '@nivo/core' +import { SpringValue, to, useTransition } from '@react-spring/web' +import { useMemo } from 'react' +import { DatumWithRect, DatumWithRectAndColor } from './types' + +export interface TransitionExtra { + enter: (datum: TDatum) => ExtraProps + leave: (datum: TDatum) => ExtraProps + update: (datum: TDatum) => ExtraProps +} + +// TODO: mode ? +export const useRectExtraTransition = ( + extraTransition?: TransitionExtra +) => + useMemo( + () => ({ + enter: (datum: TDatum) => ({ + progress: 0, + ...datum.rect, + ...(extraTransition?.enter(datum) ?? {}), + }), + update: (datum: TDatum) => ({ + progress: 1, + ...datum.rect, + ...(extraTransition?.update(datum) ?? {}), + }), + leave: (datum: TDatum) => ({ + progress: 0, + ...datum.rect, + ...(extraTransition?.leave(datum) ?? {}), + }), + }), + [extraTransition] + ) + +export const interpolateRect = ( + transformXValue: SpringValue, + transformYValue: SpringValue +) => to([transformXValue, transformYValue], (x, y) => `translate(${x}, ${y})`) + +/** + * This hook can be used to animate a group of rects. + * + * @todo `useAnimatedRect` for single rect animation + */ +export const useRectsTransition = ( + data: TDatum[], + extra?: TransitionExtra +) => { + const { animate, config: springConfig } = useMotionConfig() + + const phases = useRectExtraTransition(extra) + + const transition = useTransition< + TDatum, + { + height: number + progress: number + transformX: number + transformY: number + width: number + x0: number + x1: number + y0: number + y1: number + } & ExtraProps + >(data, { + keys: datum => datum.id, + initial: phases.update, + from: phases.enter, + enter: phases.update, + update: phases.update, + leave: phases.leave, + config: springConfig, + immediate: !animate, + }) + + return { + transition, + interpolate: interpolateRect, + } +} diff --git a/packages/rects/tsconfig.json b/packages/rects/tsconfig.json new file mode 100644 index 000000000..39c997d5e --- /dev/null +++ b/packages/rects/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.types.json", + "compilerOptions": { + "outDir": "./dist/types", + "rootDir": "./src" + }, + "include": ["src/**/*"] +} \ No newline at end of file diff --git a/packages/static/package.json b/packages/static/package.json index b5038779f..ae38f729f 100644 --- a/packages/static/package.json +++ b/packages/static/package.json @@ -34,6 +34,7 @@ "@nivo/radar": "0.79.1", "@nivo/sankey": "0.79.1", "@nivo/sunburst": "0.79.1", + "@nivo/icicles": "0.79.1", "@nivo/treemap": "0.79.1", "joi": "^17.5.0", "lodash": "^4.17.21" @@ -45,4 +46,4 @@ "publishConfig": { "access": "public" } -} +} \ No newline at end of file diff --git a/packages/static/src/mappings/icicles.ts b/packages/static/src/mappings/icicles.ts new file mode 100644 index 000000000..5a702b1a5 --- /dev/null +++ b/packages/static/src/mappings/icicles.ts @@ -0,0 +1,44 @@ +import { FunctionComponent } from 'react' +import Joi from 'joi' +import { Icicles, IciclesSvgProps } from '@nivo/icicles' +import { custom } from './common' +import { ordinalColors, inheritedColor } from './commons/colors' +import { dimensions } from './commons/dimensions' +import { OmitStrict } from '../types' + +export type IciclesApiProps = OmitStrict< + IciclesSvgProps, + 'isInteractive' | 'tooltip' | 'onClick' | 'animate' | 'motionConfig' | 'renderWrapper' +> + +export const iciclesMapping = { + component: Icicles as FunctionComponent, + schema: Joi.object().keys({ + data: custom.object().required(), + id: Joi.string(), + value: Joi.string(), + valueFormat: Joi.string(), + width: dimensions.width, + height: dimensions.height, + margin: dimensions.margin, + + colors: ordinalColors, + colorBy: Joi.any().valid('id', 'depth'), + direction: Joi.any().valid('top', 'right', 'bottom', 'left'), + inheritColorFromParent: Joi.boolean(), + childColor: inheritedColor, + borderWidth: Joi.number().min(0), + borderColor: inheritedColor, + + enableRectLabels: Joi.boolean(), + rectLabel: Joi.string(), + rectLabelsTextColor: inheritedColor, + rectLabelsOffset: Joi.number(), + rectLabelsSkipLength: Joi.number().min(0), + rectLabelsSkipPercentage: Joi.number().min(0).max(100), + }), + runtimeProps: ['width', 'height', 'colors'], + defaults: { + margin: { top: 0, right: 0, bottom: 0, left: 0 }, + }, +} diff --git a/packages/static/src/mappings/index.ts b/packages/static/src/mappings/index.ts index 889489288..2471f87c1 100644 --- a/packages/static/src/mappings/index.ts +++ b/packages/static/src/mappings/index.ts @@ -9,6 +9,7 @@ import { pieMapping } from './pie' import { radarMapping } from './radar' import { sankeyMapping } from './sankey' import { sunburstMapping } from './sunburst' +import { iciclesMapping } from './icicles' import { treemapMapping } from './treemap' export const chartsMapping = { @@ -17,6 +18,7 @@ export const chartsMapping = { calendar: calendarMapping, chord: chordMapping, heatmap: heatmapMapping, + icicles: iciclesMapping, line: lineMapping, pie: pieMapping, radar: radarMapping, diff --git a/packages/static/src/samples/index.ts b/packages/static/src/samples/index.ts index 8632b565a..2b6d052e5 100644 --- a/packages/static/src/samples/index.ts +++ b/packages/static/src/samples/index.ts @@ -11,13 +11,14 @@ import { ChartProps, ChartType, LineApiProps } from '../mappings' const keys = ['hot dogs', 'burgers', 'sandwich', 'kebab', 'fries', 'donut'] const moreKeys = [...keys, 'junk', 'sushi', 'ramen', 'curry', 'udon', 'bagel'] -export const samples: Record< - string, - { - type: ChartType - props: ChartProps +type Samples = { + [P in ChartType]?: { + type: P + props: ChartProps

} -> = { +} + +export const samples: Samples = { bar: { type: 'bar', props: { @@ -73,7 +74,7 @@ export const samples: Record< borderWidth: 2, borderColor: 'inherit:darker(0.4)', labelTextColor: 'inherit:darker(2.4)', - }, + } as ChartProps<'heatmap'>, }, line: { type: 'line', @@ -119,7 +120,7 @@ export const samples: Record< enableMarkersLabel: true, }, generateWinesTastes() - ), + ) as ChartProps<'radar'>, }, sankey: { type: 'sankey', @@ -148,7 +149,20 @@ export const samples: Record< enableArcLabels: true, arcLabelsSkipAngle: 10, arcLabelsTextColor: { from: 'color', modifiers: [['darker', 1.4]] }, - }, + } as ChartProps<'sunburst'>, + }, + icicles: { + type: 'icicles', + props: { + width: 800, + height: 800, + data: generateLibTree(), + id: 'name', + value: 'loc', + childColor: { from: 'color', modifiers: [['brighter', 0.1]] }, + enableRectLabels: true, + rectLabelsTextColor: { from: 'color', modifiers: [['darker', 1.4]] }, + } as ChartProps<'icicles'>, }, treemap: { type: 'treemap', diff --git a/tsconfig.monorepo.json b/tsconfig.monorepo.json index 99343d876..971365ed7 100644 --- a/tsconfig.monorepo.json +++ b/tsconfig.monorepo.json @@ -3,45 +3,111 @@ "references": [ // Core first because everything needs it // { "path": "./packages/core" }, - // Shared next because charts need them - { "path": "./packages/annotations" }, - { "path": "./packages/scales" }, - { "path": "./packages/axes" }, - { "path": "./packages/colors" }, - { "path": "./packages/legends" }, - { "path": "./packages/tooltip" }, - { "path": "./packages/arcs" }, - { "path": "./packages/polar-axes" }, - { "path": "./packages/voronoi" }, - + { + "path": "./packages/annotations" + }, + { + "path": "./packages/scales" + }, + { + "path": "./packages/axes" + }, + { + "path": "./packages/colors" + }, + { + "path": "./packages/legends" + }, + { + "path": "./packages/tooltip" + }, + { + "path": "./packages/arcs" + }, + { + "path": "./packages/rects" + }, + { + "path": "./packages/polar-axes" + }, + { + "path": "./packages/voronoi" + }, // Utility packages - { "path": "./packages/generators" }, - { "path": "./packages/recompose" }, - + { + "path": "./packages/generators" + }, + { + "path": "./packages/recompose" + }, // Charts now - { "path": "./packages/bar" }, - { "path": "./packages/bullet" }, - { "path": "./packages/bump" }, - { "path": "./packages/calendar" }, - { "path": "./packages/chord" }, - { "path": "./packages/circle-packing" }, - { "path": "./packages/funnel" }, - { "path": "./packages/heatmap" }, - { "path": "./packages/marimekko" }, - { "path": "./packages/network" }, - { "path": "./packages/pie" }, - { "path": "./packages/radar" }, - { "path": "./packages/radial-bar" }, - { "path": "./packages/sankey" }, - { "path": "./packages/scatterplot" }, - { "path": "./packages/sunburst" }, - { "path": "./packages/stream" }, - { "path": "./packages/swarmplot" }, - { "path": "./packages/treemap" }, - + { + "path": "./packages/bar" + }, + { + "path": "./packages/bullet" + }, + { + "path": "./packages/bump" + }, + { + "path": "./packages/calendar" + }, + { + "path": "./packages/chord" + }, + { + "path": "./packages/circle-packing" + }, + { + "path": "./packages/funnel" + }, + { + "path": "./packages/heatmap" + }, + { + "path": "./packages/icicles" + }, + { + "path": "./packages/marimekko" + }, + { + "path": "./packages/network" + }, + { + "path": "./packages/pie" + }, + { + "path": "./packages/radar" + }, + { + "path": "./packages/radial-bar" + }, + { + "path": "./packages/sankey" + }, + { + "path": "./packages/scatterplot" + }, + { + "path": "./packages/sunburst" + }, + { + "path": "./packages/stream" + }, + { + "path": "./packages/swarmplot" + }, + { + "path": "./packages/treemap" + }, // Static rendering and express middleware - { "path": "./packages/static" }, - { "path": "./packages/express" } + { + "path": "./packages/static" + }, + { + "path": "./packages/express" + } ] -} +} \ No newline at end of file diff --git a/website/package.json b/website/package.json index 88e136c84..9774fd4a4 100644 --- a/website/package.json +++ b/website/package.json @@ -17,6 +17,7 @@ "@nivo/generators": "0.79.0", "@nivo/geo": "0.79.1", "@nivo/heatmap": "0.79.1", + "@nivo/icicles": "0.79.1", "@nivo/legends": "0.79.1", "@nivo/line": "0.79.1", "@nivo/marimekko": "0.79.1", @@ -25,6 +26,7 @@ "@nivo/pie": "0.79.1", "@nivo/radar": "0.79.1", "@nivo/radial-bar": "0.79.1", + "@nivo/rects": "0.79.1", "@nivo/sankey": "0.79.1", "@nivo/scatterplot": "0.79.1", "@nivo/stream": "0.79.1", @@ -64,4 +66,4 @@ "engines": { "node": "16.x" } -} +} \ No newline at end of file diff --git a/website/src/@types/file_types.d.ts b/website/src/@types/file_types.d.ts index 96576d8f1..8f7de105c 100644 --- a/website/src/@types/file_types.d.ts +++ b/website/src/@types/file_types.d.ts @@ -5,7 +5,7 @@ type ChartMetaFlavors = { path: string }[] -declare module '*area-bump/meta.yml' { +declare module '*/area-bump/meta.yml' { const meta: { flavors: ChartMetaFlavors AreaBump: ChartMeta @@ -14,7 +14,7 @@ declare module '*area-bump/meta.yml' { export default meta } -declare module '*bar/meta.yml' { +declare module '*/bar/meta.yml' { const meta: { flavors: ChartMetaFlavors Bar: ChartMeta @@ -24,7 +24,7 @@ declare module '*bar/meta.yml' { export default meta } -declare module '*bullet/meta.yml' { +declare module '*/bullet/meta.yml' { const meta: { flavors: ChartMetaFlavors Bullet: ChartMeta @@ -33,7 +33,7 @@ declare module '*bullet/meta.yml' { export default meta } -declare module '*bump/meta.yml' { +declare module '*/bump/meta.yml' { const meta: { flavors: ChartMetaFlavors Bump: ChartMeta @@ -42,7 +42,7 @@ declare module '*bump/meta.yml' { export default meta } -declare module '*calendar/meta.yml' { +declare module '*/calendar/meta.yml' { const meta: { flavors: ChartMetaFlavors Calendar: ChartMeta @@ -52,7 +52,7 @@ declare module '*calendar/meta.yml' { export default meta } -declare module '*calendar/meta.yml' { +declare module '*/calendar/meta.yml' { const meta: { flavors: ChartMetaFlavors Calendar: ChartMeta @@ -62,7 +62,7 @@ declare module '*calendar/meta.yml' { export default meta } -declare module '*chord/meta.yml' { +declare module '*/chord/meta.yml' { const meta: { flavors: ChartMetaFlavors Chord: ChartMeta @@ -72,7 +72,7 @@ declare module '*chord/meta.yml' { export default meta } -declare module '*choropleth/meta.yml' { +declare module '*/choropleth/meta.yml' { const meta: { flavors: ChartMetaFlavors Choropleth: ChartMeta @@ -82,7 +82,7 @@ declare module '*choropleth/meta.yml' { export default meta } -declare module '*circle-packing/meta.yml' { +declare module '*/circle-packing/meta.yml' { const meta: { flavors: ChartMetaFlavors CirclePacking: ChartMeta @@ -93,7 +93,7 @@ declare module '*circle-packing/meta.yml' { export default meta } -declare module '*funnel/meta.yml' { +declare module '*/funnel/meta.yml' { const meta: { flavors: ChartMetaFlavors Funnel: ChartMeta @@ -102,7 +102,16 @@ declare module '*funnel/meta.yml' { export default meta } -declare module '*heatmap/meta.yml' { +declare module '*/geomap/meta.yml' { + const meta: { + flavors: ChartMetaFlavors + GeoMap: ChartMeta + } + + export default meta +} + +declare module '*/heatmap/meta.yml' { const meta: { flavors: ChartMetaFlavors HeatMap: ChartMeta @@ -112,7 +121,16 @@ declare module '*heatmap/meta.yml' { export default meta } -declare module '*line/meta.yml' { +declare module '*/icicles/meta.yml' { + const meta: { + flavors: ChartMetaFlavors + Icicles: ChartMeta + } + + export default meta +} + +declare module '*/line/meta.yml' { const meta: { flavors: ChartMetaFlavors Line: ChartMeta @@ -122,7 +140,7 @@ declare module '*line/meta.yml' { export default meta } -declare module '*marimekko/meta.yml' { +declare module '*/marimekko/meta.yml' { const meta: { flavors: ChartMetaFlavors Marimekko: ChartMeta @@ -131,7 +149,7 @@ declare module '*marimekko/meta.yml' { export default meta } -declare module '*network/meta.yml' { +declare module '*/network/meta.yml' { const meta: { flavors: ChartMetaFlavors Network: ChartMeta @@ -141,7 +159,7 @@ declare module '*network/meta.yml' { export default meta } -declare module '*parallel-coordinates/meta.yml' { +declare module '*/parallel-coordinates/meta.yml' { const meta: { flavors: ChartMetaFlavors ParallelCoordinates: ChartMeta @@ -151,7 +169,7 @@ declare module '*parallel-coordinates/meta.yml' { export default meta } -declare module '*pie/meta.yml' { +declare module '*/pie/meta.yml' { const meta: { flavors: ChartMetaFlavors Pie: ChartMeta @@ -161,7 +179,7 @@ declare module '*pie/meta.yml' { export default meta } -declare module '*radar/meta.yml' { +declare module '*/radar/meta.yml' { const meta: { flavors: ChartMetaFlavors Radar: ChartMeta @@ -170,7 +188,7 @@ declare module '*radar/meta.yml' { export default meta } -declare module '*radial-bar/meta.yml' { +declare module '*/radial-bar/meta.yml' { const meta: { flavors: ChartMetaFlavors RadialBar: ChartMeta @@ -179,7 +197,7 @@ declare module '*radial-bar/meta.yml' { export default meta } -declare module '*sankey/meta.yml' { +declare module '*/sankey/meta.yml' { const meta: { flavors: ChartMetaFlavors Sankey: ChartMeta @@ -188,7 +206,7 @@ declare module '*sankey/meta.yml' { export default meta } -declare module '*scatterplot/meta.yml' { +declare module '*/scatterplot/meta.yml' { const meta: { flavors: ChartMetaFlavors ScatterPlot: ChartMeta @@ -198,7 +216,7 @@ declare module '*scatterplot/meta.yml' { export default meta } -declare module '*stream/meta.yml' { +declare module '*/stream/meta.yml' { const meta: { flavors: ChartMetaFlavors Stream: ChartMeta @@ -207,7 +225,7 @@ declare module '*stream/meta.yml' { export default meta } -declare module '*sunburst/meta.yml' { +declare module '*/sunburst/meta.yml' { const meta: { flavors: ChartMetaFlavors Sunburst: ChartMeta @@ -216,7 +234,7 @@ declare module '*sunburst/meta.yml' { export default meta } -declare module '*swarmplot/meta.yml' { +declare module '*/swarmplot/meta.yml' { const meta: { flavors: ChartMetaFlavors SwarmPlot: ChartMeta @@ -226,7 +244,7 @@ declare module '*swarmplot/meta.yml' { export default meta } -declare module '*time-range/meta.yml' { +declare module '*/time-range/meta.yml' { const meta: { flavors: ChartMetaFlavors TimeRange: ChartMeta @@ -235,7 +253,7 @@ declare module '*time-range/meta.yml' { export default meta } -declare module '*treemap/meta.yml' { +declare module '*/treemap/meta.yml' { const meta: { flavors: ChartMetaFlavors TreeMap: ChartMeta @@ -246,7 +264,7 @@ declare module '*treemap/meta.yml' { export default meta } -declare module '*voronoi/meta.yml' { +declare module '*/voronoi/meta.yml' { const meta: { flavors: ChartMetaFlavors Voronoi: ChartMeta @@ -255,7 +273,7 @@ declare module '*voronoi/meta.yml' { export default meta } -declare module '*waffle/meta.yml' { +declare module '*/waffle/meta.yml' { const meta: { flavors: ChartMetaFlavors Waffle: ChartMeta diff --git a/website/src/components/home/Home.tsx b/website/src/components/home/Home.tsx index ed0ca8979..3c31c2372 100644 --- a/website/src/components/home/Home.tsx +++ b/website/src/components/home/Home.tsx @@ -19,6 +19,7 @@ import radialBar from '../../assets/captures/home/radial-bar.png' import voronoi from '../../assets/captures/home/voronoi.png' import treemap from '../../assets/captures/home/treemap.png' import sunburst from '../../assets/captures/home/sunburst.png' +import icicles from '../../assets/captures/home/sunburst.png' import sankey from '../../assets/captures/home/sankey.png' import swarmplot from '../../assets/captures/home/swarmplot.png' import marimekko from '../../assets/captures/home/marimekko.png' @@ -55,7 +56,7 @@ const Home = () => { - + ) diff --git a/website/src/components/home/HomeIciclesDemo.tsx b/website/src/components/home/HomeIciclesDemo.tsx new file mode 100644 index 000000000..e35301d82 --- /dev/null +++ b/website/src/components/home/HomeIciclesDemo.tsx @@ -0,0 +1,28 @@ +import React, { useMemo } from 'react' +import { generateLibTree } from '@nivo/generators' +import { Icicles } from '@nivo/icicles' +import { useHomeTheme } from './theme' +import { dimensions } from './dimensions' + +export const HomeIciclesDemo = () => { + const { colors, nivoTheme } = useHomeTheme() + const data = useMemo(() => generateLibTree(), []) + + return ( +

+ +
+ ) +} diff --git a/website/src/components/home/index.ts b/website/src/components/home/index.ts index 13dafeff0..d2420182a 100644 --- a/website/src/components/home/index.ts +++ b/website/src/components/home/index.ts @@ -12,6 +12,7 @@ export * from './HomeRadialBarDemo' export * from './HomeSankeyDemo' export * from './HomeStreamDemo' export * from './HomeSunburstDemo' +export * from './HomeIciclesDemo' export * from './HomeSwarmPlotDemo' export * from './HomeTreeMapDemo' export * from './HomeVoronoiDemo' diff --git a/website/src/components/icons/IciclesIcon.tsx b/website/src/components/icons/IciclesIcon.tsx new file mode 100644 index 000000000..40a24aa4d --- /dev/null +++ b/website/src/components/icons/IciclesIcon.tsx @@ -0,0 +1,132 @@ +import React from 'react' +import { arc as Arc } from 'd3-shape' +import { degreesToRadians } from '@nivo/core' +import sunburstLightNeutralImg from '../../assets/icons/sunburst-grey.png' +import sunburstLightColoredImg from '../../assets/icons/sunburst-red.png' +import sunburstDarkNeutralImg from '../../assets/icons/sunburst-dark-neutral.png' +import sunburstDarkColoredImg from '../../assets/icons/sunburst-dark-colored.png' +import { ICON_SIZE, Icon, colors, IconImg } from './styled' +import { IconType } from './types' + +const arc = Arc().padAngle(degreesToRadians(2)) + +const IciclesIconItem = ({ type }: { type: IconType }) => ( + + + + + + + + + + + + + + + +) + +export const IciclesIcon = () => ( + <> + + + + + + + + + +) diff --git a/website/src/components/icons/Icons.tsx b/website/src/components/icons/Icons.tsx index 936a3559c..9d986c5d6 100644 --- a/website/src/components/icons/Icons.tsx +++ b/website/src/components/icons/Icons.tsx @@ -24,6 +24,7 @@ import { SankeyIcon } from './SankeyIcon' import { ScatterPlotIcon } from './ScatterPlotIcon' import { StreamIcon } from './StreamIcon' import { SunburstIcon } from './SunburstIcon' +import { IciclesIcon } from './IciclesIcon' import { SwarmPlotIcon } from './SwarmPlotIcon' import { TimeRangeIcon } from './TimeRangeIcon' import { TreeMapIcon } from './TreeMapIcon' @@ -87,6 +88,7 @@ export const Icons = () => ( + diff --git a/website/src/data/components/icicles/mapper.tsx b/website/src/data/components/icicles/mapper.tsx new file mode 100644 index 000000000..32807b6b2 --- /dev/null +++ b/website/src/data/components/icicles/mapper.tsx @@ -0,0 +1,72 @@ +import React from 'react' +import styled from 'styled-components' +import { patternLinesDef } from '@nivo/core' +import { mapFormat, settingsMapper } from '../../../lib/settings' + +const TooltipWrapper = styled.div` + display: grid; + background: #fff; + grid-template-columns: 1fr 1fr; + grid-column-gap: 12px; + font-size: 12px; + border-radius: 2px; + box-shadow: 1px 1px 0 rgba(0, 0, 0, 0.15); +` +const TooltipKey = styled.span` + font-weight: 600; +` +const TooltipValue = styled.span`` + +const CustomTooltip = node => { + return ( + + id + {node.id} + value + {node.value} + percentage + {Math.round(node.percentage * 100) / 100}% + color + {node.color} + + ) +} + +export default settingsMapper( + { + valueFormat: mapFormat, + rectLabel: value => { + if (value === `d => \`\${d.id} (\${d.value})\``) return d => `${d.id} (${d.value})` + return value + }, + tooltip: (value, values) => { + if (!values['custom tooltip example']) return undefined + + return CustomTooltip + }, + defs: (value, values) => { + if (!values['showcase pattern usage']) return + + return [ + patternLinesDef('lines', { + background: 'rgba(0, 0, 0, 0)', + color: 'inherit', + rotation: -45, + lineWidth: 4, + spacing: 8, + }), + ] + }, + fill: (value, values) => { + if (!values['showcase pattern usage']) return + + return [ + { match: { id: 'set' }, id: 'lines' }, + { match: { id: 'misc' }, id: 'lines' }, + ] + }, + }, + { + exclude: ['custom tooltip example', 'showcase pattern usage'], + } +) diff --git a/website/src/data/components/icicles/meta.yml b/website/src/data/components/icicles/meta.yml new file mode 100644 index 000000000..32866f6b4 --- /dev/null +++ b/website/src/data/components/icicles/meta.yml @@ -0,0 +1,35 @@ +flavors: + - flavor: svg + path: /icicles/ + - flavor: api + path: /icicles/api/ + +Icicles: + package: "@nivo/icicles" + tags: + - hierarchy + - flat + stories: + - label: with child color modifier + link: icicles--with-child-color-modifier + - label: with child color independent of parent + link: icicles--with-child-colors-independent-of-parent + - label: with custom colors + link: icicles--with-custom-colors + - label: with formatted tooltip value + link: icicles--with-formatted-tooltip-value + - label: with custom tooltip + link: icicles--custom-tooltip + - label: with enter and leave actions + link: icicles--enter-leave-check-actions + - label: with wheel and contextmenu actions + link: icicles--wheel-contextmenu-check-actions + - label: with patterns and gradients + link: icicles--patterns-gradients + - label: drill down to children + link: icicles--children-drill-down + - label: with different direction + link: icicles--change-direction + description: | + The responsive alternative of this component is + `ResponsiveIcicles`. diff --git a/website/src/data/components/icicles/props.ts b/website/src/data/components/icicles/props.ts new file mode 100644 index 000000000..1c6bded68 --- /dev/null +++ b/website/src/data/components/icicles/props.ts @@ -0,0 +1,443 @@ +import { defaultProps, IciclesDirection } from '@nivo/icicles' +import { + groupProperties, + defsProperties, + motionProperties, + themeProperty, +} from '../../../lib/componentProperties' +import { chartDimensions, ordinalColors, isInteractive } from '../../../lib/chart-properties' +import { ChartProperty, Flavor } from '../../../types' + +const allFlavors: Flavor[] = ['svg', 'api'] + +const directions: IciclesDirection[] = ['top', 'right', 'bottom', 'left']; + +const props: ChartProperty[] = [ + { + key: 'data', + group: 'Base', + flavors: allFlavors, + help: 'Chart data, which should be immutable.', + description: ` + Chart data, which must conform to this structure + if using the default \`id\` and \`value\` accessors: + + \`\`\` + { + // must be unique for the whole dataset + id: string | number + value: number + children: { + id: string | number + value: number + children: ... + }[] + } + \`\`\` + + If using a different data structure, you must make sure + to adjust both \`id\` and \`value\`. Meaning you can provide + a completely different data structure as long as \`id\` and \`value\` + return the appropriate values. + + Immutability of the data is important as re-computations + depends on it. + `, + type: 'object', + required: true, + }, + { + key: 'id', + group: 'Base', + flavors: allFlavors, + help: 'Id accessor.', + description: ` + define id accessor, if string given, + will use \`node[value]\`, + if function given, it will be invoked + for each node and will receive the node as + first argument, it must return the node + id (string | number). + `, + type: 'string | Function', + required: false, + defaultValue: defaultProps.id, + }, + { + key: 'value', + group: 'Base', + flavors: allFlavors, + help: 'Value accessor', + description: ` + define value accessor, if string given, + will use \`node[value]\`, + if function given, it will be invoked + for each node and will receive the node as + first argument, it must return the node + value (number). + `, + type: 'string | Function', + required: false, + defaultValue: defaultProps.value, + }, + { + key: 'direction', + group: 'Base', + flavors: allFlavors, + help: 'Optional chart direction.', + description: ` + Change the reading direction of the chart. + `, + required: false, + type: directions.map(d => `'${d}'`).join(' | '), + control: { + type: 'radio', + choices: directions.map(d => ({ + label: d, + value: d + })) + }, + }, + { + key: 'valueFormat', + group: 'Base', + flavors: allFlavors, + help: 'Optional formatter for values.', + description: ` + The formatted value can then be used for labels & tooltips. + + Under the hood, nivo uses [d3-format](https://github.com/d3/d3-format), + please have a look at it for available formats, you can also pass a function + which will receive the raw value and should return the formatted one. + `, + required: false, + type: 'string | (value: number) => string | number', + control: { type: 'valueFormat' }, + }, + ...chartDimensions(allFlavors), + themeProperty(['svg', 'api']), + ordinalColors({ + flavors: allFlavors, + defaultValue: defaultProps.colors, + }), + { + key: 'colorBy', + help: `Define the property to use to assign a color to rects.`, + flavors: allFlavors, + description: ` + When using \`id\`, each node will get a new color, + and when using \`depth\` the nodes' color will depend on their depth. + `, + type: `'id' | 'depth'`, + required: false, + defaultValue: defaultProps.colorBy, + group: 'Style', + control: { + type: 'radio', + choices: [ + { label: 'id', value: 'id' }, + { label: 'depth', value: 'depth' }, + ], + }, + }, + { + key: 'inheritColorFromParent', + help: 'Inherit color from parent node starting from 2nd level.', + flavors: allFlavors, + type: 'boolean', + required: false, + defaultValue: defaultProps.inheritColorFromParent, + control: { type: 'switch' }, + group: 'Style', + }, + { + key: 'childColor', + help: 'Defines how to compute child nodes color.', + flavors: allFlavors, + type: 'string | object | Function', + required: false, + defaultValue: defaultProps.childColor, + control: { type: 'inheritedColor' }, + group: 'Style', + }, + { + key: 'borderWidth', + help: 'Node border width.', + flavors: allFlavors, + type: 'number', + required: false, + defaultValue: defaultProps.borderWidth, + control: { type: 'lineWidth' }, + group: 'Style', + }, + { + key: 'borderColor', + help: 'Defines how to compute rects border color.', + flavors: allFlavors, + type: 'string | object | Function', + required: false, + defaultValue: defaultProps.borderColor, + control: { type: 'inheritedColor' }, + group: 'Style', + }, + ...defsProperties('Style', ['svg', 'api']), + { + key: 'showcase pattern usage', + flavors: ['svg'], + help: 'Patterns.', + description: ` + You can use \`defs\` and \`fill\` properties + to use patterns, see + [dedicated guide](self:/guides/patterns) + for further information. + `, + required: false, + type: 'boolean', + control: { type: 'switch' }, + group: 'Style', + }, + { + key: 'enableRectLabels', + help: 'Enable/disable rect labels.', + flavors: allFlavors, + type: 'boolean', + required: false, + defaultValue: defaultProps.enableRectLabels, + control: { type: 'switch' }, + group: 'Rect labels', + }, + { + key: 'rectLabel', + help: 'Defines how to get label text, can be a string (used to access current node data property) or a function which will receive the actual node data.', + flavors: allFlavors, + type: 'string | Function', + required: false, + defaultValue: defaultProps.rectLabel, + group: 'Rect labels', + control: { + type: 'choices', + choices: ['id', 'value', 'formattedValue', `d => \`\${d.id} (\${d.value})\``].map( + choice => ({ + label: choice, + value: choice, + }) + ), + }, + }, + { + key: 'rectLabelsOffset', + help: ` + Define the ratio offset when centering a label. + The offset affects the vertical postion. + `, + flavors: allFlavors, + type: 'number', + required: false, + defaultValue: defaultProps.rectLabelsOffset, + group: 'Rect labels', + control: { + type: 'range', + min: 0.5, + max: 2, + step: 0.05, + }, + }, + { + key: 'rectLabelsSkipLength', + help: ` + Skip label if corresponding rect's length is lower than provided value. + "Length" is determined by width when direction is top or bottom, + and by height when direction is left or right. + `, + flavors: allFlavors, + type: 'number', + required: false, + defaultValue: defaultProps.rectLabelsSkipLength, + group: 'Rect labels', + control: { + type: 'range', + unit: 'px', + min: 0, + max: 900, + step: 1, + }, + }, + { + key: 'rectLabelsSkipPercentage', + help: ` + Skip label if corresponding rect's relative size is lower than provided value. + The size is relative to the root node considered as 100%. + This value is a percentage. + `, + flavors: allFlavors, + type: 'number', + required: false, + defaultValue: defaultProps.rectLabelsSkipPercentage, + group: 'Rect labels', + control: { + type: 'range', + min: 0, + max: 100, + step: 1, + }, + }, + { + key: 'rectLabelsTextColor', + help: 'Defines how to compute rect label text color.', + flavors: allFlavors, + type: 'string | object | Function', + required: false, + defaultValue: defaultProps.rectLabelsTextColor, + control: { type: 'inheritedColor' }, + group: 'Rect labels', + }, + { + key: 'layers', + group: 'Customization', + help: 'Defines the order of layers and add custom layers.', + flavors: ['svg'], + description: ` + You can also use this to insert extra layers + to the chart, the extra layer must be a function. + + The layer component which will receive the chart's + context & computed data and must return a valid SVG element + for the \`Icicles\` component. + + The context passed to layers has the following structure: + + \`\`\` + { + nodes: ComputedDatum[] + baseOffsetLeft: number + baseOffsetTop: number + } + \`\`\` + `, + required: false, + type: 'Array', + defaultValue: defaultProps.layers, + }, + isInteractive({ + flavors: ['svg'], + defaultValue: defaultProps.isInteractive, + }), + ...motionProperties(['svg'], defaultProps, 'react-spring'), + { + key: 'tooltip', + flavors: ['svg'], + group: 'Interactivity', + type: 'Function', + required: false, + help: 'Tooltip custom component', + description: ` + A function allowing complete tooltip customisation, + it must return a valid HTML element and will receive + the following data: + \`\`\` + { + id: string | number, + value: number, + depth: number, + color: string, + name: string + loc: number + percentage: number + // the parent datum + ancestor: object + } + \`\`\` + You can also customize the style of the tooltip + using the \`theme.tooltip\` object. + `, + }, + { + key: 'custom tooltip example', + flavors: ['svg'], + group: 'Interactivity', + required: false, + help: 'Showcase custom tooltip component.', + type: 'boolean', + control: { type: 'switch' }, + }, + { + key: 'onClick', + flavors: ['svg'], + group: 'Interactivity', + type: 'Function', + required: false, + help: 'onClick handler', + description: ` + onClick handler, will receive node data as first argument + & event as second one. The node data has the following shape: + + \`\`\` + { + id: string | number, + value: number, + depth: number, + color: string, + name: string + loc: number + percentage: number + // the parent datum + ancestor: object + } + \`\`\` + `, + }, + { + key: 'onWheel', + flavors: ['svg'], + group: 'Interactivity', + type: 'Function', + required: false, + help: 'onWheel handler', + description: ` + onWheel handler, will receive node data as first argument + & event as second one. The node data has the following shape: + + \`\`\` + { + id: string | number, + value: number, + depth: number, + color: string, + name: string + loc: number + percentage: number + // the parent datum + ancestor: object + } + \`\`\` + `, + }, + { + key: 'onContextMenu', + flavors: ['svg'], + group: 'Interactivity', + type: 'Function', + required: false, + help: 'onContextMenu handler', + description: ` + onContextMenu handler, will receive node data as first argument + & event as second one. The node data has the following shape: + + \`\`\` + { + id: string | number, + value: number, + depth: number, + color: string, + name: string + loc: number + percentage: number + // the parent datum + ancestor: object + } + \`\`\` + `, + }, +] + +export const groups = groupProperties(props) diff --git a/website/src/data/nav.ts b/website/src/data/nav.ts index c15bdea4e..75f17266c 100644 --- a/website/src/data/nav.ts +++ b/website/src/data/nav.ts @@ -10,6 +10,7 @@ import funnel from './components/funnel/meta.yml' import geomap from './components/geomap/meta.yml' import heatmap from './components/heatmap/meta.yml' import line from './components/line/meta.yml' +import icicles from './components/icicles/meta.yml' import marimekko from './components/marimekko/meta.yml' import network from './components/network/meta.yml' import parallelCoordinates from './components/parallel-coordinates/meta.yml' @@ -129,6 +130,15 @@ export const components: ChartNavData[] = [ api: true, }, }, + { + name: 'Icicles', + id: 'icicles', + tags: icicles.Icicles.tags, + flavors: { + svg: true, + api: true, + }, + }, { name: 'Line', id: 'line', diff --git a/website/src/pages/icicles/api.tsx b/website/src/pages/icicles/api.tsx new file mode 100644 index 000000000..6b917b2de --- /dev/null +++ b/website/src/pages/icicles/api.tsx @@ -0,0 +1,81 @@ +import React from 'react' +import { generateLibTree } from '@nivo/generators' +import { Seo } from '../../components/Seo' +import { ApiClient } from '../../components/components/api-client/ApiClient' +import { groups } from '../../data/components/icicles/props' +import mapper from '../../data/components/icicles/mapper' +import meta from '../../data/components/icicles/meta.yml' +import { graphql, useStaticQuery } from 'gatsby' + +const data = generateLibTree() + +const IciclesApi = () => { + const { + image: { + childImageSharp: { gatsbyImageData: image }, + }, + // TODO: change with icicles capture + } = useStaticQuery(graphql` + query { + image: file(absolutePath: { glob: "**/src/assets/captures/sunburst.png" }) { + childImageSharp { + gatsbyImageData(layout: FIXED, width: 700, quality: 100) + } + } + } + `) + + return ( + <> + + + + ) +} + +export default IciclesApi diff --git a/website/src/pages/icicles/index.js b/website/src/pages/icicles/index.js new file mode 100644 index 000000000..d15f91f56 --- /dev/null +++ b/website/src/pages/icicles/index.js @@ -0,0 +1,113 @@ +import React from 'react' +import { defaultProps, ResponsiveIcicles } from '@nivo/icicles' +import { generateLibTree } from '@nivo/generators' +import { omit } from 'lodash' +import { ComponentTemplate } from '../../components/components/ComponentTemplate' +import meta from '../../data/components/icicles/meta.yml' +import mapper from '../../data/components/icicles/mapper' +import { groups } from '../../data/components/icicles/props' +import { graphql, useStaticQuery } from 'gatsby' + +const Tooltip = () => { + /* return custom tooltip */ +} + +const generateData = () => generateLibTree() + +const initialProperties = { + margin: { + top: 10, + right: 10, + bottom: 10, + left: 10, + }, + id: 'name', + value: 'loc', + valueFormat: { format: '', enabled: false }, + borderWidth: 1, + borderColor: { theme: 'background' }, + colors: { scheme: 'nivo' }, + colorBy: 'id', + inheritColorFromParent: true, + childColor: { + from: 'color', + modifiers: [['brighter', 0.1]], + }, + enableRectLabels: true, + rectLabel: 'formattedValue', + rectLabelsTextColor: { + from: 'color', + modifiers: [['darker', 1.4]], + }, + animate: defaultProps.animate, + motionConfig: defaultProps.motionConfig, + defs: [], + fill: [], + isInteractive: true, + 'custom tooltip example': false, + tooltip: null, + 'showcase pattern usage': false, + direction: 'bottom', + rectLabelsSkipLength: defaultProps.rectLabelsSkipLength, + rectLabelsSkipPercentage: defaultProps.rectLabelsSkipPercentage, + rectLabelsOffset: defaultProps.rectLabelsOffset, +} + +const Icicles = () => { + const { + image: { + childImageSharp: { gatsbyImageData: image }, + }, + // TODO: change with icicles capture + } = useStaticQuery(graphql` + query { + image: file(absolutePath: { glob: "**/src/assets/captures/sunburst.png" }) { + childImageSharp { + gatsbyImageData(layout: FIXED, width: 700, quality: 100) + } + } + } + `) + + return ( + + {(properties, data, theme, logAction) => { + return ( + + logAction({ + type: 'click', + label: `[icicles] ${node.id} - ${node.value}: ${ + Math.round(node.percentage * 100) / 100 + }%`, + color: node.color, + // prevent cyclic dependency + data: { + ...omit(node, ['parent']), + parent: omit(node.parent, ['data', 'parent', 'children']), + }, + }) + } + /> + ) + }} + + ) +} + +export default Icicles diff --git a/website/src/pages/internal/home-demos.tsx b/website/src/pages/internal/home-demos.tsx index 966f266f2..860e6b655 100644 --- a/website/src/pages/internal/home-demos.tsx +++ b/website/src/pages/internal/home-demos.tsx @@ -15,7 +15,7 @@ import { HomeRadialBarDemo, HomeSankeyDemo, HomeStreamDemo, - HomeSunburstDemo, + HomeIciclesDemo, HomeSwarmPlotDemo, HomeTreeMapDemo, HomeVoronoiDemo, @@ -38,7 +38,7 @@ const HomeDemosPage = () => ( - +