diff --git a/.eslintrc.yml b/.eslintrc.yml index 8a02b8545..147d799db 100644 --- a/.eslintrc.yml +++ b/.eslintrc.yml @@ -17,3 +17,7 @@ globals: extends: - eslint:recommended - plugin:react/recommended + +settings: + react: + version: detect \ No newline at end of file diff --git a/.storybook/config.js b/.storybook/config.js index 497a0b3ac..94c7ce45a 100644 --- a/.storybook/config.js +++ b/.storybook/config.js @@ -63,6 +63,8 @@ function loadStories() { require('../packages/sunburst/stories/sunburst.stories') require('../packages/treemap/stories/treemap.stories') require('../packages/treemap/stories/treemapHtml.stories') + require('../packages/swarmplot/stories/SwarmPlot.stories') + // require('../packages/swarmplot/stories/SwarmPlotCanvas.stories') // require('../packages/voronoi/stories/voronoi.stories') require('../packages/waffle/stories/waffle.stories') require('../packages/waffle/stories/waffle-html.stories') diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 95aea9e25..24c3944dc 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -22,12 +22,15 @@ In order to install all the required dependencies and to establish links between the various packages, please execute the following: ``` -# please note that it can take a while as the repository contains a lot of packages make init ``` +> please note that it will take a while as this project uses a lot of dependencies… + ## Development +### Storybook + The easiest way to work on Nivo, is to use our [storybook](https://storybook.js.org/). The storybook development mode can be started via: @@ -35,11 +38,29 @@ The storybook development mode can be started via: make storybook ``` -Once you have made changes to the packages, you will need to rebuild the respective package. -In this case, you have two options: +The storybook uses the src code of each package, thus you don't have to build +to see your changes. + +### Demo/Doc website + +You can also use the demo website while working on components, it might be +useful to see how it behaves with various controls, and might be required to +update the documentation if you change the public API of components. + +The packages are automatically linked to the website, so it doesn't use +the version from npm, however if you make some change to the source +of a package, you'll have to rebuild it to see the changes. + +To automate this process, you can start a watcher on the package you're working +on, for example if you want to make some change on the `@nivo/bar` package, +you should run `make package-dev-bar` and then start the website `make website`, +this way each change you make will trigger a build and will be (almost :)) +immedialty visible on the website. + +You can also build the packages without running a watcher, you have two options: 1. Rebuild all the packages via `make packages-build` or… - 2. Rebuild only a specific package via, e.g. for the package `bar`, `PACKAGE=bar make package-build-bar`. + 2. Rebuild only a specific package, for example `make package-build-bar` for `@nivo/bar` package ### Formatting diff --git a/conf/base.yaml b/conf/base.yaml index 518349096..5d5c554f1 100644 --- a/conf/base.yaml +++ b/conf/base.yaml @@ -1,7 +1,21 @@ capture: baseUrl: http://localhost:8000 pages: - # # capture illustrations for readme + ######################################################################### + # + # CHARTS + # capture illustrations for readme + # + ######################################################################### + + - path: /swarmplot + selector: '#chart' + output: ./packages/swarmplot/doc/swarmplot.png + - path: /swarmplot/canvas + selector: '#chart' + theme: dark + output: ./packages/swarmplot/doc/swarmplot-canvas.png + # - path: /geomap # selector: .chart-tabs__content # output: ./packages/geo/doc/geomap.png @@ -130,289 +144,294 @@ capture: # selector: .chart-tabs__content # output: ./packages/waffle/doc/waffle-canvas.png - # capture icons for website - - path: /internal/icons - selector: '#bar-lightNeutral' - output: ./website/src/assets/icons/bar-light-neutral.png - - path: /internal/icons - selector: '#bar-lightColored' - output: ./website/src/assets/icons/bar-light-colored.png - - path: /internal/icons - selector: '#bar-darkNeutral' - output: ./website/src/assets/icons/bar-dark-neutral.png - - path: /internal/icons - selector: '#bar-darkColored' - output: ./website/src/assets/icons/bar-dark-colored.png + ######################################################################### + # + # ICONS + # capture charts' icons + # + ######################################################################### + # - path: /internal/icons + # selector: '#bar-lightNeutral' + # output: ./website/src/assets/icons/bar-light-neutral.png + # - path: /internal/icons + # selector: '#bar-lightColored' + # output: ./website/src/assets/icons/bar-light-colored.png + # - path: /internal/icons + # selector: '#bar-darkNeutral' + # output: ./website/src/assets/icons/bar-dark-neutral.png + # - path: /internal/icons + # selector: '#bar-darkColored' + # output: ./website/src/assets/icons/bar-dark-colored.png - - path: /internal/icons - selector: '#bullet-lightNeutral' - output: ./website/src/assets/icons/bullet-light-neutral.png - - path: /internal/icons - selector: '#bullet-lightColored' - output: ./website/src/assets/icons/bullet-light-colored.png - - path: /internal/icons - selector: '#bullet-darkNeutral' - output: ./website/src/assets/icons/bullet-dark-neutral.png - - path: /internal/icons - selector: '#bullet-darkColored' - output: ./website/src/assets/icons/bullet-dark-colored.png - - - path: /internal/icons - selector: '#circle-packing-lightNeutral' - output: ./website/src/assets/icons/circle-packing-light-neutral.png - - path: /internal/icons - selector: '#circle-packing-lightColored' - output: ./website/src/assets/icons/circle-packing-light-colored.png - - path: /internal/icons - selector: '#circle-packing-darkNeutral' - output: ./website/src/assets/icons/circle-packing-dark-neutral.png - - path: /internal/icons - selector: '#circle-packing-darkColored' - output: ./website/src/assets/icons/circle-packing-dark-colored.png - - - path: /internal/icons - selector: '#choropleth-lightNeutral' - output: ./website/src/assets/icons/choropleth-light-neutral.png - - path: /internal/icons - selector: '#choropleth-lightColored' - output: ./website/src/assets/icons/choropleth-light-colored.png - - path: /internal/icons - selector: '#choropleth-darkNeutral' - output: ./website/src/assets/icons/choropleth-dark-neutral.png - - path: /internal/icons - selector: '#choropleth-darkColored' - output: ./website/src/assets/icons/choropleth-dark-colored.png + # - path: /internal/icons + # selector: '#bullet-lightNeutral' + # output: ./website/src/assets/icons/bullet-light-neutral.png + # - path: /internal/icons + # selector: '#bullet-lightColored' + # output: ./website/src/assets/icons/bullet-light-colored.png + # - path: /internal/icons + # selector: '#bullet-darkNeutral' + # output: ./website/src/assets/icons/bullet-dark-neutral.png + # - path: /internal/icons + # selector: '#bullet-darkColored' + # output: ./website/src/assets/icons/bullet-dark-colored.png + + # - path: /internal/icons + # selector: '#circle-packing-lightNeutral' + # output: ./website/src/assets/icons/circle-packing-light-neutral.png + # - path: /internal/icons + # selector: '#circle-packing-lightColored' + # output: ./website/src/assets/icons/circle-packing-light-colored.png + # - path: /internal/icons + # selector: '#circle-packing-darkNeutral' + # output: ./website/src/assets/icons/circle-packing-dark-neutral.png + # - path: /internal/icons + # selector: '#circle-packing-darkColored' + # output: ./website/src/assets/icons/circle-packing-dark-colored.png + + # - path: /internal/icons + # selector: '#choropleth-lightNeutral' + # output: ./website/src/assets/icons/choropleth-light-neutral.png + # - path: /internal/icons + # selector: '#choropleth-lightColored' + # output: ./website/src/assets/icons/choropleth-light-colored.png + # - path: /internal/icons + # selector: '#choropleth-darkNeutral' + # output: ./website/src/assets/icons/choropleth-dark-neutral.png + # - path: /internal/icons + # selector: '#choropleth-darkColored' + # output: ./website/src/assets/icons/choropleth-dark-colored.png - - path: /internal/icons - selector: '#heatmap-lightNeutral' - output: ./website/src/assets/icons/heatmap-light-neutral.png - - path: /internal/icons - selector: '#heatmap-lightColored' - output: ./website/src/assets/icons/heatmap-light-colored.png - - path: /internal/icons - selector: '#heatmap-darkNeutral' - output: ./website/src/assets/icons/heatmap-dark-neutral.png - - path: /internal/icons - selector: '#heatmap-darkColored' - output: ./website/src/assets/icons/heatmap-dark-colored.png - - - path: /internal/icons - selector: '#geomap-lightNeutral' - output: ./website/src/assets/icons/geomap-light-neutral.png - - path: /internal/icons - selector: '#geomap-lightColored' - output: ./website/src/assets/icons/geomap-light-colored.png - - path: /internal/icons - selector: '#geomap-darkNeutral' - output: ./website/src/assets/icons/geomap-dark-neutral.png - - path: /internal/icons - selector: '#geomap-darkColored' - output: ./website/src/assets/icons/geomap-dark-colored.png - - - path: /internal/icons - selector: '#line-lightNeutral' - output: ./website/src/assets/icons/line-light-neutral.png - - path: /internal/icons - selector: '#line-lightColored' - output: ./website/src/assets/icons/line-light-colored.png - - path: /internal/icons - selector: '#line-darkNeutral' - output: ./website/src/assets/icons/line-dark-neutral.png - - path: /internal/icons - selector: '#line-darkColored' - output: ./website/src/assets/icons/line-dark-colored.png - - - path: /internal/icons - selector: '#chord-lightNeutral' - output: ./website/src/assets/icons/chord-light-neutral.png - - path: /internal/icons - selector: '#chord-lightColored' - output: ./website/src/assets/icons/chord-light-colored.png - - path: /internal/icons - selector: '#chord-darkNeutral' - output: ./website/src/assets/icons/chord-dark-neutral.png - - path: /internal/icons - selector: '#chord-darkColored' - output: ./website/src/assets/icons/chord-dark-colored.png - - - path: /internal/icons - selector: '#parallel-coordinates-lightNeutral' - output: ./website/src/assets/icons/parallel-coordinates-light-neutral.png - - path: /internal/icons - selector: '#parallel-coordinates-lightColored' - output: ./website/src/assets/icons/parallel-coordinates-light-colored.png - - path: /internal/icons - selector: '#parallel-coordinates-darkNeutral' - output: ./website/src/assets/icons/parallel-coordinates-dark-neutral.png - - path: /internal/icons - selector: '#parallel-coordinates-darkColored' - output: ./website/src/assets/icons/parallel-coordinates-dark-colored.png - - - path: /internal/icons - selector: '#pie-lightNeutral' - output: ./website/src/assets/icons/pie-light-neutral.png - - path: /internal/icons - selector: '#pie-lightColored' - output: ./website/src/assets/icons/pie-light-colored.png - - path: /internal/icons - selector: '#pie-darkNeutral' - output: ./website/src/assets/icons/pie-dark-neutral.png - - path: /internal/icons - selector: '#pie-darkColored' - output: ./website/src/assets/icons/pie-dark-colored.png - - - path: /internal/icons - selector: '#waffle-lightNeutral' - output: ./website/src/assets/icons/waffle-light-neutral.png - - path: /internal/icons - selector: '#waffle-lightColored' - output: ./website/src/assets/icons/waffle-light-colored.png - - path: /internal/icons - selector: '#waffle-darkNeutral' - output: ./website/src/assets/icons/waffle-dark-neutral.png - - path: /internal/icons - selector: '#waffle-darkColored' - output: ./website/src/assets/icons/waffle-dark-colored.png - - - path: /internal/icons - selector: '#stream-lightNeutral' - output: ./website/src/assets/icons/stream-light-neutral.png - - path: /internal/icons - selector: '#stream-lightColored' - output: ./website/src/assets/icons/stream-light-colored.png - - path: /internal/icons - selector: '#stream-darkNeutral' - output: ./website/src/assets/icons/stream-dark-neutral.png - - path: /internal/icons - selector: '#stream-darkColored' - output: ./website/src/assets/icons/stream-dark-colored.png - - - path: /internal/icons - selector: '#scatterplot-lightNeutral' - output: ./website/src/assets/icons/scatterplot-light-neutral.png - - path: /internal/icons - selector: '#scatterplot-lightColored' - output: ./website/src/assets/icons/scatterplot-light-colored.png - - path: /internal/icons - selector: '#scatterplot-darkNeutral' - output: ./website/src/assets/icons/scatterplot-dark-neutral.png - - path: /internal/icons - selector: '#scatterplot-darkColored' - output: ./website/src/assets/icons/scatterplot-dark-colored.png - - - path: /internal/icons - selector: '#radar-lightNeutral' - output: ./website/src/assets/icons/radar-light-neutral.png - - path: /internal/icons - selector: '#radar-lightColored' - output: ./website/src/assets/icons/radar-light-colored.png - - path: /internal/icons - selector: '#radar-darkNeutral' - output: ./website/src/assets/icons/radar-dark-neutral.png - - path: /internal/icons - selector: '#radar-darkColored' - output: ./website/src/assets/icons/radar-dark-colored.png - - - path: /internal/icons - selector: '#calendar-lightNeutral' - output: ./website/src/assets/icons/calendar-light-neutral.png - - path: /internal/icons - selector: '#calendar-lightColored' - output: ./website/src/assets/icons/calendar-light-colored.png - - path: /internal/icons - selector: '#calendar-darkNeutral' - output: ./website/src/assets/icons/calendar-dark-neutral.png - - path: /internal/icons - selector: '#calendar-darkColored' - output: ./website/src/assets/icons/calendar-dark-colored.png - - - path: /internal/icons - selector: '#data-lightNeutral' - output: ./website/src/assets/icons/data-light-neutral.png - - path: /internal/icons - selector: '#data-lightColored' - output: ./website/src/assets/icons/data-light-colored.png - - path: /internal/icons - selector: '#data-darkNeutral' - output: ./website/src/assets/icons/data-dark-neutral.png - - path: /internal/icons - selector: '#data-darkColored' - output: ./website/src/assets/icons/data-dark-colored.png - - - path: /internal/icons - selector: '#code-lightNeutral' - output: ./website/src/assets/icons/code-light-neutral.png - - path: /internal/icons - selector: '#code-lightColored' - output: ./website/src/assets/icons/code-light-colored.png - - path: /internal/icons - selector: '#code-darkNeutral' - output: ./website/src/assets/icons/code-dark-neutral.png - - path: /internal/icons - selector: '#code-darkColored' - output: ./website/src/assets/icons/code-dark-colored.png - - - path: /internal/icons - selector: '#treemap-lightNeutral' - output: ./website/src/assets/icons/treemap-light-neutral.png - - path: /internal/icons - selector: '#treemap-lightColored' - output: ./website/src/assets/icons/treemap-light-colored.png - - path: /internal/icons - selector: '#treemap-darkNeutral' - output: ./website/src/assets/icons/treemap-dark-neutral.png - - path: /internal/icons - selector: '#treemap-darkColored' - output: ./website/src/assets/icons/treemap-dark-colored.png - - - path: /internal/icons - selector: '#sankey-lightNeutral' - output: ./website/src/assets/icons/sankey-light-neutral.png - - path: /internal/icons - selector: '#sankey-lightColored' - output: ./website/src/assets/icons/sankey-light-colored.png - - path: /internal/icons - selector: '#sankey-darkNeutral' - output: ./website/src/assets/icons/sankey-dark-neutral.png - - path: /internal/icons - selector: '#sankey-darkColored' - output: ./website/src/assets/icons/sankey-dark-colored.png - - - path: /internal/icons - selector: '#voronoi-lightNeutral' - output: ./website/src/assets/icons/voronoi-light-neutral.png - - path: /internal/icons - selector: '#voronoi-lightColored' - output: ./website/src/assets/icons/voronoi-light-colored.png - - path: /internal/icons - selector: '#voronoi-darkNeutral' - output: ./website/src/assets/icons/voronoi-dark-neutral.png - - path: /internal/icons - selector: '#voronoi-darkColored' - output: ./website/src/assets/icons/voronoi-dark-colored.png - - - path: /internal/icons - selector: '#sunburst-lightNeutral' - output: ./website/src/assets/icons/sunburst-light-neutral.png - - path: /internal/icons - selector: '#sunburst-lightColored' - output: ./website/src/assets/icons/sunburst-light-colored.png - - path: /internal/icons - selector: '#sunburst-darkNeutral' - output: ./website/src/assets/icons/sunburst-dark-neutral.png - - path: /internal/icons - selector: '#sunburst-darkColored' - output: ./website/src/assets/icons/sunburst-dark-colored.png - - - path: /internal/icons - selector: '#swarmplot-lightNeutral' - output: ./website/src/assets/icons/swarmplot-light-neutral.png - - path: /internal/icons - selector: '#swarmplot-lightColored' - output: ./website/src/assets/icons/swarmplot-light-colored.png - - path: /internal/icons - selector: '#swarmplot-darkNeutral' - output: ./website/src/assets/icons/swarmplot-dark-neutral.png - - path: /internal/icons - selector: '#swarmplot-darkColored' - output: ./website/src/assets/icons/swarmplot-dark-colored.png \ No newline at end of file + # - path: /internal/icons + # selector: '#heatmap-lightNeutral' + # output: ./website/src/assets/icons/heatmap-light-neutral.png + # - path: /internal/icons + # selector: '#heatmap-lightColored' + # output: ./website/src/assets/icons/heatmap-light-colored.png + # - path: /internal/icons + # selector: '#heatmap-darkNeutral' + # output: ./website/src/assets/icons/heatmap-dark-neutral.png + # - path: /internal/icons + # selector: '#heatmap-darkColored' + # output: ./website/src/assets/icons/heatmap-dark-colored.png + + # - path: /internal/icons + # selector: '#geomap-lightNeutral' + # output: ./website/src/assets/icons/geomap-light-neutral.png + # - path: /internal/icons + # selector: '#geomap-lightColored' + # output: ./website/src/assets/icons/geomap-light-colored.png + # - path: /internal/icons + # selector: '#geomap-darkNeutral' + # output: ./website/src/assets/icons/geomap-dark-neutral.png + # - path: /internal/icons + # selector: '#geomap-darkColored' + # output: ./website/src/assets/icons/geomap-dark-colored.png + + # - path: /internal/icons + # selector: '#line-lightNeutral' + # output: ./website/src/assets/icons/line-light-neutral.png + # - path: /internal/icons + # selector: '#line-lightColored' + # output: ./website/src/assets/icons/line-light-colored.png + # - path: /internal/icons + # selector: '#line-darkNeutral' + # output: ./website/src/assets/icons/line-dark-neutral.png + # - path: /internal/icons + # selector: '#line-darkColored' + # output: ./website/src/assets/icons/line-dark-colored.png + + # - path: /internal/icons + # selector: '#chord-lightNeutral' + # output: ./website/src/assets/icons/chord-light-neutral.png + # - path: /internal/icons + # selector: '#chord-lightColored' + # output: ./website/src/assets/icons/chord-light-colored.png + # - path: /internal/icons + # selector: '#chord-darkNeutral' + # output: ./website/src/assets/icons/chord-dark-neutral.png + # - path: /internal/icons + # selector: '#chord-darkColored' + # output: ./website/src/assets/icons/chord-dark-colored.png + + # - path: /internal/icons + # selector: '#parallel-coordinates-lightNeutral' + # output: ./website/src/assets/icons/parallel-coordinates-light-neutral.png + # - path: /internal/icons + # selector: '#parallel-coordinates-lightColored' + # output: ./website/src/assets/icons/parallel-coordinates-light-colored.png + # - path: /internal/icons + # selector: '#parallel-coordinates-darkNeutral' + # output: ./website/src/assets/icons/parallel-coordinates-dark-neutral.png + # - path: /internal/icons + # selector: '#parallel-coordinates-darkColored' + # output: ./website/src/assets/icons/parallel-coordinates-dark-colored.png + + # - path: /internal/icons + # selector: '#pie-lightNeutral' + # output: ./website/src/assets/icons/pie-light-neutral.png + # - path: /internal/icons + # selector: '#pie-lightColored' + # output: ./website/src/assets/icons/pie-light-colored.png + # - path: /internal/icons + # selector: '#pie-darkNeutral' + # output: ./website/src/assets/icons/pie-dark-neutral.png + # - path: /internal/icons + # selector: '#pie-darkColored' + # output: ./website/src/assets/icons/pie-dark-colored.png + + # - path: /internal/icons + # selector: '#waffle-lightNeutral' + # output: ./website/src/assets/icons/waffle-light-neutral.png + # - path: /internal/icons + # selector: '#waffle-lightColored' + # output: ./website/src/assets/icons/waffle-light-colored.png + # - path: /internal/icons + # selector: '#waffle-darkNeutral' + # output: ./website/src/assets/icons/waffle-dark-neutral.png + # - path: /internal/icons + # selector: '#waffle-darkColored' + # output: ./website/src/assets/icons/waffle-dark-colored.png + + # - path: /internal/icons + # selector: '#stream-lightNeutral' + # output: ./website/src/assets/icons/stream-light-neutral.png + # - path: /internal/icons + # selector: '#stream-lightColored' + # output: ./website/src/assets/icons/stream-light-colored.png + # - path: /internal/icons + # selector: '#stream-darkNeutral' + # output: ./website/src/assets/icons/stream-dark-neutral.png + # - path: /internal/icons + # selector: '#stream-darkColored' + # output: ./website/src/assets/icons/stream-dark-colored.png + + # - path: /internal/icons + # selector: '#scatterplot-lightNeutral' + # output: ./website/src/assets/icons/scatterplot-light-neutral.png + # - path: /internal/icons + # selector: '#scatterplot-lightColored' + # output: ./website/src/assets/icons/scatterplot-light-colored.png + # - path: /internal/icons + # selector: '#scatterplot-darkNeutral' + # output: ./website/src/assets/icons/scatterplot-dark-neutral.png + # - path: /internal/icons + # selector: '#scatterplot-darkColored' + # output: ./website/src/assets/icons/scatterplot-dark-colored.png + + # - path: /internal/icons + # selector: '#radar-lightNeutral' + # output: ./website/src/assets/icons/radar-light-neutral.png + # - path: /internal/icons + # selector: '#radar-lightColored' + # output: ./website/src/assets/icons/radar-light-colored.png + # - path: /internal/icons + # selector: '#radar-darkNeutral' + # output: ./website/src/assets/icons/radar-dark-neutral.png + # - path: /internal/icons + # selector: '#radar-darkColored' + # output: ./website/src/assets/icons/radar-dark-colored.png + + # - path: /internal/icons + # selector: '#calendar-lightNeutral' + # output: ./website/src/assets/icons/calendar-light-neutral.png + # - path: /internal/icons + # selector: '#calendar-lightColored' + # output: ./website/src/assets/icons/calendar-light-colored.png + # - path: /internal/icons + # selector: '#calendar-darkNeutral' + # output: ./website/src/assets/icons/calendar-dark-neutral.png + # - path: /internal/icons + # selector: '#calendar-darkColored' + # output: ./website/src/assets/icons/calendar-dark-colored.png + + # - path: /internal/icons + # selector: '#data-lightNeutral' + # output: ./website/src/assets/icons/data-light-neutral.png + # - path: /internal/icons + # selector: '#data-lightColored' + # output: ./website/src/assets/icons/data-light-colored.png + # - path: /internal/icons + # selector: '#data-darkNeutral' + # output: ./website/src/assets/icons/data-dark-neutral.png + # - path: /internal/icons + # selector: '#data-darkColored' + # output: ./website/src/assets/icons/data-dark-colored.png + + # - path: /internal/icons + # selector: '#code-lightNeutral' + # output: ./website/src/assets/icons/code-light-neutral.png + # - path: /internal/icons + # selector: '#code-lightColored' + # output: ./website/src/assets/icons/code-light-colored.png + # - path: /internal/icons + # selector: '#code-darkNeutral' + # output: ./website/src/assets/icons/code-dark-neutral.png + # - path: /internal/icons + # selector: '#code-darkColored' + # output: ./website/src/assets/icons/code-dark-colored.png + + # - path: /internal/icons + # selector: '#treemap-lightNeutral' + # output: ./website/src/assets/icons/treemap-light-neutral.png + # - path: /internal/icons + # selector: '#treemap-lightColored' + # output: ./website/src/assets/icons/treemap-light-colored.png + # - path: /internal/icons + # selector: '#treemap-darkNeutral' + # output: ./website/src/assets/icons/treemap-dark-neutral.png + # - path: /internal/icons + # selector: '#treemap-darkColored' + # output: ./website/src/assets/icons/treemap-dark-colored.png + + # - path: /internal/icons + # selector: '#sankey-lightNeutral' + # output: ./website/src/assets/icons/sankey-light-neutral.png + # - path: /internal/icons + # selector: '#sankey-lightColored' + # output: ./website/src/assets/icons/sankey-light-colored.png + # - path: /internal/icons + # selector: '#sankey-darkNeutral' + # output: ./website/src/assets/icons/sankey-dark-neutral.png + # - path: /internal/icons + # selector: '#sankey-darkColored' + # output: ./website/src/assets/icons/sankey-dark-colored.png + + # - path: /internal/icons + # selector: '#voronoi-lightNeutral' + # output: ./website/src/assets/icons/voronoi-light-neutral.png + # - path: /internal/icons + # selector: '#voronoi-lightColored' + # output: ./website/src/assets/icons/voronoi-light-colored.png + # - path: /internal/icons + # selector: '#voronoi-darkNeutral' + # output: ./website/src/assets/icons/voronoi-dark-neutral.png + # - path: /internal/icons + # selector: '#voronoi-darkColored' + # output: ./website/src/assets/icons/voronoi-dark-colored.png + + # - path: /internal/icons + # selector: '#sunburst-lightNeutral' + # output: ./website/src/assets/icons/sunburst-light-neutral.png + # - path: /internal/icons + # selector: '#sunburst-lightColored' + # output: ./website/src/assets/icons/sunburst-light-colored.png + # - path: /internal/icons + # selector: '#sunburst-darkNeutral' + # output: ./website/src/assets/icons/sunburst-dark-neutral.png + # - path: /internal/icons + # selector: '#sunburst-darkColored' + # output: ./website/src/assets/icons/sunburst-dark-colored.png + + # - path: /internal/icons + # selector: '#swarmplot-lightNeutral' + # output: ./website/src/assets/icons/swarmplot-light-neutral.png + # - path: /internal/icons + # selector: '#swarmplot-lightColored' + # output: ./website/src/assets/icons/swarmplot-light-colored.png + # - path: /internal/icons + # selector: '#swarmplot-darkNeutral' + # output: ./website/src/assets/icons/swarmplot-dark-neutral.png + # - path: /internal/icons + # selector: '#swarmplot-darkColored' + # output: ./website/src/assets/icons/swarmplot-dark-colored.png \ No newline at end of file diff --git a/package.json b/package.json index 3748ae9e7..34bb101d4 100644 --- a/package.json +++ b/package.json @@ -20,8 +20,8 @@ "devDependencies": { "@babel/core": "^7.4.0", "@ekino/config": "^0.3.0", - "@nivo/babel-preset": "0.54.0", - "@nivo/generators": "0.54.0", + "@nivo/babel-preset": "0.55.0", + "@nivo/generators": "0.55.0", "@storybook/addon-actions": "^5.0.3", "@storybook/addon-info": "^5.0.3", "@storybook/addon-knobs": "^5.0.3", diff --git a/packages/axes/tests/axes.test.js b/packages/axes/tests/axes.test.js index 6fd70b475..bb020f920 100644 --- a/packages/axes/tests/axes.test.js +++ b/packages/axes/tests/axes.test.js @@ -20,8 +20,6 @@ describe('computeCartesianTicks()', () => { const bandScale = scaleBand() .domain(['I', 'J', 'K', 'L']) .rangeRound([0, 400]) - const width = 600 - const height = 400 describe('from linear scale', () => { it('should compute ticks for x axis', () => { diff --git a/packages/bar/src/enhance.js b/packages/bar/src/enhance.js index 6b0289e4d..8ee6a42a2 100644 --- a/packages/bar/src/enhance.js +++ b/packages/bar/src/enhance.js @@ -27,8 +27,8 @@ export default Component => withTheme(), withDimensions(), withMotion(), - withPropsOnChange(['colors', 'colorIdentity'], ({ colors, colorIdentity }) => ({ - getColor: getOrdinalColorScale(colors, colorIdentity), + withPropsOnChange(['colors', 'colorBy'], ({ colors, colorBy }) => ({ + getColor: getOrdinalColorScale(colors, colorBy), })), withPropsOnChange(['indexBy'], ({ indexBy }) => ({ getIndex: getAccessorFor(indexBy), diff --git a/packages/bar/src/props.js b/packages/bar/src/props.js index 8759fb561..90446b593 100644 --- a/packages/bar/src/props.js +++ b/packages/bar/src/props.js @@ -8,7 +8,7 @@ */ import PropTypes from 'prop-types' import { noop, defsPropTypes } from '@nivo/core' -import { ordinalColorsPropType, colorIdentityPropType } from '@nivo/colors' +import { ordinalColorsPropType, colorPropertyAccessorPropType } from '@nivo/colors' import { axisPropType } from '@nivo/axes' import { LegendPropShape } from '@nivo/legends' import BarItem from './BarItem' @@ -57,7 +57,7 @@ export const BarPropTypes = { getLabelLinkColor: PropTypes.func.isRequired, // computed colors: ordinalColorsPropType.isRequired, - colorIdentity: colorIdentityPropType.isRequired, + colorBy: colorPropertyAccessorPropType.isRequired, borderRadius: PropTypes.number.isRequired, getColor: PropTypes.func.isRequired, // computed ...defsPropTypes, @@ -114,7 +114,7 @@ export const BarDefaultProps = { labelTextColor: 'theme', colors: { scheme: 'nivo' }, - colorIdentity: 'id', + colorBy: 'id', defs: [], fill: [], borderRadius: 0, diff --git a/packages/bar/tests/Bar.test.js b/packages/bar/tests/Bar.test.js index 43631f7db..e373966a0 100644 --- a/packages/bar/tests/Bar.test.js +++ b/packages/bar/tests/Bar.test.js @@ -1,3 +1,11 @@ +/* + * This file is part of the nivo project. + * + * Copyright 2016-present, Raphaël Benitte. + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ import React from 'react' import renderer from 'react-test-renderer' import { mount } from 'enzyme' diff --git a/packages/circle-packing/src/enhance.js b/packages/circle-packing/src/enhance.js index a1828e2b8..2f3092895 100644 --- a/packages/circle-packing/src/enhance.js +++ b/packages/circle-packing/src/enhance.js @@ -30,8 +30,8 @@ const commonEnhancers = [ withHierarchy(), withDimensions(), withTheme(), - withPropsOnChange(['colors', 'colorIdentity'], ({ colors, colorIdentity }) => ({ - getColor: getOrdinalColorScale(colors, colorIdentity), + withPropsOnChange(['colors', 'colorBy'], ({ colors, colorBy }) => ({ + getColor: getOrdinalColorScale(colors, colorBy), })), withPropsOnChange(['width', 'height', 'padding'], ({ width, height, padding }) => ({ pack: pack() diff --git a/packages/circle-packing/src/props.js b/packages/circle-packing/src/props.js index 342f43711..9987d6cd6 100644 --- a/packages/circle-packing/src/props.js +++ b/packages/circle-packing/src/props.js @@ -8,7 +8,7 @@ */ import PropTypes from 'prop-types' import { noop, defsPropTypes } from '@nivo/core' -import { ordinalColorsPropType, colorIdentityPropType } from '@nivo/colors' +import { ordinalColorsPropType, colorPropertyAccessorPropType } from '@nivo/colors' import BubbleNode from './BubbleNode' import BubbleHtmlNode from './BubbleHtmlNode' @@ -21,7 +21,7 @@ const commonPropTypes = { // theme managed by `withTheme()` HOC colors: ordinalColorsPropType.isRequired, - colorIdentity: colorIdentityPropType.isRequired, + colorBy: colorPropertyAccessorPropType.isRequired, leavesOnly: PropTypes.bool.isRequired, padding: PropTypes.number.isRequired, @@ -65,7 +65,7 @@ const commonDefaultProps = { padding: 1, colors: { scheme: 'nivo' }, - colorIdentity: 'depth', + colorBy: 'depth', borderWidth: 0, borderColor: 'inherit', diff --git a/packages/colors/src/index.js b/packages/colors/src/index.js index a23d47948..5ca16f79c 100644 --- a/packages/colors/src/index.js +++ b/packages/colors/src/index.js @@ -8,5 +8,6 @@ */ export * from './schemes' export * from './ordinalColorScale' +export * from './inheritedColor' export * from './props' export * from './motion' diff --git a/packages/colors/src/inheritedColor.js b/packages/colors/src/inheritedColor.js new file mode 100644 index 000000000..704f25a77 --- /dev/null +++ b/packages/colors/src/inheritedColor.js @@ -0,0 +1,80 @@ +/* + * This file is part of the nivo project. + * + * Copyright 2016-present, Raphaël Benitte. + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +import { useMemo } from 'react' +import get from 'lodash.get' +import isPlainObject from 'lodash.isplainobject' +import { rgb } from 'd3-color' + +export const getInheritedColorGenerator = (inheritedColor, theme) => { + // user provided function + if (typeof inheritedColor === 'function') return node => inheritedColor(node) + + if (isPlainObject(inheritedColor)) { + // use color from theme + if (inheritedColor.theme !== undefined) { + if (theme === undefined) { + throw new Error(`Unable to use color from theme as no theme was provided`) + } + + const themeColor = get(theme, inheritedColor.theme) + if (themeColor === undefined) { + throw new Error(`Color from theme is undefined at path: '${inheritedColor.theme}'`) + } + + return () => themeColor + } + + // use color from parent with optional color modifiers + if (inheritedColor.from !== undefined) { + const getColor = datum => get(datum, inheritedColor.from) + + if (Array.isArray(inheritedColor.modifiers)) { + const modifiers = [] + for (const modifier of inheritedColor.modifiers) { + const [modifierType, amount] = modifier + if (modifierType === 'brighter') { + modifiers.push(color => color.brighter(amount)) + } else if (modifierType === 'darker') { + modifiers.push(color => color.darker(amount)) + } else if (modifierType === 'opacity') { + modifiers.push(color => { + color.opacity = amount + + return color + }) + } else { + throw new Error( + `Invalid color modifier: '${modifierType}', must be one of: 'brighter', 'darker', 'opacity'` + ) + } + } + + if (modifiers.length === 0) return getColor + + return datum => + modifiers + .reduce((color, modify) => modify(color), rgb(getColor(datum))) + .toString() + } + + // no modifier + return getColor + } + + throw new Error( + `Invalid color spec, you should either specify 'theme' or 'from' when using a config object` + ) + } + + // use provided color statically + return () => inheritedColor +} + +export const useInheritedColor = (parentColor, theme) => + useMemo(() => getInheritedColorGenerator(parentColor, theme), [parentColor, theme]) diff --git a/packages/colors/src/props.js b/packages/colors/src/props.js index 2cca99d7f..7215bfa47 100644 --- a/packages/colors/src/props.js +++ b/packages/colors/src/props.js @@ -21,4 +21,16 @@ export const ordinalColorsPropType = PropTypes.oneOfType([ }), ]) -export const colorIdentityPropType = PropTypes.oneOfType([PropTypes.func, PropTypes.string]) +export const colorPropertyAccessorPropType = PropTypes.oneOfType([PropTypes.func, PropTypes.string]) + +export const inheritedColorPropType = PropTypes.oneOfType([ + PropTypes.string, + PropTypes.func, + PropTypes.shape({ + theme: PropTypes.string.isRequired, + }), + PropTypes.shape({ + from: PropTypes.string.isRequired, + modifiers: PropTypes.arrayOf(PropTypes.array), + }), +]) diff --git a/packages/colors/tests/inheritedColor.test.js b/packages/colors/tests/inheritedColor.test.js new file mode 100644 index 000000000..ae001e135 --- /dev/null +++ b/packages/colors/tests/inheritedColor.test.js @@ -0,0 +1,113 @@ +/* + * This file is part of the nivo project. + * + * Copyright 2016-present, Raphaël Benitte. + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +import { rgb } from 'd3-color' +import { getInheritedColorGenerator } from '../src' + +it(`should accept user defined function`, () => { + const userFunction = datum => datum.color + const getColor = getInheritedColorGenerator(userFunction) + + expect(getColor({ color: 'red' })).toBe('red') +}) + +it(`should throw if an object is given but doesn't match theme or inheritance`, () => { + expect(() => { + getInheritedColorGenerator({}) + }).toThrow( + `Invalid color spec, you should either specify 'theme' or 'from' when using a config object` + ) +}) + +describe(`from theme`, () => { + it(`should be able to use a theme property`, () => { + const theme = { thing: { color: 'green' } } + const getColor = getInheritedColorGenerator({ theme: 'thing.color' }, theme) + + expect(getColor()).toBe('green') + }) + + it(`should throw if no theme is provided`, () => { + expect(() => { + getInheritedColorGenerator({ theme: 'color' }) + }).toThrow('Unable to use color from theme as no theme was provided') + }) + + it(`should throw if theme property is undefined`, () => { + expect(() => { + getInheritedColorGenerator({ theme: 'color' }, {}) + }).toThrow(`Color from theme is undefined at path: 'color'`) + }) +}) + +describe(`from datum`, () => { + it(`should be able to use color from datum`, () => { + const getColor = getInheritedColorGenerator({ from: 'data.color' }) + + expect(getColor({ data: { color: 'purple' } })).toBe('purple') + }) + + it(`should be able to apply a brighter modifier on inherited color`, () => { + const getColor = getInheritedColorGenerator({ + from: 'color', + modifiers: [['brighter', 1]], + }) + + expect(getColor({ color: '#ff0099' })).toBe( + rgb('#ff0099') + .brighter(1) + .toString() + ) + }) + + it(`should be able to apply a darker modifier on inherited color`, () => { + const getColor = getInheritedColorGenerator({ + from: 'color', + modifiers: [['darker', 1]], + }) + + expect(getColor({ color: '#ff0099' })).toBe( + rgb('#ff0099') + .darker(1) + .toString() + ) + }) + + it(`should be able to apply an opacity modifier on inherited color`, () => { + const getColor = getInheritedColorGenerator({ + from: 'color', + modifiers: [['opacity', 0.5]], + }) + + const expectedColor = rgb('#ff0099') + expectedColor.opacity = 0.5 + expect(getColor({ color: '#ff0099' })).toBe(expectedColor.toString()) + }) + + it(`should be able to chain several modifiers on inherited color`, () => { + const getColor = getInheritedColorGenerator({ + from: 'color', + modifiers: [['darker', 2], ['opacity', 0.5]], + }) + + const expectedColor = rgb('#ff0099').darker(2) + expectedColor.opacity = 0.5 + expect(getColor({ color: '#ff0099' })).toBe(expectedColor.toString()) + }) + + it(`should throw if modifier type is invalid`, () => { + expect(() => { + getInheritedColorGenerator({ + from: 'color', + modifiers: [['invalid']], + }) + }).toThrow( + `Invalid color modifier: 'invalid', must be one of: 'brighter', 'darker', 'opacity'` + ) + }) +}) diff --git a/packages/core/src/hooks/index.js b/packages/core/src/hooks/index.js index 63c816cb5..00d727900 100644 --- a/packages/core/src/hooks/index.js +++ b/packages/core/src/hooks/index.js @@ -8,3 +8,4 @@ */ export * from './useDimensions' export * from './usePartialTheme' +export * from './useValueFormatter' diff --git a/packages/core/src/hooks/useValueFormatter.js b/packages/core/src/hooks/useValueFormatter.js new file mode 100644 index 000000000..53df1c5be --- /dev/null +++ b/packages/core/src/hooks/useValueFormatter.js @@ -0,0 +1,31 @@ +/* + * This file is part of the nivo project. + * + * Copyright 2016-present, Raphaël Benitte. + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +import { useMemo } from 'react' +import { format as d3Format } from 'd3-format' +import { timeFormat as d3TimeFormat } from 'd3-time-format' + +export const getValueFormatter = format => { + // user defined function + if (typeof format === 'function') return format + + if (typeof format === 'string') { + // time format specifier + if (format.indexOf('time:') === 0) { + return d3TimeFormat(format.slice('5')) + } + + // standard fromat specifier + return d3Format(format) + } + + // no formatting + return v => v +} + +export const useValueFormatter = format => useMemo(() => getValueFormatter(format), [format]) diff --git a/packages/core/src/tooltip/BasicTooltip.js b/packages/core/src/tooltip/BasicTooltip.js index 8621054c3..59f24334b 100644 --- a/packages/core/src/tooltip/BasicTooltip.js +++ b/packages/core/src/tooltip/BasicTooltip.js @@ -6,21 +6,17 @@ * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -import React, { useMemo, memo } from 'react' +import React, { memo } from 'react' import PropTypes from 'prop-types' -import isFunction from 'lodash/isFunction' -import { format as d3Format } from 'd3-format' import Chip from './Chip' import { useTheme } from '../theming' +import { useValueFormatter } from '../hooks/useValueFormatter' const chipStyle = { marginRight: 7 } const BasicTooltip = memo(({ id, value: _value, format, enableChip, color, renderContent }) => { const theme = useTheme() - const formatValue = useMemo(() => { - if (!format || isFunction(format)) return format - return d3Format(format) - }, [format]) + const formatValue = useValueFormatter(format) let content if (typeof renderContent === 'function') { diff --git a/packages/generators/src/bullet.js b/packages/generators/src/bullet.js index 00aeee868..663204cd9 100644 --- a/packages/generators/src/bullet.js +++ b/packages/generators/src/bullet.js @@ -1,3 +1,11 @@ +/* + * This file is part of the nivo project. + * + * Copyright 2016-present, Raphaël Benitte. + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ import range from 'lodash/range' import random from 'lodash/random' diff --git a/packages/generators/src/chord.js b/packages/generators/src/chord.js index f8e0adcc9..a544382d9 100644 --- a/packages/generators/src/chord.js +++ b/packages/generators/src/chord.js @@ -1,3 +1,11 @@ +/* + * This file is part of the nivo project. + * + * Copyright 2016-present, Raphaël Benitte. + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ import range from 'lodash/range' import random from 'lodash/random' import { names } from './sets' diff --git a/packages/generators/src/color.js b/packages/generators/src/color.js index b2966d1cd..1843ce2cf 100644 --- a/packages/generators/src/color.js +++ b/packages/generators/src/color.js @@ -1 +1,9 @@ +/* + * This file is part of the nivo project. + * + * Copyright 2016-present, Raphaël Benitte. + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ export const randColor = () => `hsl(${Math.round(Math.random() * 360)}, 70%, 50%)` diff --git a/packages/generators/src/index.js b/packages/generators/src/index.js index ed121dbd3..20de8a22f 100644 --- a/packages/generators/src/index.js +++ b/packages/generators/src/index.js @@ -1,3 +1,11 @@ +/* + * This file is part of the nivo project. + * + * Copyright 2016-present, Raphaël Benitte. + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ import range from 'lodash/range' import random from 'lodash/random' import shuffle from 'lodash/shuffle' @@ -256,3 +264,4 @@ export * from './bullet' export * from './chord' export * from './parallelCoordinates' export * from './sankey' +export * from './swarmplot' diff --git a/packages/generators/src/parallelCoordinates.js b/packages/generators/src/parallelCoordinates.js index 277cbf3f6..055918554 100644 --- a/packages/generators/src/parallelCoordinates.js +++ b/packages/generators/src/parallelCoordinates.js @@ -1,3 +1,11 @@ +/* + * This file is part of the nivo project. + * + * Copyright 2016-present, Raphaël Benitte. + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ import random from 'lodash/random' import range from 'lodash/range' import shuffle from 'lodash/shuffle' diff --git a/packages/generators/src/sankey.js b/packages/generators/src/sankey.js index 0658037dd..77e46fd3e 100644 --- a/packages/generators/src/sankey.js +++ b/packages/generators/src/sankey.js @@ -1,3 +1,11 @@ +/* + * This file is part of the nivo project. + * + * Copyright 2016-present, Raphaël Benitte. + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ import range from 'lodash/range' import random from 'lodash/random' import shuffle from 'lodash/shuffle' diff --git a/packages/generators/src/swarmplot.js b/packages/generators/src/swarmplot.js new file mode 100644 index 000000000..2119c7afb --- /dev/null +++ b/packages/generators/src/swarmplot.js @@ -0,0 +1,59 @@ +/* + * This file is part of the nivo project. + * + * Copyright 2016-present, Raphaël Benitte. + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +import range from 'lodash/range' +import random from 'lodash/random' +import shuffle from 'lodash/shuffle' + +const randomPrice = () => random(0, 500) +const randomVolume = () => random(4, 20) +const randomCategory = () => random(3, 17) + +export const generateSwarmPlotData = (groups, { min = 60, max = 100, categoryCount = 0 }) => ({ + groups, + data: groups.reduce( + (acc, group, groupIndex) => [ + ...acc, + ...range(random(min, max)) + .map(() => randomPrice()) + .map((price, index) => { + const datum = { + id: `${groupIndex}.${index}`, + group, + price, + volume: randomVolume(), + } + + if (categoryCount > 0) { + datum.categories = range(categoryCount).map(randomCategory) + } + + return datum + }), + ], + [] + ), +}) + +export const randomizeSwarmPlotData = previousData => ({ + groups: previousData.groups, + data: previousData.data.map(d => { + const datum = { + ...d, + group: shuffle(previousData.groups)[0], + price: randomPrice(), + volume: randomVolume(), + } + + if (d.categories !== undefined) { + datum.categories = range(3).map(randomCategory) + } + + return datum + }), +}) diff --git a/packages/pie/src/hooks.js b/packages/pie/src/hooks.js new file mode 100644 index 000000000..67f4e1cf0 --- /dev/null +++ b/packages/pie/src/hooks.js @@ -0,0 +1,67 @@ +/* + * This file is part of the nivo project. + * + * Copyright 2016-present, Raphaël Benitte. + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +import { useMemo } from 'react' +import get from 'lodash/get' +import { arc as d3Arc, pie as d3Pie } from 'd3-shape' +import { degreesToRadians, radiansToDegrees } from '@nivo/core' + +export const usePie = ({ + data, + radius, + value = v => v, + startAngle = 0, + endAngle = 360, + innerRadius = 0, + cornerRadius = 0, + sortByValue = false, + padAngle = 0, +}) => { + const arcGenerator = useMemo( + () => + d3Arc() + .outerRadius(radius) + .innerRadius(innerRadius) + .cornerRadius(cornerRadius), + [radius, innerRadius, cornerRadius] + ) + + const getValue = useMemo(() => (typeof value === 'function' ? value : d => get(d, value)), [ + value, + ]) + + const pie = useMemo(() => { + const computedPie = d3Pie() + .value(getValue) + .padAngle(degreesToRadians(padAngle)) + .startAngle(degreesToRadians(startAngle)) + .endAngle(degreesToRadians(endAngle)) + + if (sortByValue !== true) computedPie.sortValues(null) + + return computedPie + }, [getValue, padAngle, startAngle, endAngle, sortByValue]) + + const arcs = useMemo( + () => + pie(data).map(arc => { + const angle = arc.endAngle - (arc.endAngle - arc.startAngle) * 0.5 + + return { + ...arc, + startAngleDeg: radiansToDegrees(angle.startAngle), + endAngleDeg: radiansToDegrees(arc.endAngle), + angle, + angleDeg: radiansToDegrees(angle), + } + }), + [data, pie] + ) + + return { arcs, arcGenerator } +} diff --git a/packages/pie/src/index.js b/packages/pie/src/index.js index 72d376287..b1c283710 100644 --- a/packages/pie/src/index.js +++ b/packages/pie/src/index.js @@ -12,3 +12,4 @@ export { default as ResponsivePie } from './ResponsivePie' export { default as PieCanvas } from './PieCanvas' export { default as ResponsivePieCanvas } from './ResponsivePieCanvas' export * from './props' +export * from './hooks' diff --git a/packages/scatterplot/src/ScatterPlotTooltip.js b/packages/scatterplot/src/ScatterPlotTooltip.js index a01628d77..4a4e657fb 100644 --- a/packages/scatterplot/src/ScatterPlotTooltip.js +++ b/packages/scatterplot/src/ScatterPlotTooltip.js @@ -10,7 +10,7 @@ import React from 'react' import PropTypes from 'prop-types' import { BasicTooltip } from '@nivo/core' -const ScatterPlotTooltip = ({ point: { data }, color, format, tooltip, theme }) => ( +const ScatterPlotTooltip = ({ point: { data }, color, format, theme, tooltip }) => ( withPropsOnChange( ['points', 'width', 'height', 'debugMesh'], ({ points, width, height, debugMesh }) => { - const points2d = computeMeshPoints({ - points, - xAccessor: 'x', - yAccessor: 'y', - }).points + const points2d = computeMeshPoints({ points }).points return computeMesh({ points: points2d, width, height, debug: debugMesh }) } diff --git a/packages/swarmplot/README.md b/packages/swarmplot/README.md index cedf3ab9b..b246315a2 100644 --- a/packages/swarmplot/README.md +++ b/packages/swarmplot/README.md @@ -6,10 +6,10 @@ [documentation](http://nivo.rocks/swarmplot) -![SwarmPlot](./doc/swarmplot.png) +![SwarmPlot](https://raw.githubusercontent.com/plouc/nivo/master/packages/swarmplot/doc/swarmplot.png) ## SwarmPlotCanvas [documentation](http://nivo.rocks/swarmplot/canvas) -![SwarmPlotCanvas](./doc/swarmplot-canvas.png) +![SwarmPlotCanvas](https://raw.githubusercontent.com/plouc/nivo/master/packages/swarmplot/doc/swarmplot-canvas.png) diff --git a/packages/swarmplot/doc/swarmplot-canvas.png b/packages/swarmplot/doc/swarmplot-canvas.png new file mode 100644 index 000000000..36fed0fd7 Binary files /dev/null and b/packages/swarmplot/doc/swarmplot-canvas.png differ diff --git a/packages/swarmplot/doc/swarmplot.png b/packages/swarmplot/doc/swarmplot.png new file mode 100644 index 000000000..7a6964ef2 Binary files /dev/null and b/packages/swarmplot/doc/swarmplot.png differ diff --git a/packages/swarmplot/index.d.ts b/packages/swarmplot/index.d.ts new file mode 100644 index 000000000..07db52183 --- /dev/null +++ b/packages/swarmplot/index.d.ts @@ -0,0 +1,84 @@ +import { Component } from 'react' +import { Box, MotionProps, Dimensions, Theme } from '@nivo/core' +import { OrdinalColorsInstruction } from '@nivo/colors' + +declare module '@nivo/swarmplot' { + export interface ComputedNode { + id: string + index: number + group: string + value: string + x: number + y: number + size: number + color: string + data: Datum + } + + type DatumAccessor = (datum: Datum) => T + + type ComputedNodeAccessor = (node: ComputedNode) => T + + export interface DynamicSizeSpec { + key: string + values: [number, number] + sizes: [number, number] + } + + export type SwarmPlotMouseHandler = ( + node: ComputedNode, + event: React.MouseEvent + ) => void + + interface CommonSwarmPlotProps { + data: Datum[] + + margin?: Box + + groups: string[] + groupBy?: string + identity?: string | DatumAccessor + value?: string | DatumAccessor + valueScale?: any + size?: number | DatumAccessor | DynamicSizeSpec + layout?: 'horizontal' | 'vertical' + gap?: number + + forceStrength?: number + simulationIterations?: number + + layers: any[] + + colors?: OrdinalColorsInstruction + theme?: Theme + borderWidth?: number | ComputedNodeAccessor + borderColor?: any + + enableGridX?: boolean + gridXValues?: number[] + enableGridY?: boolean + gridYValues?: number[] + + axisTop?: any + axisRight?: any + axisBottom?: any + axisLeft?: any + + isInteractive?: boolean + onMouseEnter?: SwarmPlotMouseHandler + onMouseMove?: SwarmPlotMouseHandler + onMouseLeave?: SwarmPlotMouseHandler + } + + export type SwarmPlotProps = CommonSwarmPlotProps & MotionProps + + export class SwarmPlot extends Component {} + export class ResponsiveSwarmPlot extends Component {} + + export type SwarmPlotCanvasProps = CommonSwarmPlotProps & { + pixelRatio?: number + } + + export class SwarmPlotCanvas extends Component {} + export class ResponsiveSwarmPlotCanvas extends Component {} +} diff --git a/packages/swarmplot/package.json b/packages/swarmplot/package.json index 2b5b8a630..059aaf5c9 100644 --- a/packages/swarmplot/package.json +++ b/packages/swarmplot/package.json @@ -29,12 +29,11 @@ "@nivo/core": "0.55.0", "@nivo/legends": "0.55.0", "@nivo/scales": "0.55.0", + "@nivo/voronoi": "0.55.0", "d3-force": "^2.0.1", - "d3-quadtree": "^1.0.6", "d3-scale": "^3.0.0", "lodash": "^4.17.11", - "react-motion": "^0.5.2", - "recompose": "^0.30.0" + "react-motion": "^0.5.2" }, "peerDependencies": { "prop-types": ">= 15.5.10 < 16.0.0", diff --git a/packages/swarmplot/src/AnimatedSwarmPlotNodes.js b/packages/swarmplot/src/AnimatedSwarmPlotNodes.js index c618b687f..c07debcfa 100644 --- a/packages/swarmplot/src/AnimatedSwarmPlotNodes.js +++ b/packages/swarmplot/src/AnimatedSwarmPlotNodes.js @@ -6,71 +6,111 @@ * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -import React, { memo } from 'react' +import React, { memo, Fragment } from 'react' +import PropTypes from 'prop-types' import { TransitionMotion, spring } from 'react-motion' import { interpolateColor, getInterpolatedColor } from '@nivo/colors' -const willEnter = ({ style, ...rest }) => ({ +const willEnter = ({ style }) => ({ x: style.x.val, y: style.y.val, - scale: 0, + size: style.size.val, colorR: style.colorR.val, colorG: style.colorG.val, colorB: style.colorB.val, + scale: 0, }) const willLeave = springConfig => ({ style }) => ({ x: style.x, y: style.y, - scale: spring(0, springConfig), + size: style.size, colorR: style.colorR, colorG: style.colorG, colorB: style.colorB, + scale: spring(0, springConfig), }) -const AnimatedSwarmPlotNodes = memo(({ nodes, nodeSize, motionStiffness, motionDamping }) => { - const springConfig = { - stiffness: motionStiffness, - damping: motionDamping, - } +const AnimatedSwarmPlotNodes = memo( + ({ + nodes, + renderNode, + getBorderWidth, + getBorderColor, + motionStiffness, + motionDamping, + isInteractive, + onMouseEnter, + onMouseMove, + onMouseLeave, + onClick, + }) => { + const springConfig = { + stiffness: motionStiffness, + damping: motionDamping, + } - return ( - ({ - key: node.uid, - data: node, - style: { - x: spring(node.x, springConfig), - y: spring(node.y, springConfig), - scale: spring(1, springConfig), - ...interpolateColor(node.color, springConfig), - }, - }))} - > - {interpolatedStyles => ( - - {interpolatedStyles.map(({ key, style, data: node }) => { - const color = getInterpolatedColor(style) + return ( + ({ + key: node.id, + data: node, + style: { + x: spring(node.x, springConfig), + y: spring(node.y, springConfig), + size: spring(node.size, springConfig), + ...interpolateColor(node.color, springConfig), + scale: spring(1, springConfig), + }, + }))} + > + {interpolatedStyles => ( + <> + {interpolatedStyles.map(({ key, style, data: node }) => { + const color = getInterpolatedColor(style) - return ( - - ) - })} - - )} - - ) -}) + return ( + + {renderNode({ + node, + x: style.x, + y: style.y, + size: style.size, + scale: style.scale, + color, + borderWidth: getBorderWidth(node), + borderColor: getBorderColor(node), + isInteractive, + onMouseEnter, + onMouseMove, + onMouseLeave, + onClick, + })} + + ) + })} + + )} + + ) + } +) AnimatedSwarmPlotNodes.displayName = 'AnimatedSwarmPlotNodes' +AnimatedSwarmPlotNodes.propTypes = { + nodes: PropTypes.array.isRequired, + renderNode: PropTypes.func.isRequired, + getBorderWidth: PropTypes.func.isRequired, + getBorderColor: PropTypes.func.isRequired, + motionStiffness: PropTypes.number.isRequired, + motionDamping: PropTypes.number.isRequired, + isInteractive: PropTypes.bool.isRequired, + onMouseEnter: PropTypes.func, + onMouseMove: PropTypes.func, + onMouseLeave: PropTypes.func, + onClick: PropTypes.func, +} export default AnimatedSwarmPlotNodes diff --git a/packages/swarmplot/src/StaticSwarmPlotNodes.js b/packages/swarmplot/src/StaticSwarmPlotNodes.js index e69de29bb..cb5c67e19 100644 --- a/packages/swarmplot/src/StaticSwarmPlotNodes.js +++ b/packages/swarmplot/src/StaticSwarmPlotNodes.js @@ -0,0 +1,60 @@ +/* + * This file is part of the nivo project. + * + * Copyright 2016-present, Raphaël Benitte. + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +import React, { memo, Fragment } from 'react' +import PropTypes from 'prop-types' + +const StaticSwarmPlotNodes = memo( + ({ + nodes, + renderNode, + getBorderWidth, + getBorderColor, + isInteractive, + onMouseEnter, + onMouseMove, + onMouseLeave, + onClick, + }) => { + return nodes.map(node => { + return ( + + {renderNode({ + node, + x: node.x, + y: node.y, + size: node.size, + color: node.color, + borderWidth: getBorderWidth(node), + borderColor: getBorderColor(node), + isInteractive, + onMouseEnter, + onMouseMove, + onMouseLeave, + onClick, + })} + + ) + }) + } +) + +StaticSwarmPlotNodes.displayName = 'StaticSwarmPlotNodes' +StaticSwarmPlotNodes.propTypes = { + nodes: PropTypes.array.isRequired, + renderNode: PropTypes.func.isRequired, + getBorderWidth: PropTypes.func.isRequired, + getBorderColor: PropTypes.func.isRequired, + isInteractive: PropTypes.bool.isRequired, + onMouseEnter: PropTypes.func, + onMouseMove: PropTypes.func, + onMouseLeave: PropTypes.func, + onClick: PropTypes.func, +} + +export default StaticSwarmPlotNodes diff --git a/packages/swarmplot/src/SwarmPlot.js b/packages/swarmplot/src/SwarmPlot.js index 85df6d292..5e6f4ff18 100644 --- a/packages/swarmplot/src/SwarmPlot.js +++ b/packages/swarmplot/src/SwarmPlot.js @@ -6,38 +6,57 @@ * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -import React, { memo } from 'react' -import { TransitionMotion, spring } from 'react-motion' -import { SvgWrapper, withContainer, useDimensions, useTheme, useTooltip, Grid } from '@nivo/core' -import { interpolateColor, getInterpolatedColor } from '@nivo/colors' -import { Axes } from '@nivo/axes' +import React, { memo, Fragment } from 'react' +import { SvgWrapper, withContainer, useDimensions, useTheme } from '@nivo/core' +import { useInheritedColor } from '@nivo/colors' +import { Axes, Grid } from '@nivo/axes' +import { Mesh } from '@nivo/voronoi' import { SwarmPlotPropTypes, SwarmPlotDefaultProps } from './props' -import { useSwarmPlot } from './hooks' +import { useSwarmPlot, useBorderWidth, useNodeMouseHandlers } from './hooks' import AnimatedSwarmPlotNodes from './AnimatedSwarmPlotNodes' +import StaticSwarmPlotNodes from './StaticSwarmPlotNodes' +import SwarmPlotNode from './SwarmPlotNode' const SwarmPlot = memo( ({ width, height, margin: partialMargin, - colors, data, + groups, + groupBy, + identity, + label, + value, + valueFormat, + valueScale, + size, + spacing, layout, + gap, forceStrength, simulationIterations, - scale, - gap, - nodeSize, - nodePadding, + layers, + renderNode, + colors, + colorBy, borderWidth, - + borderColor, enableGridX, + gridXValues, enableGridY, + gridYValues, axisTop, axisRight, axisBottom, axisLeft, - + isInteractive, + useMesh, + debugMesh, + onMouseEnter, + onMouseMove, + onMouseLeave, + onClick, animate, motionStiffness, motionDamping, @@ -48,34 +67,48 @@ const SwarmPlot = memo( partialMargin ) const theme = useTheme() - const [showTooltip, hideTooltip] = useTooltip() const { nodes, xScale, yScale } = useSwarmPlot({ width: innerWidth, height: innerHeight, data, + groups, + groupBy, + identity, + label, + value, + valueFormat, + valueScale, + size, + spacing, layout, - scale, gap, - nodeSize, - nodePadding, colors, + colorBy, forceStrength, simulationIterations, }) - return ( - + const getBorderWidth = useBorderWidth(borderWidth) + const getBorderColor = useInheritedColor(borderColor, theme) + + const layerById = { + grid: ( + ), + axes: ( - {animate && ( - - )} + ), + mesh: null, + } + + const enableNodeInteractivity = isInteractive && !useMesh + const handlers = useNodeMouseHandlers({ + isEnabled: isInteractive, + onMouseEnter, + onMouseMove, + onMouseLeave, + onClick, + }) + + if (animate) { + layerById.nodes = ( + + ) + } else { + layerById.nodes = ( + + ) + } + + if (isInteractive === true && useMesh === true) { + layerById.mesh = ( + + ) + } + + const layerContext = { + nodes, + xScale, + yScale, + innerWidth, + innerHeight, + outerWidth, + outerHeight, + margin, + getBorderColor, + getBorderWidth, + animate, + motionStiffness, + motionDamping, + } + + return ( + + {layers.map((layer, i) => { + if (layerById[layer] !== undefined) { + return layerById[layer] + } + if (typeof layer === 'function') { + return {layer(layerContext)} + } + + return null + })} ) } @@ -107,6 +222,9 @@ const SwarmPlot = memo( SwarmPlot.displayName = 'SwarmPlot' SwarmPlot.propTypes = SwarmPlotPropTypes -SwarmPlot.defaultProps = SwarmPlotDefaultProps +SwarmPlot.defaultProps = { + ...SwarmPlotDefaultProps, + renderNode: props => , // eslint-disable-line react/display-name +} export default withContainer(SwarmPlot) diff --git a/packages/swarmplot/src/SwarmPlotCanvas.js b/packages/swarmplot/src/SwarmPlotCanvas.js index ad4e17b57..abff2d2af 100644 --- a/packages/swarmplot/src/SwarmPlotCanvas.js +++ b/packages/swarmplot/src/SwarmPlotCanvas.js @@ -1 +1,304 @@ -export default () => null +/* + * This file is part of the nivo project. + * + * Copyright 2016-present, Raphaël Benitte. + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +import React, { memo, useRef, useState, useEffect, useCallback } from 'react' +import { + getRelativeCursor, + isCursorInRect, + withContainer, + useDimensions, + useTheme, + useTooltip, +} from '@nivo/core' +import { useInheritedColor } from '@nivo/colors' +import { renderAxesToCanvas, renderGridLinesToCanvas } from '@nivo/axes' +import { useVoronoiMesh, renderVoronoiToCanvas, renderVoronoiCellToCanvas } from '@nivo/voronoi' +import { SwarmPlotCanvasDefaultProps, SwarmPlotCanvasPropTypes } from './props' +import { useSwarmPlot, useBorderWidth } from './hooks' +import SwarmPlotTooltip from './SwarmPlotTooltip' + +export const renderCanvasNode = (ctx, { node, getBorderWidth, getBorderColor }) => { + const nodeBorderWidth = getBorderWidth(node) + if (nodeBorderWidth > 0) { + ctx.strokeStyle = getBorderColor(node) + ctx.lineWidth = nodeBorderWidth + } + + ctx.beginPath() + ctx.arc(node.x, node.y, node.size / 2, 0, 2 * Math.PI) + ctx.fillStyle = node.color + ctx.fill() + + if (nodeBorderWidth > 0) { + ctx.stroke() + } +} + +const SwarmPlotCanvas = memo( + ({ + pixelRatio, + width, + height, + margin: partialMargin, + data, + groups, + groupBy, + identity, + label, + value, + valueFormat, + valueScale, + size, + spacing, + layout, + gap, + forceStrength, + simulationIterations, + layers, + renderNode, + colors, + colorBy, + borderWidth, + borderColor, + enableGridX, + gridXValues, + enableGridY, + gridYValues, + axisTop, + axisRight, + axisBottom, + axisLeft, + isInteractive, + onMouseEnter, + onMouseMove, + onMouseLeave, + onClick, + debugMesh, + }) => { + const canvasEl = useRef(null) + const [currentNode, setCurrentNode] = useState(null) + const { margin, innerWidth, innerHeight, outerWidth, outerHeight } = useDimensions( + width, + height, + partialMargin + ) + const theme = useTheme() + const [showTooltip, hideTooltip] = useTooltip() + + const { nodes, xScale, yScale } = useSwarmPlot({ + width: innerWidth, + height: innerHeight, + data, + groups, + groupBy, + identity, + label, + value, + valueFormat, + valueScale, + size, + spacing, + layout, + gap, + colors, + colorBy, + forceStrength, + simulationIterations, + }) + + const getBorderWidth = useBorderWidth(borderWidth) + const getBorderColor = useInheritedColor(borderColor, theme) + + const { delaunay, voronoi } = useVoronoiMesh({ + points: nodes, + width: innerWidth, + height: innerHeight, + debug: debugMesh, + }) + + useEffect(() => { + canvasEl.current.width = outerWidth * pixelRatio + canvasEl.current.height = outerHeight * pixelRatio + + const ctx = canvasEl.current.getContext('2d') + + ctx.scale(pixelRatio, pixelRatio) + + ctx.fillStyle = theme.background + ctx.fillRect(0, 0, outerWidth, outerHeight) + ctx.translate(margin.left, margin.top) + + layers.forEach(layer => { + if (layer === 'grid' && theme.grid.line.strokeWidth > 0) { + ctx.lineWidth = theme.grid.line.strokeWidth + ctx.strokeStyle = theme.grid.line.stroke + + enableGridX && + renderGridLinesToCanvas(ctx, { + width: innerWidth, + height: innerHeight, + scale: xScale, + axis: 'x', + values: gridXValues, + }) + + enableGridY && + renderGridLinesToCanvas(ctx, { + width: innerWidth, + height: innerHeight, + scale: yScale, + axis: 'y', + values: gridYValues, + }) + } + + if (layer === 'axes') { + renderAxesToCanvas(ctx, { + xScale, + yScale, + width: innerWidth, + height: innerHeight, + top: axisTop, + right: axisRight, + bottom: axisBottom, + left: axisLeft, + theme, + }) + } + + if (layer === 'nodes') { + nodes.forEach(node => { + renderNode(ctx, { + node, + getBorderWidth, + getBorderColor, + }) + }) + } + + if (layer === 'mesh' && debugMesh === true) { + renderVoronoiToCanvas(ctx, voronoi) + if (currentNode) { + renderVoronoiCellToCanvas(ctx, voronoi, currentNode.index) + } + } + + if (typeof layer === 'function') { + layer(ctx, { + nodes, + innerWidth, + innerHeight, + outerWidth, + outerHeight, + margin, + xScale, + yScale, + }) + } + }) + }, [ + canvasEl, + innerWidth, + innerHeight, + outerWidth, + outerHeight, + margin, + pixelRatio, + theme, + layers, + nodes, + xScale, + yScale, + voronoi, + currentNode, + ]) + + const getNodeFromMouseEvent = useCallback( + event => { + const [x, y] = getRelativeCursor(canvasEl.current, event) + if (!isCursorInRect(margin.left, margin.top, innerWidth, innerHeight, x, y)) + return null + + const nodeIndex = delaunay.find(x - margin.left, y - margin.top) + return nodes[nodeIndex] + }, + [canvasEl, margin, innerWidth, innerHeight, delaunay, setCurrentNode] + ) + + const handleMouseHover = useCallback( + event => { + const node = getNodeFromMouseEvent(event) + setCurrentNode(node) + onMouseMove && onMouseMove(node, event) + if (node) { + showTooltip(, event) + if ((!currentNode || currentNode.id !== node.id) && onMouseEnter) { + onMouseEnter(node, event) + } + if (currentNode && currentNode.id !== node.id && onMouseLeave) { + onMouseLeave(currentNode, event) + } + } else { + currentNode && onMouseLeave && onMouseLeave(currentNode, event) + hideTooltip() + } + }, + [ + getNodeFromMouseEvent, + currentNode, + onMouseEnter, + onMouseLeave, + showTooltip, + hideTooltip, + ] + ) + + const handleMouseLeave = useCallback( + event => { + hideTooltip() + setCurrentNode(null) + onMouseLeave && onMouseLeave(currentNode, event) + }, + [hideTooltip, setCurrentNode, currentNode, onMouseLeave] + ) + + const handleClick = useCallback( + event => { + const node = getNodeFromMouseEvent(event) + node && onClick && onClick(node, event) + }, + [getNodeFromMouseEvent, onClick] + ) + + return ( + + ) + } +) + +SwarmPlotCanvas.displayName = 'SwarmPlotCanvas' +SwarmPlotCanvas.propTypes = SwarmPlotCanvasPropTypes +SwarmPlotCanvas.defaultProps = { + ...SwarmPlotCanvasDefaultProps, + renderNode: renderCanvasNode, +} + +export default withContainer(SwarmPlotCanvas) diff --git a/packages/swarmplot/src/SwarmPlotNode.js b/packages/swarmplot/src/SwarmPlotNode.js index e69de29bb..8e66e8433 100644 --- a/packages/swarmplot/src/SwarmPlotNode.js +++ b/packages/swarmplot/src/SwarmPlotNode.js @@ -0,0 +1,77 @@ +/* + * This file is part of the nivo project. + * + * Copyright 2016-present, Raphaël Benitte. + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +import React, { memo, useCallback } from 'react' +import PropTypes from 'prop-types' + +const SwarmPlotNode = memo( + ({ + node, + x, + y, + size, + scale, + color, + borderWidth, + borderColor, + onMouseEnter, + onMouseMove, + onMouseLeave, + onClick, + }) => { + const handleMouseEnter = useCallback(event => onMouseEnter && onMouseEnter(node, event), [ + node, + onMouseEnter, + ]) + const handleMouseMove = useCallback(event => onMouseMove && onMouseEnter(node, event), [ + node, + onMouseMove, + ]) + const handleMouseLeave = useCallback(event => onMouseLeave && onMouseLeave(node, event), [ + node, + onMouseLeave, + ]) + const handleClick = useCallback(event => onClick && onClick(node, event), [node, onClick]) + + return ( + + ) + } +) + +SwarmPlotNode.displayName = 'SwarmPlotNode' +SwarmPlotNode.propTypes = { + node: PropTypes.object.isRequired, + x: PropTypes.number.isRequired, + y: PropTypes.number.isRequired, + size: PropTypes.number.isRequired, + scale: PropTypes.number.isRequired, + color: PropTypes.string.isRequired, + borderWidth: PropTypes.number.isRequired, + borderColor: PropTypes.string.isRequired, + isInteractive: PropTypes.bool.isRequired, + onMouseEnter: PropTypes.func, + onMouseMove: PropTypes.func, + onMouseLeave: PropTypes.func, + onClick: PropTypes.func, +} +SwarmPlotNode.defaultProps = { + scale: 1, +} + +export default SwarmPlotNode diff --git a/packages/swarmplot/src/SwarmPlotTooltip.js b/packages/swarmplot/src/SwarmPlotTooltip.js index b5f7baa0c..94001f049 100644 --- a/packages/swarmplot/src/SwarmPlotTooltip.js +++ b/packages/swarmplot/src/SwarmPlotTooltip.js @@ -10,23 +10,21 @@ import React from 'react' import PropTypes from 'prop-types' import { BasicTooltip } from '@nivo/core' -const SwarmPlotTooltip = ({ node, format, tooltip, theme }) => ( +const SwarmPlotTooltip = ({ node }) => ( ) SwarmPlotTooltip.propTypes = { - node: PropTypes.shape({}).isRequired, - format: PropTypes.oneOfType([PropTypes.string, PropTypes.func]), - tooltip: PropTypes.func, - theme: PropTypes.object.isRequired, + node: PropTypes.shape({ + label: PropTypes.string.isRequired, + value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired, + color: PropTypes.string.isRequired, + }).isRequired, } export default SwarmPlotTooltip diff --git a/packages/swarmplot/src/compute.js b/packages/swarmplot/src/compute.js index 56a863d5b..4e0a981bd 100644 --- a/packages/swarmplot/src/compute.js +++ b/packages/swarmplot/src/compute.js @@ -6,93 +6,125 @@ * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -import { scaleOrdinal } from 'd3-scale' +import get from 'lodash/get' +import isString from 'lodash/isString' +import isNumber from 'lodash/isNumber' +import isPlainObject from 'lodash/isPlainObject' +import { scaleOrdinal, scaleLinear } from 'd3-scale' import { forceSimulation, forceX, forceY, forceCollide } from 'd3-force' -import { generateSeriesAxis, computeScale } from '@nivo/scales' +import { computeScale } from '@nivo/scales' -export const computeSwarmPlotValueScale = ({ axis, scale, data, width, height }) => { - const values = generateSeriesAxis(data, axis, scale, { - getValue: d => d.value, - setValue: (d, v) => { - d.value = v - }, - }) +export const getSizeGenerator = size => { + if (typeof size === 'function') return size + if (isNumber(size)) return () => size + if (isPlainObject(size)) { + if (!isString(size.key)) { + throw new Error( + 'Size is invalid, key should be a string pointing to the property to use to determine node size' + ) + } + if (!Array.isArray(size.values) || size.values.length !== 2) { + throw new Error( + 'Size is invalid, values spec should be an array containing two values, min and max' + ) + } + if (!Array.isArray(size.sizes) || size.sizes.length !== 2) { + throw new Error( + 'Size is invalid, sizes spec should be an array containing two values, min and max' + ) + } - return computeScale({ ...scale, axis }, { [axis]: values }, width, height) + const sizeScale = scaleLinear() + .domain([size.values[0], size.values[1]]) + .range([size.sizes[0], size.sizes[1]]) + + return d => sizeScale(get(d, size.key)) + } + + throw new Error('Size is invalid, it should be either a function, a number or an object') +} + +export const computeValueScale = ({ width, height, axis, getValue, scale, data }) => { + const values = data.map(getValue) + const min = Math.min(...values) + const max = Math.max(...values) + + return computeScale({ ...scale, axis }, { [axis]: { min, max } }, width, height) } -export const computeSwarmPlotOrdinalScale = ({ axis, data, gap, width, height }) => { - const serieCount = data.length - let serieSize - if (data.length === 0) { - serieSize = axis === 'x' ? height : width - } else if (axis === 'x') { - serieSize = (height - gap * (serieCount - 1)) / serieCount +export const computeOrdinalScale = ({ width, height, axis, groups, gap }) => { + if (!Array.isArray(groups) || groups.length === 0) { + throw new Error(`'groups' should be an array containing at least one item`) + } + + const groupCount = groups.length + + let groupSize + if (axis === 'x') { + groupSize = (height - gap * (groupCount - 1)) / groupCount } else if (axis === 'y') { - serieSize = (width - gap * (serieCount - 1)) / serieCount + groupSize = (width - gap * (groupCount - 1)) / groupCount } - const range = data.map((d, i) => i * (serieSize + gap) + serieSize / 2) - const domain = data.map(d => d.id) + const range = groups.map((g, i) => i * (groupSize + gap) + groupSize / 2) - return scaleOrdinal(range).domain(domain) + return scaleOrdinal(range).domain(groups) } -export const computeSwarmPlotForces = ({ - axis, - valueScale, - ordinalScale, - nodeSize, - nodePadding, - forceStrength, -}) => { - const collisionRadius = (nodeSize + nodePadding / 2) / 2 - const collisionForce = forceCollide(collisionRadius) +export const computeForces = ({ axis, valueScale, ordinalScale, spacing, forceStrength }) => { + const collisionForce = forceCollide(d => d.size / 2 + spacing / 2) let xForce let yForce if (axis === 'x') { - xForce = forceX(d => valueScale(d.value)).strength(forceStrength) - yForce = forceY(d => ordinalScale(d.serieId)) + xForce = forceX(d => { + //console.log(d) + return valueScale(d.value) + }).strength(forceStrength) + yForce = forceY(d => ordinalScale(d.group)) } else if (axis === 'y') { - xForce = forceX(d => ordinalScale(d.serieId)) + xForce = forceX(d => ordinalScale(d.group)) yForce = forceY(d => valueScale(d.value)).strength(forceStrength) } return { x: xForce, y: yForce, collision: collisionForce } } -export const computeSwarmPlotNodes = ({ +export const computeNodes = ({ data, + getIdentity, layout, + getValue, valueScale, + getGroup, ordinalScale, + getSize, forces, simulationIterations, }) => { - const axis = layout === 'horizontal' ? 'x' : 'y' - const otherAxis = axis === 'x' ? 'y' : 'x' - - const nodes = data.reduce((acc, serie) => { - const serieNodes = serie.data.map(d => ({ - ...d, - uid: `${serie.id}.${d.id}`, - serieId: serie.id, - })) - const simulation = forceSimulation(serieNodes) - .force('x', forces.x) - .force('y', forces.y) - .force('collide', forces.collision) - .stop() - - simulation.tick(simulationIterations) - - return [...acc, ...simulation.nodes()] - }, []) + const config = { + horizontal: ['x', 'y'], + vertical: ['y', 'x'], + } + + const simulatedNodes = data.map(d => ({ + id: getIdentity(d), + group: getGroup(d), + value: getValue(d), + size: getSize(d), + data: { ...d }, + })) + const simulation = forceSimulation(simulatedNodes) + .force('x', forces.x) + .force('y', forces.y) + .force('collide', forces.collision) + .stop() + + simulation.tick(simulationIterations) return { - [`${axis}Scale`]: valueScale, - [`${otherAxis}Scale`]: ordinalScale, - nodes, + [`${config[layout][0]}Scale`]: valueScale, + [`${config[layout][1]}Scale`]: ordinalScale, + nodes: simulation.nodes(), } } diff --git a/packages/swarmplot/src/hooks.js b/packages/swarmplot/src/hooks.js index 89e53ebc7..33ed85c08 100644 --- a/packages/swarmplot/src/hooks.js +++ b/packages/swarmplot/src/hooks.js @@ -6,122 +6,229 @@ * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -import { useMemo } from 'react' +import React, { useMemo, useCallback } from 'react' +import get from 'lodash/get' +import { useValueFormatter, useTooltip } from '@nivo/core' import { useOrdinalColorScale } from '@nivo/colors' import { - computeSwarmPlotValueScale, - computeSwarmPlotOrdinalScale, - computeSwarmPlotForces, - computeSwarmPlotNodes, + computeValueScale, + computeOrdinalScale, + computeForces, + computeNodes, + getSizeGenerator, } from './compute' +import SwarmPlotTooltip from './SwarmPlotTooltip' -export const useSwarmPlotValueScale = ({ axis, scale, data, width, height }) => { - return useMemo(() => computeSwarmPlotValueScale({ axis, scale, data, width, height }), [ - axis, - scale, - data, +export const useValueScale = ({ width, height, axis, getValue, scale, data }) => + useMemo( + () => + computeValueScale({ + width, + height, + axis, + getValue, + scale, + data, + }), + [width, height, axis, getValue, scale, data] + ) + +export const useOrdinalScale = ({ width, height, axis, groups, gap }) => + useMemo(() => computeOrdinalScale({ width, height, axis, groups, gap }), [ width, height, - ]) -} - -export const useSwarmPlotOrdinalScale = ({ axis, data, gap, width, height }) => { - return useMemo(() => computeSwarmPlotOrdinalScale({ axis, data, gap, width, height }), [ axis, - data, + groups, gap, - width, - height, ]) -} -export const useSwarmPlotForces = ({ - axis, - valueScale, - ordinalScale, - nodeSize, - nodePadding, - forceStrength, -}) => { - return useMemo( +export const useForces = ({ axis, valueScale, ordinalScale, getSize, spacing, forceStrength }) => + useMemo( () => - computeSwarmPlotForces({ + computeForces({ axis, valueScale, ordinalScale, - nodeSize, - nodePadding, + getSize, + spacing, forceStrength, }), - [axis, valueScale, ordinalScale, nodeSize, nodePadding, forceStrength] + [axis, valueScale, ordinalScale, getSize, spacing, forceStrength] ) + +const useSize = size => useMemo(() => getSizeGenerator(size), [size]) + +const getAccessor = instruction => { + if (typeof instruction === 'function') return instruction + return d => get(d, instruction) } export const useSwarmPlot = ({ - data, - layout, - colors, - scale, width, height, + data, + identity, + label, + groups, + groupBy, + value, + valueFormat, + valueScale: valueScaleConfig, + size, + spacing, + layout, gap, - nodeSize, - nodePadding, + colors, + colorBy, forceStrength, simulationIterations, }) => { const axis = layout === 'horizontal' ? 'x' : 'y' - const valueScale = useSwarmPlotValueScale({ - axis, - scale, - data, + const getIdentity = useMemo(() => getAccessor(identity), [identity]) + const getLabel = useMemo(() => getAccessor(label), [label]) + const getValue = useMemo(() => getAccessor(value), [value]) + const formatValue = useValueFormatter(valueFormat) + const getGroup = useMemo(() => getAccessor(groupBy), [groupBy]) + const getSize = useSize(size) + const getColor = useOrdinalColorScale(colors, colorBy) + + const valueScale = useValueScale({ width, height, - }) - const ordinalScale = useSwarmPlotOrdinalScale({ axis, + getValue, + scale: valueScaleConfig, data, - gap, + }) + + const ordinalScale = useOrdinalScale({ width, height, + axis, + groups, + gap, }) - const forces = useSwarmPlotForces({ + + const forces = useForces({ axis, valueScale, ordinalScale, - nodeSize, - nodePadding, + spacing, forceStrength, }) const { nodes, xScale, yScale } = useMemo( () => - computeSwarmPlotNodes({ + computeNodes({ data, + getIdentity, layout, + getValue, valueScale, + getGroup, ordinalScale, + getSize, forces, simulationIterations, }), - [data, layout, valueScale, ordinalScale, forces, simulationIterations] + [ + data, + getIdentity, + layout, + getValue, + valueScale, + getGroup, + ordinalScale, + getSize, + forces, + simulationIterations, + ] ) - const colorScale = useOrdinalColorScale(colors, 'serieId') - - const coloredNodes = useMemo(() => nodes.map(node => ({ ...node, color: colorScale(node) }))) + const augmentedNodes = useMemo( + () => + nodes.map(node => ({ + id: node.id, + index: node.index, + group: node.group, + label: getLabel(node), + value: node.value, + formattedValue: formatValue(node.value), + x: node.x, + y: node.y, + size: node.size, + color: getColor(node), + data: node.data, + })), + [nodes, getLabel, formatValue, getColor] + ) return { - nodes: coloredNodes, + nodes: augmentedNodes, xScale, yScale, - colorScale, + getColor, } } -/* -withPropsOnChange(['borderColor'], ({ borderColor }) => ({ - getBorderColor: getInheritedColorGenerator(borderColor), -})), -*/ +export const useBorderWidth = borderWidth => + useMemo(() => { + if (typeof borderWidth === 'function') return borderWidth + return () => borderWidth + }, [borderWidth]) + +export const useNodeMouseHandlers = ({ + isEnabled, + onMouseEnter, + onMouseMove, + onMouseLeave, + onClick, +}) => { + const [showTooltip, hideTooltip] = useTooltip() + const showNodeTooltip = useMemo( + () => (node, event) => showTooltip(, event), + [showTooltip] + ) + + const mouseEnterHandler = useCallback( + (node, event) => { + if (!isEnabled) return + showNodeTooltip(node, event) + onMouseEnter && onMouseEnter(node, event) + }, + [isEnabled, onMouseEnter] + ) + + const mouseMoveHandler = useCallback( + (node, event) => { + if (!isEnabled) return + showNodeTooltip(node, event) + onMouseMove && onMouseMove(node, event) + }, + [isEnabled, onMouseMove] + ) + + const mouseLeaveHandler = useCallback( + (node, event) => { + if (!isEnabled) return + hideTooltip() + onMouseLeave && onMouseLeave(node, event) + }, + [isEnabled, onMouseLeave] + ) + + const clickHandler = useCallback( + (node, event) => { + isEnabled && onClick && onClick(node, event) + }, + [isEnabled, onClick] + ) + + return { + onMouseEnter: mouseEnterHandler, + onMouseMove: mouseMoveHandler, + onMouseLeave: mouseLeaveHandler, + onClick: clickHandler, + } +} diff --git a/packages/swarmplot/src/index.js b/packages/swarmplot/src/index.js index ea900cc89..cbf4c34ff 100644 --- a/packages/swarmplot/src/index.js +++ b/packages/swarmplot/src/index.js @@ -10,5 +10,7 @@ export { default as SwarmPlot } from './SwarmPlot' export { default as ResponsiveSwarmPlot } from './ResponsiveSwarmPlot' export { default as SwarmPlotCanvas } from './SwarmPlotCanvas' export { default as ResponsiveSwarmPlotCanvas } from './ResponsiveSwarmPlotCanvas' +export { default as SwarmPlotTooltip } from './SwarmPlotTooltip' +export * from './compute' export * from './hooks' export * from './props' diff --git a/packages/swarmplot/src/props.js b/packages/swarmplot/src/props.js index aef9182bf..edf9394ae 100644 --- a/packages/swarmplot/src/props.js +++ b/packages/swarmplot/src/props.js @@ -8,56 +8,68 @@ */ import PropTypes from 'prop-types' import { axisPropType } from '@nivo/axes' -import { ordinalColorsPropType } from '@nivo/colors' +import { motionPropTypes } from '@nivo/core' +import { + ordinalColorsPropType, + inheritedColorPropType, + colorPropertyAccessorPropType, +} from '@nivo/colors' import { scalePropType } from '@nivo/scales' const commonPropTypes = { - data: PropTypes.arrayOf( + data: PropTypes.arrayOf(PropTypes.object).isRequired, + + groups: PropTypes.arrayOf(PropTypes.string).isRequired, + groupBy: PropTypes.oneOfType([PropTypes.string, PropTypes.func]).isRequired, + identity: PropTypes.oneOfType([PropTypes.string, PropTypes.func]).isRequired, + label: PropTypes.oneOfType([PropTypes.string, PropTypes.func]).isRequired, + value: PropTypes.oneOfType([PropTypes.string, PropTypes.func]).isRequired, + valueScale: scalePropType.isRequired, + size: PropTypes.oneOfType([ + PropTypes.number, PropTypes.shape({ - id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired, - data: PropTypes.arrayOf( - PropTypes.shape({ - value: PropTypes.oneOfType([ - PropTypes.string, - PropTypes.number, - PropTypes.instanceOf(Date), - ]).isRequired, - }) - ).isRequired, - }) - ).isRequired, - - scale: scalePropType.isRequired, - + key: PropTypes.string.isRequired, + values: PropTypes.arrayOf(PropTypes.number).isRequired, + sizes: PropTypes.arrayOf(PropTypes.number).isRequired, + }), + PropTypes.func, + ]).isRequired, layout: PropTypes.oneOf(['horizontal', 'vertical']).isRequired, + gap: PropTypes.number.isRequired, + forceStrength: PropTypes.number.isRequired, simulationIterations: PropTypes.number.isRequired, - //layers: PropTypes.arrayOf( - // PropTypes.oneOfType([PropTypes.oneOf(['grid', 'axes', 'nodes']), PropTypes.func]) - //).isRequired, - // renderNode: PropTypes.func.isRequired, + layers: PropTypes.arrayOf( + PropTypes.oneOfType([PropTypes.oneOf(['grid', 'axes', 'nodes', 'mesh']), PropTypes.func]) + ).isRequired, + renderNode: PropTypes.func.isRequired, - gap: PropTypes.number.isRequired, - nodeSize: PropTypes.number.isRequired, - nodePadding: PropTypes.number.isRequired, colors: ordinalColorsPropType.isRequired, - borderWidth: PropTypes.number.isRequired, - borderColor: PropTypes.any.isRequired, + colorBy: colorPropertyAccessorPropType.isRequired, + borderWidth: PropTypes.oneOfType([PropTypes.number, PropTypes.func]), + borderColor: inheritedColorPropType.isRequired, enableGridX: PropTypes.bool.isRequired, + gridXValues: PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.string, PropTypes.number])), enableGridY: PropTypes.bool.isRequired, + gridYValues: PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.string, PropTypes.number])), + axisTop: axisPropType, axisRight: axisPropType, axisBottom: axisPropType, axisLeft: axisPropType, isInteractive: PropTypes.bool.isRequired, - onMouseEnter: PropTypes.func.isRequired, - onMouseMove: PropTypes.func.isRequired, - onMouseLeave: PropTypes.func.isRequired, - onClick: PropTypes.func.isRequired, + onMouseEnter: PropTypes.func, + onMouseMove: PropTypes.func, + onMouseLeave: PropTypes.func, + onClick: PropTypes.func, + useMesh: PropTypes.bool.isRequired, + debugMesh: PropTypes.bool.isRequired, tooltip: PropTypes.any, + + ...motionPropTypes, } export const SwarmPlotPropTypes = { @@ -70,35 +82,36 @@ export const SwarmPlotCanvasPropTypes = { } const commonDefaultProps = { - scale: { - type: 'linear', - min: 0, - max: 'auto', - }, + groupBy: 'group', + identity: 'id', + label: 'id', + value: 'value', + valueScale: { type: 'linear', min: 0, max: 'auto' }, + size: 6, + spacing: 2, + layout: 'vertical', + gap: 0, - layout: 'horizontal', - forceStrength: 4, - simulationIterations: 160, - layers: ['grid', 'axes', 'nodes'], + forceStrength: 1, + simulationIterations: 120, + + layers: ['grid', 'axes', 'nodes', 'mesh'], - gap: 0, - nodeSize: 6, - nodePadding: 2, colors: { scheme: 'nivo' }, + colorBy: 'group', borderWidth: 0, - borderColor: 'inherit:darker(.3)', + borderColor: 'none', enableGridX: true, enableGridY: true, axisTop: {}, + axisRight: {}, axisBottom: {}, axisLeft: {}, isInteractive: true, - onMouseEnter: () => {}, - onMouseLeave: () => {}, - onMouseMove: () => {}, - onClick: () => {}, + useMesh: false, + debugMesh: false, animate: true, motionStiffness: 90, diff --git a/packages/swarmplot/stories/SwarmPlot.stories.js b/packages/swarmplot/stories/SwarmPlot.stories.js new file mode 100644 index 000000000..9568dab67 --- /dev/null +++ b/packages/swarmplot/stories/SwarmPlot.stories.js @@ -0,0 +1,43 @@ +/* + * This file is part of the nivo project. + * + * Copyright 2016-present, Raphaël Benitte. + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +import React from 'react' +import { storiesOf } from '@storybook/react' +import { generateSwarmPlotData } from '@nivo/generators' +import { SwarmPlot } from '../src' +import SwarmPlotLayers from './SwarmPlotLayers' +import SwarmPlotRenderNode from './SwarmPlotRenderNode' + +const commonProps = { + width: 600, + height: 360, + margin: { + top: 40, + right: 40, + bottom: 40, + left: 40, + }, + groupBy: 'group', + identity: 'id', + value: 'price', + valueScale: { + type: 'linear', + min: 0, + max: 500, + }, + size: 10, + ...generateSwarmPlotData(['group A', 'group B', 'group C'], { min: 40, max: 60 }), +} + +const stories = storiesOf('SwarmPlot', module) + +stories.add('default', () => ) + +stories.add('extra layers', () => ) + +stories.add('custom node rendering', () => ) diff --git a/packages/swarmplot/stories/SwarmPlotCanvas.stories.js b/packages/swarmplot/stories/SwarmPlotCanvas.stories.js new file mode 100644 index 000000000..c3030dd72 --- /dev/null +++ b/packages/swarmplot/stories/SwarmPlotCanvas.stories.js @@ -0,0 +1,40 @@ +/* + * This file is part of the nivo project. + * + * Copyright 2016-present, Raphaël Benitte. + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +import React from 'react' +import { storiesOf } from '@storybook/react' +import { generateSwarmPlotData } from '@nivo/generators' +import { SwarmPlotCanvas } from '../src' + +const commonProps = { + width: 600, + height: 360, + margin: { + top: 40, + right: 40, + bottom: 40, + left: 40, + }, + groupBy: 'group', + identity: 'id', + value: 'price', + valueScale: { + type: 'linear', + min: 0, + max: 500, + }, + size: 10, + ...generateSwarmPlotData(['group A', 'group B', 'group C', 'group D', 'group E'], { + min: 40, + max: 60, + }), +} + +const stories = storiesOf('SwarmPlotCanvas', module) + +stories.add('default', () => ) diff --git a/packages/swarmplot/stories/SwarmPlotLayers.js b/packages/swarmplot/stories/SwarmPlotLayers.js new file mode 100644 index 000000000..c69f8a3e7 --- /dev/null +++ b/packages/swarmplot/stories/SwarmPlotLayers.js @@ -0,0 +1,159 @@ +/* + * This file is part of the nivo project. + * + * Copyright 2016-present, Raphaël Benitte. + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +import React, { useMemo, useState } from 'react' +import { generateSwarmPlotData } from '@nivo/generators' +import { PatternLines } from '../../core/src' +import { SwarmPlot } from '../src' + +const backgroundLayer = ({ xScale, innerHeight }) => ( + <> + + + + + + the sweet spot + + + the sweet spot + + +) + +const Annotations = ({ nodes, margin, innerWidth, currentIndex }) => { + const node = nodes[currentIndex] + const radius = node.size * 0.6 + const labelBefore = node.x > innerWidth / 2 + const labelX = labelBefore ? node.x - 140 : node.x + 140 + const linePath = ` + M${node.x},${node.y - radius} + L${node.x},${margin.top * -0.5} + L${labelX},${margin.top * -0.5} + ` + + return ( + + + + + + + Annotation + + + ) +} + +const SwarmPlotLayers = () => { + const data = useMemo(() => generateSwarmPlotData(['group'], { min: 60, max: 60 }), []) + const [currentIndex, setCurrentIndex] = useState(13) + + return ( + , + ]} + theme={{ background: 'rgb(199, 234, 229)' }} + colors={{ scheme: 'brown_blueGreen' }} + colorBy="id" + borderWidth={4} + borderColor="rgb(199, 234, 229)" + onClick={node => setCurrentIndex(node.index)} + enableGridY={false} + axisLeft={null} + axisRight={null} + layout="horizontal" + /> + ) +} + +export default SwarmPlotLayers diff --git a/packages/swarmplot/stories/SwarmPlotRenderNode.js b/packages/swarmplot/stories/SwarmPlotRenderNode.js new file mode 100644 index 000000000..cf6dce35f --- /dev/null +++ b/packages/swarmplot/stories/SwarmPlotRenderNode.js @@ -0,0 +1,137 @@ +/* + * This file is part of the nivo project. + * + * Copyright 2016-present, Raphaël Benitte. + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +import React, { useMemo } from 'react' +import { generateSwarmPlotData } from '@nivo/generators' +import { useOrdinalColorScale } from '../../colors/src' +import { usePie } from '../../pie/src' +import { SwarmPlot } from '../src' + +const CustomNode = d => { + const getArcColor = useOrdinalColorScale({ scheme: 'purple_orange' }, v => v) + const { arcs, arcGenerator } = usePie({ + data: d.node.data.categories, + radius: d.size / 2, + innerRadius: (d.size / 2) * 0.7, + sortByValue: true, + }) + + return ( + + + + {arcs.map((arc, i) => { + return + })} + {d.size > 52 && ( + + {d.node.value} + + )} + + ) +} + +const shadowsLayer = ({ nodes }) => { + return nodes.map(node => ( + + )) +} + +const theme = { + background: 'rgb(216, 218, 235)', + axis: { + ticks: { + line: { + stroke: 'rgb(84, 39, 136)', + }, + text: { + fill: 'rgb(84, 39, 136)', + fontWeight: 600, + }, + }, + legend: { + text: { + fill: 'rgb(84, 39, 136)', + fontSize: 15, + }, + }, + }, + grid: { + line: { + stroke: 'rgb(128, 115, 172)', + strokeDasharray: '2 4', + strokeWidth: 2, + }, + }, +} + +const SwarmPlotRenderNode = () => { + const data = useMemo( + () => generateSwarmPlotData(['group'], { min: 32, max: 32, categoryCount: 9 }), + [] + ) + + return ( + } + layers={['grid', 'axes', shadowsLayer, 'nodes']} + layout="horizontal" + theme={theme} + /> + ) +} + +export default SwarmPlotRenderNode diff --git a/packages/swarmplot/tests/.eslintrc.yml b/packages/swarmplot/tests/.eslintrc.yml new file mode 100644 index 000000000..2f8de9aea --- /dev/null +++ b/packages/swarmplot/tests/.eslintrc.yml @@ -0,0 +1,2 @@ +env: + jest: true diff --git a/packages/swarmplot/tests/compute.test.js b/packages/swarmplot/tests/compute.test.js new file mode 100644 index 000000000..0b0326b51 --- /dev/null +++ b/packages/swarmplot/tests/compute.test.js @@ -0,0 +1,146 @@ +/* + * This file is part of the nivo project. + * + * Copyright 2016-present, Raphaël Benitte. + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +import { getSizeGenerator, computeValueScale, computeOrdinalScale } from '../src' + +describe(`getSizeGenerator`, () => { + it(`should accept a fixed value`, () => { + const generator = getSizeGenerator(10) + + expect(generator()).toBe(10) + }) + + it(`should accept a custom function`, () => { + const sizeFunction = () => 20 + const generator = getSizeGenerator(sizeFunction) + + expect(generator).toBe(sizeFunction) + expect(generator()).toBe(20) + }) + + describe('varying size using object spec', () => { + it(`should throw if key is undefined`, () => { + expect(() => { + getSizeGenerator({}) + }).toThrow( + 'Size is invalid, key should be a string pointing to the property to use to determine node size' + ) + }) + it(`should throw if key is not a string`, () => { + expect(() => { + getSizeGenerator({ key: 0 }) + }).toThrow( + 'Size is invalid, key should be a string pointing to the property to use to determine node size' + ) + }) + it(`should throw if values is not an array with two values`, () => { + expect(() => { + getSizeGenerator({ key: 'size' }) + }).toThrow( + 'Size is invalid, values spec should be an array containing two values, min and max' + ) + expect(() => { + getSizeGenerator({ key: 'size', values: 'string' }) + }).toThrow( + 'Size is invalid, values spec should be an array containing two values, min and max' + ) + expect(() => { + getSizeGenerator({ key: 'size', values: [0] }) + }).toThrow( + 'Size is invalid, values spec should be an array containing two values, min and max' + ) + }) + it(`should throw if sizes is not an array with two values`, () => { + expect(() => { + getSizeGenerator({ key: 'size', values: [0, 1] }) + }).toThrow( + 'Size is invalid, sizes spec should be an array containing two values, min and max' + ) + expect(() => { + getSizeGenerator({ key: 'size', values: [0, 1], sizes: 'string' }) + }).toThrow( + 'Size is invalid, sizes spec should be an array containing two values, min and max' + ) + expect(() => { + getSizeGenerator({ key: 'size', values: [0, 1], sizes: [0] }) + }).toThrow( + 'Size is invalid, sizes spec should be an array containing two values, min and max' + ) + }) + it(`should return a dynamic size function`, () => { + const generator = getSizeGenerator({ + key: 'value', + values: [0, 1], + sizes: [0, 10], + }) + expect(generator({ value: 0.5 })).toBe(5) + }) + }) + + it(`should throw if size config is invalid`, () => { + expect(() => { + getSizeGenerator('whatever') + }).toThrow('Size is invalid, it should be either a function, a number or an object') + }) +}) + +describe(`computeValueScale`, () => { + it(`should be able to compute a linear scale`, () => { + const data = [0, 2, 4, 8, 10] + const scale = computeValueScale({ + data, + width: 100, + height: 0, + axis: 'x', + getValue: d => d, + scale: { + type: 'linear', + min: 'auto', + max: 'auto', + }, + }) + + expect(scale.domain()).toEqual([0, 10]) + expect(scale.range()).toEqual([0, 100]) + expect(scale(5)).toBe(50) + }) +}) + +describe(`computeOrdinalScale`, () => { + const ordinalScaleArgs = { + width: 100, + height: 100, + axis: 'x', + groups: ['A', 'B', 'C', 'D'], + gap: 0, + } + it(`should throw if groups is not an array`, () => { + expect(() => { + computeOrdinalScale({ + ...ordinalScaleArgs, + groups: 'string', + }) + }).toThrow(`'groups' should be an array containing at least one item`) + }) + it(`should throw if groups doesn't contain at least one item`, () => { + expect(() => { + computeOrdinalScale({ + ...ordinalScaleArgs, + groups: [], + }) + }).toThrow(`'groups' should be an array containing at least one item`) + }) + it(`should compute a valid scale`, () => { + const scale = computeOrdinalScale(ordinalScaleArgs) + + expect(scale.domain()).toEqual(ordinalScaleArgs.groups) + expect(scale.range()).toEqual([12.5, 37.5, 62.5, 87.5]) + expect(scale('A')).toBe(12.5) + expect(scale('C')).toBe(62.5) + }) +}) diff --git a/packages/treemap/src/enhance.js b/packages/treemap/src/enhance.js index 1eeb5c6ab..4db9c08ea 100644 --- a/packages/treemap/src/enhance.js +++ b/packages/treemap/src/enhance.js @@ -37,8 +37,8 @@ const commonEnhancers = [ withDimensions(), withTheme(), withMotion(), - withPropsOnChange(['colors'], ({ colors }) => ({ - getColor: getOrdinalColorScale(colors, 'depth'), + withPropsOnChange(['colors', 'colorBy'], ({ colors, colorBy }) => ({ + getColor: getOrdinalColorScale(colors, colorBy), })), withPropsOnChange(['identity'], ({ identity }) => ({ getIdentity: getAccessorFor(identity), diff --git a/packages/treemap/src/props.js b/packages/treemap/src/props.js index d611af560..6affe7908 100644 --- a/packages/treemap/src/props.js +++ b/packages/treemap/src/props.js @@ -8,7 +8,7 @@ */ import PropTypes from 'prop-types' import { noop, treeMapTilePropType, defsPropTypes } from '@nivo/core' -import { ordinalColorsPropType } from '@nivo/colors' +import { ordinalColorsPropType, colorPropertyAccessorPropType } from '@nivo/colors' import TreeMapNode from './TreeMapNode' import TreeMapHtmlNode from './TreeMapHtmlNode' @@ -28,6 +28,7 @@ const commonPropTypes = { // styling // theme managed by `withTheme()` HOC colors: ordinalColorsPropType.isRequired, + colorBy: colorPropertyAccessorPropType.isRequired, leavesOnly: PropTypes.bool.isRequired, tile: treeMapTilePropType.isRequired, @@ -80,6 +81,7 @@ const commonDefaultProps = { leavesOnly: false, colors: { scheme: 'nivo' }, + colorIdentity: 'depth', enableLabel: true, label: 'id', diff --git a/packages/voronoi/src/Mesh.js b/packages/voronoi/src/Mesh.js index 098abf19e..1c085f4b0 100644 --- a/packages/voronoi/src/Mesh.js +++ b/packages/voronoi/src/Mesh.js @@ -6,143 +6,121 @@ * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -import React, { Component } from 'react' +import React, { useRef, useState, useCallback, useMemo } from 'react' import PropTypes from 'prop-types' -import compose from 'recompose/compose' -import defaultProps from 'recompose/defaultProps' -import withPropsOnChange from 'recompose/withPropsOnChange' import { getRelativeCursor } from '@nivo/core' -import { computeMeshPoints, computeMesh } from './computeMesh' - -class Mesh extends Component { - static propTypes = { - width: PropTypes.number.isRequired, - height: PropTypes.number.isRequired, - points: PropTypes.array.isRequired, - xAccessor: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.func]) - .isRequired, - yAccessor: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.func]) - .isRequired, - onMouseEnter: PropTypes.func, - onMouseMove: PropTypes.func, - onMouseLeave: PropTypes.func, - onClick: PropTypes.func, - debug: PropTypes.bool.isRequired, - - delaunay: PropTypes.shape({ - find: PropTypes.func.isRequired, - }).isRequired, - voronoi: PropTypes.shape({ - renderCell: PropTypes.func.isRequired, - }), - voronoiPath: PropTypes.string, - } - - state = { - index: null, - } - - constructor(props) { - super(props) - this.setRectRef = element => { - this.rect = element - } - } - - handleMouseIn = (handler, event) => { - const { delaunay, points } = this.props - - const [x, y] = getRelativeCursor(this.rect, event) - const index = delaunay.find(x, y) - - if (handler !== undefined) { - handler(points[index], event) - } - - if (this.state.index !== index) { - this.setState({ index }) - } - } - - handleMouseEnter = event => { - this.handleMouseIn(this.props.onMouseEnter, event) - } - - handleMouseMove = event => { - this.handleMouseIn(this.props.onMouseMove, event) - } - - handleMouseLeave = event => { - const { onMouseLeave, points } = this.props - const { index } = this.state - - if (onMouseLeave !== undefined) { - onMouseLeave(points[index], event) - } - - this.setState({ index: null }) - } - - handleClick = event => { - const { onClick, points } = this.props - const { index } = this.state - - if (onClick === undefined || index === null) return - - onClick(points[index], event) - } - - render() { - const { width, height, voronoi, voronoiPath, debug } = this.props - const { index } = this.state - - return ( - - {debug && } - {index !== null && debug && ( - - )} - - - ) - } -} - -const enhance = compose( - defaultProps({ - xAccessor: 'x', - yAccessor: 'y', - debug: false, - }), - withPropsOnChange(['points', 'xAccessor', 'yAccessor'], ({ points, xAccessor, yAccessor }) => ({ - points2d: computeMeshPoints({ points, xAccessor, yAccessor }).points, - })), - withPropsOnChange( - ['points2d', 'width', 'height', 'debug'], - ({ points2d, width, height, debug }) => { - const mesh = computeMesh({ points: points2d, width, height, debug }) - - let voronoiPath - if (debug === true) { - voronoiPath = mesh.voronoi.render() +import { useVoronoiMesh } from './hooks' + +const Mesh = ({ + nodes, + width, + height, + x, + y, + debug, + + onMouseEnter, + onMouseMove, + onMouseLeave, + onClick, +}) => { + const elementRef = useRef(null) + const [currentIndex, setCurrentIndex] = useState(null) + + const { delaunay, voronoi } = useVoronoiMesh({ + points: nodes, + x, + y, + width, + height, + debug, + }) + const voronoiPath = useMemo(() => (debug ? voronoi.render() : undefined)) + + const getIndexAndNodeFromEvent = useCallback( + event => { + const [x, y] = getRelativeCursor(elementRef.current, event) + const index = delaunay.find(x, y) + + return [index, index !== undefined ? nodes[index] : null] + }, + [delaunay] + ) + const handleMouseEnter = useCallback( + event => { + const [index, node] = getIndexAndNodeFromEvent(event) + if (currentIndex !== index) setCurrentIndex(index) + node && onMouseEnter && onMouseEnter(node, event) + }, + [getIndexAndNodeFromEvent, setCurrentIndex] + ) + const handleMouseMove = useCallback( + event => { + const [index, node] = getIndexAndNodeFromEvent(event) + if (currentIndex !== index) setCurrentIndex(index) + node && onMouseMove && onMouseMove(node, event) + }, + [getIndexAndNodeFromEvent, setCurrentIndex] + ) + const handleMouseLeave = useCallback( + event => { + setCurrentIndex(null) + if (onMouseLeave) { + let previousNode + if (currentIndex !== undefined && currentIndex !== null) { + previousNode = nodes[currentIndex] + } + onMouseLeave(previousNode, event) } + }, + [setCurrentIndex] + ) + const handleClick = useCallback( + event => { + const [index, node] = getIndexAndNodeFromEvent(event) + if (currentIndex !== index) setCurrentIndex(index) + onClick && onClick(node, event) + }, + [getIndexAndNodeFromEvent, setCurrentIndex] + ) - return { - ...mesh, - voronoiPath, - } - } + return ( + + {debug && } + {currentIndex !== null && debug && ( + + )} + + ) -) +} + +Mesh.propTypes = { + width: PropTypes.number.isRequired, + height: PropTypes.number.isRequired, + nodes: PropTypes.array.isRequired, + x: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.func]).isRequired, + y: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.func]).isRequired, + onMouseEnter: PropTypes.func, + onMouseMove: PropTypes.func, + onMouseLeave: PropTypes.func, + onClick: PropTypes.func, + debug: PropTypes.bool.isRequired, +} +Mesh.defaultProps = { + x: 'x', + y: 'y', + debug: false, +} -export default enhance(Mesh) +export default Mesh diff --git a/packages/voronoi/src/computeMesh.js b/packages/voronoi/src/computeMesh.js index ae77082dd..670ab9933 100644 --- a/packages/voronoi/src/computeMesh.js +++ b/packages/voronoi/src/computeMesh.js @@ -2,13 +2,19 @@ import { Delaunay } from 'd3-delaunay' const getAccessor = directive => (typeof directive === 'function' ? directive : d => d[directive]) -export const computeMeshPoints = ({ points, xAccessor, yAccessor }) => { - const getX = getAccessor(xAccessor) - const getY = getAccessor(yAccessor) +/** + * The delaunay generator requires an array + * where each point is defined as an array + * of 2 elements: [x: number, y: number]. + * + * Points represent the raw input data + * and x/y represent accessors to x & y. + */ +export const computeMeshPoints = ({ points, x = 'x', y = 'y' }) => { + const getX = getAccessor(x) + const getY = getAccessor(y) - return { - points: points.map(p => [getX(p), getY(p)]), - } + return points.map(p => [getX(p), getY(p)]) } export const computeMesh = ({ points, width, height, debug }) => { diff --git a/packages/voronoi/src/hooks.js b/packages/voronoi/src/hooks.js new file mode 100644 index 000000000..44d961a44 --- /dev/null +++ b/packages/voronoi/src/hooks.js @@ -0,0 +1,21 @@ +/* + * This file is part of the nivo project. + * + * Copyright 2016-present, Raphaël Benitte. + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +import { useMemo } from 'react' +import { computeMeshPoints, computeMesh } from './computeMesh' + +export const useVoronoiMesh = ({ points, x, y, width, height, debug }) => { + const points2d = useMemo(() => computeMeshPoints({ points, x, y }), [points, x, y]) + + return useMemo(() => computeMesh({ points: points2d, width, height, debug }), [ + points2d, + width, + height, + debug, + ]) +} diff --git a/packages/voronoi/src/index.js b/packages/voronoi/src/index.js index d48c7236e..0f4a4890f 100644 --- a/packages/voronoi/src/index.js +++ b/packages/voronoi/src/index.js @@ -12,3 +12,4 @@ export { default as Mesh } from './Mesh' export * from './computeMesh' export * from './meshCanvas' export * from './props' +export * from './hooks' diff --git a/packages/voronoi/src/meshCanvas.js b/packages/voronoi/src/meshCanvas.js index 69d2d8fb0..5350100f7 100644 --- a/packages/voronoi/src/meshCanvas.js +++ b/packages/voronoi/src/meshCanvas.js @@ -1,3 +1,12 @@ +/* + * This file is part of the nivo project. + * + * Copyright 2016-present, Raphaël Benitte. + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + export const renderVoronoiToCanvas = (ctx, voronoi) => { ctx.save() diff --git a/scripts/capture.js b/scripts/capture.js index c568ab63f..d8538ef25 100644 --- a/scripts/capture.js +++ b/scripts/capture.js @@ -2,13 +2,25 @@ const puppeteer = require('puppeteer') const chalk = require('chalk') const config = require('@ekino/config') -const capture = async (page, baseUrl, { path, selector, output }) => { +const capture = async (page, baseUrl, { path, selector, output, theme }) => { const url = `${baseUrl}${path}?capture=1` console.log(chalk`{yellow Capturing {white ${path}}} {dim (selector: ${selector})}`) + if (path.indexOf('/icons') !== -1) { + await page.setViewport({ width: 1400, height: 4000 }) + } else { + await page.setViewport({ width: 1400, height: 900 }) + } + await page.goto(url) + if (theme !== undefined) { + const themeSelector = `#${theme}Theme` + await page.waitFor(themeSelector) + await page.click(themeSelector) + } + await page.waitFor(selector) const element = await page.$(selector) if (element === null) { @@ -37,7 +49,6 @@ const captureAll = async config => { headless: true }) const page = await browser.newPage() - await page.setViewport({ width: 1400, height: 4000 }) for (let pageConfig of config.pages) { await capture(page, config.baseUrl, pageConfig) diff --git a/tsconfig.json b/tsconfig.json index 7fe007080..7b74480d5 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -5,7 +5,9 @@ "target": "es2017", "alwaysStrict": true, "sourceMap": true, - "moduleResolution": "node" + "jsx": "react", + "moduleResolution": "node", + "esModuleInterop": true }, "include": ["./packages/*/index.d.ts"] } diff --git a/website/gatsby-browser.js b/website/gatsby-browser.js index 9b2fe9d6a..523e35e72 100644 --- a/website/gatsby-browser.js +++ b/website/gatsby-browser.js @@ -15,4 +15,13 @@ export const wrapPageElement = ({ element }) => { {element} ) +} + +export const onServiceWorkerUpdateReady = () => { + const answer = window.confirm([ + `The documentation has been updated,`, + `would you like to reload to display the latest version?` + ].join('')) + + if (answer === true) window.location.reload() } \ No newline at end of file diff --git a/website/gatsby-config.js b/website/gatsby-config.js index c7fa12973..33af4ba86 100644 --- a/website/gatsby-config.js +++ b/website/gatsby-config.js @@ -39,8 +39,6 @@ module.exports = { icon: `src/assets/icons/nivo-icon.png`, // This path is relative to the root of the site. }, }, - // this (optional) plugin enables Progressive Web App + Offline functionality - // To learn more, visit: https://gatsby.dev/offline - // `gatsby-plugin-offline`, + `gatsby-plugin-offline`, ], } diff --git a/website/src/assets/stories/SwarmPlotLayers.png b/website/src/assets/stories/SwarmPlotLayers.png new file mode 100644 index 000000000..3bb8a750f Binary files /dev/null and b/website/src/assets/stories/SwarmPlotLayers.png differ diff --git a/website/src/assets/stories/SwarmPlotRenderNode.png b/website/src/assets/stories/SwarmPlotRenderNode.png new file mode 100644 index 000000000..e95762c1e Binary files /dev/null and b/website/src/assets/stories/SwarmPlotRenderNode.png differ diff --git a/website/src/components/ThemeSelector.js b/website/src/components/ThemeSelector.js index 63eb7999f..c43ea3879 100644 --- a/website/src/components/ThemeSelector.js +++ b/website/src/components/ThemeSelector.js @@ -41,7 +41,11 @@ const ThemeSelector = () => { return ( - setTheme('light')}> + setTheme('light')} + > light { onChange={toggleTheme} colors={colors} /> - setTheme('dark')}> + setTheme('dark')} + > dark diff --git a/website/src/components/components/ActionsLoggerLog.js b/website/src/components/components/ActionsLoggerLog.js index 293dd7e68..91f8cb853 100644 --- a/website/src/components/components/ActionsLoggerLog.js +++ b/website/src/components/components/ActionsLoggerLog.js @@ -17,9 +17,13 @@ const ActionHeader = styled.div` background: ${({ theme }) => theme.colors.cardBackground}; border-bottom: 1px solid ${({ theme }) => theme.colors.borderLight}; display: grid; - grid-template-columns: 60px auto 60px; + grid-template-columns: 60px 8px auto 60px; align-items: center; cursor: pointer; + + &:hover { + bakcground: ${({ theme }) => theme.colors.cardAltBackground}; + } ` const ActionType = styled.span` @@ -27,6 +31,20 @@ const ActionType = styled.span` opacity: 0.5; ` +const Color = styled.span` + height: 100%; + display: flex; + align-items: center; + justify-content: center; +` + +const ColorChip = styled.span` + width: 8px; + height: 8px; + display: block; + border-radius: 6px; +` + const ActionLabel = styled.span` font-weight: 600; padding: 7px 12px; @@ -54,6 +72,11 @@ const ActionsLoggerLog = ({ action }) => { {action.type} + + {action.color && ( + + )} + {action.label} {isOpen ? '-' : '{ … }'} diff --git a/website/src/components/components/ComponentTabs.js b/website/src/components/components/ComponentTabs.js index f3f55ba17..db06ffe26 100644 --- a/website/src/components/components/ComponentTabs.js +++ b/website/src/components/components/ComponentTabs.js @@ -38,7 +38,7 @@ const ComponentTabs = ({ let content if (currentTab === 'chart') { - content = {children} + content = {children} } else if (currentTab === 'code') { content = ( @@ -217,6 +217,10 @@ const NodeCount = styled.span` border-right: 1px solid ${({ theme }) => theme.colors.border}; font-size: 13px; padding: 5px 11px; + + .isCapturing & { + display: none; + } ` const Code = styled.div` diff --git a/website/src/components/components/ComponentTemplate.js b/website/src/components/components/ComponentTemplate.js index 7bc18261f..33cae3777 100644 --- a/website/src/components/components/ComponentTemplate.js +++ b/website/src/components/components/ComponentTemplate.js @@ -44,7 +44,10 @@ const ComponentTemplate = ({ const [settings, setSettings] = useState(initialProperties) const [data, setData] = useState(hasData ? generateData() : null) - const diceRoll = useCallback(() => setData(hasData ? generateData() : null), [setData]) + const diceRoll = useCallback(() => setData(hasData ? generateData(data) : null), [ + data, + setData, + ]) const [actions, logAction] = useActionsLogger() @@ -107,10 +110,7 @@ ComponentTemplate.propTypes = { stories: PropTypes.arrayOf( PropTypes.shape({ label: PropTypes.string.isRequired, - link: PropTypes.shape({ - kind: PropTypes.string.isRequired, - story: PropTypes.string.isRequired, - }).isRequired, + link: PropTypes.string.isRequired, }) ), }).isRequired, diff --git a/website/src/components/components/Stories.js b/website/src/components/components/Stories.js index 307b337c9..b6117b02f 100644 --- a/website/src/components/components/Stories.js +++ b/website/src/components/components/Stories.js @@ -14,10 +14,8 @@ import VisitIcon from 'react-icons/lib/md/keyboard-arrow-right' import media from '../../theming/mediaQueries' import config from '../../data/config' -const buildStoryLink = ({ kind, story }) => - `${config.storybookUrl}?path=/story/${encodeURIComponent( - kind.toLowerCase() - )}--${encodeURIComponent(snakeCase(story))}` +const buildStoryLink = link => + `${config.storybookUrl}?path=/story/${encodeURIComponent(link.toLowerCase())}` const Wrapper = styled.div` position: fixed; @@ -134,10 +132,7 @@ Stories.propTypes = { stories: PropTypes.arrayOf( PropTypes.shape({ label: PropTypes.string.isRequired, - link: PropTypes.shape({ - kind: PropTypes.string.isRequired, - story: PropTypes.string.isRequired, - }).isRequired, + link: PropTypes.string.isRequired, }) ), } diff --git a/website/src/components/icons/SwarmPlotIcon.js b/website/src/components/icons/SwarmPlotIcon.js index 749e3ee36..8dcd9d350 100644 --- a/website/src/components/icons/SwarmPlotIcon.js +++ b/website/src/components/icons/SwarmPlotIcon.js @@ -49,22 +49,19 @@ const values = [ const chartProps = { width: ICON_SIZE, height: ICON_SIZE, - scale: { + valueScale: { type: 'linear', min: 1, max: 9, }, forceStrength: 6, simulationIterations: 400, - data: [ - { - id: 'A', - data: values.map((v, i) => ({ - id: i, - value: v, - })), - }, - ], + groups: ['A'], + data: values.map((v, i) => ({ + id: i, + group: 'A', + value: v, + })), margin: { top: 6, right: 6, @@ -78,8 +75,8 @@ const chartProps = { axisRight: null, axisBottom: null, axisLeft: null, - nodeSize: 10, - nodePadding: 1, + size: 10, + spacing: 1, isInteractive: false, animate: true, } diff --git a/website/src/data/components/bar/meta.yml b/website/src/data/components/bar/meta.yml index 29d163770..4e6221acd 100644 --- a/website/src/data/components/bar/meta.yml +++ b/website/src/data/components/bar/meta.yml @@ -13,33 +13,19 @@ Bar: - isomorphic stories: - label: Using markers - link: - kind: Bar - story: with marker + link: bar--with-marker - label: Stacked diverging bar chart - link: - kind: Bar - story: diverging stacked + link: bar--diverging-stacked - label: Grouped diverging bar chart - link: - kind: Bar - story: diverging grouped + link: bar--diverging-grouped - label: Custom bar element - link: - kind: Bar - story: custom bar item + link: bar--custom-bar-item - label: Formatting values - link: - kind: Bar - story: with formatted values + link: bar--with-formatted-values - label: Using custom tooltip - link: - kind: Bar - story: custom tooltip + link: bar--custom-tooltip - label: Custom axis ticks - link: - kind: Bar - story: custom axis ticks + link: bar--custom-axis-ticks description: | Bar chart which can display multiple data series, stacked or side by side. Also supports both vertical and horizontal layout, with negative values descending diff --git a/website/src/data/components/bar/props.js b/website/src/data/components/bar/props.js index 31d69fc8b..278d981f2 100644 --- a/website/src/data/components/bar/props.js +++ b/website/src/data/components/bar/props.js @@ -256,7 +256,7 @@ const props = [ group: 'Style', }, { - key: 'colorIdentity', + key: 'colorBy', scopes: '*', type: 'string | Function', help: 'Property used to determine node color.', @@ -268,7 +268,7 @@ const props = [ to the color generator. `, required: false, - defaultValue: defaults.colorIdentity, + defaultValue: defaults.colorBy, controlType: 'choices', group: 'Style', controlOptions: { diff --git a/website/src/data/components/bubble/meta.yml b/website/src/data/components/bubble/meta.yml index 136959fad..6ed3df492 100644 --- a/website/src/data/components/bubble/meta.yml +++ b/website/src/data/components/bubble/meta.yml @@ -16,13 +16,9 @@ Bubble: - isomorphic stories: - label: Using formatted values - link: - kind: Bubble - story: with formatted values + link: bubble--with-formatted-values - label: Using custom tooltip - link: - kind: Bubble - story: custom tooltip + link: bubble--custom-tooltip description: | Bubble chart using circle packing with zooming ability. You can fully customize it using `nodeComponent` property diff --git a/website/src/data/components/bubble/props.js b/website/src/data/components/bubble/props.js index 6eb267787..6ccacea18 100644 --- a/website/src/data/components/bubble/props.js +++ b/website/src/data/components/bubble/props.js @@ -158,7 +158,7 @@ const props = [ group: 'Style', }, { - key: 'colorIdentity', + key: 'colorBy', scopes: ['Bubble', 'BubbleHtml', 'BubbleCanvas'], type: 'string | Function', help: 'Property used to determine node color.', @@ -170,7 +170,7 @@ const props = [ to the color generator. `, required: false, - defaultValue: defaults.colorIdentity, + defaultValue: defaults.colorBy, controlType: 'choices', group: 'Style', controlOptions: { diff --git a/website/src/data/components/bullet/meta.yml b/website/src/data/components/bullet/meta.yml index a1fd3dd6c..7f81e8533 100644 --- a/website/src/data/components/bullet/meta.yml +++ b/website/src/data/components/bullet/meta.yml @@ -9,21 +9,13 @@ Bullet: - isomorphic stories: - label: custom range - link: - kind: Bullet - story: custom range + link: bullet--custom-range - label: custom measure - link: - kind: Bullet - story: custom measure + link: bullet--custom-measure - label: custom marker - link: - kind: Bullet - story: custom marker + link: bullet--custom-marker - label: custom title - link: - kind: Bullet - story: custom title + link: bullet--custom-title description: | Bullet chart supporting multiple ranges/measures/markers. diff --git a/website/src/data/components/geo/mapper.js b/website/src/data/components/geo/mapper.js index f9477bad2..32ea9dc1c 100644 --- a/website/src/data/components/geo/mapper.js +++ b/website/src/data/components/geo/mapper.js @@ -14,25 +14,29 @@ const TooltipWrapper = styled.div` display: grid; grid-template-columns: 1fr 1fr; grid-column-gap: 12px; + font-size: 13px; + background: ${({ theme }) => theme.colors.cardBackground}; + padding: 10px 20px; + color: ${({ color }) => color}; + border: 2px solid ${({ color }) => color}; + box-shadow: 9px 16px 0 rgba(0, 0, 0, 0.15); ` const TooltipKey = styled.span` font-weight: 600; ` const TooltipValue = styled.span`` -const CustomTooltip = node => { +const CustomTooltip = ({ feature }) => { return ( - + + Custom tooltip + id - {node.id} + {feature.id} value - {node.value} - index - {node.index} - indexValue - {node.indexValue} + {feature.value} color - {node.color} + {feature.color} ) } diff --git a/website/src/data/components/geo/props.js b/website/src/data/components/geo/props.js index 5d6c942d2..4d7b7a742 100644 --- a/website/src/data/components/geo/props.js +++ b/website/src/data/components/geo/props.js @@ -332,9 +332,6 @@ const props = [ A function allowing complete tooltip customisation, it must return a valid HTML element and will receive the node's data. - - You can also customize the tooltip style - using the \`theme.tooltip\` object. `, }, { diff --git a/website/src/data/components/heatmap/meta.yml b/website/src/data/components/heatmap/meta.yml index 4e5b0e433..216b86ba5 100644 --- a/website/src/data/components/heatmap/meta.yml +++ b/website/src/data/components/heatmap/meta.yml @@ -13,13 +13,9 @@ HeatMap: - isomorphic stories: - label: Custom cell component - link: - kind: heatmap - story: Custom cell component + link: heatmap--custom-cell-component - label: Custom tooltip - link: - kind: heatmap - story: Custom tooltip + link: heatmap-custom-tooltip description: | An heat map matrix, you can chose between various colors scales or pass yours, you also have the ability to change the cell shape diff --git a/website/src/data/components/line/meta.yml b/website/src/data/components/line/meta.yml index 4f63cecd6..34f8723c5 100644 --- a/website/src/data/components/line/meta.yml +++ b/website/src/data/components/line/meta.yml @@ -11,53 +11,29 @@ Line: - isomorphic stories: - label: stacked lines - link: - kind: Line - story: stacked lines + link: line--stacked-lines - label: linear x scale - link: - kind: Line - story: linear x scale + link: line--linear-x-scale - label: time x scale - link: - kind: Line - story: time x scale + link: line--time-x-scale - label: logarithmic y scale - link: - kind: Line - story: logarithmic y scale + link: line--logarithmic-y-scale - label: real time chart - link: - kind: Line - story: real time chart + link: line--real-time-chart - label: custom dot symbol - link: - kind: Line - story: custom dot symbol + link: line--custom-dot-symbol - label: adding markers - link: - kind: Line - story: adding markers + link: line--adding-markers - label: holes in data - link: - kind: Line - story: holes in data + link: line--holes-in-data - label: different serie lengths - link: - kind: Line - story: different serie lengths + link: line--different-serie-lengths - label: custom min/max y - link: - kind: Line - story: custom min/max y + link: line--custom-min-max-y - label: formatting axis values - link: - kind: Line - story: formatting axis values + link: line--formatting-axis-values - label: formatting tooltip values - link: - kind: Line - story: formatting tooltip values + link: line--formatting-tooltip-values description: | Line chart with stacking ability. diff --git a/website/src/data/components/radar/meta.yml b/website/src/data/components/radar/meta.yml index bdf2d4d71..4bd447886 100644 --- a/website/src/data/components/radar/meta.yml +++ b/website/src/data/components/radar/meta.yml @@ -12,13 +12,9 @@ Radar: - isomorphic stories: - label: Formatting tooltip value - link: - kind: Radar - story: with formatted values + link: radar--with-formatted-values - label: Custom label component - link: - kind: Radar - story: custom label component + link: radar--custom-label-component description: | Generates a radar chart from an array of data. Note that margin object does not take grid labels into account, diff --git a/website/src/data/components/scatterplot/meta.yml b/website/src/data/components/scatterplot/meta.yml index 7dc55fa69..292f39967 100644 --- a/website/src/data/components/scatterplot/meta.yml +++ b/website/src/data/components/scatterplot/meta.yml @@ -11,29 +11,17 @@ ScatterPlot: - isomorphic stories: - label: Using time scales - link: - kind: ScatterPlot - story: using time scales + link: scatterplot--using-time-scales - label: Using logarithmic scales - link: - kind: ScatterPlot - story: using logarithmic scales + link: scatterplot--using-logarithmic-scales - label: Varying symbol size - link: - kind: ScatterPlot - story: varying symbol size + link: scatterplot--varying-symbol-size - label: Custom tooltip - link: - kind: ScatterPlot - story: custom tooltip + link: scatterplot--custom-tooltip - label: Synchronizing charts - link: - kind: ScatterPlot - story: synchronizing charts + link: scatterplot--synchronizing-charts - label: Using mouse enter/leave - link: - kind: ScatterPlot - story: using mouse enter/leave + link: scatterplot--using-mouse-enter-leave description: | A scatter plot chart, which can display several data series. @@ -55,17 +43,11 @@ ScatterPlotCanvas: - canvas stories: - label: Using time scales - link: - kind: ScatterPlot - story: using time scales + link: scatterplot--using-time-scales - label: Varying symbol size - link: - kind: ScatterPlot - story: varying symbol size + link: scatterplot--varying-symbol-size - label: Custom tooltip - link: - kind: ScatterPlot - story: custom tooltip + link: scatterplot--custom-tooltip description: | A variation around the [ScatterPlot](self:/scatterplot) component. Well suited for large data sets as it does not impact DOM tree depth, diff --git a/website/src/data/components/swarmplot/generator.js b/website/src/data/components/swarmplot/generator.js index 5608197ad..54d1808b9 100644 --- a/website/src/data/components/swarmplot/generator.js +++ b/website/src/data/components/swarmplot/generator.js @@ -6,31 +6,18 @@ * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -import range from 'lodash/range' -import random from 'lodash/random' +import { generateSwarmPlotData, randomizeSwarmPlotData } from '@nivo/generators' -const ids = ['serie A', 'serie B', 'serie C', 'serie D', 'serie E', 'serie F', 'serie G'] +const allGroups = ['group A', 'group B', 'group C', 'group D', 'group E', 'group F', 'group G'] -const generateDataSet = size => { - const values = range(size).map(() => random(0, 500)) - values.sort() +export const generateLightDataSet = previousData => { + if (previousData !== undefined) return randomizeSwarmPlotData(previousData) - return values.map((value, id) => ({ - id, - value, - })) + return generateSwarmPlotData(allGroups.slice(0, 3), { min: 50, max: 80 }) } -export const generateLightDataSet = () => { - return ids.slice(0, 3).map(id => ({ - id, - data: generateDataSet(60 + Math.round(Math.random() * 40)), - })) -} +export const generateHeavyDataSet = previousData => { + if (previousData !== undefined) return randomizeSwarmPlotData(previousData) -export const generateHeavyDataSet = () => { - return ids.map(id => ({ - id, - data: generateDataSet(180 + Math.round(Math.random() * 100)), - })) + return generateSwarmPlotData(allGroups, { min: 60, max: 100 }) } diff --git a/website/src/data/components/swarmplot/mapper.js b/website/src/data/components/swarmplot/mapper.js index f4867fe86..a9cac4290 100644 --- a/website/src/data/components/swarmplot/mapper.js +++ b/website/src/data/components/swarmplot/mapper.js @@ -6,14 +6,11 @@ * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -import { settingsMapper, mapAxis, mapInheritedColor } from '../../../lib/settings' +import { settingsMapper, mapAxis } from '../../../lib/settings' export default settingsMapper({ - /* axisTop: mapAxis('top'), axisRight: mapAxis('right'), axisBottom: mapAxis('bottom'), axisLeft: mapAxis('left'), - borderColor: mapInheritedColor, - */ }) diff --git a/website/src/data/components/swarmplot/meta.yml b/website/src/data/components/swarmplot/meta.yml index 8b6874b20..76d0e9cac 100644 --- a/website/src/data/components/swarmplot/meta.yml +++ b/website/src/data/components/swarmplot/meta.yml @@ -9,10 +9,32 @@ SwarmPlot: tags: - svg - isomorphic - stories: [] + stories: + - label: Custom node rendering + link: swarmplot--custom-node-rendering + - label: Adding extra layers + link: swarmplot--extra-layers description: | - A swarm plot component which can also be used to make beeswarm plot - when using a single data serie. + A swarm plot component which can also be used to make a beeswarm plot + when using a single group. + + This chart can display 2 data dimensions, a categorical one: **groups**, + and a quantitative one: **values**. + + You can optionally add a third quantitative dimension if you enable + **dynamic node size**, please have a look at the `size` property + for further information. + + You can also enable a voronoi mesh to capture user's + interactions, using the `useMesh` property. + + This example uses 3 dimensions, the grouping is done by the `group` + property while the value is determined by the `price` property, + node size is dynamic and depends on the `volume` property. + + Under the hood, this chart uses [d3-force](https://github.com/d3/d3-force) + with a simulation involving collisions and x/y forces, the quality of + the simulation can be adjusted, the strength of the *value* force too. The responsive alternative of this component is `ResponsiveSwarmPlot`. @@ -20,12 +42,18 @@ SwarmPlotCanvas: package: '@nivo/swarmplot' tags: - canvas - stories: [] description: | A variation around the [SwarmPlot](self:/swarmplot) component. Well suited for large data sets as it does not impact DOM tree depth, however you'll lose the isomorphic ability and transitions. + You can optionally enable a voronoi mesh to capture user's + interactions, using the `useMesh` property. + + Even if the canvas implementation is faster, please note that + if you have a lot of nodes **calculating the underlying simulation + will involve a lot of computing and will affect performance**. + The responsive alternative of this component is `ResponsiveSwarmPlotCanvas`. diff --git a/website/src/data/components/swarmplot/props.js b/website/src/data/components/swarmplot/props.js index 4338db449..523478d68 100644 --- a/website/src/data/components/swarmplot/props.js +++ b/website/src/data/components/swarmplot/props.js @@ -9,8 +9,8 @@ import { SwarmPlotDefaultProps } from '@nivo/swarmplot' import { motionProperties, - defsProperties, getPropertiesGroupsControls, + axesProperties, } from '../../../lib/componentProperties' const defaults = SwarmPlotDefaultProps @@ -20,60 +20,112 @@ const props = [ key: 'data', help: 'Chart data.', description: ` - The Chart data is an array of datum which must conform to this structure: - - \`\`\` - Array<{ - // Identifier of the serie - id: string | number - data: { - // Identifier of the datum - id: string | number - value: number - } - }> - \`\`\` + This Chart's doesn't have a predefined structure, + you must use a schema which match \`groupBy\`, + \`identity\` and \`value\` properties. `, type: 'object[]', group: 'Base', required: true, }, { - key: 'width', - scopes: ['api'], - help: 'Chart width.', + key: 'groups', + group: 'Base', + type: 'string[]', + required: true, + help: 'Available groups.', + }, + { + key: 'groupBy', + group: 'Base', + type: 'string | Function', + required: false, + help: + 'Propety used to group nodes, must return a group which is available in the groups property.', + defaultValue: defaults.groupBy, + }, + { + key: 'identity', + group: 'Base', + type: 'string | Function', + required: false, + help: `Property used to retrieve the node's unique identifier.`, description: ` - not required if using responsive alternative - of the component \`\`. + This property will determine the identifier of a datum + amongst the whole data set, thus, it's really important + that it's unique. + + It is especially important to have proper identifier + when enabling animations, as it will be used to determine + if a node is a new one or should transition from previous + to next state. `, - type: 'number', - required: true, + defaultValue: defaults.identity, }, { - key: 'height', - scopes: ['api'], - help: 'Chart height.', + key: 'label', + group: 'Base', + type: 'string | Function', + required: false, + help: `Control node label.`, + defaultValue: defaults.label, + }, + { + key: 'value', + group: 'Base', + type: 'string | Function', + required: false, + help: `Property used to retrieve the node's value.`, + defaultValue: defaults.value, + }, + { + key: 'valueFormat', + group: 'Base', + type: 'string | Function', + required: false, + help: `Optional value formatter.`, + }, + { + key: 'size', + group: 'Base', + type: 'number | object | Function', + required: false, + help: `How to compute node size, static or dynamic.`, description: ` - not required if using responsive alternative - of the component \`\`. + If you provide a **number**, all nodes will have the same + **fixed size**. + + You can also use an object to define a varying size, + it must conform to the following interface: + + \`\`\` + { + key: string + values: [min: number, max: number] + sizes: [min: number, max: number] + } + \`\`\` + + Then the size of each node will **depend on the value + of \`key\` and \`sizes\`**. + + If you use a **custom function**, it will receive the current + node and must **return a number**. `, - type: 'number', - required: true, + defaultValue: defaults.size, }, { - key: 'layout', - scopes: '*', - help: `Chart layout.`, - type: 'string', + key: 'spacing', + help: 'Spacing between nodes.', + type: 'number', required: false, - defaultValue: defaults.layout, - controlType: 'radio', + defaultValue: defaults.spacing, + controlType: 'range', group: 'Base', controlOptions: { - choices: [ - { label: 'horizontal', value: 'horizontal' }, - { label: 'vertical', value: 'vertical' }, - ], + unit: 'px', + min: 0, + max: 20, }, }, { @@ -89,12 +141,16 @@ const props = [ simulation will try to **align the nodes with their corresponding values** on the value axis, resulting in a narrower chart. + + Please note that increasing this value will sometimes + require to **increase the quality of the simulation** + via the \`simulationIterations\` property. `, type: 'number', required: false, defaultValue: defaults.forceStrength, controlType: 'range', - group: 'Base', + group: 'Simulation', controlOptions: { step: 0.2, min: 0.2, @@ -112,24 +168,10 @@ const props = [ required: false, defaultValue: defaults.simulationIterations, controlType: 'range', - group: 'Base', + group: 'Simulation', controlOptions: { - min: 100, - max: 300, - }, - }, - { - key: 'gap', - help: 'Gap between each serie.', - type: 'number', - required: false, - defaultValue: defaults.gap, - controlType: 'range', - group: 'Base', - controlOptions: { - unit: 'px', - min: 0, - max: 100, + min: 60, + max: 260, }, }, { @@ -140,52 +182,76 @@ const props = [ defaultValue: 'Depends on device', type: `number`, controlType: 'range', - group: 'Base', + group: 'Layout', controlOptions: { min: 1, max: 2, }, }, { - key: 'nodeSize', - help: 'Size of the nodes.', + key: 'width', + scopes: ['api'], + group: 'Layout', + help: 'Chart width.', + description: ` + not required if using responsive alternative + of the component \`\`. + `, type: 'number', + required: true, + }, + { + key: 'height', + scopes: ['api'], + group: 'Layout', + help: 'Chart height.', + description: ` + not required if using responsive alternative + of the component \`\`. + `, + type: 'number', + required: true, + }, + { + key: 'margin', + help: 'Chart margin.', + type: 'object', required: false, - defaultValue: defaults.nodeSize, - controlType: 'range', - group: 'Nodes', + controlType: 'margin', + group: 'Layout', + }, + { + key: 'layout', + scopes: '*', + help: `Chart layout.`, + type: 'string', + required: false, + defaultValue: defaults.layout, + controlType: 'radio', + group: 'Layout', controlOptions: { - unit: 'px', - min: 2, - max: 20, + choices: [ + { label: 'horizontal', value: 'horizontal' }, + { label: 'vertical', value: 'vertical' }, + ], }, }, { - key: 'nodePadding', - help: 'Padding between nodes.', + key: 'gap', + help: 'Gap between each serie.', type: 'number', required: false, - defaultValue: defaults.nodePadding, + defaultValue: defaults.gap, controlType: 'range', - group: 'Nodes', + group: 'Layout', controlOptions: { unit: 'px', min: 0, - max: 20, + max: 100, }, }, - { - key: 'margin', - scopes: '*', - help: 'Chart margin.', - type: 'object', - required: false, - controlType: 'margin', - group: 'Base', - }, { key: 'colors', - scopes: '*', help: 'Defines how to compute node color.', description: ` The colors property is used to determine the **ordinal color scale** @@ -201,8 +267,8 @@ const props = [ Please have a look at [the dedicated guide](self:/guides/colors) for available schemes. - If you wish to use **color bound to the data** you pass to the chart, - you can also use this form: + If you wish to use **color already defined on the data** + you passed to the chart, you can also use this form: \`\`\` colors={{ datum: 'color' }} @@ -222,20 +288,101 @@ const props = [ controlType: 'ordinalColors', group: 'Style', }, + { + key: 'colorBy', + group: 'Style', + help: 'Property or accessor function to be used with colors.', + description: ` + When using a color scheme or an array of colors, + you'll generate a color scale, this scale will + receive a value which will be translated to a color. + + This property define the way we get this value, + it can be either a \`string\` or a custom function. + + Please have a look at [the colors guide](self:/guides/colors) + for further information. + `, + type: `Function | string`, + required: false, + defaultValue: defaults.colorBy, + controlType: 'choices', + group: 'Style', + controlOptions: { + choices: ['group', 'id'].map(key => ({ + label: key, + value: key, + })), + }, + }, { key: 'borderWidth', - scopes: '*', help: 'Control node border width.', - type: 'number', + type: 'number | Function', required: false, defaultValue: defaults.borderWidth, controlType: 'lineWidth', group: 'Style', }, + { + key: 'layers', + group: 'Customization', + help: 'Defines the order of layers and add custom layers.', + description: ` + Defines the order of layers, available layers are: + \`grid\`, \`axes\`, \`nodes\`, \`mesh\`. + + You can also use this to insert extra layers + to the chart, the extra layer must be a function. + + The layer function which will receive the chart's + context & computed data and must return a valid SVG element + for the \`SwarmPlot\` component. + + When using the canvas implementation, the function + will receive the canvas 2d context as first argument + and the chart's context and computed data as second. + + Please make sure to use \`context.save()\` and + \`context.restore()\` if you make some global + modifications to the 2d context inside this function + to avoid side effects. + + You can see a live example of custom layers + [here](storybook:/swarmplot--extra-layers). + `, + required: false, + type: 'Array', + defaultValue: defaults.layers, + }, + { + key: 'renderNode', + group: 'Customization', + help: 'Override default node rendering.', + description: ` + This property can be used to completely + customize the way nodes are rendered. + + when using the SVG implementation, you should + return a valid SVG node. + + When using canvas, the rendering function will + receive the canvas 2d context as first argument. + + Please make sure to use \`context.save()\` and + \`context.restore()\` if you make some global + modifications to the 2d context inside this function + to avoid side effects. + + You can see a live example of custom node rendering + [here](storybook:/swarmplot--custom-node-rendering). + `, + required: false, + type: 'Function', + }, /* { key: 'borderColor', - scopes: '*', help: 'Method to compute border color.', type: 'string | Function', required: false, @@ -247,10 +394,41 @@ const props = [ }, }, */ - ...defsProperties(['SwarmPlot']), + { + key: 'enableGridX', + group: 'Grid & Axes', + help: 'Enable/disable x grid.', + type: 'boolean', + required: false, + defaultValue: defaults.enableGridX, + controlType: 'switch', + }, + { + key: 'gridXValues', + group: 'Grid & Axes', + help: 'Specify values to use for vertical grid lines.', + type: 'Array', + required: false, + }, + { + key: 'enableGridY', + group: 'Grid & Axes', + help: 'Enable/disable y grid.', + type: 'boolean', + required: false, + defaultValue: defaults.enableGridY, + controlType: 'switch', + }, + { + key: 'gridYValues', + group: 'Grid & Axes', + help: 'Specify values to use for horizontal grid lines.', + type: 'Array', + required: false, + }, + ...axesProperties, { key: 'isInteractive', - scopes: ['TreeMap', 'TreeMapHTML', 'TreeMapCanvas'], help: 'Enable/disable interactivity.', type: 'boolean', required: false, @@ -258,13 +436,63 @@ const props = [ controlType: 'switch', group: 'Interactivity', }, + { + key: 'useMesh', + help: 'Use a mesh to detect mouse interactions.', + type: 'boolean', + required: false, + defaultValue: defaults.useMesh, + controlType: 'switch', + group: 'Interactivity', + }, + { + key: 'debugMesh', + help: 'Display mesh used to detect mouse interactions (voronoi cells).', + type: 'boolean', + required: false, + defaultValue: defaults.debugMesh, + controlType: 'switch', + group: 'Interactivity', + }, + { + key: 'onMouseEnter', + group: 'Interactivity', + help: 'onMouseEnter handler.', + type: '(node, event) => void', + required: false, + }, + { + key: 'onMouseMove', + group: 'Interactivity', + help: 'onMouseMove handler.', + type: '(node, event) => void', + required: false, + }, + { + key: 'onMouseLeave', + group: 'Interactivity', + help: 'onMouseLeave handler.', + type: '(node, event) => void', + required: false, + }, { key: 'onClick', group: 'Interactivity', - scopes: ['TreeMap', 'TreeMapHTML', 'TreeMapCanvas'], - help: 'onClick handler, it receives clicked node data and style plus mouse event.', + help: 'onClick handler.', + type: '(node, event) => void', + required: false, + }, + { + key: 'tooltip', + group: 'Interactivity', type: 'Function', required: false, + help: 'Custom tooltip component.', + description: ` + A function allowing complete tooltip customisation, + it must return a valid HTML + element and will receive the node's data. + `, }, ...motionProperties(['SwarmPlot'], defaults), ] diff --git a/website/src/data/components/treemap/generator.js b/website/src/data/components/treemap/generator.js index deb29d303..f475a904f 100644 --- a/website/src/data/components/treemap/generator.js +++ b/website/src/data/components/treemap/generator.js @@ -16,13 +16,13 @@ const HEAVY_NODE_COUNT = 600 export const generateHeavyDataSet = () => { const children = range(HEAVY_NODE_COUNT).map(i => ({ - id: `node.${i}`, + name: `node.${i}`, value: random(10, 100000), })) return { root: { - id: 'root', + name: 'root', children, }, nodeCount: HEAVY_NODE_COUNT, diff --git a/website/src/pages/bar/api.js b/website/src/pages/bar/api.js index 66f0c19ca..47a801e64 100644 --- a/website/src/pages/bar/api.js +++ b/website/src/pages/bar/api.js @@ -42,7 +42,7 @@ const BarApi = () => { indexBy: 'country', colors: { scheme: 'nivo' }, - colorIdentity: 'id', + colorBy: 'id', borderRadius: 0, borderWidth: 0, borderColor: { diff --git a/website/src/pages/bar/canvas.js b/website/src/pages/bar/canvas.js index abbf908f5..d3454b196 100644 --- a/website/src/pages/bar/canvas.js +++ b/website/src/pages/bar/canvas.js @@ -41,7 +41,7 @@ const initialProperties = { reverse: false, colors: { scheme: 'red_blue' }, - colorIdentity: 'id', + colorBy: 'id', borderWidth: 0, borderColor: { type: 'inherit:darker', @@ -129,6 +129,7 @@ const BarCanvas = () => { logAction({ type: 'click', label: `[bar] ${node.id} - ${node.indexValue}: ${node.value}`, + color: node.color, data: node, }) } diff --git a/website/src/pages/bar/index.js b/website/src/pages/bar/index.js index 1beac888f..6e24f2058 100644 --- a/website/src/pages/bar/index.js +++ b/website/src/pages/bar/index.js @@ -39,7 +39,7 @@ const initialProperties = { reverse: false, colors: { scheme: 'nivo' }, - colorIdentity: 'id', + colorBy: 'id', defs: [ patternDotsDef('dots', { background: 'inherit', @@ -178,6 +178,7 @@ const Bar = () => { logAction({ type: 'click', label: `[bar] ${node.id} - ${node.indexValue}: ${node.value}`, + color: node.color, data: node, }) } diff --git a/website/src/pages/bubble/canvas.js b/website/src/pages/bubble/canvas.js index 428b8260a..493046265 100644 --- a/website/src/pages/bubble/canvas.js +++ b/website/src/pages/bubble/canvas.js @@ -42,7 +42,7 @@ const initialProperties = { value: 'value', colors: { scheme: 'yellow_orange_red' }, - colorIdentity: 'name', + colorBy: 'name', padding: 1, leavesOnly: true, @@ -95,6 +95,7 @@ const BubbleCanvas = () => { logAction({ type: 'click', label: `${node.id}: ${node.value}`, + color: node.color, data: node, }) }} diff --git a/website/src/pages/bubble/html.js b/website/src/pages/bubble/html.js index 86ec06852..7eb624fee 100644 --- a/website/src/pages/bubble/html.js +++ b/website/src/pages/bubble/html.js @@ -24,7 +24,7 @@ const initialProperties = { identity: 'name', value: 'loc', colors: { scheme: 'paired' }, - colorIdentity: 'depth', + colorBy: 'depth', padding: 1, leavesOnly: false, @@ -76,6 +76,7 @@ const BubbleHtml = () => { logAction({ type: 'click', label: `${node.id}: ${node.value}`, + color: node.color, data: node, }) }} diff --git a/website/src/pages/bubble/index.js b/website/src/pages/bubble/index.js index 0aa9fbc46..d3607dc4a 100644 --- a/website/src/pages/bubble/index.js +++ b/website/src/pages/bubble/index.js @@ -25,7 +25,7 @@ const initialProperties = { identity: 'name', value: 'loc', colors: { scheme: 'nivo' }, - colorIdentity: 'depth', + colorBy: 'depth', padding: 6, leavesOnly: false, @@ -87,6 +87,7 @@ const Bubble = () => { logAction({ type: 'click', label: `${node.id}: ${node.value}`, + color: node.color, data: node, }) }} diff --git a/website/src/pages/bullet/index.js b/website/src/pages/bullet/index.js index c3ba809ec..3385a3506 100644 --- a/website/src/pages/bullet/index.js +++ b/website/src/pages/bullet/index.js @@ -71,6 +71,7 @@ const Bullet = () => { logAction({ type: 'click', label: `[range] ${range.id}: [${range.v0}, ${range.v1}]`, + color: range.color, data: range, }) }} @@ -78,6 +79,7 @@ const Bullet = () => { logAction({ type: 'click', label: `[measure] ${measure.id}: [${measure.v0}, ${measure.v1}]`, + color: measure.color, data: measure, }) }} @@ -85,6 +87,7 @@ const Bullet = () => { logAction({ type: 'click', label: `[marker] ${marker.id}: ${marker.value}`, + color: marker.color, data: marker, }) }} diff --git a/website/src/pages/calendar/canvas.js b/website/src/pages/calendar/canvas.js index 3a2258df4..e4ef198c8 100644 --- a/website/src/pages/calendar/canvas.js +++ b/website/src/pages/calendar/canvas.js @@ -102,6 +102,7 @@ const CalendarCanvas = () => { logAction({ type: 'click', label: `[day] ${day.day}: ${day.value}`, + color: day.color, data: day, }) }} diff --git a/website/src/pages/calendar/index.js b/website/src/pages/calendar/index.js index 143391d34..f2c3508e3 100644 --- a/website/src/pages/calendar/index.js +++ b/website/src/pages/calendar/index.js @@ -99,6 +99,7 @@ const Calendar = () => { logAction({ type: 'click', label: `[day] ${day.day}: ${day.value}`, + color: day.color, data: day, }) }} diff --git a/website/src/pages/choropleth/canvas.js b/website/src/pages/choropleth/canvas.js index 3bc082cfe..abb7133f2 100644 --- a/website/src/pages/choropleth/canvas.js +++ b/website/src/pages/choropleth/canvas.js @@ -101,6 +101,7 @@ const ChoroplethCanvas = () => { label: `${feature.label}: ${feature.formattedValue} (${ feature.id })`, + color: feature.color, data: { label: feature.label, value: feature.value, diff --git a/website/src/pages/choropleth/index.js b/website/src/pages/choropleth/index.js index 42991b71c..3562f7fa4 100644 --- a/website/src/pages/choropleth/index.js +++ b/website/src/pages/choropleth/index.js @@ -116,6 +116,7 @@ const Choropleth = () => { label: `${feature.label}: ${feature.formattedValue} (${ feature.id })`, + color: feature.color, data: omit(feature, 'geometry'), }) }} diff --git a/website/src/pages/heatmap/canvas.js b/website/src/pages/heatmap/canvas.js index e6fb81d54..6b3284485 100644 --- a/website/src/pages/heatmap/canvas.js +++ b/website/src/pages/heatmap/canvas.js @@ -135,6 +135,7 @@ const HeatMapCanvas = () => { logAction({ type: 'click', label: `[cell] ${cell.yKey}.${cell.xKey}: ${cell.value}`, + color: cell.color, data: cell, }) }} diff --git a/website/src/pages/heatmap/index.js b/website/src/pages/heatmap/index.js index 0948b020c..e3ca8f4b6 100644 --- a/website/src/pages/heatmap/index.js +++ b/website/src/pages/heatmap/index.js @@ -144,6 +144,7 @@ const HeatMap = () => { logAction({ type: 'click', label: `[cell] ${cell.yKey}.${cell.xKey}: ${cell.value}`, + color: cell.color, data: cell, }) }} diff --git a/website/src/pages/scatterplot/index.js b/website/src/pages/scatterplot/index.js index 522783193..35f73fe5c 100644 --- a/website/src/pages/scatterplot/index.js +++ b/website/src/pages/scatterplot/index.js @@ -87,8 +87,8 @@ const initialProperties = { motionDamping: 15, isInteractive: true, - useMesh: false, - debugMesh: false, + useMesh: true, + debugMesh: true, 'custom tooltip example': false, tooltip: null, diff --git a/website/src/pages/swarmplot/canvas.js b/website/src/pages/swarmplot/canvas.js index 86205dbd1..6a76c792d 100644 --- a/website/src/pages/swarmplot/canvas.js +++ b/website/src/pages/swarmplot/canvas.js @@ -14,99 +14,90 @@ import mapper from '../../data/components/scatterplot/mapper' import { groupsByScope } from '../../data/components/swarmplot/props' import { generateHeavyDataSet } from '../../data/components/swarmplot/generator' -const initialProperties = { - margin: { - top: 60, - right: 140, - bottom: 70, - left: 90, - }, - - xScale: { +const initialProperties = Object.freeze({ + pixelRatio: + typeof window !== 'undefined' && window.devicePixelRatio ? window.devicePixelRatio : 1, + groupBy: 'group', + identity: 'id', + value: 'price', + valueFormat: '$.2f', + valueScale: { type: 'linear', min: 0, - max: 'auto', + max: 500, }, - yScale: { - type: 'linear', - min: 0, - max: 'auto', + size: { + key: 'volume', + values: [4, 20], + sizes: [4, 12], }, + spacing: 1, + layout: SwarmPlotCanvasDefaultProps.layout, + gap: SwarmPlotCanvasDefaultProps.gap, - pixelRatio: - typeof window !== 'undefined' && window.devicePixelRatio ? window.devicePixelRatio : 1, - - colors: 'nivo', - colorBy: 'serie.id', - - symbolSize: 4, - symbolShape: 'circle', + forceStrength: 1, + simulationIterations: 60, + colors: { scheme: 'paired' }, + colorBy: 'group', + borderWidth: 0, + borderColor: { + from: 'color', + modifiers: [['darker', 0.6]], + }, + margin: { + top: 80, + right: 100, + bottom: 80, + left: 100, + }, + enableGridX: true, + enableGridY: true, axisTop: { - enable: false, + enable: true, orient: 'top', - tickSize: 5, + tickSize: 10, tickPadding: 5, tickRotation: 0, - legend: '', - legendOffset: 36, + legend: 'group if vertical, price if horizontal', + legendPosition: 'middle', + legendOffset: -46, }, axisRight: { - enable: false, + enable: true, orient: 'right', - tickSize: 5, + tickSize: 10, tickPadding: 5, tickRotation: 0, - legend: '', - legendOffset: 0, + legend: 'price if vertical, group if horizontal', + legendPosition: 'middle', + legendOffset: 76, }, axisBottom: { enable: true, orient: 'bottom', - tickSize: 5, + tickSize: 10, tickPadding: 5, tickRotation: 0, - legend: 'weight', + legend: 'group if vertical, price if horizontal', legendPosition: 'middle', - legendOffset: 36, - format: d => `${d} kg`, + legendOffset: 46, }, axisLeft: { enable: true, orient: 'left', - tickSize: 5, + tickSize: 10, tickPadding: 5, tickRotation: 0, - legend: 'size', + legend: 'price if vertical, group if horizontal', legendPosition: 'middle', - legendOffset: -40, - format: d => `${d} cm`, + legendOffset: -76, }, - enableGridX: true, - enableGridY: true, - - animate: true, - motionStiffness: 90, - motionDamping: 15, - isInteractive: true, useMesh: true, debugMesh: false, - - legends: [ - { - anchor: 'bottom-right', - direction: 'column', - translateX: 130, - itemWidth: 100, - itemHeight: 12, - itemsSpacing: 5, - symbolSize: 12, - symbolShape: 'circle', - }, - ], -} +}) const ScatterPlotCanvas = () => { return ( @@ -120,20 +111,28 @@ const ScatterPlotCanvas = () => { initialProperties={initialProperties} defaultProperties={SwarmPlotCanvasDefaultProps} propertiesMapper={mapper} + codePropertiesMapper={(properties, data) => ({ + groups: data.groups, + ...properties, + })} generateData={generateHeavyDataSet} + getTabData={data => data.data} + getDataSize={data => data.data.length} > {(properties, data, theme, logAction) => { return ( { logAction({ type: 'click', - label: `[point] serie: ${node.serie.id}, x: ${node.x}, y: ${ - node.y + label: `[node] id: ${node.id}, group: ${node.group}, value: ${ + node.value }`, + color: node.color, data: node, }) }} diff --git a/website/src/pages/swarmplot/index.js b/website/src/pages/swarmplot/index.js index 370a3cf97..5fcbd8b55 100644 --- a/website/src/pages/swarmplot/index.js +++ b/website/src/pages/swarmplot/index.js @@ -14,98 +14,92 @@ import mapper from '../../data/components/swarmplot/mapper' import { groupsByScope } from '../../data/components/swarmplot/props' import { generateLightDataSet } from '../../data/components/swarmplot/generator' -const initialProperties = { - layout: 'horizontal', - forceStrength: 4, - simulationIterations: 160, - gap: SwarmPlotDefaultProps.gap, - colors: SwarmPlotDefaultProps.colors, - nodeSize: 14, - nodePadding: 4, - borderWidth: 1, - borderColor: { - type: 'inherit:darker', - gamma: 0.4, - }, - scale: { +const initialProperties = Object.freeze({ + groupBy: 'group', + identity: 'id', + value: 'price', + valueFormat: '$.2f', + valueScale: { type: 'linear', min: 0, max: 500, }, + size: { + key: 'volume', + values: [4, 20], + sizes: [6, 20], + }, + spacing: 2, + layout: SwarmPlotDefaultProps.layout, + gap: SwarmPlotDefaultProps.gap, + + forceStrength: 4, + simulationIterations: 100, + + colors: SwarmPlotDefaultProps.colors, + colorBy: 'group', + borderWidth: 0, + borderColor: { + from: 'color', + modifiers: [['darker', 0.6], ['opacity', 0.5]], + }, margin: { top: 80, - right: 80, + right: 100, bottom: 80, - left: 80, + left: 100, }, + enableGridX: true, + enableGridY: true, axisTop: { enable: true, orient: 'top', - tickSize: 5, + tickSize: 10, tickPadding: 5, tickRotation: 0, - legend: '', + legend: 'group if vertical, price if horizontal', legendPosition: 'middle', - legendOffset: 36, + legendOffset: -46, }, axisRight: { - enable: false, + enable: true, orient: 'right', - tickSize: 5, + tickSize: 10, tickPadding: 5, tickRotation: 0, - legend: '', + legend: 'price if vertical, group if horizontal', legendPosition: 'middle', - legendOffset: 0, + legendOffset: 76, }, axisBottom: { enable: true, orient: 'bottom', - tickSize: 5, + tickSize: 10, tickPadding: 5, tickRotation: 0, - legend: '', + legend: 'group if vertical, price if horizontal', legendPosition: 'middle', legendOffset: 46, }, axisLeft: { enable: true, orient: 'left', - tickSize: 5, + tickSize: 10, tickPadding: 5, tickRotation: 0, - legend: '', + legend: 'price if vertical, group if horizontal', legendPosition: 'middle', - legendOffset: -60, + legendOffset: -76, }, - legends: [ - { - anchor: 'bottom-right', - direction: 'column', - translateX: 100, - itemWidth: 80, - itemHeight: 20, - itemTextColor: '#999', - symbolSize: 12, - symbolShape: 'circle', - onClick: d => { - alert(JSON.stringify(d, null, ' ')) - }, - effects: [ - { - on: 'hover', - style: { - itemTextColor: '#000', - }, - }, - ], - }, - ], + isInteractive: true, + useMesh: false, + debugMesh: false, + animate: true, - motionStiffness: 150, - motionDamping: 18, -} + motionStiffness: 50, + motionDamping: 10, +}) const ScatterPlot = () => { return ( @@ -119,20 +113,28 @@ const ScatterPlot = () => { initialProperties={initialProperties} defaultProperties={SwarmPlotDefaultProps} propertiesMapper={mapper} + codePropertiesMapper={(properties, data) => ({ + groups: data.groups, + ...properties, + })} generateData={generateLightDataSet} + getTabData={data => data.data} + getDataSize={data => data.data.length} > {(properties, data, theme, logAction) => { return ( { logAction({ type: 'click', - label: `[point] serie: ${node.serie.id}, x: ${node.x}, y: ${ - node.y + label: `[node] id: ${node.id}, group: ${node.group}, value: ${ + node.value }`, + color: node.color, data: node, }) }} diff --git a/website/src/pages/treemap/canvas.js b/website/src/pages/treemap/canvas.js index 7e93c4e42..b4434cbf0 100644 --- a/website/src/pages/treemap/canvas.js +++ b/website/src/pages/treemap/canvas.js @@ -16,8 +16,10 @@ import { groupsByScope } from '../../data/components/treemap/props' import { generateHeavyDataSet } from '../../data/components/treemap/generator' const initialProperties = { + identity: 'name', + value: 'value', tile: 'squarify', - leavesOnly: false, + leavesOnly: true, innerPadding: 2, outerPadding: 8, @@ -32,7 +34,7 @@ const initialProperties = { typeof window !== 'undefined' && window.devicePixelRatio ? window.devicePixelRatio : 1, enableLabel: true, - label: 'loc', + label: 'value', labelFormat: '.0s', labelSkipSize: 18, labelTextColor: { @@ -41,7 +43,8 @@ const initialProperties = { }, orientLabel: true, - colors: { scheme: 'nivo' }, + colors: { scheme: 'red_blue' }, + colorBy: 'name', borderWidth: 0, borderColor: { type: 'inherit:darker', @@ -75,7 +78,8 @@ const TreeMapCanvas = () => { onClick={node => { logAction({ type: 'click', - label: `[cell] ${node.id}: ${node.value}`, + label: `[node] ${node.id}: ${node.value}`, + color: node.color, data: omit(node, ['parent', 'children']), }) }} diff --git a/website/src/pages/treemap/html.js b/website/src/pages/treemap/html.js index c7d5c6cc4..6f92b296b 100644 --- a/website/src/pages/treemap/html.js +++ b/website/src/pages/treemap/html.js @@ -41,6 +41,7 @@ const initialProperties = { orientLabel: true, colors: { scheme: 'red_yellow_blue' }, + colorBy: 'depth', borderWidth: 0, borderColor: { type: 'inherit:darker', @@ -78,7 +79,8 @@ const TreeMapHtml = () => { onClick={node => { logAction({ type: 'click', - label: `[cell] ${node.id}: ${node.value}`, + label: `[node] ${node.id}: ${node.value}`, + color: node.color, data: omit(node, ['parent', 'children']), }) }} diff --git a/website/src/pages/treemap/index.js b/website/src/pages/treemap/index.js index dc5b95316..c05ae9b21 100644 --- a/website/src/pages/treemap/index.js +++ b/website/src/pages/treemap/index.js @@ -41,6 +41,7 @@ const initialProperties = { orientLabel: true, colors: { scheme: 'nivo' }, + colorBy: 'depth', borderWidth: 0, borderColor: { type: 'inherit:darker', @@ -78,7 +79,8 @@ const TreeMap = () => { onClick={node => { logAction({ type: 'click', - label: `[cell] ${node.id}: ${node.value}`, + label: `[node] ${node.id}: ${node.value}`, + color: node.color, data: omit(node, ['parent', 'children']), }) }} diff --git a/website/src/pages/waffle/canvas.js b/website/src/pages/waffle/canvas.js index 7ba633e6c..cf40514c8 100644 --- a/website/src/pages/waffle/canvas.js +++ b/website/src/pages/waffle/canvas.js @@ -156,6 +156,7 @@ const WaffleCanvas = () => { logAction({ type: 'click', label: `[cell] ${label}`, + color: node.color, data: node, }) }} diff --git a/website/src/pages/waffle/html.js b/website/src/pages/waffle/html.js index 4bcad77c4..b061cf0e2 100644 --- a/website/src/pages/waffle/html.js +++ b/website/src/pages/waffle/html.js @@ -105,6 +105,7 @@ const WaffleHtml = () => { logAction({ type: 'click', label: `[cell] ${label}`, + color: node.color, data: node, }) }} diff --git a/website/src/pages/waffle/index.js b/website/src/pages/waffle/index.js index cdfb28fdf..3fd0dfd25 100644 --- a/website/src/pages/waffle/index.js +++ b/website/src/pages/waffle/index.js @@ -134,6 +134,7 @@ const Waffle = () => { logAction({ type: 'click', label: `[cell] ${label}`, + color: node.color, data: node, }) }} diff --git a/website/src/theming/nivo.js b/website/src/theming/nivo.js index d17ff2043..d5a686b36 100644 --- a/website/src/theming/nivo.js +++ b/website/src/theming/nivo.js @@ -28,8 +28,8 @@ export default { }, legend: { text: { - fill: '#889eae', - fontSize: 12, + fill: '#6f6f6f', + fontSize: 13, fontWeight: 500, }, }, @@ -71,8 +71,8 @@ export default { }, legend: { text: { - fill: '#8d9cab', - fontSize: 12, + fill: '#ccd7e2', + fontSize: 13, fontWeight: 500, }, }, diff --git a/yarn.lock b/yarn.lock index f5ec96232..5bbc81a00 100644 --- a/yarn.lock +++ b/yarn.lock @@ -943,13 +943,6 @@ dependencies: regenerator-runtime "^0.13.2" -"@babel/runtime@^7.3.1": - version "7.4.3" - resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.4.3.tgz#79888e452034223ad9609187a0ad1fe0d2ad4bdc" - integrity sha512-9lsJwJLxDh/T3Q3SZszfWOTkk3pHbkmH+3KY+zwIDmsNlxsumuhS2TH3NIpktU4kNvfzy+k3eLT7aTJSPTo0OA== - dependencies: - regenerator-runtime "^0.13.2" - "@babel/template@7.0.0-beta.44": version "7.0.0-beta.44" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.0.0-beta.44.tgz#f8832f4fdcee5d59bf515e595fc5106c529b394f" @@ -1974,24 +1967,6 @@ call-me-maybe "^1.0.1" glob-to-regexp "^0.3.0" -"@nivo/babel-preset@0.54.0": - version "0.54.0" - resolved "https://registry.yarnpkg.com/@nivo/babel-preset/-/babel-preset-0.54.0.tgz#d2a5abb9506454bc84b8616561663b86a1e0851c" - integrity sha512-iFFqN5bRdttfcfd8BGJJ4NY7hTpoqueapcZsKAKM0y2YY5fPG2CdBUqR9i7wM+uk5AtmX2OCF2hxgkI54hgFqg== - dependencies: - "@babel/plugin-proposal-class-properties" "^7.4.0" - "@babel/preset-env" "^7.4.1" - babel-plugin-lodash "^3.3.4" - -"@nivo/generators@0.54.0": - version "0.54.0" - resolved "https://registry.yarnpkg.com/@nivo/generators/-/generators-0.54.0.tgz#6f4b6e6fdba3e90823b1ffc815952dbf0333343c" - integrity sha512-kK3t7JDguJVbzLq1N7uVOUJODLNw3AQcmU+lDKN3sps/7WXBfW9SNWKp4GIkNaQ42iwZR85/rUGofVjtYqQeEg== - dependencies: - d3-time "^1.0.10" - d3-time-format "^2.1.3" - lodash "^4.17.4" - "@nodelib/fs.stat@^1.1.2": version "1.1.3" resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-1.1.3.tgz#2b5a3ab3f918cca48a8c754c08168e3f03eba61b" @@ -14822,13 +14797,6 @@ react-side-effect@^1.1.0: exenv "^1.2.1" shallowequal "^1.0.1" -react-spring@^8.0.19: - version "8.0.19" - resolved "https://registry.yarnpkg.com/react-spring/-/react-spring-8.0.19.tgz#62f4f396b4b73fa402838200a1c80374338cb12e" - integrity sha512-DjrwjXqqVEitj6e6GqdW5dUp1BoVyeFQhEcXvPfoQxwyIVSJ9smNt8CNjSvoQqRujVllE7XKaJRWSZO/ewd1/A== - dependencies: - "@babel/runtime" "^7.3.1" - react-syntax-highlighter@^8.0.1: version "8.1.0" resolved "https://registry.yarnpkg.com/react-syntax-highlighter/-/react-syntax-highlighter-8.1.0.tgz#59103ff17a828a27ed7c8f035ae2558f09b6b78c"