├── .gitignore ├── LICENSE ├── README.md ├── assets └── teddy.png ├── lerna.json ├── package.json ├── packages ├── charts │ ├── .babelrc │ ├── .npmrc │ ├── README.md │ ├── docs │ │ └── description.md │ ├── package.json │ ├── rollup.config.js │ └── src │ │ ├── Bar │ │ └── index.js │ │ ├── Chart │ │ ├── Chart.scss │ │ ├── WithTooltip.js │ │ └── index.js │ │ ├── HorizontalBar │ │ └── index.js │ │ ├── HorizontalStackedBar │ │ └── index.js │ │ ├── Line │ │ ├── HoverLine.js │ │ └── index.js │ │ ├── Scatterplot │ │ └── index.js │ │ ├── VerticalGroupedBar │ │ └── index.js │ │ └── index.js ├── components │ ├── .babelrc │ ├── .npmrc │ ├── README.md │ ├── docs │ │ └── description.md │ ├── package.json │ ├── rollup.config.js │ └── src │ │ ├── ButtonGroup │ │ ├── ButtonGroup.scss │ │ └── index.js │ │ ├── CheckboxGroup │ │ ├── CheckboxGroup.scss │ │ ├── README.md │ │ └── index.js │ │ ├── Search │ │ ├── README.md │ │ ├── Search.scss │ │ └── index.js │ │ ├── Select │ │ ├── README.md │ │ ├── Select.scss │ │ └── index.js │ │ ├── Slider │ │ ├── README.md │ │ ├── Slider.scss │ │ └── index.js │ │ ├── Toggle │ │ ├── README.md │ │ ├── Toggle.scss │ │ └── index.js │ │ └── index.js ├── data-table │ ├── .babelrc │ ├── .npmrc │ ├── README.md │ ├── docs │ │ └── description.md │ ├── package.json │ ├── rollup.config.js │ └── src │ │ ├── DataTable │ │ ├── DataTable.scss │ │ ├── Pagination.js │ │ └── index.js │ │ ├── DataTableWithSearch │ │ ├── index.js │ │ └── withSearch.js │ │ └── index.js ├── maps │ ├── .babelrc │ ├── .npmrc │ ├── README.md │ ├── docs │ │ └── description.md │ ├── package.json │ ├── rollup.config.js │ └── src │ │ ├── Cartogram │ │ ├── Cartogram.scss │ │ ├── index.js │ │ └── layout.js │ │ ├── Choropleth │ │ └── index.js │ │ ├── LoadGeometry │ │ └── index.js │ │ ├── Pindrop │ │ └── index.js │ │ ├── Projection │ │ └── index.js │ │ └── index.js ├── meta │ ├── .babelrc │ ├── .npmrc │ ├── README.md │ ├── docs │ │ └── description.md │ ├── package.json │ ├── rollup.config.js │ └── src │ │ ├── ChartContainer │ │ ├── ChartContainer.scss │ │ └── index.js │ │ ├── Description │ │ ├── Description.scss │ │ └── index.js │ │ ├── Source │ │ ├── Source.scss │ │ └── index.js │ │ ├── Title │ │ ├── Title.scss │ │ └── index.js │ │ └── index.js ├── scss │ ├── _box-shadow.scss │ ├── _breakpoints.scss │ ├── _colors.scss │ ├── _spacing.scss │ ├── _widths.scss │ └── package.json ├── storybook │ ├── .npmrc │ ├── package.json │ └── src │ │ ├── addons.js │ │ ├── config.js │ │ ├── lib │ │ └── colors.js │ │ └── stories │ │ ├── index.js │ │ └── newamericadotorg.lite.css └── timeline │ ├── .babelrc │ ├── .npmrc │ ├── package.json │ ├── rollup.config.js │ └── src │ ├── Timeline │ ├── ContentArea.js │ ├── Timeline.scss │ ├── TimelineControl.js │ ├── dodge.js │ └── index.js │ └── index.js └── scripts ├── buildDocs.sh └── template.js /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | dist 4 | *.log 5 | data 6 | public 7 | .env 8 | .storybook/dist 9 | package-lock.json -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 New America 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Teddy Logo](./assets/teddy.png) 2 | 3 | ## Teddy 4 | 5 | Teddy is a library of charts, maps, and user interface components for data visualization, built with React and vx. 6 | 7 | [**Demo and examples**](https://data.newamerica.org/component-library/) 8 | 9 | ## Usage 10 | 11 | Example: 12 | 13 | ```jsx 14 | import { Chart, Bar } from "@newamerica/charts"; 15 | import "@newamerica/charts/dist/styles.css"; 16 | 17 | const MyChart = () => ( 18 |
{datum.value}
} 22 | > 23 | {({ width, height, handleMouseMove, handleMouseLeave }) => ( 24 | d.x} 29 | y={d => +d.y} 30 | handleMouseMove={handleMouseMove} 31 | handleMouseLeave={handleMouseLeave} 32 | /> 33 | )} 34 |
35 | ); 36 | ``` 37 | 38 | ## Packages and docs 39 | 40 | _More charts and documentation coming soon_ 41 | 42 | **Charts** ([docs](./packages/charts/README.md)) 43 | 44 | ```bash 45 | npm install --save @newamerica/charts 46 | ``` 47 | 48 | - Bar 49 | - HorizontalBar 50 | - HorizontalStackedBar 51 | - VerticalGroupedBar 52 | - Line 53 | - Scatterplot 54 | 55 | **Maps** ([docs](./packages/maps/README.md)) 56 | 57 | ```bash 58 | npm install --save @newamerica/maps 59 | ``` 60 | 61 | - Pindrop 62 | - Choropleth 63 | - Cartogram 64 | - Hexgrid (coming soon) 65 | 66 | **Data Table** ([docs](./packages/data-table/README.md)) 67 | 68 | ```bash 69 | npm install --save @newamerica/data-table 70 | ``` 71 | 72 | - DataTable 73 | - DataTableWithSearch 74 | 75 | **Timeline** 76 | 77 | ```bash 78 | npm install --save @newamerica/timeline 79 | ``` 80 | 81 | - Timeline 82 | 83 | **Components** ([docs](./packages/components/README.md)) 84 | 85 | ```bash 86 | npm install --save @newamerica/components 87 | ``` 88 | 89 | - ButtonGroup 90 | - CheckboxGroup 91 | - Search 92 | - Select 93 | - Slider 94 | - Toggle 95 | 96 | **Meta** ([docs](./packages/meta/README.md)) 97 | 98 | ```bash 99 | npm install --save @newamerica/meta 100 | ``` 101 | 102 | - ChartContainer 103 | - Title 104 | - Description 105 | - Source 106 | 107 | ## To do 108 | 109 | - [x] add prop type checks to all packages 110 | - [x] generate documentation from prop types 111 | - [ ] add mobile touch events for tooltip interactions 112 | - [ ] project website 113 | - [ ] improve accessibility across packages, especially for UI components 114 | 115 | ## Development 116 | 117 | Clone this repo: 118 | 119 | ```bash 120 | git clone https://github.com/newamericafoundation/teddy.git 121 | ``` 122 | 123 | Install [lerna](https://github.com/lerna/lerna) globally: 124 | 125 | ```bash 126 | npm i -g lerna 127 | ``` 128 | 129 | Bootstrap all packages. This installs package dependencies (equivalent to `npm install` in every package folder), but hoists dependencies required by multiple packages up to the top level `node_modules`. It also symlinks `@newamerica` dependencies to that package's `packages//dist` folder. 130 | 131 | ```bash 132 | lerna bootstrap --hoist 133 | ``` 134 | 135 | To publish new package versions to npm: 136 | 137 | ```bash 138 | lerna publish 139 | ``` 140 | 141 | **Local development** 142 | 143 | Watch file changes in all packages and create development builds. This runs `rollup -c -w --environment BUILD:development` inside of every package: 144 | 145 | ```bash 146 | lerna run start --parallel 147 | ``` 148 | 149 | If you just want to work on one or a couple packages, run something like this instead (it'll be a bit lighter on your computer, because it won't spawn separate subprocesses to watch/build every single package). 150 | 151 | ``` 152 | lerna run start --parallel --scope @newamerica/charts @newamerica/maps 153 | ``` 154 | 155 | Now you can start storybook to develop charts/maps/components locally. Packages will be rebuilt automatically on file changes and storybook will hot reload those changes. Go to `packages/storybook` and run: 156 | 157 | ```bash 158 | npm run storybook 159 | ``` 160 | 161 | **Docs** 162 | 163 | To generate documentation from component prop-types, run this from the root of the repo, or run `npm run docs` in an individual package folder: 164 | 165 | ```bash 166 | lerna run docs 167 | ``` 168 | -------------------------------------------------------------------------------- /assets/teddy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/newamericafoundation/teddy/0ec5ff34159f9c099f96339174ede5e9cb556fa6/assets/teddy.png -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "packages": [ 3 | "packages/*" 4 | ], 5 | "version": "independent" 6 | } 7 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "teddy", 3 | "version": "0.0.1", 4 | "description": "A charting and component library for New America data visualization projects", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "docs": "react-docgen packages/charts" 9 | }, 10 | "author": "lorenries", 11 | "license": "MIT", 12 | "devDependencies": { 13 | "lerna": "^3.10.5", 14 | "react-docgen": "^3.0.0", 15 | "react-docgen-markdown-renderer": "^1.1.0" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /packages/charts/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/preset-react", 4 | ["@babel/preset-env", { "modules": false }] 5 | ], 6 | "plugins": [ 7 | "@babel/plugin-proposal-class-properties", 8 | "@babel/plugin-proposal-object-rest-spread" 9 | ], 10 | "ignore": ["node_modules/**"] 11 | } 12 | -------------------------------------------------------------------------------- /packages/charts/.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false -------------------------------------------------------------------------------- /packages/charts/README.md: -------------------------------------------------------------------------------- 1 | # @newamerica/charts 2 | 3 | A collection of reusable, fully responsive charting components for data visualization. 4 | 5 | ### Installation 6 | 7 | ``` 8 | npm install @newamerica/charts --save 9 | ``` 10 | 11 | ### Usage Example 12 | 13 | ```jsx 14 | import { Chart, Bar } from "@newamerica/charts"; 15 | import "@newamerica/charts/dist/styles.css"; 16 | 17 | const MyChart = () => ( 18 |
{datum.value}
} 22 | > 23 | {({ width, height, handleMouseMove, handleMouseLeave }) => ( 24 | d.x} 29 | y={d => +d.y} 30 | handleMouseMove={handleMouseMove} 31 | handleMouseLeave={handleMouseLeave} 32 | /> 33 | )} 34 |
35 | ); 36 | ``` 37 | 38 | While not required, the base `Chart` component is helpful, because it creates a fully responsive svg container for your chart (it uses a polyfilled version of the Intersection Observer API to watch for _debounced_ changes in screen size and resizes the svg accordingly). It can also optionally take care of rendering chart tooltips. 39 | 40 | Children must be passed in via a [render prop](https://reactjs.org/docs/render-props.html), and automatically receive the current `width` and `height` of the chart's svg. If the `renderTooltip` prop is defined, children will also receive the `handleMouseMove` and `handleMouseLeave` functions for calling tooltips. 41 | 42 | ⚠️ If you choose not to use the `Chart` component, be aware that all other chart types will return an svg `g` element, so you'd have to render those inside of an svg on your own. 43 | 44 | 45 | ## Components 46 | 47 | 48 | 49 | - [Bar](#bar) 50 | - [Chart](#chart) 51 | - [HorizontalBar](#horizontalbar) 52 | - [HorizontalStackedBar](#horizontalstackedbar) 53 | - [Line](#line) 54 | - [Scatterplot](#scatterplot) 55 | - [VerticalGroupedBar](#verticalgroupedbar) 56 | 57 | ## API 58 | 59 | 60 | 61 | 62 | ### Bar 63 | 64 | From [`./src/Bar/index.js`](./src/Bar/index.js) 65 | 66 | 67 | 68 | prop | type | default | required | description 69 | ---- | :----: | :-------: | :--------: | ----------- 70 | **color** | `String` | `"#22C8A3"` | :x: | 71 | **data** | `Array` | | :white_check_mark: | 72 | **handleMouseLeave** | `Function` | | :x: | 73 | **handleMouseMove** | `Function` | | :x: | 74 | **height** | `Number` | | :white_check_mark: | 75 | **margin** | `Shape` | `{ top: 10, left: 55, right: 10, bottom: 30 }` | :x: | 76 | **margin.bottom** | `Number` | | :x: | 77 | **margin.left** | `Number` | | :x: | 78 | **margin.right** | `Number` | | :x: | 79 | **margin.top** | `Number` | | :x: | 80 | **numTicksY** | `Union` | `5` | :x: | You can specify the number of y axis ticks directly, or pass in a function which will receive the chart's computed height as an argument. 81 | **width** | `Number` | | :white_check_mark: | 82 | **x** | `Function` | | :white_check_mark: | Accessor function for x axis values 83 | **xAxisLabel** | `String` | | :x: | 84 | **xFormat** | `Function` | | :x: | Formatting function for x axis tick labels 85 | **y** | `Function` | | :white_check_mark: | Accessor function for y axis values 86 | **yAxisLabel** | `String` | | :x: | 87 | **yFormat** | `Function` | | :x: | Formatting function for y axis tick labels 88 | 89 | 90 | 91 | 92 | 93 | ### Chart 94 | 95 | From [`./src/Chart/index.js`](./src/Chart/index.js) 96 | 97 | The base Chart component for all charts and maps. 98 | This takes care of creating a responsive svg and rendering tooltips. 99 | 100 | prop | type | default | required | description 101 | ---- | :----: | :-------: | :--------: | ----------- 102 | **aspectRatio** | `(custom validator)` | | :x: | The aspectRatio of the chart. This is a number that is multiplied by the chart's computed width to calculate the chart's height. The chart MUST receive either a height or and aspectRatio prop. 103 | **children** | `Function` | | :white_check_mark: | A function that is passed the caculated width and height of the chart, as well as tooltip functions (if the renderTooltip prop is defined) 104 | **height** | `(custom validator)` | | :x: | The height of the chart. Can either be a string (i.e. `100%` or `8rem`) or a number representing a pixel value. The chart MUST receive either a height or and aspectRatio prop. 105 | **maxWidth** | `Union` | `"100%"` | :x: | The max width of the chart. Can either be a string (i.e. `100%` or `8rem`) or a number representing a pixel value. 106 | **renderAnnotation** | `Function` | | :x: | A function that returns a component for an annotation, which is rendered at the very bottom of the svg. It receive's the chart's current width and height (which are helpful to have for annotation positioning). 107 | **renderLegend** | `Function` | | :x: | A function that returns a component for the chart's legend. This is rendered as a div above the chart's svg. 108 | **renderTooltip** | `Function` | | :x: | A function that returns a component for the chart's tooltip. It receives event, datum, and any other arguments passed into the `handleMouseMove` function. 109 | 110 | 111 | 112 | 113 | 114 | ### HorizontalBar 115 | 116 | From [`./src/HorizontalBar/index.js`](./src/HorizontalBar/index.js) 117 | 118 | 119 | 120 | prop | type | default | required | description 121 | ---- | :----: | :-------: | :--------: | ----------- 122 | **color** | `String` | `"#22C8A3"` | :x: | 123 | **data** | `Array` | | :white_check_mark: | 124 | **handleMouseLeave** | `Function` | | :x: | 125 | **handleMouseMove** | `Function` | | :x: | 126 | **height** | `Number` | | :white_check_mark: | 127 | **margin** | `Shape` | `{ top: 10, left: 50, right: 10, bottom: 20 }` | :x: | 128 | **margin.bottom** | `Number` | | :x: | 129 | **margin.left** | `Number` | | :x: | 130 | **margin.right** | `Number` | | :x: | 131 | **margin.top** | `Number` | | :x: | 132 | **numTicksX** | `Union` | `6` | :x: | 133 | **width** | `Number` | | :white_check_mark: | 134 | **x** | `Function` | | :white_check_mark: | 135 | **xAxisLabel** | `String` | | :x: | 136 | **xFormat** | `Function` | | :x: | 137 | **y** | `Function` | | :white_check_mark: | 138 | **yAxisLabel** | `String` | | :x: | 139 | **yFormat** | `Function` | | :x: | 140 | **yLabelOffset** | `String` | `"-0.5em"` | :x: | 141 | 142 | 143 | 144 | 145 | 146 | ### HorizontalStackedBar 147 | 148 | From [`./src/HorizontalStackedBar/index.js`](./src/HorizontalStackedBar/index.js) 149 | 150 | 151 | 152 | prop | type | default | required | description 153 | ---- | :----: | :-------: | :--------: | ----------- 154 | **colors** | `Array` | | :white_check_mark: | 155 | **data** | `Array` | | :white_check_mark: | 156 | **handleMouseLeave** | `Function` | | :x: | 157 | **handleMouseMove** | `Function` | | :x: | 158 | **height** | `Number` | | :white_check_mark: | 159 | **keys** | `Array` | | :white_check_mark: | An array of strings with the column keys of each bar 160 | **margin** | `Shape` | `{ top: 10, left: 60, right: 40, bottom: 40 }` | :x: | 161 | **margin.bottom** | `Number` | | :x: | 162 | **margin.left** | `Number` | | :x: | 163 | **margin.right** | `Number` | | :x: | 164 | **margin.top** | `Number` | | :x: | 165 | **numTicksX** | `Union` | | :x: | 166 | **width** | `Number` | | :white_check_mark: | 167 | **xAxisLabel** | `String` | | :x: | 168 | **xFormat** | `Function` | | :x: | 169 | **y** | `Function` | | :white_check_mark: | Accessor function for y axis values 170 | **yAxisLabel** | `String` | | :x: | 171 | **yFormat** | `Function` | | :x: | 172 | 173 | 174 | 175 | 176 | 177 | ### Line 178 | 179 | From [`./src/Line/index.js`](./src/Line/index.js) 180 | 181 | 182 | 183 | prop | type | default | required | description 184 | ---- | :----: | :-------: | :--------: | ----------- 185 | **data** | `Array` | | :white_check_mark: | 186 | **handleMouseLeave** | `Function` | | :x: | 187 | **handleMouseMove** | `Function` | | :x: | 188 | **height** | `Number` | | :white_check_mark: | 189 | **margin** | `Shape` | `{ top: 10, left: 55, bottom: 30, right: 10 }` | :x: | 190 | **margin.bottom** | `Number` | | :x: | 191 | **margin.left** | `Number` | | :x: | 192 | **margin.right** | `Number` | | :x: | 193 | **margin.top** | `Number` | | :x: | 194 | **numTicksX** | `Union` | `10` | :x: | You can specify the number of x axis ticks directly, or pass in a function which will receive the chart's computed width as an argument. 195 | **numTicksY** | `Union` | `5` | :x: | You can specify the number of y axis ticks directly, or pass in a function which will receive the chart's computed height as an argument. 196 | **stroke** | `String` | `"#22C8A3"` | :x: | 197 | **strokeWidth** | `Number` | `2` | :x: | 198 | **tooltipOpen** | `Boolean` | | :x: | 199 | **width** | `Number` | | :white_check_mark: | 200 | **x** | `Function` | | :white_check_mark: | 201 | **xAxisLabel** | `String` | | :x: | 202 | **xFormat** | `Function` | | :x: | 203 | **y** | `Function` | | :white_check_mark: | 204 | **yAxisLabel** | `String` | | :x: | 205 | **yFormat** | `Function` | | :x: | 206 | 207 | 208 | 209 | 210 | 211 | ### Scatterplot 212 | 213 | From [`./src/Scatterplot/index.js`](./src/Scatterplot/index.js) 214 | 215 | 216 | 217 | prop | type | default | required | description 218 | ---- | :----: | :-------: | :--------: | ----------- 219 | **circleFill** | `Union` | `"rgba(76,129,219, 0.4)"` | :x: | A string for each circle's fill, or a function that will receive that circle's datum 220 | **circleRadius** | `Union` | `5` | :x: | A number for the circle's radius, or a function that will receive that circle's datum for [radius scaling](https://bl.ocks.org/guilhermesimoes/e6356aa90a16163a6f917f53600a2b4a). 221 | **circleStroke** | `Union` | `"#4C81DB"` | :x: | A string for each circle's stroke, or a function that will receive that circle's datum 222 | **data** | `Array` | | :white_check_mark: | 223 | **handleMouseLeave** | `Function` | | :x: | 224 | **handleMouseMove** | `Function` | | :x: | 225 | **height** | `Number` | | :white_check_mark: | 226 | **margin** | `Shape` | `{ top: 10, bottom: 50, left: 55, right: 10 }` | :x: | 227 | **margin.bottom** | `Number` | | :x: | 228 | **margin.left** | `Number` | | :x: | 229 | **margin.right** | `Number` | | :x: | 230 | **margin.top** | `Number` | | :x: | 231 | **numTicksX** | `Union` | `5` | :x: | You can specify the number of x axis ticks directly, or pass in a function which will receive the chart's computed width as an argument. 232 | **numTicksY** | `Union` | `5` | :x: | You can specify the number of y axis ticks directly, or pass in a function which will receive the chart's computed height as an argument. 233 | **width** | `Number` | | :white_check_mark: | 234 | **x** | `Function` | | :white_check_mark: | Accessor function for x axis values 235 | **xAxisLabel** | `String` | | :x: | 236 | **xFormat** | `Function` | | :x: | Formatting function for x axis tick labels 237 | **y** | `Function` | | :white_check_mark: | Accessor function for y axis values 238 | **yAxisLabel** | `String` | | :x: | 239 | **yFormat** | `Function` | | :x: | Formatting function for y axis tick labels 240 | 241 | 242 | 243 | 244 | 245 | ### VerticalGroupedBar 246 | 247 | From [`./src/VerticalGroupedBar/index.js`](./src/VerticalGroupedBar/index.js) 248 | 249 | 250 | 251 | prop | type | default | required | description 252 | ---- | :----: | :-------: | :--------: | ----------- 253 | **colors** | `Array` | | :white_check_mark: | 254 | **data** | `Array` | | :white_check_mark: | 255 | **handleMouseLeave** | `Function` | | :x: | 256 | **handleMouseMove** | `Function` | | :x: | 257 | **height** | `Number` | | :white_check_mark: | 258 | **keys** | `Array` | | :white_check_mark: | An array of strings with the keys for each bar 259 | **margin** | `Shape` | `{ top: 40, left: 40, right: 40, bottom: 40 }` | :x: | 260 | **margin.bottom** | `Number` | | :x: | 261 | **margin.left** | `Number` | | :x: | 262 | **margin.right** | `Number` | | :x: | 263 | **margin.top** | `Number` | | :x: | 264 | **numTicksY** | `Union` | `5` | :x: | 265 | **tooltipOpen** | `Boolean` | | :x: | 266 | **width** | `Number` | | :white_check_mark: | 267 | **x** | `Function` | | :white_check_mark: | Accessor function for x axis values 268 | **xAxisLabel** | `String` | | :x: | 269 | **xFormat** | `Function` | | :x: | 270 | **yAxisLabel** | `String` | | :x: | 271 | **yFormat** | `Function` | | :x: | 272 | 273 | 274 | 275 | -------------------------------------------------------------------------------- /packages/charts/docs/description.md: -------------------------------------------------------------------------------- 1 | # @newamerica/charts 2 | 3 | A collection of reusable, fully responsive charting components for data visualization. 4 | 5 | ### Installation 6 | 7 | ``` 8 | npm install @newamerica/charts --save 9 | ``` 10 | 11 | ### Usage Example 12 | 13 | ```jsx 14 | import { Chart, Bar } from "@newamerica/charts"; 15 | import "@newamerica/charts/dist/styles.css"; 16 | 17 | const MyChart = () => ( 18 |
{datum.value}
} 22 | > 23 | {({ width, height, handleMouseMove, handleMouseLeave }) => ( 24 | d.x} 29 | y={d => +d.y} 30 | handleMouseMove={handleMouseMove} 31 | handleMouseLeave={handleMouseLeave} 32 | /> 33 | )} 34 |
35 | ); 36 | ``` 37 | 38 | While not required, the base `Chart` component is helpful, because it creates a fully responsive svg container for your chart (it uses a polyfilled version of the Intersection Observer API to watch for _debounced_ changes in screen size and resizes the svg accordingly). It can also optionally take care of rendering chart tooltips. 39 | 40 | Children must be passed in via a [render prop](https://reactjs.org/docs/render-props.html), and automatically receive the current `width` and `height` of the chart's svg. If the `renderTooltip` prop is defined, children will also receive the `handleMouseMove` and `handleMouseLeave` functions for calling tooltips. 41 | 42 | ⚠️ If you choose not to use the `Chart` component, be aware that all other chart types will return an svg `g` element, so you'd have to render those inside of an svg on your own. 43 | -------------------------------------------------------------------------------- /packages/charts/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@newamerica/charts", 3 | "version": "0.0.4", 4 | "description": "All the charts for all your hearts", 5 | "main": "dist/index.umd.js", 6 | "module": "dist/index.es.js", 7 | "repository": { 8 | "type": "git", 9 | "url": "https://github.com/newamericafoundation/teddy.git" 10 | }, 11 | "publishConfig": { 12 | "access": "public" 13 | }, 14 | "sideEffects": false, 15 | "scripts": { 16 | "start": "rollup -c -w --environment BUILD:development", 17 | "build": "rollup -c --environment BUILD:production", 18 | "prepublish": "rm -rf ./dist && npm run build", 19 | "docs": "cd ./docs && ../../../node_modules/.bin/react-docgen ../src/** -e WithTooltip.js -e HoverLine.js | ../../../scripts/buildDocs.sh", 20 | "test": "echo \"Error: no test specified\" && exit 1" 21 | }, 22 | "keywords": [ 23 | "vx", 24 | "react", 25 | "d3", 26 | "charts", 27 | "data", 28 | "visualization" 29 | ], 30 | "dependencies": { 31 | "@vx/axis": "0.0.182", 32 | "@vx/event": "0.0.182", 33 | "@vx/grid": "0.0.183", 34 | "@vx/group": "0.0.183", 35 | "@vx/mock-data": "0.0.182", 36 | "@vx/responsive": "0.0.182", 37 | "@vx/scale": "0.0.182", 38 | "@vx/shape": "0.0.183", 39 | "@vx/tooltip": "0.0.182", 40 | "d3-array": "^1.2.4", 41 | "prop-types": "^15.6.2" 42 | }, 43 | "peerDependencies": { 44 | "react": "^16.2.0" 45 | }, 46 | "devDependencies": { 47 | "@babel/core": "^7.0.0", 48 | "@babel/plugin-proposal-class-properties": "^7.0.0", 49 | "@babel/plugin-proposal-object-rest-spread": "^7.0.0", 50 | "@babel/preset-env": "^7.0.0", 51 | "@babel/preset-react": "^7.0.0", 52 | "autoprefixer": "^9.4.5", 53 | "node-sass": "^4.9.2", 54 | "rollup": "^1.1.0", 55 | "rollup-plugin-babel": "^4.3.1", 56 | "rollup-plugin-node-resolve": "^4.0.0", 57 | "rollup-plugin-postcss": "^1.6.3", 58 | "rollup-plugin-terser": "^4.0.2" 59 | }, 60 | "author": "lorenries", 61 | "license": "MIT" 62 | } 63 | -------------------------------------------------------------------------------- /packages/charts/rollup.config.js: -------------------------------------------------------------------------------- 1 | import resolve from "rollup-plugin-node-resolve"; 2 | import babel from "rollup-plugin-babel"; 3 | import { terser } from "rollup-plugin-terser"; 4 | import postcss from "rollup-plugin-postcss"; 5 | import autoprefixer from "autoprefixer"; 6 | import pkg from "./package.json"; 7 | 8 | const deps = Object.keys({ 9 | ...pkg.dependencies, 10 | ...pkg.peerDependencies 11 | }); 12 | 13 | const globals = deps.reduce((o, name) => { 14 | if (name.includes("@vx/")) { 15 | o[name] = "vx"; 16 | } 17 | if (name.includes("d3-")) { 18 | o[name] = "d3"; 19 | } 20 | if (name === "react") { 21 | o[name] = "React"; 22 | } 23 | if (name === "react-dom") { 24 | o[name] = "ReactDOM"; 25 | } 26 | if (name === "prop-types") { 27 | o[name] = "PropTypes"; 28 | } 29 | if (name === "classnames") { 30 | o[name] = "classNames"; 31 | } 32 | return o; 33 | }, {}); 34 | 35 | export default [ 36 | { 37 | input: "src/index.js", 38 | external: deps, 39 | plugins: [ 40 | resolve(), 41 | babel({ 42 | exclude: "node_modules/**" 43 | }), 44 | postcss({ 45 | extensions: [".css", ".scss"], 46 | plugins: [autoprefixer], 47 | minimize: true, 48 | inject: false, 49 | extract: "dist/styles.css" 50 | }), 51 | process.env.BUILD === "production" && terser() 52 | ], 53 | output: { 54 | file: pkg.main, 55 | format: "umd", 56 | name: "charts", 57 | globals 58 | } 59 | }, 60 | { 61 | input: "src/index.js", 62 | external: deps, 63 | plugins: [ 64 | resolve(), 65 | babel({ 66 | exclude: "node_modules/**" 67 | }), 68 | postcss({ 69 | extensions: [".css", ".scss"], 70 | plugins: [autoprefixer], 71 | minimize: true, 72 | inject: false, 73 | extract: "dist/styles.css" 74 | }), 75 | process.env.BUILD === "production" && terser() 76 | ], 77 | output: { file: pkg.module, format: "es", globals } 78 | } 79 | ]; 80 | -------------------------------------------------------------------------------- /packages/charts/src/Bar/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import { Group } from "@vx/group"; 4 | import { AxisBottom, AxisLeft } from "@vx/axis"; 5 | import { scaleBand, scaleLinear } from "@vx/scale"; 6 | import { GridRows } from "@vx/grid"; 7 | import { max } from "d3-array"; 8 | 9 | const Bar = ({ 10 | width, 11 | height, 12 | handleMouseMove, 13 | handleMouseLeave, 14 | data, 15 | x, 16 | y, 17 | xFormat, 18 | yFormat, 19 | xAxisLabel, 20 | yAxisLabel, 21 | numTicksY, 22 | color, 23 | margin 24 | }) => { 25 | const xMax = width - margin.left - margin.right; 26 | const yMax = height - margin.top - margin.bottom; 27 | 28 | const xScale = scaleBand({ 29 | rangeRound: [0, xMax], 30 | domain: data.map(d => x(d)), 31 | padding: 0.2 32 | }); 33 | 34 | const yScale = scaleLinear({ 35 | rangeRound: [yMax, 0], 36 | domain: [0, max(data, y)] 37 | }); 38 | 39 | return ( 40 | 41 | 48 | 49 | {data.map((datum, i) => { 50 | return ( 51 | 59 | handleMouseMove ? handleMouseMove({ event, data, datum }) : null 60 | } 61 | onMouseLeave={handleMouseLeave ? handleMouseLeave : null} 62 | /> 63 | ); 64 | })} 65 | 66 | ({ 75 | textAnchor: "end", 76 | verticalAnchor: "middle" 77 | })} 78 | label={yAxisLabel} 79 | labelProps={{ 80 | textAnchor: "middle", 81 | verticalAnchor: "end" 82 | }} 83 | /> 84 | ({ 92 | textAnchor: "middle", 93 | width: xScale.bandwidth(), 94 | verticalAnchor: "middle" 95 | })} 96 | labelProps={{ 97 | dy: "3em", 98 | textAnchor: "middle", 99 | y: 0 100 | }} 101 | /> 102 | 103 | ); 104 | }; 105 | 106 | Bar.propTypes = { 107 | width: PropTypes.number.isRequired, 108 | height: PropTypes.number.isRequired, 109 | handleMouseMove: PropTypes.func, 110 | handleMouseLeave: PropTypes.func, 111 | data: PropTypes.array.isRequired, 112 | /** 113 | * Accessor function for x axis values 114 | */ 115 | x: PropTypes.func.isRequired, 116 | /** 117 | * Accessor function for y axis values 118 | */ 119 | y: PropTypes.func.isRequired, 120 | /** 121 | * Formatting function for x axis tick labels 122 | */ 123 | xFormat: PropTypes.func, 124 | /** 125 | * Formatting function for y axis tick labels 126 | */ 127 | yFormat: PropTypes.func, 128 | xAxisLabel: PropTypes.string, 129 | yAxisLabel: PropTypes.string, 130 | /** 131 | * You can specify the number of y axis ticks directly, or pass in a function which will receive the chart's computed height as an argument. 132 | */ 133 | numTicksY: PropTypes.oneOfType([PropTypes.number, PropTypes.func]), 134 | color: PropTypes.string, 135 | margin: PropTypes.shape({ 136 | top: PropTypes.number, 137 | right: PropTypes.number, 138 | bottom: PropTypes.number, 139 | left: PropTypes.number 140 | }) 141 | }; 142 | 143 | Bar.defaultProps = { 144 | numTicksY: 5, 145 | color: "#22C8A3", 146 | margin: { 147 | top: 10, 148 | left: 55, 149 | right: 10, 150 | bottom: 30 151 | } 152 | }; 153 | 154 | export default Bar; 155 | -------------------------------------------------------------------------------- /packages/charts/src/Chart/Chart.scss: -------------------------------------------------------------------------------- 1 | .dv-Chart { 2 | width: 100%; 3 | margin-left: auto; 4 | margin-right: auto; 5 | } 6 | 7 | .dv-legend-container { 8 | position: absolute; 9 | width: 100%; 10 | display: flex; 11 | justify-content: center; 12 | } 13 | 14 | .vx-legend-item:last-child .vx-legend-label { 15 | margin-right: 0 !important; 16 | } 17 | 18 | .vx-legend-label { 19 | font-size: 12px; 20 | } 21 | 22 | .vx-axis-label { 23 | font-size: 10px; 24 | text-transform: uppercase; 25 | font-weight: bold; 26 | letter-spacing: 0.05em; 27 | fill: #333; 28 | } 29 | 30 | .vx-axis-line { 31 | stroke: rgba(0, 0, 0, 0.2); 32 | } 33 | 34 | .vx-axis-tick { 35 | font-size: 11px; 36 | fill: #333; 37 | font-weight: normal; 38 | font-family: Circular; 39 | .vx-line { 40 | stroke: rgba(0, 0, 0, 0.2); 41 | } 42 | } 43 | 44 | .vx-rows .vx-line, 45 | .vx-columns .vx-line { 46 | stroke: #ddd; 47 | } 48 | 49 | .annotation-note-label { 50 | font-size: 12px; 51 | font-family: Circular; 52 | fill: #333; 53 | } 54 | -------------------------------------------------------------------------------- /packages/charts/src/Chart/WithTooltip.js: -------------------------------------------------------------------------------- 1 | // code adapted from Chris Williams' data-ui: https://github.com/williaster/data-ui/blob/master/packages/shared/src/enhancer/WithTooltip.jsx 2 | 3 | import React from "react"; 4 | import PropTypes from "prop-types"; 5 | import { withTooltip, TooltipWithBounds } from "@vx/tooltip"; 6 | import { localPoint } from "@vx/event"; 7 | 8 | class WithTooltip extends React.Component { 9 | constructor(props) { 10 | super(props); 11 | this.handleMouseMove = this.handleMouseMove.bind(this); 12 | this.handleMouseLeave = this.handleMouseLeave.bind(this); 13 | this.tooltipTimeout = null; 14 | } 15 | 16 | componentWillUnmount() { 17 | if (this.tooltipTimeout) { 18 | clearTimeout(this.tooltipTimeout); 19 | } 20 | } 21 | 22 | handleMouseMove({ event, datum, coords, ...rest }) { 23 | const { showTooltip } = this.props; 24 | if (this.tooltipTimeout) { 25 | clearTimeout(this.tooltipTimeout); 26 | } 27 | 28 | let tooltipCoords = { x: 0, y: 0 }; 29 | if (event && event.target && event.target.ownerSVGElement) { 30 | tooltipCoords = localPoint(event.target.ownerSVGElement, event); 31 | } 32 | 33 | tooltipCoords = { ...tooltipCoords, ...coords }; 34 | 35 | showTooltip({ 36 | tooltipLeft: tooltipCoords.x, 37 | tooltipTop: tooltipCoords.y, 38 | tooltipData: { 39 | event, 40 | datum, 41 | ...rest 42 | } 43 | }); 44 | } 45 | 46 | handleMouseLeave() { 47 | const { hideTooltip } = this.props; 48 | this.tooltipTimeout = setTimeout(() => { 49 | hideTooltip(); 50 | }, 200); 51 | } 52 | 53 | render() { 54 | const { 55 | children, 56 | tooltipData, 57 | tooltipOpen, 58 | tooltipLeft, 59 | tooltipTop, 60 | renderTooltip 61 | } = this.props; 62 | 63 | const { handleMouseMove, handleMouseLeave } = this; 64 | 65 | const tooltipContent = tooltipOpen && renderTooltip(tooltipData); 66 | 67 | return ( 68 | 69 | {children({ handleMouseMove, handleMouseLeave, tooltipOpen })} 70 | {tooltipOpen && ( 71 | 80 | {tooltipContent} 81 | 82 | )} 83 | 84 | ); 85 | } 86 | } 87 | 88 | WithTooltip.propTypes = { 89 | children: PropTypes.func, 90 | tooltipData: PropTypes.object, 91 | tooltipOpen: PropTypes.bool, 92 | tooltipLeft: PropTypes.number, 93 | tooltipTop: PropTypes.number, 94 | renderTooltip: PropTypes.func.isRequired 95 | }; 96 | 97 | export default withTooltip(WithTooltip); 98 | -------------------------------------------------------------------------------- /packages/charts/src/Chart/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import { ParentSize } from "@vx/responsive"; 4 | import WithTooltip from "./WithTooltip"; 5 | import "./Chart.scss"; 6 | 7 | /** 8 | * The base Chart component for all charts and maps. 9 | * This takes care of creating a responsive svg and rendering tooltips. 10 | */ 11 | const Chart = ({ 12 | maxWidth, 13 | height, 14 | aspectRatio, 15 | renderTooltip, 16 | children, 17 | ...rest 18 | }) => { 19 | return ( 20 |
21 | 22 | {({ width, height: computedHeight }) => { 23 | if (width < 10) return null; 24 | 25 | const chartHeight = height ? computedHeight : width * aspectRatio; 26 | 27 | if (renderTooltip) { 28 | return ( 29 | 30 | {({ handleMouseMove, handleMouseLeave, tooltipOpen }) => ( 31 | 32 | {children({ 33 | width, 34 | height: chartHeight, 35 | handleMouseMove, 36 | handleMouseLeave, 37 | tooltipOpen, 38 | ...rest 39 | })} 40 | 41 | )} 42 | 43 | ); 44 | } else { 45 | return ( 46 | 47 | {children({ width, height: chartHeight, ...rest })} 48 | 49 | ); 50 | } 51 | }} 52 | 53 |
54 | ); 55 | }; 56 | 57 | Chart.propTypes = { 58 | /** 59 | * The max width of the chart. Can either be a string (i.e. `100%` or `8rem`) or a number representing a pixel value. 60 | */ 61 | maxWidth: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), 62 | /** 63 | * The height of the chart. Can either be a string (i.e. `100%` or `8rem`) or a number representing a pixel value. 64 | * The chart MUST receive either a height or and aspectRatio prop. 65 | */ 66 | height: (props, propName, componentName) => { 67 | if (!props.height && !props.aspectRatio) { 68 | return new Error( 69 | `One of props 'height' or 'aspectRatio' was not specified in '${componentName}'.` 70 | ); 71 | } 72 | if ( 73 | !props.aspectRatio && 74 | (typeof props.height !== "string" && typeof props.height !== "number") 75 | ) { 76 | return new Error( 77 | `'${propName}' prop in '${componentName}' must be a number or a string.` 78 | ); 79 | } 80 | }, 81 | /** 82 | * The aspectRatio of the chart. This is a number that is multiplied by the chart's computed width to calculate the chart's height. 83 | * The chart MUST receive either a height or and aspectRatio prop. 84 | */ 85 | aspectRatio: (props, propName, componentName) => { 86 | if (!props.height && !props.aspectRatio) { 87 | return new Error( 88 | `One of props 'height' or 'aspectRatio' was not specified in '${componentName}'.` 89 | ); 90 | } 91 | if (!props.height && typeof props.aspectRatio !== "number") { 92 | return new Error( 93 | `'${propName}' prop in '${componentName}' must be a number.` 94 | ); 95 | } 96 | }, 97 | /** 98 | * A function that returns a component for the chart's tooltip. 99 | * It receives event, datum, and any other arguments passed into the `handleMouseMove` function. 100 | */ 101 | renderTooltip: PropTypes.func, 102 | /** 103 | * A function that returns a component for the chart's legend. This is rendered as a div above the chart's svg. 104 | */ 105 | renderLegend: PropTypes.func, 106 | /** 107 | * A function that returns a component for an annotation, which is rendered at the very bottom of the svg. 108 | * It receive's the chart's current width and height (which are helpful to have for annotation positioning). 109 | */ 110 | renderAnnotation: PropTypes.func, 111 | /** 112 | * A function that is passed the caculated width and height of the chart, as well as tooltip functions (if the renderTooltip prop is defined) 113 | */ 114 | children: PropTypes.func.isRequired 115 | }; 116 | 117 | Chart.defaultProps = { 118 | maxWidth: "100%" 119 | }; 120 | 121 | export default Chart; 122 | -------------------------------------------------------------------------------- /packages/charts/src/HorizontalBar/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import { Bar } from "@vx/shape"; 4 | import { Group } from "@vx/group"; 5 | import { AxisBottom, AxisLeft } from "@vx/axis"; 6 | import { scaleBand, scaleLinear } from "@vx/scale"; 7 | import { GridColumns } from "@vx/grid"; 8 | import { max } from "d3-array"; 9 | 10 | const HorizontalBar = ({ 11 | width, 12 | height, 13 | handleMouseMove, 14 | handleMouseLeave, 15 | data, 16 | x, 17 | y, 18 | xFormat, 19 | yFormat, 20 | xAxisLabel, 21 | yAxisLabel, 22 | yLabelOffset, 23 | numTicksX, 24 | color, 25 | margin 26 | }) => { 27 | const xMax = width - margin.left - margin.right; 28 | const yMax = height - margin.top - margin.bottom; 29 | 30 | const yScale = scaleBand({ 31 | rangeRound: [0, yMax], 32 | domain: data.map(y), 33 | padding: 0.2 34 | }); 35 | 36 | const xScale = scaleLinear({ 37 | rangeRound: [0, xMax], 38 | domain: [0, max(data, x)] 39 | }); 40 | 41 | return ( 42 | 43 | 50 | 51 | {data.map((datum, i) => { 52 | return ( 53 | 61 | handleMouseMove ? handleMouseMove({ event, data, datum }) : null 62 | } 63 | onMouseLeave={handleMouseLeave ? handleMouseLeave : null} 64 | /> 65 | ); 66 | })} 67 | 68 | ({ 74 | width: margin.left, 75 | textAnchor: "end", 76 | verticalAnchor: "middle", 77 | dx: "-0.3em" 78 | })} 79 | label={yAxisLabel} 80 | labelProps={{ 81 | dx: yLabelOffset, 82 | textAnchor: "middle", 83 | verticalAnchor: "end" 84 | }} 85 | /> 86 | ({ 97 | textAnchor: "middle", 98 | verticalAnchor: "end" 99 | })} 100 | labelProps={{ 101 | dy: "2.5em", 102 | textAnchor: "middle", 103 | verticalAnchor: "start" 104 | }} 105 | /> 106 | 107 | ); 108 | }; 109 | 110 | HorizontalBar.propTypes = { 111 | width: PropTypes.number.isRequired, 112 | height: PropTypes.number.isRequired, 113 | handleMouseMove: PropTypes.func, 114 | handleMouseLeave: PropTypes.func, 115 | data: PropTypes.array.isRequired, 116 | x: PropTypes.func.isRequired, 117 | y: PropTypes.func.isRequired, 118 | xFormat: PropTypes.func, 119 | yFormat: PropTypes.func, 120 | xAxisLabel: PropTypes.string, 121 | yAxisLabel: PropTypes.string, 122 | yLabelOffset: PropTypes.string, 123 | numTicksX: PropTypes.oneOfType([PropTypes.number, PropTypes.func]), 124 | color: PropTypes.string, 125 | margin: PropTypes.shape({ 126 | top: PropTypes.number, 127 | right: PropTypes.number, 128 | bottom: PropTypes.number, 129 | left: PropTypes.number 130 | }) 131 | }; 132 | 133 | HorizontalBar.defaultProps = { 134 | margin: { 135 | top: 10, 136 | left: 50, 137 | right: 10, 138 | bottom: 20 139 | }, 140 | color: "#22C8A3", 141 | numTicksX: 6, 142 | yLabelOffset: "-0.5em" 143 | }; 144 | 145 | export default HorizontalBar; 146 | -------------------------------------------------------------------------------- /packages/charts/src/HorizontalStackedBar/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import { BarStackHorizontal } from "@vx/shape"; 4 | import { Group } from "@vx/group"; 5 | import { AxisBottom, AxisLeft } from "@vx/axis"; 6 | import { scaleBand, scaleLinear, scaleOrdinal } from "@vx/scale"; 7 | import { GridColumns } from "@vx/grid"; 8 | import { max } from "d3-array"; 9 | 10 | const HorizontalStackedBar = ({ 11 | width, 12 | height, 13 | handleMouseMove, 14 | handleMouseLeave, 15 | data, 16 | y, 17 | yFormat, 18 | xFormat, 19 | yAxisLabel, 20 | xAxisLabel, 21 | numTicksX, 22 | keys, 23 | colors, 24 | margin 25 | }) => { 26 | const totals = data.reduce((acc, cur) => { 27 | const t = keys.reduce((total, key) => { 28 | total += +cur[key]; 29 | return total; 30 | }, 0); 31 | acc.push(t); 32 | return acc; 33 | }, []); 34 | 35 | const xMax = width - margin.left - margin.right; 36 | const yMax = height - margin.top - margin.bottom; 37 | 38 | const colorScale = scaleOrdinal({ 39 | domain: keys, 40 | range: colors 41 | }); 42 | const xScale = scaleLinear({ 43 | rangeRound: [0, xMax], 44 | domain: [0, max(totals)], 45 | nice: true 46 | }); 47 | const yScale = scaleBand({ 48 | rangeRound: [yMax, 0], 49 | domain: data.map(y), 50 | padding: 0.2 51 | }); 52 | 53 | return ( 54 | 55 | 62 | 71 | {barStacks => { 72 | return barStacks.map(barStack => 73 | barStack.bars.map(bar => ( 74 | 83 | handleMouseMove 84 | ? handleMouseMove({ event, data, datum: bar }) 85 | : null 86 | } 87 | /> 88 | )) 89 | ); 90 | }} 91 | 92 | ({ 99 | width: margin.left, 100 | textAnchor: "end", 101 | verticalAnchor: "middle", 102 | dx: "-0.3em" 103 | })} 104 | /> 105 | ({ 115 | textAnchor: "middle", 116 | verticalAnchor: "end" 117 | })} 118 | label={xAxisLabel} 119 | labelProps={{ 120 | dy: "2.5em", 121 | textAnchor: "middle", 122 | verticalAnchor: "start" 123 | }} 124 | /> 125 | 126 | ); 127 | }; 128 | 129 | HorizontalStackedBar.propTypes = { 130 | width: PropTypes.number.isRequired, 131 | height: PropTypes.number.isRequired, 132 | handleMouseMove: PropTypes.func, 133 | handleMouseLeave: PropTypes.func, 134 | data: PropTypes.array.isRequired, 135 | /** 136 | * Accessor function for y axis values 137 | */ 138 | y: PropTypes.func.isRequired, 139 | /** 140 | * An array of strings with the column keys of each bar 141 | */ 142 | keys: PropTypes.array.isRequired, 143 | colors: PropTypes.array.isRequired, 144 | xFormat: PropTypes.func, 145 | yFormat: PropTypes.func, 146 | xAxisLabel: PropTypes.string, 147 | yAxisLabel: PropTypes.string, 148 | numTicksX: PropTypes.oneOfType([PropTypes.number, PropTypes.func]), 149 | margin: PropTypes.shape({ 150 | top: PropTypes.number, 151 | right: PropTypes.number, 152 | bottom: PropTypes.number, 153 | left: PropTypes.number 154 | }) 155 | }; 156 | 157 | HorizontalStackedBar.defaultProps = { 158 | margin: { 159 | top: 10, 160 | left: 60, 161 | right: 40, 162 | bottom: 40 163 | } 164 | }; 165 | 166 | export default HorizontalStackedBar; 167 | -------------------------------------------------------------------------------- /packages/charts/src/Line/HoverLine.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Line } from "@vx/shape"; 3 | 4 | export default ({ top, bottom, tooltipLeft, tooltipTop }) => { 5 | return ( 6 | 7 | 15 | 23 | 30 | 31 | ); 32 | }; 33 | -------------------------------------------------------------------------------- /packages/charts/src/Line/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import { Group } from "@vx/group"; 4 | import { LinePath } from "@vx/shape"; 5 | import { AxisLeft, AxisBottom } from "@vx/axis"; 6 | import { scaleLinear } from "@vx/scale"; 7 | import { curveBasis } from "@vx/curve"; 8 | import { GridRows } from "@vx/grid"; 9 | import { localPoint } from "@vx/event"; 10 | import { bisector, max, extent } from "d3-array"; 11 | import HoverLine from "./HoverLine"; 12 | 13 | class Line extends React.Component { 14 | constructor(props) { 15 | super(props); 16 | this.state = { x: 0, y: 0 }; 17 | this.handleMouseEvent = this.handleMouseEvent.bind(this); 18 | } 19 | 20 | handleMouseEvent = ({ 21 | data, 22 | event, 23 | tooltipParentFunc, 24 | xAccessor, 25 | yAccessor, 26 | xScale, 27 | yScale, 28 | margin 29 | }) => { 30 | const bisect = bisector(xAccessor).left; 31 | let { x } = localPoint(event.target.ownerSVGElement, event); 32 | x = x - margin.left; 33 | const x0 = xScale.invert(x); 34 | const index = bisect(data, x0); 35 | if (index > data.length - 1 || index < 1) return; 36 | const d0 = data[index - 1]; 37 | const d1 = data[index]; 38 | const d = x0 - xScale(xAccessor(d0)) > xScale(xAccessor(d1)) - x0 ? d1 : d0; 39 | const xPos = xScale(xAccessor(d)); 40 | const yPos = yScale(yAccessor(d)); 41 | tooltipParentFunc({ 42 | datum: d, 43 | coords: { x: xPos + margin.left, y: yPos + margin.top } 44 | }); 45 | this.setState({ x: xPos, y: yPos }); 46 | }; 47 | 48 | render() { 49 | const { 50 | width, 51 | height, 52 | handleMouseMove, 53 | handleMouseLeave, 54 | tooltipOpen, 55 | data, 56 | x, 57 | y, 58 | xAxisLabel, 59 | yAxisLabel, 60 | yFormat, 61 | xFormat, 62 | numTicksX, 63 | numTicksY, 64 | margin, 65 | stroke, 66 | strokeWidth 67 | } = this.props; 68 | 69 | const xMax = width - margin.left - margin.right; 70 | const yMax = height - margin.top - margin.bottom; 71 | 72 | const xScale = scaleLinear({ 73 | domain: extent(data, x), 74 | range: [0, xMax] 75 | }); 76 | 77 | const yScale = scaleLinear({ 78 | domain: [0, max(data, y)], 79 | range: [yMax, 0] 80 | }); 81 | return ( 82 | 83 | 84 | xScale(x(d))} 87 | y={d => yScale(y(d))} 88 | stroke={stroke} 89 | strokeWidth={strokeWidth} 90 | curve={curveBasis} 91 | /> 92 | { 99 | handleMouseMove 100 | ? this.handleMouseEvent({ 101 | event, 102 | data, 103 | xScale, 104 | yScale, 105 | margin, 106 | xAccessor: x, 107 | yAccessor: y, 108 | tooltipParentFunc: handleMouseMove 109 | }) 110 | : null; 111 | }} 112 | onMouseLeave={handleMouseLeave ? handleMouseLeave : null} 113 | /> 114 | {tooltipOpen && ( 115 | 121 | )} 122 | ({ 129 | textAnchor: "end", 130 | verticalAnchor: "middle" 131 | })} 132 | label={yAxisLabel} 133 | labelProps={{ 134 | textAnchor: "middle", 135 | verticalAnchor: "end" 136 | }} 137 | /> 138 | ({ 146 | textAnchor: "middle", 147 | verticalAnchor: "middle" 148 | })} 149 | tickFormat={d => d} 150 | label={xAxisLabel} 151 | labelProps={{ 152 | dy: "2.5em", 153 | textAnchor: "middle", 154 | verticalAnchor: "start" 155 | }} 156 | /> 157 | 158 | ); 159 | } 160 | } 161 | 162 | Line.propTypes = { 163 | width: PropTypes.number.isRequired, 164 | height: PropTypes.number.isRequired, 165 | handleMouseMove: PropTypes.func, 166 | handleMouseLeave: PropTypes.func, 167 | tooltipOpen: PropTypes.bool, 168 | data: PropTypes.array.isRequired, 169 | x: PropTypes.func.isRequired, 170 | y: PropTypes.func.isRequired, 171 | xFormat: PropTypes.func, 172 | yFormat: PropTypes.func, 173 | xAxisLabel: PropTypes.string, 174 | yAxisLabel: PropTypes.string, 175 | /** 176 | * You can specify the number of y axis ticks directly, or pass in a function which will receive the chart's computed height as an argument. 177 | */ 178 | numTicksY: PropTypes.oneOfType([PropTypes.number, PropTypes.func]), 179 | /** 180 | * You can specify the number of x axis ticks directly, or pass in a function which will receive the chart's computed width as an argument. 181 | */ 182 | numTicksX: PropTypes.oneOfType([PropTypes.number, PropTypes.func]), 183 | stroke: PropTypes.string, 184 | strokeWidth: PropTypes.number, 185 | margin: PropTypes.shape({ 186 | top: PropTypes.number, 187 | right: PropTypes.number, 188 | bottom: PropTypes.number, 189 | left: PropTypes.number 190 | }) 191 | }; 192 | 193 | Line.defaultProps = { 194 | numTicksX: 10, 195 | numTicksY: 5, 196 | stroke: "#22C8A3", 197 | strokeWidth: 2, 198 | margin: { top: 10, left: 55, bottom: 30, right: 10 } 199 | }; 200 | 201 | export default Line; 202 | -------------------------------------------------------------------------------- /packages/charts/src/Scatterplot/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import { Group } from "@vx/group"; 4 | import { scaleLinear } from "@vx/scale"; 5 | import { AxisLeft, AxisBottom } from "@vx/axis"; 6 | import { Grid } from "@vx/grid"; 7 | import { max } from "d3-array"; 8 | 9 | const Scatterplot = ({ 10 | width, 11 | height, 12 | data, 13 | handleMouseMove, 14 | handleMouseLeave, 15 | x, 16 | y, 17 | xAxisLabel, 18 | yAxisLabel, 19 | yFormat, 20 | xFormat, 21 | circleRadius, 22 | numTicksX, 23 | numTicksY, 24 | circleStroke, 25 | circleFill, 26 | margin 27 | }) => { 28 | if (width < 100) return; 29 | 30 | const xMax = width - margin.left - margin.right; 31 | const yMaxRange = height - margin.top - margin.bottom; 32 | const yMaxDomain = max(data, y); 33 | const xMaxDomain = max(data, x); 34 | 35 | const xScale = scaleLinear({ 36 | domain: [0, xMaxDomain], 37 | range: [0, xMax], 38 | clamp: true 39 | }); 40 | 41 | const yScale = scaleLinear({ 42 | domain: [0, yMaxDomain], 43 | range: [yMaxRange, 0], 44 | clamp: true 45 | }); 46 | 47 | return ( 48 | 49 | 59 | 60 | {data.map((point, i) => { 61 | return ( 62 | 84 | handleMouseMove 85 | ? handleMouseMove({ event, data, datum: point }) 86 | : null 87 | } 88 | onMouseLeave={handleMouseLeave ? handleMouseLeave : null} 89 | /> 90 | ); 91 | })} 92 | 93 | ({ 103 | textAnchor: "end", 104 | verticalAnchor: "middle" 105 | })} 106 | label={yAxisLabel} 107 | labelProps={{ 108 | textAnchor: "middle", 109 | verticalAnchor: "end" 110 | }} 111 | /> 112 | ({ 122 | textAnchor: "middle", 123 | verticalAnchor: "end" 124 | })} 125 | label={xAxisLabel} 126 | labelProps={{ 127 | dy: "2.5em", 128 | textAnchor: "middle", 129 | verticalAnchor: "start", 130 | y: 0 131 | }} 132 | /> 133 | 134 | ); 135 | }; 136 | 137 | Scatterplot.propTypes = { 138 | width: PropTypes.number.isRequired, 139 | height: PropTypes.number.isRequired, 140 | handleMouseMove: PropTypes.func, 141 | handleMouseLeave: PropTypes.func, 142 | data: PropTypes.array.isRequired, 143 | /** 144 | * Accessor function for x axis values 145 | */ 146 | x: PropTypes.func.isRequired, 147 | /** 148 | * Accessor function for y axis values 149 | */ 150 | y: PropTypes.func.isRequired, 151 | /** 152 | * Formatting function for x axis tick labels 153 | */ 154 | xFormat: PropTypes.func, 155 | /** 156 | * Formatting function for y axis tick labels 157 | */ 158 | yFormat: PropTypes.func, 159 | xAxisLabel: PropTypes.string, 160 | yAxisLabel: PropTypes.string, 161 | /** 162 | * You can specify the number of x axis ticks directly, or pass in a function which will receive the chart's computed width as an argument. 163 | */ 164 | numTicksX: PropTypes.oneOfType([PropTypes.number, PropTypes.func]), 165 | /** 166 | * You can specify the number of y axis ticks directly, or pass in a function which will receive the chart's computed height as an argument. 167 | */ 168 | numTicksY: PropTypes.oneOfType([PropTypes.number, PropTypes.func]), 169 | /** 170 | * A number for the circle's radius, or a function that will receive that circle's datum for [radius scaling](https://bl.ocks.org/guilhermesimoes/e6356aa90a16163a6f917f53600a2b4a). 171 | */ 172 | circleRadius: PropTypes.oneOfType([PropTypes.number, PropTypes.func]), 173 | /** 174 | * A string for each circle's stroke, or a function that will receive that circle's datum 175 | */ 176 | circleStroke: PropTypes.oneOfType([PropTypes.string, PropTypes.func]), 177 | /** 178 | * A string for each circle's fill, or a function that will receive that circle's datum 179 | */ 180 | circleFill: PropTypes.oneOfType([PropTypes.string, PropTypes.func]), 181 | margin: PropTypes.shape({ 182 | top: PropTypes.number, 183 | right: PropTypes.number, 184 | bottom: PropTypes.number, 185 | left: PropTypes.number 186 | }) 187 | }; 188 | 189 | Scatterplot.defaultProps = { 190 | circleRadius: 5, 191 | numTicksX: 5, 192 | numTicksY: 5, 193 | margin: { 194 | top: 10, 195 | bottom: 50, 196 | left: 55, 197 | right: 10 198 | }, 199 | circleStroke: "#4C81DB", 200 | circleFill: "rgba(76,129,219, 0.4)" 201 | }; 202 | 203 | export default Scatterplot; 204 | -------------------------------------------------------------------------------- /packages/charts/src/VerticalGroupedBar/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import { BarGroup } from "@vx/shape"; 4 | import { Group } from "@vx/group"; 5 | import { AxisBottom, AxisLeft } from "@vx/axis"; 6 | import { scaleBand, scaleLinear, scaleOrdinal } from "@vx/scale"; 7 | import { GridRows } from "@vx/grid"; 8 | import { max } from "d3-array"; 9 | 10 | const VerticalGroupedBar = ({ 11 | width, 12 | height, 13 | handleMouseMove, 14 | handleMouseLeave, 15 | data, 16 | x, 17 | keys, 18 | xFormat, 19 | yFormat, 20 | xAxisLabel, 21 | yAxisLabel, 22 | numTicksY, 23 | colors, 24 | margin 25 | }) => { 26 | const xMax = width - margin.left - margin.right; 27 | const yMax = height - margin.top - margin.bottom; 28 | 29 | const colorScale = scaleOrdinal({ 30 | domain: keys, 31 | range: colors 32 | }); 33 | const x0Scale = scaleBand({ 34 | rangeRound: [0, xMax], 35 | domain: data.map(x), 36 | padding: 0.2 37 | }); 38 | const x1Scale = scaleBand({ 39 | rangeRound: [0, x0Scale.bandwidth()], 40 | domain: keys, 41 | padding: 0.1 42 | }); 43 | const yScale = scaleLinear({ 44 | rangeRound: [yMax, 0], 45 | domain: [ 46 | 0, 47 | max(data, d => { 48 | return max(keys, key => d[key]); 49 | }) 50 | ] 51 | }); 52 | 53 | return ( 54 | 55 | 56 | 66 | {barGroups => { 67 | return barGroups.map(barGroup => { 68 | return ( 69 | 73 | {barGroup.bars.map(bar => { 74 | return ( 75 | 85 | handleMouseMove 86 | ? handleMouseMove({ event, data, datum: bar }) 87 | : null 88 | } 89 | onMouseLeave={handleMouseLeave ? handleMouseLeave : null} 90 | /> 91 | ); 92 | })} 93 | 94 | ); 95 | }); 96 | }} 97 | 98 | ({ 105 | textAnchor: "end", 106 | verticalAnchor: "middle" 107 | })} 108 | label={yAxisLabel} 109 | labelProps={{ 110 | textAnchor: "middle", 111 | verticalAnchor: "end" 112 | }} 113 | /> 114 | ({ 122 | textAnchor: "middle", 123 | width: x0Scale.bandwidth(), 124 | verticalAnchor: "middle" 125 | })} 126 | labelProps={{ 127 | dy: "3em", 128 | textAnchor: "middle", 129 | y: 0 130 | }} 131 | /> 132 | 133 | ); 134 | }; 135 | 136 | VerticalGroupedBar.propTypes = { 137 | width: PropTypes.number.isRequired, 138 | height: PropTypes.number.isRequired, 139 | handleMouseMove: PropTypes.func, 140 | handleMouseLeave: PropTypes.func, 141 | tooltipOpen: PropTypes.bool, 142 | data: PropTypes.array.isRequired, 143 | /** 144 | * Accessor function for x axis values 145 | */ 146 | x: PropTypes.func.isRequired, 147 | /** 148 | * An array of strings with the keys for each bar 149 | */ 150 | keys: PropTypes.array.isRequired, 151 | xFormat: PropTypes.func, 152 | yFormat: PropTypes.func, 153 | xAxisLabel: PropTypes.string, 154 | yAxisLabel: PropTypes.string, 155 | numTicksY: PropTypes.oneOfType([PropTypes.number, PropTypes.func]), 156 | colors: PropTypes.array.isRequired, 157 | margin: PropTypes.shape({ 158 | top: PropTypes.number, 159 | right: PropTypes.number, 160 | bottom: PropTypes.number, 161 | left: PropTypes.number 162 | }) 163 | }; 164 | 165 | VerticalGroupedBar.defaultProps = { 166 | numTicksY: 5, 167 | margin: { 168 | top: 40, 169 | left: 40, 170 | right: 40, 171 | bottom: 40 172 | } 173 | }; 174 | 175 | export default VerticalGroupedBar; 176 | -------------------------------------------------------------------------------- /packages/charts/src/index.js: -------------------------------------------------------------------------------- 1 | export { default as Chart } from "./Chart"; 2 | export { default as Bar } from "./Bar"; 3 | export { default as HorizontalBar } from "./HorizontalBar"; 4 | export { default as HorizontalStackedBar } from "./HorizontalStackedBar"; 5 | export { default as VerticalGroupedBar } from "./VerticalGroupedBar"; 6 | export { default as Line } from "./Line"; 7 | export { default as Scatterplot } from "./Scatterplot"; 8 | -------------------------------------------------------------------------------- /packages/components/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/preset-react", 4 | ["@babel/preset-env", { "modules": false }] 5 | ], 6 | "plugins": [ 7 | "@babel/plugin-proposal-class-properties", 8 | "@babel/plugin-proposal-object-rest-spread" 9 | ], 10 | "ignore": ["node_modules/**"] 11 | } 12 | -------------------------------------------------------------------------------- /packages/components/.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false -------------------------------------------------------------------------------- /packages/components/README.md: -------------------------------------------------------------------------------- 1 | # @newamerica/components 2 | 3 | A collection of user interface components that are helpful for complicated data visualizations. 4 | 5 | ### Installation 6 | 7 | ``` 8 | npm install @newamerica/components --save 9 | ``` 10 | 11 | 12 | ## Components 13 | 14 | 15 | 16 | - [ButtonGroup](#buttongroup) 17 | - [CheckboxGroup](#checkboxgroup) 18 | - [Search](#search) 19 | - [Select](#select) 20 | - [Slider](#slider) 21 | - [Toggle](#toggle) 22 | 23 | ## API 24 | 25 | 26 | 27 | 28 | ### ButtonGroup 29 | 30 | From [`./src/ButtonGroup/index.js`](./src/ButtonGroup/index.js) 31 | 32 | 33 | 34 | prop | type | default | required | description 35 | ---- | :----: | :-------: | :--------: | ----------- 36 | **active** | `Union` | | :x: | 37 | **onChange** | `Function` | | :white_check_mark: | This function will receive the currently selected button's id 38 | **options** | `Array[]` | | :white_check_mark: | 39 | **options[].id** | `Union` | | :x: | 40 | **options[].text** | `String` | | :x: | 41 | 42 | 43 | 44 | 45 | 46 | ### CheckboxGroup 47 | 48 | From [`./src/CheckboxGroup/index.js`](./src/CheckboxGroup/index.js) 49 | 50 | 51 | 52 | prop | type | default | required | description 53 | ---- | :----: | :-------: | :--------: | ----------- 54 | **onChange** | `Function` | | :white_check_mark: | This function will receive an object with all checkbox values. 55 | **options** | `Array[]` | | :white_check_mark: | 56 | **options[].checked** | `Boolean` | | :x: | 57 | **options[].id** | `Union` | | :x: | 58 | **options[].label** | `String` | | :x: | 59 | **orientation** | `Enum("vertical","horizontal")` | `"vertical"` | :x: | 60 | **selectButtons** | `Boolean` | `false` | :x: | If true, adds buttons that let the user select and deselect all checkboxes at once. 61 | **style** | `Object` | | :x: | 62 | **title** | `String` | | :x: | 63 | 64 | 65 | 66 | 67 | 68 | ### Search 69 | 70 | From [`./src/Search/index.js`](./src/Search/index.js) 71 | 72 | 73 | 74 | prop | type | default | required | description 75 | ---- | :----: | :-------: | :--------: | ----------- 76 | **className** | `String` | | :x: | 77 | **onChange** | `Function` | | :white_check_mark: | This function will receive the current value of the search box 78 | **placeholder** | `String` | | :x: | 79 | **style** | `Object` | | :x: | 80 | 81 | 82 | 83 | 84 | 85 | ### Select 86 | 87 | From [`./src/Select/index.js`](./src/Select/index.js) 88 | 89 | 90 | 91 | prop | type | default | required | description 92 | ---- | :----: | :-------: | :--------: | ----------- 93 | **className** | `String` | | :x: | 94 | **onChange** | `Function` | | :white_check_mark: | This function will receive the current value of the select dropdown. 95 | **options** | `Array[]` | | :white_check_mark: | 96 | **selected** | `String` | | :x: | 97 | 98 | 99 | 100 | 101 | 102 | ### Slider 103 | 104 | From [`./src/Slider/index.js`](./src/Slider/index.js) 105 | 106 | 107 | 108 | prop | type | default | required | description 109 | ---- | :----: | :-------: | :--------: | ----------- 110 | **id** | `String` | | :x: | 111 | **label** | `String` | | :white_check_mark: | 112 | **max** | `Number` | | :white_check_mark: | 113 | **min** | `Number` | | :white_check_mark: | 114 | **onChange** | `Function` | | :white_check_mark: | This function will receive the entire event when the slider has changed. Use `event.target.value` to get the current slider value. 115 | **step** | `Number` | | :x: | 116 | 117 | 118 | 119 | 120 | 121 | ### Toggle 122 | 123 | From [`./src/Toggle/index.js`](./src/Toggle/index.js) 124 | 125 | 126 | 127 | prop | type | default | required | description 128 | ---- | :----: | :-------: | :--------: | ----------- 129 | **checked** | `Boolean` | `false` | :x: | 130 | **id** | `String` | | :x: | 131 | **offLabel** | `String` | | :white_check_mark: | 132 | **onChange** | `Function` | | :white_check_mark: | This function will receive a boolean value for whether or not the toggle is on/off. 133 | **onLabel** | `String` | | :white_check_mark: | 134 | 135 | 136 | 137 | -------------------------------------------------------------------------------- /packages/components/docs/description.md: -------------------------------------------------------------------------------- 1 | # @newamerica/components 2 | 3 | A collection of user interface components that are helpful for complicated data visualizations. 4 | 5 | ### Installation 6 | 7 | ``` 8 | npm install @newamerica/components --save 9 | ``` 10 | -------------------------------------------------------------------------------- /packages/components/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@newamerica/components", 3 | "version": "0.0.6", 4 | "description": "Components to build stuff with", 5 | "main": "dist/index.umd.js", 6 | "module": "dist/index.es.js", 7 | "repository": { 8 | "type": "git", 9 | "url": "https://github.com/newamericafoundation/teddy.git" 10 | }, 11 | "publishConfig": { 12 | "access": "public" 13 | }, 14 | "sideEffects": false, 15 | "scripts": { 16 | "start": "rollup -c -w --environment BUILD:development", 17 | "build": "rollup -c --environment BUILD:production", 18 | "prepublish": "rm -rf ./dist && npm run build", 19 | "docs": "cd ./docs && ../../../node_modules/.bin/react-docgen ../src/** | ../../../scripts/buildDocs.sh", 20 | "test": "echo \"Error: no test specified\" && exit 1" 21 | }, 22 | "keywords": [ 23 | "vx", 24 | "react", 25 | "d3", 26 | "charts", 27 | "data", 28 | "visualization" 29 | ], 30 | "peerDependencies": { 31 | "react": "^16.2.0" 32 | }, 33 | "devDependencies": { 34 | "@babel/core": "^7.0.0", 35 | "@babel/plugin-proposal-class-properties": "^7.0.0", 36 | "@babel/plugin-proposal-object-rest-spread": "^7.0.0", 37 | "@babel/preset-env": "^7.0.0", 38 | "@babel/preset-react": "^7.0.0", 39 | "autoprefixer": "^9.4.5", 40 | "node-sass": "^4.9.2", 41 | "rollup": "^1.1.0", 42 | "rollup-plugin-babel": "^4.3.1", 43 | "rollup-plugin-node-resolve": "^4.0.0", 44 | "rollup-plugin-postcss": "^1.6.3", 45 | "rollup-plugin-terser": "^4.0.2" 46 | }, 47 | "author": "lorenries", 48 | "license": "MIT", 49 | "dependencies": { 50 | "prop-types": "^15.6.2" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /packages/components/rollup.config.js: -------------------------------------------------------------------------------- 1 | import resolve from "rollup-plugin-node-resolve"; 2 | import babel from "rollup-plugin-babel"; 3 | import { terser } from "rollup-plugin-terser"; 4 | import postcss from "rollup-plugin-postcss"; 5 | import autoprefixer from "autoprefixer"; 6 | import pkg from "./package.json"; 7 | 8 | const deps = Object.keys({ 9 | ...pkg.dependencies, 10 | ...pkg.peerDependencies 11 | }); 12 | 13 | const globals = deps.reduce((o, name) => { 14 | if (name.includes("@vx/")) { 15 | o[name] = "vx"; 16 | } 17 | if (name.includes("d3-")) { 18 | o[name] = "d3"; 19 | } 20 | if (name === "react") { 21 | o[name] = "React"; 22 | } 23 | if (name === "react-dom") { 24 | o[name] = "ReactDOM"; 25 | } 26 | if (name === "prop-types") { 27 | o[name] = "PropTypes"; 28 | } 29 | if (name === "classnames") { 30 | o[name] = "classNames"; 31 | } 32 | return o; 33 | }, {}); 34 | 35 | export default [ 36 | { 37 | input: "src/index.js", 38 | external: deps, 39 | plugins: [ 40 | resolve(), 41 | babel({ 42 | exclude: "node_modules/**" 43 | }), 44 | postcss({ 45 | extensions: [".css", ".scss"], 46 | plugins: [autoprefixer], 47 | minimize: true, 48 | inject: false, 49 | extract: "dist/styles.css" 50 | }), 51 | process.env.BUILD === "production" && terser() 52 | ], 53 | output: { 54 | file: pkg.main, 55 | format: "umd", 56 | name: "charts", 57 | globals 58 | } 59 | }, 60 | { 61 | input: "src/index.js", 62 | external: deps, 63 | plugins: [ 64 | resolve(), 65 | babel({ 66 | exclude: "node_modules/**" 67 | }), 68 | postcss({ 69 | extensions: [".css", ".scss"], 70 | plugins: [autoprefixer], 71 | minimize: true, 72 | inject: false, 73 | extract: "dist/styles.css" 74 | }), 75 | process.env.BUILD === "production" && terser() 76 | ], 77 | output: { file: pkg.module, format: "es", globals } 78 | } 79 | ]; 80 | -------------------------------------------------------------------------------- /packages/components/src/ButtonGroup/ButtonGroup.scss: -------------------------------------------------------------------------------- 1 | .dv-btn { 2 | font-size: 14px; 3 | text-transform: none; 4 | font-weight: normal; 5 | letter-spacing: normal; 6 | padding: 1rem 2rem; 7 | background-color: #fff; 8 | border: none; 9 | border-top: 1px solid #333; 10 | border-bottom: 1px solid #333; 11 | border-right: 1px solid #333; 12 | &:first-child { 13 | border-left: 1px solid #333; 14 | } 15 | &:hover { 16 | cursor: pointer; 17 | } 18 | &.dv-btn-active { 19 | background-color: #333; 20 | color: #fff; 21 | } 22 | } 23 | 24 | .dv-btn-group { 25 | display: flex; 26 | } -------------------------------------------------------------------------------- /packages/components/src/ButtonGroup/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import "./ButtonGroup.scss"; 4 | 5 | class ButtonGroup extends React.Component { 6 | constructor(props) { 7 | super(props); 8 | this.state = { active: this.props.active }; 9 | this.handleClick = this.handleClick.bind(this); 10 | } 11 | 12 | handleClick(e) { 13 | e.preventDefault(); 14 | this.setState({ active: e.target.id }, () => 15 | this.props.onChange(this.state.active) 16 | ); 17 | } 18 | 19 | render() { 20 | const { options } = this.props; 21 | const { active } = this.state; 22 | return ( 23 |
24 | {options.map(option => ( 25 | 33 | ))} 34 |
35 | ); 36 | } 37 | } 38 | 39 | ButtonGroup.propTypes = { 40 | options: PropTypes.arrayOf( 41 | PropTypes.shape({ 42 | text: PropTypes.string, 43 | id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]) 44 | }) 45 | ).isRequired, 46 | /** 47 | * This function will receive the currently selected button's id 48 | */ 49 | onChange: PropTypes.func.isRequired, 50 | active: PropTypes.oneOfType([PropTypes.string, PropTypes.number]) 51 | }; 52 | 53 | export default ButtonGroup; 54 | -------------------------------------------------------------------------------- /packages/components/src/CheckboxGroup/CheckboxGroup.scss: -------------------------------------------------------------------------------- 1 | .dv-Checkbox__container { 2 | display: flex; 3 | } 4 | 5 | .dv-Checkbox__title { 6 | padding-bottom: 0.5rem; 7 | font-weight: bold; 8 | font-size: 0.875rem; 9 | } 10 | 11 | .dv-Checkbox__container-vertical { 12 | flex-direction: column; 13 | } 14 | 15 | .dv-Checkbox__container-horizontal { 16 | flex-direction: row; 17 | div { 18 | padding-right: 1rem; 19 | } 20 | } 21 | 22 | .dv-Checkbox { 23 | cursor: pointer; 24 | input { 25 | position: absolute; 26 | visibility: visible; 27 | overflow: hidden; 28 | clip: rect(0, 0, 0, 0); 29 | margin: -1px; 30 | padding: 0; 31 | width: 1px; 32 | height: 1px; 33 | border: 0; 34 | white-space: nowrap; 35 | 36 | -webkit-appearance: checkbox; 37 | &:checked { 38 | + .dv-Checkbox__label::before { 39 | border-color: #333; 40 | background-color: #333; 41 | } 42 | + .dv-Checkbox__label::after { 43 | opacity: 1; 44 | transform: scale(1) rotate(-45deg); 45 | } 46 | } 47 | } 48 | &__label { 49 | position: relative; 50 | display: -webkit-box; 51 | display: flex; 52 | -webkit-box-align: center; 53 | align-items: center; 54 | margin: 0; 55 | padding: 0.5rem 0 0.5rem 1.5rem; 56 | min-height: 1rem; 57 | font-size: 0.875rem; 58 | cursor: pointer; 59 | 60 | user-select: none; 61 | } 62 | &__label::before { 63 | position: absolute; 64 | top: calc(50% - 8px); 65 | left: 0; 66 | box-sizing: border-box; 67 | width: 18px; 68 | height: 18px; 69 | border: 2px solid #333; 70 | background-color: transparent; 71 | content: ""; 72 | } 73 | &__label::after { 74 | position: absolute; 75 | top: calc(50% - 2px); 76 | left: 5px; 77 | box-sizing: border-box; 78 | width: 9px; 79 | height: 5px; 80 | border-bottom: 2px solid #fff; 81 | border-left: 2px solid #fff; 82 | background: none; 83 | color: #fff; 84 | content: ""; 85 | transform: scale(0) rotate(-45deg); 86 | } 87 | &__select { 88 | margin: 0; 89 | margin-top: 0.5rem; 90 | padding: 0; 91 | padding-bottom: 0.25rem; 92 | border: none; 93 | text-align: left; 94 | text-decoration: underline; 95 | text-transform: none; 96 | letter-spacing: normal; 97 | font-weight: normal; 98 | font-size: 0.875rem; 99 | cursor: pointer; 100 | 101 | -webkit-appearance: none; 102 | -moz-appearance: none; 103 | } 104 | &__bullet::after { 105 | padding: 0 0.25rem; 106 | content: "\2022"; 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /packages/components/src/CheckboxGroup/README.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/newamericafoundation/teddy/0ec5ff34159f9c099f96339174ede5e9cb556fa6/packages/components/src/CheckboxGroup/README.md -------------------------------------------------------------------------------- /packages/components/src/CheckboxGroup/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import "./CheckboxGroup.scss"; 4 | 5 | class CheckboxGroup extends React.Component { 6 | constructor(props) { 7 | super(props); 8 | this.state = {}; 9 | this.props.options.forEach(val => { 10 | this.state[val.id] = val.checked ? true : false; 11 | }); 12 | this.handleChange = this.handleChange.bind(this); 13 | this.selectAll = this.selectAll.bind(this); 14 | this.deselectAll = this.deselectAll.bind(this); 15 | } 16 | 17 | handleChange(e) { 18 | this.setState( 19 | { 20 | [e.target.id]: e.target.checked 21 | }, 22 | () => this.props.onChange(this.state) 23 | ); 24 | } 25 | 26 | selectAll() { 27 | const options = Object.keys(this.state); 28 | const newState = {}; 29 | options.forEach(option => { 30 | newState[option] = true; 31 | }); 32 | this.setState(newState, () => this.props.onChange(this.state)); 33 | } 34 | 35 | deselectAll() { 36 | const options = Object.keys(this.state); 37 | const newState = {}; 38 | options.forEach(option => { 39 | newState[option] = false; 40 | }); 41 | this.setState(newState, () => this.props.onChange(this.state)); 42 | } 43 | 44 | render() { 45 | const { orientation, options, selectButtons, style, title } = this.props; 46 | return ( 47 |
57 | {title ? {title} : null} 58 | {options.map((option, i) => ( 59 |
60 | 66 | 69 |
70 | ))} 71 | {selectButtons ? ( 72 |
73 | 76 | 77 | 80 |
81 | ) : null} 82 |
83 | ); 84 | } 85 | } 86 | 87 | CheckboxGroup.propTypes = { 88 | options: PropTypes.arrayOf( 89 | PropTypes.shape({ 90 | label: PropTypes.string, 91 | id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), 92 | checked: PropTypes.bool 93 | }) 94 | ).isRequired, 95 | /** 96 | * This function will receive an object with all checkbox values. 97 | */ 98 | onChange: PropTypes.func.isRequired, 99 | orientation: PropTypes.oneOf(["vertical", "horizontal"]), 100 | /** 101 | * If true, adds buttons that let the user select and deselect all checkboxes at once. 102 | */ 103 | selectButtons: PropTypes.bool, 104 | style: PropTypes.object, 105 | title: PropTypes.string 106 | }; 107 | 108 | CheckboxGroup.defaultProps = { 109 | orientation: "vertical", 110 | selectButtons: false 111 | }; 112 | 113 | export default CheckboxGroup; 114 | -------------------------------------------------------------------------------- /packages/components/src/Search/README.md: -------------------------------------------------------------------------------- 1 | # Search 2 | 3 | A simple search box with sensible styles. 4 | 5 | ### Usage 6 | 7 | ```js 8 | import { Search } from "./components/Search"; 9 | ``` 10 | 11 | ### Properties 12 | 13 | - `onChange` - A function that will be passed the search input value whenever the value changes. 14 | - `placeholder` - The placeholder text for the inside of the search box. 15 | - `className` - Any additional classes to be passed to the search box. 16 | - `style` - Any additional styles to be passed to the search box. 17 | 18 | | propName | propType | defaultValue | isRequired | 19 | |-------------|----------|--------------|------------| 20 | | onChange | func | - | + | 21 | | placeholder | string | "Search..." | - | 22 | | className | string | - | - | 23 | | style | object | - | - | 24 | -------------------------------------------------------------------------------- /packages/components/src/Search/Search.scss: -------------------------------------------------------------------------------- 1 | .dv-search { 2 | width: 16rem; 3 | padding: 0.5rem 0.5rem 0.5rem 2rem; 4 | margin: 1rem 0; 5 | font-size: 1rem; 6 | color: #111; 7 | background-color: #fff; 8 | background-image: url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiA/PjxzdmcgaGVpZ2h0PSIzMnB4IiB2ZXJzaW9uPSIxLjEiIHZpZXdCb3g9IjAgMCAzMiAzMiIgd2lkdGg9IjMycHgiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6c2tldGNoPSJodHRwOi8vd3d3LmJvaGVtaWFuY29kaW5nLmNvbS9za2V0Y2gvbnMiIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIj48dGl0bGUvPjxkZXNjLz48ZGVmcy8+PGcgZmlsbD0ibm9uZSIgZmlsbC1ydWxlPSJldmVub2RkIiBpZD0iUGFnZS0xIiBzdHJva2U9Im5vbmUiIHN0cm9rZS13aWR0aD0iMSI+PGcgZmlsbD0iIzkyOTI5MiIgaWQ9Imljb24tMTExLXNlYXJjaCI+PHBhdGggZD0iTTE5LjQyNzExNjQsMjEuNDI3MTE2NCBDMTguMDM3MjQ5NSwyMi40MTc0ODAzIDE2LjMzNjY1MjIsMjMgMTQuNSwyMyBDOS44MDU1NzkzOSwyMyA2LDE5LjE5NDQyMDYgNiwxNC41IEM2LDkuODA1NTc5MzkgOS44MDU1NzkzOSw2IDE0LjUsNiBDMTkuMTk0NDIwNiw2IDIzLDkuODA1NTc5MzkgMjMsMTQuNSBDMjMsMTYuMzM2NjUyMiAyMi40MTc0ODAzLDE4LjAzNzI0OTUgMjEuNDI3MTE2NCwxOS40MjcxMTY0IEwyNy4wMTE5MTc2LDI1LjAxMTkxNzYgQzI3LjU2MjExODYsMjUuNTYyMTE4NiAyNy41NTc1MzEzLDI2LjQ0MjQ2ODcgMjcuMDExNzE4NSwyNi45ODgyODE1IEwyNi45ODgyODE1LDI3LjAxMTcxODUgQzI2LjQ0Mzg2NDgsMjcuNTU2MTM1MiAyNS41NTc2MjA0LDI3LjU1NzYyMDQgMjUuMDExOTE3NiwyNy4wMTE5MTc2IEwxOS40MjcxMTY0LDIxLjQyNzExNjQgTDE5LjQyNzExNjQsMjEuNDI3MTE2NCBaIE0xNC41LDIxIEMxOC4wODk4NTExLDIxIDIxLDE4LjA4OTg1MTEgMjEsMTQuNSBDMjEsMTAuOTEwMTQ4OSAxOC4wODk4NTExLDggMTQuNSw4IEMxMC45MTAxNDg5LDggOCwxMC45MTAxNDg5IDgsMTQuNSBDOCwxOC4wODk4NTExIDEwLjkxMDE0ODksMjEgMTQuNSwyMSBMMTQuNSwyMSBaIiBpZD0ic2VhcmNoIi8+PC9nPjwvZz48L3N2Zz4=); 9 | background-size: 1.5rem; 10 | background-repeat: no-repeat; 11 | background-position-y: center; 12 | background-position-x: 0.25rem; 13 | outline: 0; 14 | border: 1px solid #d4d4d4; 15 | } -------------------------------------------------------------------------------- /packages/components/src/Search/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import "./Search.scss"; 4 | 5 | class Search extends React.Component { 6 | constructor(props) { 7 | super(props); 8 | this.state = { 9 | search: "" 10 | }; 11 | } 12 | 13 | updateSearch(e) { 14 | this.setState({ search: e.target.value }, () => 15 | this.props.onChange(this.state.search) 16 | ); 17 | } 18 | 19 | render() { 20 | const { placeholder, className, style } = this.props; 21 | return ( 22 | 30 | ); 31 | } 32 | } 33 | 34 | Search.propTypes = { 35 | /** 36 | * This function will receive the current value of the search box 37 | */ 38 | onChange: PropTypes.func.isRequired, 39 | placeholder: PropTypes.string, 40 | className: PropTypes.string, 41 | style: PropTypes.object 42 | }; 43 | 44 | export default Search; 45 | -------------------------------------------------------------------------------- /packages/components/src/Select/README.md: -------------------------------------------------------------------------------- 1 | # Select 2 | 3 | A simple select with sensible styles. 4 | 5 | ### Usage 6 | 7 | ```js 8 | import { Select } from "./components/Select"; 9 | ``` 10 | 11 | ### Properties 12 | 13 | - `onChange` - A function that will be passed the selected value whenever the input changes. 14 | - `options` - An array of strings, which will be used to create the `` elements inside the select box. 15 | - `selected` - An optional string to choose the default selected option. 16 | - `className` - Any additional classes to be passed to the select. 17 | - `style` - Any additional styles to be passed to the select. 18 | 19 | | propName | propType | defaultValue | isRequired | 20 | |-----------|----------|--------------|------------| 21 | | onChange | func | - | + | 22 | | options | array | - | + | 23 | | selected | string | - | - | 24 | | className | string | - | - | 25 | | style | object | - | - | 26 | -------------------------------------------------------------------------------- /packages/components/src/Select/Select.scss: -------------------------------------------------------------------------------- 1 | .dv-select { 2 | background-color: #fff; 3 | border: 1px solid #ddd; 4 | border-radius: 0; 5 | padding: 0.5rem; 6 | margin: 0.5rem 0; 7 | padding-right: 2rem; 8 | font-size: 14px; 9 | -webkit-appearance: none; 10 | -moz-appearance: none; 11 | background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' height='20' width='20' fill='%23CCC' viewBox='0 0 20 20'%3E%3Cpath d='M4.516 7.548c0.436-0.446 1.043-0.481 1.576 0l3.908 3.747 3.908-3.747c0.533-0.481 1.141-0.446 1.574 0 0.436 0.445 0.408 1.197 0 1.615-0.406 0.418-4.695 4.502-4.695 4.502-0.217 0.223-0.502 0.335-0.787 0.335s-0.57-0.112-0.789-0.335c0 0-4.287-4.084-4.695-4.502s-0.436-1.17 0-1.615z'%3E%3C/path%3E%3C/svg%3E"); 12 | background-position: top 50% right 0.5rem; 13 | background-origin: padding-box; 14 | background-repeat: no-repeat; 15 | } 16 | -------------------------------------------------------------------------------- /packages/components/src/Select/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import "./Select.scss"; 4 | 5 | class Select extends React.Component { 6 | constructor(props) { 7 | super(props); 8 | this.onSelectChange = this.onSelectChange.bind(this); 9 | this.state = { value: this.props.selected || "" }; 10 | } 11 | 12 | onSelectChange(e) { 13 | this.setState( 14 | { 15 | value: e.target.value 16 | }, 17 | () => this.props.onChange(this.state.value) 18 | ); 19 | } 20 | 21 | render() { 22 | const { 23 | options, 24 | selected, 25 | onChange, 26 | className, 27 | ...otherProps 28 | } = this.props; 29 | return ( 30 | 42 | ); 43 | } 44 | } 45 | 46 | Select.propTypes = { 47 | /** 48 | * This function will receive the current value of the select dropdown. 49 | */ 50 | onChange: PropTypes.func.isRequired, 51 | options: PropTypes.arrayOf(PropTypes.string).isRequired, 52 | selected: PropTypes.string, 53 | className: PropTypes.string 54 | }; 55 | 56 | export default Select; 57 | -------------------------------------------------------------------------------- /packages/components/src/Slider/README.md: -------------------------------------------------------------------------------- 1 | # Slider 2 | 3 | ### Usage 4 | 5 | ```js 6 | import { Slider } from "./components/Slider"; 7 | ``` 8 | 9 | ### Properties 10 | 11 | - `onChange` - A function that will be passed the selected value whenever the slider changes. 12 | - `min` - The minimum value of the slider. 13 | - `max` - The maximum value of the slider. 14 | - `step` - The step between values. 15 | - `value` - The slider's default value. 16 | 17 | | propName | propType | defaultValue | isRequired | 18 | |----------|----------|--------------|------------| 19 | | onChange | func | - | + | 20 | | min | string | - | + | 21 | | max | string | - | + | 22 | | step | string | 1 | + | 23 | | value | string | - | + | 24 | -------------------------------------------------------------------------------- /packages/components/src/Slider/Slider.scss: -------------------------------------------------------------------------------- 1 | .dv-range-slider { 2 | width: 100%; 3 | padding-bottom: 1rem; 4 | margin-bottom: 1rem; 5 | } 6 | 7 | .dv-range-slider__range { 8 | -webkit-appearance: none; 9 | box-sizing: border-box; 10 | width: 100%; 11 | height: 5px; 12 | border-radius: 10px; 13 | background: #e3e3e3; 14 | outline: none; 15 | padding: 0; 16 | margin: 0; // Range Handle 17 | &::-webkit-slider-thumb { 18 | appearance: none; 19 | -webkit-appearance: none; 20 | width: 16px; 21 | height: 16px; 22 | border-radius: 50%; 23 | background: #fff; 24 | box-shadow: inset 0 0 0 6px #2dd1ac; 25 | cursor: pointer; 26 | } 27 | &::-moz-range-thumb { 28 | width: 16px; 29 | height: 16px; 30 | border: 0; 31 | border-radius: 50%; 32 | background: #fff; 33 | box-shadow: inset 0 0 0 6px #2dd1ac; 34 | cursor: pointer; 35 | } 36 | &::-moz-range-progress { 37 | background-color: #2dd1ac; 38 | } 39 | &::-ms-fill-lower { 40 | background-color: #2dd1ac; 41 | } 42 | } 43 | 44 | // Firefox Overrides 45 | ::-moz-range-track { 46 | background: #e3e3e3; 47 | border: 0; 48 | } 49 | 50 | input::-moz-focus-inner, 51 | input::-moz-focus-outer { 52 | border: 0; 53 | } 54 | 55 | .dv-range-slider__label-container { 56 | display: flex; 57 | justify-content: space-between; 58 | align-items: center; 59 | padding-bottom: 0.5rem; 60 | } 61 | 62 | .dv-range-slider__label { 63 | font-size: 14px; 64 | color: #333333; 65 | } 66 | 67 | .dv-range-slider__value { 68 | font-size: 14px; 69 | color: #888888; 70 | } -------------------------------------------------------------------------------- /packages/components/src/Slider/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import "./Slider.scss"; 4 | 5 | class Slider extends React.Component { 6 | constructor(props) { 7 | super(props); 8 | this.state = { value: this.props.value }; 9 | this.handleChange = this.handleChange.bind(this); 10 | } 11 | 12 | handleChange(e) { 13 | this.setState({ value: e.target.value }, () => 14 | this.props.onChange(this.state.value) 15 | ); 16 | } 17 | 18 | render() { 19 | const { label, min, max, step, id } = this.props; 20 | const { value } = this.state; 21 | const gradValue = Math.round((+value / +max) * 1 * 100); 22 | return ( 23 |
24 |
25 | {label} 26 | 27 | {value} out of {max} 28 | 29 |
30 | 44 |
45 | ); 46 | } 47 | } 48 | 49 | Slider.propTypes = { 50 | /** 51 | * This function will receive the entire event when the slider has changed. Use `event.target.value` to get the current slider value. 52 | */ 53 | onChange: PropTypes.func.isRequired, 54 | label: PropTypes.string.isRequired, 55 | min: PropTypes.number.isRequired, 56 | max: PropTypes.number.isRequired, 57 | step: PropTypes.number, 58 | id: PropTypes.string 59 | }; 60 | 61 | export default Slider; 62 | -------------------------------------------------------------------------------- /packages/components/src/Toggle/README.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/newamericafoundation/teddy/0ec5ff34159f9c099f96339174ede5e9cb556fa6/packages/components/src/Toggle/README.md -------------------------------------------------------------------------------- /packages/components/src/Toggle/Toggle.scss: -------------------------------------------------------------------------------- 1 | .dv-toggle-container { 2 | display: flex; 3 | align-items: center; 4 | } 5 | 6 | .dv-toggle { 7 | padding-right: 1rem; 8 | } 9 | 10 | .dv-toggle__input { 11 | display: none; 12 | + .dv-toggle__button { 13 | display: block; 14 | position: relative; 15 | width: 4rem; 16 | height: 2rem; 17 | padding: 3px; 18 | background: #d5d5d5; 19 | outline: 0; 20 | border-radius: 1rem; 21 | transition: all 0.4s ease; 22 | cursor: pointer; 23 | user-select: none; 24 | &:after, 25 | &:before { 26 | position: relative; 27 | display: block; 28 | content: ""; 29 | width: 50%; 30 | height: 100%; 31 | } 32 | &:after { 33 | left: 0; 34 | border-radius: 100%; 35 | background: #fff; 36 | transition: all 0.2s ease; 37 | } 38 | &:before { 39 | display: none; 40 | } 41 | } 42 | &:checked + .dv-toggle__button { 43 | background: #d5d5d5; 44 | } 45 | &:checked + .dv-toggle__button:after { 46 | left: 50%; 47 | } 48 | } 49 | 50 | .dv-toggle__label { 51 | font-size: 14px; 52 | } 53 | -------------------------------------------------------------------------------- /packages/components/src/Toggle/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import "./Toggle.scss"; 4 | 5 | class Toggle extends React.Component { 6 | constructor(props) { 7 | super(props); 8 | this.state = { checked: this.props.checked }; 9 | this.handleChange = this.handleChange.bind(this); 10 | } 11 | 12 | handleChange(e) { 13 | this.setState({ checked: e.target.checked }, () => 14 | this.props.onChange(this.state.checked) 15 | ); 16 | } 17 | 18 | render() { 19 | const { onLabel, offLabel, id } = this.props; 20 | const { checked } = this.state; 21 | return ( 22 |
23 |
24 | 31 |
33 | {checked ? onLabel : offLabel} 34 |
35 | ); 36 | } 37 | } 38 | 39 | Toggle.propTypes = { 40 | /** 41 | * This function will receive a boolean value for whether or not the toggle is on/off. 42 | */ 43 | onChange: PropTypes.func.isRequired, 44 | checked: PropTypes.bool, 45 | onLabel: PropTypes.string.isRequired, 46 | offLabel: PropTypes.string.isRequired, 47 | id: PropTypes.string 48 | }; 49 | 50 | Toggle.defaultProps = { 51 | checked: false 52 | }; 53 | 54 | export default Toggle; 55 | -------------------------------------------------------------------------------- /packages/components/src/index.js: -------------------------------------------------------------------------------- 1 | export { default as ButtonGroup } from "./ButtonGroup"; 2 | export { default as CheckboxGroup } from "./CheckboxGroup"; 3 | export { default as Search } from "./Search"; 4 | export { default as Select } from "./Select"; 5 | export { default as Slider } from "./Slider"; 6 | export { default as Toggle } from "./Toggle"; 7 | -------------------------------------------------------------------------------- /packages/data-table/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/preset-react", 4 | ["@babel/preset-env", { "modules": false }] 5 | ], 6 | "plugins": [ 7 | "@babel/plugin-proposal-class-properties", 8 | "@babel/plugin-proposal-object-rest-spread" 9 | ], 10 | "ignore": ["node_modules/**"] 11 | } 12 | -------------------------------------------------------------------------------- /packages/data-table/.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false -------------------------------------------------------------------------------- /packages/data-table/README.md: -------------------------------------------------------------------------------- 1 | # @newamerica/data-table 2 | 3 | A nicely styled, responsive data table, with options for pagination and search. This basically wraps [react-table](https://react-table.js.org) with some extra functionality and custom styling. 4 | 5 | ### Installation 6 | 7 | ``` 8 | npm install @newamerica/data-table --save 9 | ``` 10 | 11 | ### Usage Example 12 | 13 | ```jsx 14 | import { DataTable, DataTableWithSearch } from "@newamerica/data-table"; 15 | import "@newamerica/data-table/dist/styles.css"; 16 | 17 | const columns = [ 18 | { 19 | Header: // string for the column header, 20 | Accessor: // accessor string, 21 | // ^ this is the bare minimum, but react-table accepts a lot more, like custom cell renderers etc... 22 | } 23 | ] 24 | 25 | const MyTable = () => ( 26 | 27 | ); 28 | ``` 29 | 30 | 31 | ## Components 32 | 33 | 34 | 35 | - [DataTable](#datatable) 36 | 37 | ## API 38 | 39 | 40 | 41 | 42 | ### DataTable 43 | 44 | From [`./src/DataTable/index.js`](./src/DataTable/index.js) 45 | 46 | All extra props will be passed directly to the `ReactTable` component. See docs for that [here](https://react-table.js.org). 47 | 48 | TODO: 49 | - [ ] add functionality for a sticky first column 50 | - [ ] add functionality for a select dropdown in addition to a search box 51 | 52 | prop | type | default | required | description 53 | ---- | :----: | :-------: | :--------: | ----------- 54 | **children** | `ReactElement` | | :x: | 55 | **columns** | `Array[]` | | :white_check_mark: | 56 | **data** | `Array` | | :white_check_mark: | 57 | **showPagination** | `Boolean` | `true` | :x: | 58 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /packages/data-table/docs/description.md: -------------------------------------------------------------------------------- 1 | # @newamerica/data-table 2 | 3 | A nicely styled, responsive data table, with options for pagination and search. This basically wraps [react-table](https://react-table.js.org) with some extra functionality and custom styling. 4 | 5 | ### Installation 6 | 7 | ``` 8 | npm install @newamerica/data-table --save 9 | ``` 10 | 11 | ### Usage Example 12 | 13 | ```jsx 14 | import { DataTable, DataTableWithSearch } from "@newamerica/data-table"; 15 | import "@newamerica/data-table/dist/styles.css"; 16 | 17 | const columns = [ 18 | { 19 | Header: // string for the column header, 20 | Accessor: // accessor string, 21 | // ^ this is the bare minimum, but react-table accepts a lot more, like custom cell renderers etc... 22 | } 23 | ] 24 | 25 | const MyTable = () => ( 26 | 27 | ); 28 | ``` 29 | -------------------------------------------------------------------------------- /packages/data-table/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@newamerica/data-table", 3 | "version": "0.0.9", 4 | "description": "Tables on tables", 5 | "main": "dist/index.umd.js", 6 | "module": "dist/index.es.js", 7 | "repository": { 8 | "type": "git", 9 | "url": "https://github.com/newamericafoundation/teddy.git" 10 | }, 11 | "publishConfig": { 12 | "access": "public" 13 | }, 14 | "sideEffects": false, 15 | "scripts": { 16 | "start": "rollup -c -w --environment BUILD:development", 17 | "build": "rollup -c --environment BUILD:production", 18 | "prepublish": "rm -rf ./dist && npm run build", 19 | "docs": "cd ./docs && ../../../node_modules/.bin/react-docgen ../src/** -e Pagination.js | ../../../scripts/buildDocs.sh", 20 | "test": "echo \"Error: no test specified\" && exit 1" 21 | }, 22 | "keywords": [ 23 | "vx", 24 | "react", 25 | "d3", 26 | "charts", 27 | "data", 28 | "visualization" 29 | ], 30 | "dependencies": { 31 | "@newamerica/components": "^0.0.6", 32 | "@newamerica/scss": "^0.0.2", 33 | "prop-types": "^15.6.2", 34 | "react-table": "6.8.6" 35 | }, 36 | "peerDependencies": { 37 | "react": "^16.2.0" 38 | }, 39 | "devDependencies": { 40 | "@babel/core": "^7.0.0", 41 | "@babel/plugin-proposal-class-properties": "^7.0.0", 42 | "@babel/plugin-proposal-object-rest-spread": "^7.0.0", 43 | "@babel/preset-env": "^7.0.0", 44 | "@babel/preset-react": "^7.0.0", 45 | "autoprefixer": "^9.4.5", 46 | "node-sass": "^4.9.2", 47 | "rollup": "^1.1.0", 48 | "rollup-plugin-babel": "^4.3.1", 49 | "rollup-plugin-node-resolve": "^4.0.0", 50 | "rollup-plugin-postcss": "^1.6.3", 51 | "rollup-plugin-terser": "^4.0.2" 52 | }, 53 | "author": "lorenries", 54 | "license": "MIT" 55 | } 56 | -------------------------------------------------------------------------------- /packages/data-table/rollup.config.js: -------------------------------------------------------------------------------- 1 | import resolve from "rollup-plugin-node-resolve"; 2 | import babel from "rollup-plugin-babel"; 3 | import { terser } from "rollup-plugin-terser"; 4 | import postcss from "rollup-plugin-postcss"; 5 | import autoprefixer from "autoprefixer"; 6 | import pkg from "./package.json"; 7 | 8 | const deps = Object.keys({ 9 | ...pkg.dependencies, 10 | ...pkg.peerDependencies 11 | }); 12 | 13 | const globals = deps.reduce((o, name) => { 14 | if (name.includes("@vx/")) { 15 | o[name] = "vx"; 16 | } 17 | if (name.includes("d3-")) { 18 | o[name] = "d3"; 19 | } 20 | if (name === "react") { 21 | o[name] = "React"; 22 | } 23 | if (name === "react-dom") { 24 | o[name] = "ReactDOM"; 25 | } 26 | if (name === "prop-types") { 27 | o[name] = "PropTypes"; 28 | } 29 | if (name === "classnames") { 30 | o[name] = "classNames"; 31 | } 32 | if (name === "react-table") { 33 | o[name] = "ReactTable"; 34 | } 35 | return o; 36 | }, {}); 37 | 38 | export default [ 39 | { 40 | input: "src/index.js", 41 | external: deps, 42 | plugins: [ 43 | resolve(), 44 | babel({ 45 | exclude: "node_modules/**" 46 | }), 47 | postcss({ 48 | extensions: [".css", ".scss"], 49 | plugins: [autoprefixer], 50 | minimize: true, 51 | inject: false, 52 | extract: "dist/styles.css" 53 | }), 54 | process.env.BUILD === "production" && terser() 55 | ], 56 | output: { 57 | file: pkg.main, 58 | format: "umd", 59 | name: "charts", 60 | globals 61 | } 62 | }, 63 | { 64 | input: "src/index.js", 65 | external: deps, 66 | plugins: [ 67 | resolve(), 68 | babel({ 69 | exclude: "node_modules/**" 70 | }), 71 | postcss({ 72 | extensions: [".css", ".scss"], 73 | plugins: [autoprefixer], 74 | minimize: true, 75 | inject: false, 76 | extract: "dist/styles.css" 77 | }), 78 | process.env.BUILD === "production" && terser() 79 | ], 80 | output: { file: pkg.module, format: "es", globals } 81 | } 82 | ]; 83 | -------------------------------------------------------------------------------- /packages/data-table/src/DataTable/Pagination.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const defaultButton = props => ( 4 | 7 | ); 8 | 9 | export default class Pagination extends React.Component { 10 | constructor(props) { 11 | super(); 12 | this.getSafePage = this.getSafePage.bind(this); 13 | this.changePage = this.changePage.bind(this); 14 | this.applyPage = this.applyPage.bind(this); 15 | 16 | this.state = { 17 | page: props.page 18 | }; 19 | } 20 | 21 | componentWillReceiveProps(nextProps) { 22 | this.setState({ page: nextProps.page }); 23 | } 24 | 25 | getSafePage(page) { 26 | if (Number.isNaN(page)) { 27 | page = this.props.page; 28 | } 29 | return Math.min(Math.max(page, 0), this.props.pages - 1); 30 | } 31 | 32 | changePage(page) { 33 | page = this.getSafePage(page); 34 | this.setState({ page }); 35 | if (this.props.page !== page) { 36 | this.props.onPageChange(page); 37 | } 38 | } 39 | 40 | applyPage(e) { 41 | if (e) { 42 | e.preventDefault(); 43 | } 44 | const page = this.state.page; 45 | this.changePage(page === "" ? this.props.page : page); 46 | } 47 | 48 | render() { 49 | const { 50 | // Computed 51 | pages, 52 | // Props 53 | page, 54 | sortedData, 55 | showPageSizeOptions, 56 | pageSizeOptions, 57 | pageSize, 58 | showPageJump, 59 | canPrevious, 60 | canNext, 61 | onPageSizeChange, 62 | className, 63 | PreviousComponent = defaultButton, 64 | NextComponent = defaultButton 65 | } = this.props; 66 | 67 | return ( 68 |
69 | 70 | Showing {page * pageSize + 1} to{" "} 71 | {page === pages - 1 ? sortedData.length : page * pageSize + pageSize}{" "} 72 | of {sortedData.length} entries 73 | 74 |
75 |
76 | { 78 | if (!canPrevious) return; 79 | this.changePage(page - 1); 80 | }} 81 | disabled={!canPrevious} 82 | > 83 | {this.props.previousText} 84 | 85 |
86 |
87 | 88 | {this.props.pageText}{" "} 89 | {showPageJump ? ( 90 |
91 | { 94 | const val = e.target.value; 95 | const page = val - 1; 96 | if (val === "") { 97 | return this.setState({ page: val }); 98 | } 99 | this.setState({ page: this.getSafePage(page) }); 100 | }} 101 | value={this.state.page === "" ? "" : this.state.page + 1} 102 | onBlur={this.applyPage} 103 | onKeyPress={e => { 104 | if (e.which === 13 || e.keyCode === 13) { 105 | this.applyPage(); 106 | } 107 | }} 108 | /> 109 |
110 | ) : ( 111 | {page + 1} 112 | )}{" "} 113 | {this.props.ofText} 114 | {pages || 1} 115 |
116 |
117 |
118 | { 120 | if (!canNext) return; 121 | this.changePage(page + 1); 122 | }} 123 | disabled={!canNext} 124 | > 125 | {this.props.nextText} 126 | 127 |
128 |
129 |
130 | ); 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /packages/data-table/src/DataTable/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import ReactTable from "react-table"; 4 | import Pagination from "./Pagination"; 5 | import "react-table/react-table.css"; 6 | import "./DataTable.scss"; 7 | 8 | /** 9 | * All extra props will be passed directly to the `ReactTable` component. See docs for that [here](https://react-table.js.org). 10 | * 11 | * TODO: 12 | * - [ ] add functionality for a sticky first column 13 | * - [ ] add functionality for a select dropdown in addition to a search box 14 | */ 15 | const DataTable = ({ data, columns, showPagination, children, ...rest }) => ( 16 |
17 | {children} 18 | 27 |
28 | ); 29 | 30 | DataTable.propTypes = { 31 | data: PropTypes.array.isRequired, 32 | columns: PropTypes.arrayOf(PropTypes.object).isRequired, 33 | showPagination: PropTypes.bool, 34 | children: PropTypes.element 35 | }; 36 | 37 | DataTable.defaultProps = { 38 | showPagination: true 39 | }; 40 | 41 | export default DataTable; 42 | -------------------------------------------------------------------------------- /packages/data-table/src/DataTableWithSearch/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import DataTable from "../DataTable"; 3 | import withSearch from "./WithSearch"; 4 | 5 | /** 6 | * Wraps the DataTable component with a search box. 7 | */ 8 | export default withSearch(DataTable); 9 | -------------------------------------------------------------------------------- /packages/data-table/src/DataTableWithSearch/withSearch.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Search } from "@newamerica/components"; 3 | import "@newamerica/components/dist/styles.css"; 4 | 5 | export default function withSearch(Table) { 6 | return class extends React.Component { 7 | constructor(props) { 8 | super(props); 9 | this.state = { search: "" }; 10 | } 11 | 12 | handleChange(searchString) { 13 | this.setState({ search: searchString }); 14 | } 15 | 16 | render() { 17 | const search = this.state.search; 18 | const { data, ...otherProps } = this.props; 19 | let _data = data; 20 | if (search.length > 0) { 21 | _data = data.filter(row => { 22 | const columns = Object.keys(row); 23 | return columns.some( 24 | column => 25 | typeof row[column] === "string" && 26 | row[column].toLowerCase().includes(search.toLowerCase()) 27 | ); 28 | }); 29 | } 30 | return ( 31 | 32 | 33 |
34 | ); 35 | } 36 | }; 37 | } 38 | -------------------------------------------------------------------------------- /packages/data-table/src/index.js: -------------------------------------------------------------------------------- 1 | export { default as DataTable } from "./DataTable"; 2 | export { default as DataTableWithSearch } from "./DataTableWithSearch"; 3 | -------------------------------------------------------------------------------- /packages/maps/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/preset-react", 4 | ["@babel/preset-env", { "modules": false }] 5 | ], 6 | "plugins": [ 7 | "@babel/plugin-proposal-class-properties", 8 | "@babel/plugin-proposal-object-rest-spread" 9 | ], 10 | "ignore": ["node_modules/**"] 11 | } 12 | -------------------------------------------------------------------------------- /packages/maps/.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false -------------------------------------------------------------------------------- /packages/maps/README.md: -------------------------------------------------------------------------------- 1 | # @newamerica/maps 2 | 3 | Components for creating fully responsive maps, with flexibility to change the loaded geometry, projections, overlays, and tooltips. 4 | 5 | ### Installation 6 | 7 | ``` 8 | npm install @newamerica/maps --save 9 | ``` 10 | 11 | ### Usage Example 12 | 13 | ```jsx 14 | import { Pindrop } from "@newamerica/maps"; 15 | import { Chart } from "@newamerica/charts"; 16 | import "@newamerica/charts/dist/styles.css"; 17 | import "@newamerica/maps/dist/styles.css"; 18 | 19 | const MyMap = () => ( 20 | 21 | {({ width, height }) => ( 22 | 29 | )} 30 | 31 | ); 32 | ``` 33 | 34 | 35 | ## Components 36 | 37 | 38 | 39 | - [Cartogram](#cartogram) 40 | - [Choropleth](#choropleth) 41 | - [LoadGeometry](#loadgeometry) 42 | - [Pindrop](#pindrop) 43 | - [Projection](#projection) 44 | 45 | ## API 46 | 47 | 48 | 49 | 50 | ### Cartogram 51 | 52 | From [`./src/Cartogram/index.js`](./src/Cartogram/index.js) 53 | 54 | Cartogram map 55 | 56 | ⚠ this chart wraps the base `Chart` component in `@newamerica/charts`, because it relies on an internally calculated aspect ratio. 57 | 58 | prop | type | default | required | description 59 | ---- | :----: | :-------: | :--------: | ----------- 60 | **colors** | `Array` | `["#e6dcff", "#504a70"]` | :x: | 61 | **data** | `Array` | | :white_check_mark: | 62 | **idAccessor** | `Function` | `d => d.id` | :x: | 63 | **mapFill** | `String` | `"#cbcbcd"` | :x: | 64 | **mapStroke** | `String` | `"#fff"` | :x: | 65 | **maxWidth** | `Union` | | :x: | 66 | **numStops** | `Number` | `6` | :x: | 67 | **renderTooltip** | `Function` | | :x: | 68 | **valueAccessor** | `Function` | | :white_check_mark: | 69 | 70 | 71 | 72 | 73 | 74 | ### Choropleth 75 | 76 | From [`./src/Choropleth/index.js`](./src/Choropleth/index.js) 77 | 78 | Choropleth map 79 | TODO: legend and margins 80 | 81 | prop | type | default | required | description 82 | ---- | :----: | :-------: | :--------: | ----------- 83 | **colors** | `Array` | `["#e6dcff", "#504a70"]` | :x: | An array of two colors, from which the color scale will be calculated 84 | **data** | `Array` | | :white_check_mark: | 85 | **geometry** | `Enum("world","us")` | | :white_check_mark: | 86 | **handleMouseLeave** | `Function` | | :x: | 87 | **handleMouseMove** | `Function` | | :x: | 88 | **height** | `Number` | | :white_check_mark: | 89 | **idAccessor** | `Function` | `d => d.id` | :x: | An accessor function for the state, country, or county FIPS code in your data. This is necessary to match politcal boundaries in the feature collection to your data. 90 | **mapFill** | `String` | `"#cbcbcd"` | :x: | 91 | **mapStroke** | `String` | `"#fff"` | :x: | 92 | **numStops** | `Number` | `6` | :x: | The number of color stops 93 | **projection** | `Enum("mercator","equalEarth","albersUsa")` | | :white_check_mark: | 94 | **valueAccessor** | `Function` | | :white_check_mark: | An accessor function for the data that's colored on the map 95 | **width** | `Number` | | :white_check_mark: | 96 | 97 | 98 | 99 | 100 | 101 | ### LoadGeometry 102 | 103 | From [`./src/LoadGeometry/index.js`](./src/LoadGeometry/index.js) 104 | 105 | Loads a geojson from our S3 bucket, and calls your child function with the topojson feature. 106 | 107 | prop | type | default | required | description 108 | ---- | :----: | :-------: | :--------: | ----------- 109 | **children** | `Function` | | :white_check_mark: | 110 | **geometry** | `Enum("world","us")` | | :white_check_mark: | 111 | 112 | 113 | 114 | 115 | 116 | ### Pindrop 117 | 118 | From [`./src/Pindrop/index.js`](./src/Pindrop/index.js) 119 | 120 | Pindrop map component 121 | TODO: implement overlap detection with an optional `preventOverlap` prop 122 | 123 | prop | type | default | required | description 124 | ---- | :----: | :-------: | :--------: | ----------- 125 | **circleFill** | `Union` | `"#2ebcb3"` | :x: | A string for each circle's fill, or a function that will receive that circle's datum 126 | **circleRadius** | `Union` | `5` | :x: | A number for the circle's radius, or a function that will receive that point's datum for [radius scaling](https://bl.ocks.org/guilhermesimoes/e6356aa90a16163a6f917f53600a2b4a). 127 | **circleStroke** | `Union` | `"#fff"` | :x: | A string for each circle's stroke, or a function that will receive that circle's datum 128 | **data** | `Array` | | :white_check_mark: | 129 | **geometry** | `Enum("world","us")` | | :white_check_mark: | 130 | **handleMouseLeave** | `Function` | | :x: | 131 | **handleMouseMove** | `Function` | | :x: | 132 | **height** | `Number` | | :white_check_mark: | 133 | **mapFill** | `String` | `"#cbcbcd"` | :x: | 134 | **mapStroke** | `String` | `"#fff"` | :x: | 135 | **projection** | `Enum("mercator","equalEarth","albersUsa")` | | :white_check_mark: | 136 | **width** | `Number` | | :white_check_mark: | 137 | 138 | 139 | 140 | 141 | 142 | ### Projection 143 | 144 | From [`./src/Projection/index.js`](./src/Projection/index.js) 145 | 146 | Component for all projections. 147 | 148 | prop | type | default | required | description 149 | ---- | :----: | :-------: | :--------: | ----------- 150 | **center** | `Array` | | :x: | 151 | **children** | `Function` | | :x: | 152 | **clipAngle** | `Number` | | :x: | 153 | **clipExtent** | `Array` | | :x: | 154 | **data** | `Array` | | :white_check_mark: | 155 | **fitExtent** | `Array` | | :x: | 156 | **fitSize** | `Array` | | :x: | 157 | **innerRef** | `Function` | | :x: | 158 | **pathFunc** | `Function` | | :x: | 159 | **precision** | `Number` | | :x: | 160 | **projection** | `Enum("mercator","equalEarth","albersUsa")` | `"mercator"` | :x: | 161 | **projectionFunc** | `Function` | | :x: | 162 | **rotate** | `Array` | | :x: | 163 | **scale** | `Number` | | :x: | 164 | **translate** | `Array` | | :x: | 165 | 166 | 167 | 168 | -------------------------------------------------------------------------------- /packages/maps/docs/description.md: -------------------------------------------------------------------------------- 1 | # @newamerica/maps 2 | 3 | Components for creating fully responsive maps, with flexibility to change the loaded geometry, projections, overlays, and tooltips. 4 | 5 | ### Installation 6 | 7 | ``` 8 | npm install @newamerica/maps --save 9 | ``` 10 | 11 | ### Usage Example 12 | 13 | ```jsx 14 | import { Pindrop } from "@newamerica/maps"; 15 | import { Chart } from "@newamerica/charts"; 16 | import "@newamerica/charts/dist/styles.css"; 17 | import "@newamerica/maps/dist/styles.css"; 18 | 19 | const MyMap = () => ( 20 | 21 | {({ width, height }) => ( 22 | 29 | )} 30 | 31 | ); 32 | ``` 33 | -------------------------------------------------------------------------------- /packages/maps/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@newamerica/maps", 3 | "version": "0.0.3", 4 | "description": "Nothing is certain in this life but death, taxes and requests for geographic data to be represented on a map", 5 | "main": "dist/index.umd.js", 6 | "module": "dist/index.es.js", 7 | "repository": { 8 | "type": "git", 9 | "url": "https://github.com/newamericafoundation/teddy.git" 10 | }, 11 | "publishConfig": { 12 | "access": "public" 13 | }, 14 | "sideEffects": false, 15 | "scripts": { 16 | "start": "rollup -c -w --environment BUILD:development", 17 | "build": "rollup -c --environment BUILD:production", 18 | "prepublish": "rm -rf ./dist && npm run build", 19 | "docs": "cd ./docs && ../../../node_modules/.bin/react-docgen ../src/** | ../../../scripts/buildDocs.sh", 20 | "test": "echo \"Error: no test specified\" && exit 1" 21 | }, 22 | "keywords": [ 23 | "vx", 24 | "react", 25 | "d3", 26 | "maps", 27 | "data", 28 | "visualization" 29 | ], 30 | "dependencies": { 31 | "@newamerica/charts": "^0.0.4", 32 | "@vx/group": "0.0.183", 33 | "@vx/scale": "0.0.182", 34 | "@vx/text": "0.0.183", 35 | "d3-array": "^1.2.4", 36 | "d3-collection": "^1.0.7", 37 | "d3-geo": "^1.11.3", 38 | "d3-interpolate": "^1.3.2", 39 | "prop-types": "^15.6.2", 40 | "topojson-client": "^3.0.0" 41 | }, 42 | "peerDependencies": { 43 | "react": "^16.2.0" 44 | }, 45 | "devDependencies": { 46 | "@babel/core": "^7.0.0", 47 | "@babel/plugin-proposal-class-properties": "^7.0.0", 48 | "@babel/plugin-proposal-object-rest-spread": "^7.0.0", 49 | "@babel/preset-env": "^7.0.0", 50 | "@babel/preset-react": "^7.0.0", 51 | "autoprefixer": "^9.4.5", 52 | "node-sass": "^4.9.2", 53 | "rollup": "^1.1.0", 54 | "rollup-plugin-babel": "^4.3.1", 55 | "rollup-plugin-node-resolve": "^4.0.0", 56 | "rollup-plugin-postcss": "^1.6.3", 57 | "rollup-plugin-terser": "^4.0.2" 58 | }, 59 | "author": "lorenries", 60 | "license": "MIT" 61 | } 62 | -------------------------------------------------------------------------------- /packages/maps/rollup.config.js: -------------------------------------------------------------------------------- 1 | import resolve from "rollup-plugin-node-resolve"; 2 | import babel from "rollup-plugin-babel"; 3 | import { terser } from "rollup-plugin-terser"; 4 | import postcss from "rollup-plugin-postcss"; 5 | import autoprefixer from "autoprefixer"; 6 | import pkg from "./package.json"; 7 | 8 | const deps = Object.keys({ 9 | ...pkg.dependencies, 10 | ...pkg.peerDependencies 11 | }); 12 | 13 | const globals = deps.reduce((o, name) => { 14 | if (name.includes("@vx/")) { 15 | o[name] = "vx"; 16 | } 17 | if (name.includes("d3-")) { 18 | o[name] = "d3"; 19 | } 20 | if (name === "react") { 21 | o[name] = "React"; 22 | } 23 | if (name === "react-dom") { 24 | o[name] = "ReactDOM"; 25 | } 26 | if (name === "prop-types") { 27 | o[name] = "PropTypes"; 28 | } 29 | if (name === "classnames") { 30 | o[name] = "classNames"; 31 | } 32 | return o; 33 | }, {}); 34 | 35 | export default [ 36 | { 37 | input: "src/index.js", 38 | external: deps, 39 | plugins: [ 40 | resolve(), 41 | babel({ 42 | exclude: "node_modules/**" 43 | }), 44 | postcss({ 45 | extensions: [".css", ".scss"], 46 | plugins: [autoprefixer], 47 | minimize: true, 48 | inject: false, 49 | extract: "dist/styles.css" 50 | }), 51 | process.env.BUILD === "production" && terser() 52 | ], 53 | output: { 54 | file: pkg.main, 55 | format: "umd", 56 | name: "charts", 57 | globals 58 | } 59 | }, 60 | { 61 | input: "src/index.js", 62 | external: deps, 63 | plugins: [ 64 | resolve(), 65 | babel({ 66 | exclude: "node_modules/**" 67 | }), 68 | postcss({ 69 | extensions: [".css", ".scss"], 70 | plugins: [autoprefixer], 71 | minimize: true, 72 | inject: false, 73 | extract: "dist/styles.css" 74 | }), 75 | process.env.BUILD === "production" && terser() 76 | ], 77 | output: { file: pkg.module, format: "es", globals } 78 | } 79 | ]; 80 | -------------------------------------------------------------------------------- /packages/maps/src/Cartogram/Cartogram.scss: -------------------------------------------------------------------------------- 1 | .dv-Cartogram__label { 2 | font-size: 12px; 3 | font-family: Circular; 4 | color: #333; 5 | } 6 | -------------------------------------------------------------------------------- /packages/maps/src/Cartogram/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import { Chart } from "@newamerica/charts"; 4 | import { Text } from "@vx/text"; 5 | import { scaleLinear, scaleQuantize } from "@vx/scale"; 6 | import { quantize, interpolateRgb } from "d3-interpolate"; 7 | import { max, extent } from "d3-array"; 8 | import { map } from "d3-collection"; 9 | import layout from "./layout"; 10 | import "./Cartogram.scss"; 11 | 12 | /** 13 | * Cartogram map 14 | * 15 | * ⚠ this chart wraps the base `Chart` component in `@newamerica/charts`, because it relies on an internally calculated aspect ratio. 16 | */ 17 | const Cartogram = ({ 18 | maxWidth, 19 | data, 20 | renderTooltip, 21 | valueAccessor, 22 | idAccessor, 23 | mapStroke, 24 | mapFill, 25 | colors, 26 | numStops 27 | }) => { 28 | const dataMap = map(data, idAccessor); 29 | const colorArray = quantize(interpolateRgb(colors[0], colors[1]), numStops); 30 | const colorScale = scaleQuantize({ 31 | domain: extent(data, valueAccessor), 32 | range: colorArray 33 | }); 34 | const x = d => d.x; 35 | const y = d => d.y; 36 | const boxesWide = max(layout, x) + 1; 37 | const boxesTall = max(layout, y) + 1; 38 | const ratio = boxesTall / boxesWide; 39 | return ( 40 | 45 | {({ width, height, handleMouseMove, handleMouseLeave }) => { 46 | if (width < 10) return; 47 | const boxWidth = width / boxesWide; 48 | const textOffset = boxWidth / 2; 49 | const xScale = scaleLinear({ 50 | domain: [0, boxesWide], 51 | range: [0, width] 52 | }); 53 | const yScale = scaleLinear({ 54 | domain: [0, boxesTall], 55 | range: [0, height] 56 | }); 57 | return ( 58 | 59 | {layout.map((state, i) => { 60 | const xPos = xScale(x(state)); 61 | const yPos = yScale(y(state)); 62 | const datum = dataMap.get(state.fips); 63 | return ( 64 | handleMouseMove({ event, datum })} 67 | onMouseLeave={handleMouseLeave} 68 | > 69 | 77 | 84 | {width < 640 ? state.short : state.abb} 85 | 86 | 87 | ); 88 | })} 89 | 90 | ); 91 | }} 92 | 93 | ); 94 | }; 95 | 96 | Cartogram.propTypes = { 97 | maxWidth: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), 98 | data: PropTypes.array.isRequired, 99 | renderTooltip: PropTypes.func, 100 | valueAccessor: PropTypes.func.isRequired, 101 | idAccessor: PropTypes.func, 102 | mapStroke: PropTypes.string, 103 | mapFill: PropTypes.string, 104 | colors: PropTypes.array, 105 | numStops: PropTypes.number 106 | }; 107 | 108 | Cartogram.defaultProps = { 109 | idAccessor: d => d.id, 110 | mapStroke: "#fff", 111 | mapFill: "#cbcbcd", 112 | colors: ["#e6dcff", "#504a70"], 113 | numStops: 6 114 | }; 115 | 116 | export default Cartogram; 117 | -------------------------------------------------------------------------------- /packages/maps/src/Cartogram/layout.js: -------------------------------------------------------------------------------- 1 | const layout = [ 2 | { 3 | x: 0, 4 | y: 0, 5 | short: "AK", 6 | long: "Alaska", 7 | abb: "Alaska", 8 | fips: "02" 9 | }, 10 | { 11 | x: 10, 12 | y: 0, 13 | short: "ME", 14 | long: "Maine", 15 | abb: "Maine", 16 | fips: "23" 17 | }, 18 | { 19 | x: 9, 20 | y: 1, 21 | short: "VT", 22 | long: "Vermont", 23 | abb: "Vt.", 24 | fips: "50" 25 | }, 26 | { 27 | x: 10, 28 | y: 1, 29 | short: "NH", 30 | long: "New Hampshire", 31 | abb: "N.H.", 32 | fips: "33" 33 | }, 34 | { 35 | x: 0, 36 | y: 2, 37 | short: "WA", 38 | long: "Washington", 39 | abb: "Wash.", 40 | fips: "53" 41 | }, 42 | { 43 | x: 1, 44 | y: 2, 45 | short: "ID", 46 | long: "Idaho", 47 | abb: "Idaho", 48 | fips: "16" 49 | }, 50 | { 51 | x: 2, 52 | y: 2, 53 | short: "MT", 54 | long: "Montana", 55 | abb: "Mont.", 56 | fips: "30" 57 | }, 58 | { 59 | x: 3, 60 | y: 2, 61 | short: "ND", 62 | long: "North Dakota", 63 | abb: "N.D.", 64 | fips: "38" 65 | }, 66 | { 67 | x: 4, 68 | y: 2, 69 | short: "MN", 70 | long: "Minnesota", 71 | abb: "Minn.", 72 | fips: "27" 73 | }, 74 | { 75 | x: 6, 76 | y: 2, 77 | short: "MI", 78 | long: "Michigan", 79 | abb: "Mich.", 80 | fips: "26" 81 | }, 82 | { 83 | x: 8, 84 | y: 2, 85 | short: "NY", 86 | long: "New York", 87 | abb: "N.Y.", 88 | fips: "36" 89 | }, 90 | { 91 | x: 9, 92 | y: 2, 93 | short: "MA", 94 | long: "Massachusetts", 95 | abb: "Mass.", 96 | fips: "25" 97 | }, 98 | { 99 | x: 10, 100 | y: 2, 101 | short: "RI", 102 | long: "Rhode Island", 103 | abb: "R.I.", 104 | fips: "44" 105 | }, 106 | { 107 | x: 0, 108 | y: 3, 109 | short: "OR", 110 | long: "Oregon", 111 | abb: "Ore.", 112 | fips: "41" 113 | }, 114 | { 115 | x: 1, 116 | y: 3, 117 | short: "UT", 118 | long: "Utah", 119 | abb: "Utah", 120 | fips: "49" 121 | }, 122 | { 123 | x: 2, 124 | y: 3, 125 | short: "WY", 126 | long: "Wyoming", 127 | abb: "Wyo.", 128 | fips: "56" 129 | }, 130 | { 131 | x: 3, 132 | y: 3, 133 | short: "SD", 134 | long: "South Dakota", 135 | abb: "S.D.", 136 | fips: "46" 137 | }, 138 | { 139 | x: 4, 140 | y: 3, 141 | short: "IA", 142 | long: "Iowa", 143 | abb: "Iowa", 144 | fips: "19" 145 | }, 146 | { 147 | x: 5, 148 | y: 3, 149 | short: "WI", 150 | long: "Wisconsin", 151 | abb: "Wis.", 152 | fips: "55" 153 | }, 154 | { 155 | x: 6, 156 | y: 3, 157 | short: "IN", 158 | long: "Indiana", 159 | abb: "Ind.", 160 | fips: "18" 161 | }, 162 | { 163 | x: 7, 164 | y: 3, 165 | short: "OH", 166 | long: "Ohio", 167 | abb: "Ohio", 168 | fips: "39" 169 | }, 170 | { 171 | x: 8, 172 | y: 3, 173 | short: "PA", 174 | long: "Pennsylvania", 175 | abb: "Pa.", 176 | fips: "42" 177 | }, 178 | { 179 | x: 9, 180 | y: 3, 181 | short: "NJ", 182 | long: "New Jersey", 183 | abb: "N.J.", 184 | fips: "34" 185 | }, 186 | { 187 | x: 10, 188 | y: 3, 189 | short: "CT", 190 | long: "Connecticut", 191 | abb: "Conn.", 192 | fips: "09" 193 | }, 194 | { 195 | x: 0, 196 | y: 4, 197 | short: "CA", 198 | long: "California", 199 | abb: "Calif.", 200 | fips: "06" 201 | }, 202 | { 203 | x: 1, 204 | y: 4, 205 | short: "NV", 206 | long: "Nevada", 207 | abb: "Nev.", 208 | fips: "32" 209 | }, 210 | { 211 | x: 2, 212 | y: 4, 213 | short: "CO", 214 | long: "Colorado", 215 | abb: "Colo.", 216 | fips: "08" 217 | }, 218 | { 219 | x: 3, 220 | y: 4, 221 | short: "NE", 222 | long: "Nebraska", 223 | abb: "Neb.", 224 | fips: "31" 225 | }, 226 | { 227 | x: 4, 228 | y: 4, 229 | short: "MO", 230 | long: "Missouri", 231 | abb: "Mo.", 232 | fips: "29" 233 | }, 234 | { 235 | x: 5, 236 | y: 4, 237 | short: "IL", 238 | long: "Illinois", 239 | abb: "Ill.", 240 | fips: "17" 241 | }, 242 | { 243 | x: 6, 244 | y: 4, 245 | short: "KY", 246 | long: "Kentucky", 247 | abb: "Ky.", 248 | fips: "21" 249 | }, 250 | { 251 | x: 7, 252 | y: 4, 253 | short: "WV", 254 | long: "West Virginia", 255 | abb: "W.Va.", 256 | fips: "54" 257 | }, 258 | { 259 | x: 8, 260 | y: 4, 261 | short: "VA", 262 | long: "Virginia", 263 | abb: "Va.", 264 | fips: "51" 265 | }, 266 | { 267 | x: 9, 268 | y: 4, 269 | short: "MD", 270 | long: "Maryland", 271 | abb: "Md.", 272 | fips: "24" 273 | }, 274 | { 275 | x: 10, 276 | y: 4, 277 | short: "DE", 278 | long: "Delaware", 279 | abb: "Del.", 280 | fips: "10" 281 | }, 282 | { 283 | x: 1, 284 | y: 5, 285 | short: "AZ", 286 | long: "Arizona", 287 | abb: "Ariz.", 288 | fips: "04" 289 | }, 290 | { 291 | x: 2, 292 | y: 5, 293 | short: "NM", 294 | long: "New Mexico", 295 | abb: "N.M.", 296 | fips: "35" 297 | }, 298 | { 299 | x: 3, 300 | y: 5, 301 | short: "KS", 302 | long: "Kansas", 303 | abb: "Kan.", 304 | fips: "20" 305 | }, 306 | { 307 | x: 4, 308 | y: 5, 309 | short: "AR", 310 | long: "Arkansas", 311 | abb: "Ark.", 312 | fips: "05" 313 | }, 314 | { 315 | x: 5, 316 | y: 5, 317 | short: "TN", 318 | long: "Tennessee", 319 | abb: "Tenn.", 320 | fips: "47" 321 | }, 322 | { 323 | x: 6, 324 | y: 5, 325 | short: "NC", 326 | long: "North Carolina", 327 | abb: "N.C.", 328 | fips: "37" 329 | }, 330 | { 331 | x: 7, 332 | y: 5, 333 | short: "SC", 334 | long: "South Carolina", 335 | abb: "S.C.", 336 | fips: "45" 337 | }, 338 | { 339 | x: 8, 340 | y: 5, 341 | short: "DC", 342 | long: "District of Columbia", 343 | abb: "D.C.", 344 | fips: "11" 345 | }, 346 | { 347 | x: 3, 348 | y: 6, 349 | short: "OK", 350 | long: "Oklahoma", 351 | abb: "Okla.", 352 | fips: "40" 353 | }, 354 | { 355 | x: 4, 356 | y: 6, 357 | short: "LA", 358 | long: "Louisiana", 359 | abb: "La.", 360 | fips: "22" 361 | }, 362 | { 363 | x: 5, 364 | y: 6, 365 | short: "MS", 366 | long: "Mississippi", 367 | abb: "Miss.", 368 | fips: "28" 369 | }, 370 | { 371 | x: 6, 372 | y: 6, 373 | short: "AL", 374 | long: "Alabama", 375 | abb: "Ala.", 376 | fips: "01" 377 | }, 378 | { 379 | x: 7, 380 | y: 6, 381 | short: "GA", 382 | long: "Georgia", 383 | abb: "Ga.", 384 | fips: "13" 385 | }, 386 | { 387 | x: 0, 388 | y: 7, 389 | short: "HI", 390 | long: "Hawaii", 391 | abb: "Hawaii", 392 | fips: "15" 393 | }, 394 | { 395 | x: 3, 396 | y: 7, 397 | short: "TX", 398 | long: "Texas", 399 | abb: "Texas", 400 | fips: "48" 401 | }, 402 | { 403 | x: 8, 404 | y: 7, 405 | short: "FL", 406 | long: "Florida", 407 | abb: "Fla.", 408 | fips: "12" 409 | } 410 | ]; 411 | 412 | export default layout; 413 | -------------------------------------------------------------------------------- /packages/maps/src/Choropleth/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import { scaleQuantize } from "@vx/scale"; 4 | import { map } from "d3-collection"; 5 | import { extent } from "d3-array"; 6 | import { quantize, interpolateRgb } from "d3-interpolate"; 7 | import LoadGeometry from "../LoadGeometry"; 8 | import Projection from "../Projection"; 9 | 10 | /** 11 | * Choropleth map 12 | * TODO: legend and margins 13 | */ 14 | const Choropleth = ({ 15 | width, 16 | height, 17 | handleMouseMove, 18 | handleMouseLeave, 19 | data, 20 | valueAccessor, 21 | geometry, 22 | projection, 23 | colors, 24 | numStops, 25 | mapStroke, 26 | mapFill, 27 | idAccessor 28 | }) => { 29 | const dataMap = map(data, idAccessor); 30 | const colorArray = quantize(interpolateRgb(colors[0], colors[1]), numStops); 31 | const colorScale = scaleQuantize({ 32 | domain: extent(data, valueAccessor), 33 | range: colorArray 34 | }); 35 | return ( 36 | 37 | {feature => ( 38 | 43 | {topo => ( 44 | 45 | {topo.features.map((f, i) => { 46 | const datum = dataMap.get(f.feature.id); 47 | return ( 48 | 55 | handleMouseMove ? handleMouseMove({ event, datum }) : null 56 | } 57 | onMouseLeave={handleMouseLeave ? handleMouseLeave : null} 58 | /> 59 | ); 60 | })} 61 | 62 | )} 63 | 64 | )} 65 | 66 | ); 67 | }; 68 | 69 | Choropleth.propTypes = { 70 | width: PropTypes.number.isRequired, 71 | height: PropTypes.number.isRequired, 72 | handleMouseMove: PropTypes.func, 73 | handleMouseLeave: PropTypes.func, 74 | data: PropTypes.array.isRequired, 75 | /** 76 | * An accessor function for the data that's colored on the map 77 | */ 78 | valueAccessor: PropTypes.func.isRequired, 79 | geometry: PropTypes.oneOf(["world", "us"]).isRequired, 80 | projection: PropTypes.oneOf(["mercator", "equalEarth", "albersUsa"]) 81 | .isRequired, 82 | /** 83 | * An array of two colors, from which the color scale will be calculated 84 | */ 85 | colors: PropTypes.array, 86 | /** 87 | * The number of color stops 88 | */ 89 | numStops: PropTypes.number, 90 | mapStroke: PropTypes.string, 91 | mapFill: PropTypes.string, 92 | /** 93 | * An accessor function for the state, country, or county FIPS code in your data. This is necessary to match politcal boundaries in the feature collection to your data. 94 | */ 95 | idAccessor: PropTypes.func 96 | }; 97 | 98 | Choropleth.defaultProps = { 99 | colors: ["#e6dcff", "#504a70"], 100 | numStops: 6, 101 | mapStroke: "#fff", 102 | mapFill: "#cbcbcd", 103 | idAccessor: d => d.id 104 | }; 105 | 106 | export default Choropleth; 107 | -------------------------------------------------------------------------------- /packages/maps/src/LoadGeometry/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import { feature } from "topojson-client"; 4 | 5 | /** 6 | * Loads a geojson from our S3 bucket, and calls your child function with the topojson feature. 7 | */ 8 | class LoadGeometry extends React.Component { 9 | constructor(props) { 10 | super(props); 11 | this.state = { 12 | feature: null 13 | }; 14 | } 15 | async componentDidMount() { 16 | if (this.props.geometry === "world") { 17 | const response = await fetch( 18 | "https://s3-us-west-2.amazonaws.com/na-data-projects/geography/world.json" 19 | ); 20 | const world = await response.json(); 21 | this.setState({ 22 | feature: feature(world, world.objects.countries) 23 | }); 24 | } else if (this.props.geometry === "us") { 25 | const response = await fetch( 26 | "https://s3-us-west-2.amazonaws.com/na-data-projects/geography/us.json" 27 | ); 28 | const us = await response.json(); 29 | this.setState({ 30 | feature: feature(us, us.objects.states) 31 | }); 32 | } 33 | } 34 | render() { 35 | if (!this.state.feature) return null; 36 | return this.props.children(this.state.feature); 37 | } 38 | } 39 | 40 | LoadGeometry.propTypes = { 41 | geometry: PropTypes.oneOf(["world", "us"]).isRequired, 42 | children: PropTypes.func.isRequired 43 | }; 44 | 45 | export default LoadGeometry; 46 | -------------------------------------------------------------------------------- /packages/maps/src/Pindrop/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import LoadGeometry from "../LoadGeometry"; 4 | import Projection from "../Projection"; 5 | 6 | /** 7 | * Pindrop map component 8 | * TODO: implement overlap detection with an optional `preventOverlap` prop 9 | */ 10 | class Pindrop extends React.Component { 11 | constructor(props) { 12 | super(props); 13 | this.state = { proj: null }; 14 | this.projectionFunc = this.projectionFunc.bind(this); 15 | } 16 | 17 | projectionFunc = proj => { 18 | this.setState({ proj }); 19 | }; 20 | 21 | render() { 22 | const { 23 | width, 24 | height, 25 | handleMouseMove, 26 | handleMouseLeave, 27 | data, 28 | geometry, 29 | projection, 30 | circleRadius, 31 | circleFill, 32 | circleStroke, 33 | mapFill, 34 | mapStroke 35 | } = this.props; 36 | const { proj } = this.state; 37 | return ( 38 | 39 | {feature => ( 40 | 41 | 49 | {proj && 50 | data.map((datum, i) => { 51 | return ( 52 | { 72 | handleMouseMove 73 | ? handleMouseMove({ event, datum }) 74 | : null; 75 | }} 76 | onMouseLeave={handleMouseLeave ? handleMouseLeave : null} 77 | /> 78 | ); 79 | })} 80 | 81 | )} 82 | 83 | ); 84 | } 85 | } 86 | 87 | Pindrop.propTypes = { 88 | width: PropTypes.number.isRequired, 89 | height: PropTypes.number.isRequired, 90 | handleMouseMove: PropTypes.func, 91 | handleMouseLeave: PropTypes.func, 92 | data: PropTypes.array.isRequired, 93 | geometry: PropTypes.oneOf(["world", "us"]).isRequired, 94 | projection: PropTypes.oneOf(["mercator", "equalEarth", "albersUsa"]) 95 | .isRequired, 96 | /** 97 | * A number for the circle's radius, or a function that will receive that point's datum for [radius scaling](https://bl.ocks.org/guilhermesimoes/e6356aa90a16163a6f917f53600a2b4a). 98 | */ 99 | circleRadius: PropTypes.oneOfType([PropTypes.number, PropTypes.func]), 100 | /** 101 | * A string for each circle's fill, or a function that will receive that circle's datum 102 | */ 103 | circleFill: PropTypes.oneOfType([PropTypes.string, PropTypes.func]), 104 | /** 105 | * A string for each circle's stroke, or a function that will receive that circle's datum 106 | */ 107 | circleStroke: PropTypes.oneOfType([PropTypes.string, PropTypes.func]), 108 | mapFill: PropTypes.string, 109 | mapStroke: PropTypes.string 110 | }; 111 | 112 | Pindrop.defaultProps = { 113 | circleRadius: 5, 114 | circleFill: "#2ebcb3", 115 | circleStroke: "#fff", 116 | mapFill: "#cbcbcd", 117 | mapStroke: "#fff" 118 | }; 119 | 120 | export default Pindrop; 121 | -------------------------------------------------------------------------------- /packages/maps/src/Projection/index.js: -------------------------------------------------------------------------------- 1 | // simplified version of https://github.com/hshoff/vx/blob/master/packages/vx-geo/src/projections/Projection.js\ 2 | import React from "react"; 3 | import PropTypes from "prop-types"; 4 | import { Group } from "@vx/group"; 5 | import { geoAlbersUsa, geoEqualEarth, geoMercator, geoPath } from "d3-geo"; 6 | 7 | const projectionMapping = { 8 | mercator: () => geoMercator(), 9 | albersUsa: () => geoAlbersUsa(), 10 | equalEarth: () => geoEqualEarth() 11 | }; 12 | /** 13 | * Component for all projections. 14 | */ 15 | class Projection extends React.Component { 16 | constructor(props) { 17 | super(props); 18 | } 19 | 20 | /** 21 | * Prevent React from re-rendering the Projection on mouse move events 22 | * Only update when the chart width has changed 23 | */ 24 | shouldComponentUpdate(nextProps) { 25 | const { fitSize } = this.props; 26 | if (fitSize[0][0] !== nextProps.fitSize[0][0]) return true; 27 | return false; 28 | } 29 | 30 | render() { 31 | const { 32 | data, 33 | projection, 34 | projectionFunc, 35 | pathFunc, 36 | clipAngle, 37 | clipExtent, 38 | scale, 39 | translate, 40 | center, 41 | rotate, 42 | precision, 43 | fitExtent, 44 | fitSize, 45 | innerRef, 46 | children, 47 | ...restProps 48 | } = this.props; 49 | 50 | const currProjection = projectionMapping[projection](); 51 | 52 | if (clipAngle) currProjection.clipAngle(clipAngle); 53 | if (clipExtent) currProjection.clipExtent(clipExtent); 54 | if (scale) currProjection.scale(scale); 55 | if (translate) currProjection.translate(translate); 56 | if (center) currProjection.center(center); 57 | if (rotate) currProjection.rotate(rotate); 58 | if (precision) currProjection.rotate(precision); 59 | if (fitExtent) currProjection.fitExtent(...fitExtent); 60 | if (fitSize) currProjection.fitSize(...fitSize); 61 | 62 | const path = geoPath().projection(currProjection); 63 | 64 | const features = data.map((feature, i) => { 65 | return { 66 | feature, 67 | type: projection, 68 | projection: currProjection, 69 | index: i, 70 | centroid: path.centroid(feature), 71 | path: path(feature) 72 | }; 73 | }); 74 | 75 | if (children) return children({ path, features }); 76 | 77 | return ( 78 | 79 | {features.map((feature, i) => { 80 | return ( 81 | 82 | 88 | 89 | ); 90 | })} 91 | {pathFunc && pathFunc(path)} 92 | {projectionFunc && projectionFunc(currProjection)} 93 | 94 | ); 95 | } 96 | } 97 | 98 | Projection.propTypes = { 99 | data: PropTypes.array.isRequired, 100 | projection: PropTypes.oneOf(["mercator", "equalEarth", "albersUsa"]), 101 | projectionFunc: PropTypes.func, 102 | pathFunc: PropTypes.func, 103 | clipAngle: PropTypes.number, 104 | clipExtent: PropTypes.array, 105 | scale: PropTypes.number, 106 | translate: PropTypes.array, 107 | center: PropTypes.array, 108 | rotate: PropTypes.array, 109 | precision: PropTypes.number, 110 | fitExtent: PropTypes.array, 111 | fitSize: PropTypes.array, 112 | innerRef: PropTypes.func, 113 | children: PropTypes.func 114 | }; 115 | 116 | Projection.defaultProps = { 117 | projection: "mercator" 118 | }; 119 | 120 | export default Projection; 121 | -------------------------------------------------------------------------------- /packages/maps/src/index.js: -------------------------------------------------------------------------------- 1 | export { default as Choropleth } from "./Choropleth"; 2 | export { default as Pindrop } from "./Pindrop"; 3 | export { default as Cartogram } from "./Cartogram"; 4 | -------------------------------------------------------------------------------- /packages/meta/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/preset-react", 4 | ["@babel/preset-env", { "modules": false }] 5 | ], 6 | "plugins": [ 7 | "@babel/plugin-proposal-class-properties", 8 | "@babel/plugin-proposal-object-rest-spread" 9 | ], 10 | "ignore": ["node_modules/**"] 11 | } 12 | -------------------------------------------------------------------------------- /packages/meta/.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false -------------------------------------------------------------------------------- /packages/meta/README.md: -------------------------------------------------------------------------------- 1 | # @newamerica/meta 2 | 3 | Components for wrapping charts with New America-styled backgrounds, titles, descriptions, and sources. 4 | 5 | ### Installation 6 | 7 | ``` 8 | npm install @newamerica/meta --save 9 | ``` 10 | 11 | ### Usage Example 12 | 13 | ```jsx 14 | import { ChartContainer, Title, Description, Source } from "@newamerica/meta"; 15 | import "@newamerica/meta/dist/styles.css"; 16 | 17 | const MyChart = () => ( 18 | 19 | This is a title 20 | This is a description 21 | // your chart here 22 | This is a source 23 | 24 | ); 25 | ``` 26 | 27 | 28 | ## Components 29 | 30 | 31 | 32 | - [ChartContainer](#chartcontainer) 33 | - [Description](#description) 34 | - [Source](#source) 35 | - [Title](#title) 36 | 37 | ## API 38 | 39 | 40 | 41 | 42 | ### ChartContainer 43 | 44 | From [`./src/ChartContainer/index.js`](./src/ChartContainer/index.js) 45 | 46 | 47 | 48 | prop | type | default | required | description 49 | ---- | :----: | :-------: | :--------: | ----------- 50 | **children** | `*` | | :x: | 51 | **className** | `String` | | :x: | 52 | **full** | `Boolean` | `false` | :x: | Wraps your children in a div with the class `dv-ChartContainer__child` 53 | **noBackground** | `Boolean` | `false` | :x: | Removes the light gray background and padding from the chart container 54 | **style** | `Object` | | :x: | 55 | 56 | 57 | 58 | 59 | 60 | ### Description 61 | 62 | From [`./src/Description/index.js`](./src/Description/index.js) 63 | 64 | 65 | 66 | prop | type | default | required | description 67 | ---- | :----: | :-------: | :--------: | ----------- 68 | **children** | `*` | | :x: | 69 | **className** | `String` | | :x: | 70 | **style** | `Object` | | :x: | 71 | 72 | 73 | 74 | 75 | 76 | ### Source 77 | 78 | From [`./src/Source/index.js`](./src/Source/index.js) 79 | 80 | 81 | 82 | prop | type | default | required | description 83 | ---- | :----: | :-------: | :--------: | ----------- 84 | **children** | `*` | | :x: | 85 | **className** | `String` | | :x: | 86 | **style** | `Object` | | :x: | 87 | 88 | 89 | 90 | 91 | 92 | ### Title 93 | 94 | From [`./src/Title/index.js`](./src/Title/index.js) 95 | 96 | 97 | 98 | prop | type | default | required | description 99 | ---- | :----: | :-------: | :--------: | ----------- 100 | **children** | `*` | | :x: | 101 | **className** | `String` | | :x: | 102 | **style** | `Object` | | :x: | 103 | 104 | 105 | 106 | -------------------------------------------------------------------------------- /packages/meta/docs/description.md: -------------------------------------------------------------------------------- 1 | # @newamerica/meta 2 | 3 | Components for wrapping charts with New America-styled backgrounds, titles, descriptions, and sources. 4 | 5 | ### Installation 6 | 7 | ``` 8 | npm install @newamerica/meta --save 9 | ``` 10 | 11 | ### Usage Example 12 | 13 | ```jsx 14 | import { ChartContainer, Title, Description, Source } from "@newamerica/meta"; 15 | import "@newamerica/meta/dist/styles.css"; 16 | 17 | const MyChart = () => ( 18 | 19 | This is a title 20 | This is a description 21 | // your chart here 22 | This is a source 23 | 24 | ); 25 | ``` 26 | -------------------------------------------------------------------------------- /packages/meta/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@newamerica/meta", 3 | "version": "0.0.4", 4 | "description": "Title, description, and source components for data viz projects", 5 | "main": "dist/index.umd.js", 6 | "module": "dist/index.es.js", 7 | "repository": { 8 | "type": "git", 9 | "url": "https://github.com/newamericafoundation/teddy.git" 10 | }, 11 | "publishConfig": { 12 | "access": "public" 13 | }, 14 | "sideEffects": false, 15 | "scripts": { 16 | "start": "rollup -c -w --environment BUILD:development", 17 | "build": "rollup -c --environment BUILD:production", 18 | "prepublish": "rm -rf ./dist && npm run build", 19 | "docs": "cd ./docs && ../../../node_modules/.bin/react-docgen ../src/** | ../../../scripts/buildDocs.sh", 20 | "test": "echo \"Error: no test specified\" && exit 1" 21 | }, 22 | "dependencies": { 23 | "@newamerica/scss": "^0.0.2", 24 | "prop-types": "^15.6.2" 25 | }, 26 | "peerDependencies": { 27 | "react": "^16.2.0" 28 | }, 29 | "devDependencies": { 30 | "@babel/core": "^7.0.0", 31 | "@babel/plugin-proposal-class-properties": "^7.0.0", 32 | "@babel/plugin-proposal-object-rest-spread": "^7.0.0", 33 | "@babel/preset-env": "^7.0.0", 34 | "@babel/preset-react": "^7.0.0", 35 | "autoprefixer": "^9.4.5", 36 | "node-sass": "^4.9.2", 37 | "rollup": "^1.1.0", 38 | "rollup-plugin-babel": "^4.3.1", 39 | "rollup-plugin-node-resolve": "^4.0.0", 40 | "rollup-plugin-postcss": "^1.6.3", 41 | "rollup-plugin-terser": "^4.0.2" 42 | }, 43 | "author": "lorenries", 44 | "license": "MIT" 45 | } 46 | -------------------------------------------------------------------------------- /packages/meta/rollup.config.js: -------------------------------------------------------------------------------- 1 | import resolve from "rollup-plugin-node-resolve"; 2 | import babel from "rollup-plugin-babel"; 3 | import { terser } from "rollup-plugin-terser"; 4 | import postcss from "rollup-plugin-postcss"; 5 | import autoprefixer from "autoprefixer"; 6 | import pkg from "./package.json"; 7 | 8 | const deps = Object.keys({ 9 | ...pkg.dependencies, 10 | ...pkg.peerDependencies 11 | }); 12 | 13 | const globals = deps.reduce((o, name) => { 14 | if (name.includes("@vx/")) { 15 | o[name] = "vx"; 16 | } 17 | if (name.includes("d3-")) { 18 | o[name] = "d3"; 19 | } 20 | if (name === "react") { 21 | o[name] = "React"; 22 | } 23 | if (name === "react-dom") { 24 | o[name] = "ReactDOM"; 25 | } 26 | if (name === "prop-types") { 27 | o[name] = "PropTypes"; 28 | } 29 | if (name === "classnames") { 30 | o[name] = "classNames"; 31 | } 32 | return o; 33 | }, {}); 34 | 35 | export default [ 36 | { 37 | input: "src/index.js", 38 | external: deps, 39 | plugins: [ 40 | resolve(), 41 | babel({ 42 | exclude: "node_modules/**" 43 | }), 44 | postcss({ 45 | extensions: [".css", ".scss"], 46 | plugins: [autoprefixer], 47 | minimize: true, 48 | inject: false, 49 | extract: "dist/styles.css" 50 | }), 51 | process.env.BUILD === "production" && terser() 52 | ], 53 | output: { 54 | file: pkg.main, 55 | format: "umd", 56 | name: "charts", 57 | globals 58 | } 59 | }, 60 | { 61 | input: "src/index.js", 62 | external: deps, 63 | plugins: [ 64 | resolve(), 65 | babel({ 66 | exclude: "node_modules/**" 67 | }), 68 | postcss({ 69 | extensions: [".css", ".scss"], 70 | plugins: [autoprefixer], 71 | minimize: true, 72 | inject: false, 73 | extract: "dist/styles.css" 74 | }), 75 | process.env.BUILD === "production" && terser() 76 | ], 77 | output: { file: pkg.module, format: "es", globals } 78 | } 79 | ]; 80 | -------------------------------------------------------------------------------- /packages/meta/src/ChartContainer/ChartContainer.scss: -------------------------------------------------------------------------------- 1 | @import "~@newamerica/scss/spacing"; 2 | .dv-ChartContainer { 3 | width: 100%; 4 | position: relative; 5 | padding: $s3; 6 | background-color: #f5f5f5; 7 | &-nobg { 8 | padding: 0; 9 | background-color: transparent; 10 | } 11 | } 12 | 13 | .dv-ChartContainer__child { 14 | max-width: 1200px; 15 | margin-left: auto; 16 | margin-right: auto; 17 | } -------------------------------------------------------------------------------- /packages/meta/src/ChartContainer/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import "./ChartContainer.scss"; 4 | 5 | const ChartContainer = ({ children, style, className, full, noBackground }) => ( 6 |
12 | {full ? ( 13 |
{children}
14 | ) : ( 15 | children 16 | )} 17 |
18 | ); 19 | 20 | ChartContainer.propTypes = { 21 | children: PropTypes.any, 22 | style: PropTypes.object, 23 | className: PropTypes.string, 24 | /** 25 | * Wraps your children in a div with the class `dv-ChartContainer__child` 26 | */ 27 | full: PropTypes.bool, 28 | /** 29 | * Removes the light gray background and padding from the chart container 30 | */ 31 | noBackground: PropTypes.bool 32 | }; 33 | 34 | ChartContainer.defaultProps = { 35 | full: false, 36 | noBackground: false 37 | }; 38 | 39 | export default ChartContainer; 40 | -------------------------------------------------------------------------------- /packages/meta/src/Description/Description.scss: -------------------------------------------------------------------------------- 1 | .dv-chart__description { 2 | display: block; 3 | font-size: 14px; 4 | padding-bottom: 1rem; 5 | } -------------------------------------------------------------------------------- /packages/meta/src/Description/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import "./Description.scss"; 4 | 5 | const Description = ({ children, className, style }) => ( 6 | 10 | {children} 11 | 12 | ); 13 | 14 | Description.propTypes = { 15 | children: PropTypes.any, 16 | className: PropTypes.string, 17 | style: PropTypes.object 18 | }; 19 | 20 | export default Description; 21 | -------------------------------------------------------------------------------- /packages/meta/src/Source/Source.scss: -------------------------------------------------------------------------------- 1 | @import "~@newamerica/scss/spacing"; 2 | .dv-chart__source { 3 | display: block; 4 | padding-top: $s3; 5 | font-size: 0.75rem; 6 | } 7 | -------------------------------------------------------------------------------- /packages/meta/src/Source/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import "./Source.scss"; 4 | 5 | const Source = ({ children, className, style }) => ( 6 | 10 | {children} 11 | 12 | ); 13 | 14 | Source.propTypes = { 15 | children: PropTypes.any, 16 | className: PropTypes.string, 17 | style: PropTypes.object 18 | }; 19 | 20 | export default Source; 21 | -------------------------------------------------------------------------------- /packages/meta/src/Title/Title.scss: -------------------------------------------------------------------------------- 1 | .dv-chart__title { 2 | font-size: 1.125rem; 3 | line-height: 1.35rem; 4 | margin: 0; 5 | padding-bottom: 1rem; 6 | } 7 | -------------------------------------------------------------------------------- /packages/meta/src/Title/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import "./Title.scss"; 4 | 5 | const Title = ({ children, className, style }) => ( 6 |

7 | {children} 8 |

9 | ); 10 | 11 | Title.propTypes = { 12 | children: PropTypes.any, 13 | className: PropTypes.string, 14 | style: PropTypes.object 15 | }; 16 | 17 | export default Title; 18 | -------------------------------------------------------------------------------- /packages/meta/src/index.js: -------------------------------------------------------------------------------- 1 | export { default as ChartContainer } from "./ChartContainer"; 2 | export { default as Title } from "./Title"; 3 | export { default as Description } from "./Description"; 4 | export { default as Source } from "./Source"; 5 | -------------------------------------------------------------------------------- /packages/scss/_box-shadow.scss: -------------------------------------------------------------------------------- 1 | @mixin standard-shadow { 2 | $s: 0 2px 5px 0 rgba(0, 0, 0, 0.15), 0 2px 10px 0 rgba(0, 0, 0, 0.1); 3 | -webkit-box-shadow: $s; 4 | box-shadow: $s; 5 | } -------------------------------------------------------------------------------- /packages/scss/_breakpoints.scss: -------------------------------------------------------------------------------- 1 | $breakpoints: ( 2 | small: 0, 3 | medium: 640px, 4 | large: 1024px, 5 | xlarge: 1320px, 6 | xxlarge: 1440px 7 | ); 8 | @mixin breakpoint($breakpoint) { 9 | @if map-has-key($breakpoints, $breakpoint) { 10 | @media (min-width: #{map-get($breakpoints, $breakpoint)}) { 11 | @content; 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /packages/scss/_colors.scss: -------------------------------------------------------------------------------- 1 | $_color-palette: ( 2 | na-turquoise: #2ebcb3, 3 | na-light-turquoise: #46ccc3, 4 | na-dark-turquoise: #2a8e88, 5 | oti-blue: #5ba4da, 6 | oti-light-blue: #75bef4, 7 | dark-oti-blue: #477da3, 8 | cinnabar-red: #e75c64, 9 | dark-cinnabar-red: #477da3, 10 | na-black: #2c2f35, 11 | na-light-black: #5f6268, 12 | na-black-70: rgba(44, 47, 53, 0.7), 13 | na-black-40: #6b6d71, 14 | na-black-30: #c0c1c3, 15 | na-black-10: #e0e0e1, 16 | na-black-05: #f4f4f4, 17 | na-white: #ffffff, 18 | shadow: rgba(0, 0, 0, 0.25), 19 | overlay: rgba(0, 0, 0, 0.2) 20 | ); 21 | /* 22 | * Color palette usage: 23 | * background-color: palette-get(cinnabar-red); 24 | */ 25 | 26 | @function palette-get($key) { 27 | @return map-get($_color-palette, $key); 28 | } 29 | -------------------------------------------------------------------------------- /packages/scss/_spacing.scss: -------------------------------------------------------------------------------- 1 | $s0: 0; 2 | $s1: 0.25rem; 3 | $s2: 0.5rem; 4 | $s3: 1rem; 5 | $s4: 2rem; 6 | $s5: 4rem; 7 | $s6: 8rem; 8 | $s7: 16rem; 9 | -------------------------------------------------------------------------------- /packages/scss/_widths.scss: -------------------------------------------------------------------------------- 1 | $w1: 1rem; 2 | $w2: 2rem; 3 | $w3: 4rem; 4 | $w4: 8rem; 5 | $w5: 16rem; 6 | $mw1: 1rem; 7 | $mw2: 2rem; 8 | $mw3: 4rem; 9 | $mw4: 8rem; 10 | $mw5: 16rem; 11 | $mw6: 32rem; 12 | $mw7: 48rem; 13 | $mw8: 64rem; 14 | $mw9: 96rem; 15 | -------------------------------------------------------------------------------- /packages/scss/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@newamerica/scss", 3 | "version": "0.0.2", 4 | "description": "Sass partials shared across packages", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/newamericafoundation/teddy.git" 8 | }, 9 | "publishConfig": { 10 | "access": "public" 11 | }, 12 | "scripts": { 13 | "test": "echo \"Error: no test specified\" && exit 1" 14 | }, 15 | "keywords": [ 16 | "scss" 17 | ], 18 | "author": "lorenries", 19 | "license": "MIT" 20 | } 21 | -------------------------------------------------------------------------------- /packages/storybook/.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false -------------------------------------------------------------------------------- /packages/storybook/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@newamerica/storybook", 3 | "version": "0.0.11", 4 | "description": "Storybook for teddy components", 5 | "main": "index.js", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/newamericafoundation/teddy.git" 9 | }, 10 | "private": true, 11 | "scripts": { 12 | "test": "", 13 | "storybook": "start-storybook -p 9001 -c ./src", 14 | "build-storybook": "build-storybook -c ./src -o ./dist && aws s3 sync ./dist s3://datadotnewamerica/component-library && aws cloudfront create-invalidation --distribution-id E15K2IVEDI1Y6H --paths \"/component-library/*\"" 15 | }, 16 | "author": "lorenries", 17 | "license": "MIT", 18 | "dependencies": { 19 | "@newamerica/charts": "^0.0.4", 20 | "@newamerica/components": "^0.0.6", 21 | "@newamerica/data-table": "^0.0.9", 22 | "@newamerica/maps": "^0.0.3", 23 | "@newamerica/meta": "^0.0.4", 24 | "@newamerica/timeline": "^0.0.6", 25 | "@vx/mock-data": "0.0.182", 26 | "react": "^16.3.0", 27 | "react-annotation": "^2.1.5", 28 | "react-dom": "^16.3.0" 29 | }, 30 | "devDependencies": { 31 | "@babel/core": "^7.0.0", 32 | "@sambego/storybook-styles": "^1.0.0", 33 | "@storybook/addon-knobs": "^4.1.4", 34 | "@storybook/addon-options": "^4.1.4", 35 | "@storybook/react": "^4.1.4", 36 | "babel-loader": "^8.0.5", 37 | "css-loader": "^2.1.0", 38 | "node-sass": "^4.9.2", 39 | "sass-loader": "^7.1.0", 40 | "storybook-readme": "^4.0.5", 41 | "style-loader": "^0.23.1", 42 | "webpack": "^4.28.4" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /packages/storybook/src/addons.js: -------------------------------------------------------------------------------- 1 | import "@storybook/addon-options/register"; 2 | -------------------------------------------------------------------------------- /packages/storybook/src/config.js: -------------------------------------------------------------------------------- 1 | import { configure, addDecorator } from "@storybook/react"; 2 | import { withOptions } from "@storybook/addon-options"; 3 | 4 | addDecorator( 5 | withOptions({ 6 | name: "Teddy", 7 | showAddonPanel: false 8 | }) 9 | ); 10 | 11 | function loadStories() { 12 | require("./stories/index.js"); 13 | } 14 | 15 | configure(loadStories, module); 16 | -------------------------------------------------------------------------------- /packages/storybook/src/lib/colors.js: -------------------------------------------------------------------------------- 1 | const colors = { 2 | turquoise: { 3 | very_light: "#97DED9", 4 | very_light_2: "#62CDC6", 5 | light: "#2EBCB3", 6 | medium: "#1A8A84", 7 | dark: "#005753" 8 | }, 9 | blue: { 10 | very_light: "#ADD2ED", 11 | very_light_2: "#84BBE4", 12 | light: "#5BA4DA", 13 | medium: "#4378A0", 14 | dark: "#234A67", 15 | very_dark: "#1B384E" 16 | }, 17 | red: { light: "#E75C64", medium: "#A64046", dark: "#692025" }, 18 | purple: { 19 | very_light: "#bd9fc6", 20 | light: "#A076AC", 21 | medium: "#74557E", 22 | dark: "#48304F" 23 | }, 24 | grey: { 25 | light: "#EAEAEB", 26 | medium_light: "#CBCBCD", 27 | medium: "#ABACAE", 28 | dark: "#2C2F35" 29 | }, 30 | orange: { light: "#f19348", medium: "#ac6a31", dark: "#6d3f13" }, 31 | yellow: { light: "#f4dc70", medium: "#ae9f51", dark: "#6c642f" }, 32 | brown: { light: "#bf9963", medium: "#8d7248", dark: "#574527" }, 33 | white: "#FFFFFF", 34 | black: "#2c2f35" 35 | }; 36 | 37 | function getDefaultColor(code) { 38 | return colors[code].light; 39 | } 40 | 41 | export { colors, getDefaultColor }; 42 | -------------------------------------------------------------------------------- /packages/storybook/src/stories/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { storiesOf } from "@storybook/react"; 3 | import styles from "@sambego/storybook-styles"; 4 | import { 5 | Chart, 6 | Bar, 7 | HorizontalBar, 8 | HorizontalStackedBar, 9 | VerticalGroupedBar, 10 | Line, 11 | Scatterplot 12 | } from "@newamerica/charts"; 13 | import { 14 | Search, 15 | Select, 16 | Slider, 17 | CheckboxGroup, 18 | Toggle, 19 | ButtonGroup 20 | } from "@newamerica/components"; 21 | import { Pindrop, Choropleth, Cartogram } from "@newamerica/maps"; 22 | import { ChartContainer, Title, Description, Source } from "@newamerica/meta"; 23 | import { Timeline } from "@newamerica/timeline"; 24 | import { DataTableWithSearch } from "@newamerica/data-table"; 25 | import { cityTemperature } from "@vx/mock-data"; 26 | import { AnnotationCalloutCircle } from "react-annotation"; 27 | import { colors } from "../lib/colors"; 28 | import "./newamericadotorg.lite.css"; 29 | import "@newamerica/charts/dist/styles.css"; 30 | import "@newamerica/data-table/dist/styles.css"; 31 | import "@newamerica/components/dist/styles.css"; 32 | import "@newamerica/timeline/dist/styles.css"; 33 | import "@newamerica/meta/dist/styles.css"; 34 | import "@newamerica/maps/dist/styles.css"; 35 | 36 | function getRandomInt(min, max) { 37 | min = Math.ceil(min); 38 | max = Math.floor(max); 39 | return Math.floor(Math.random() * (max - min)) + min; //The maximum is exclusive and the minimum is inclusive 40 | } 41 | 42 | export default ChartContainer; 43 | 44 | class LoadData extends React.Component { 45 | constructor(props) { 46 | super(props); 47 | this.state = { 48 | data: null 49 | }; 50 | } 51 | componentDidMount() { 52 | fetch(this.props.url) 53 | .then(data => data.json()) 54 | .then(data => { 55 | this.setState({ data }); 56 | }); 57 | } 58 | render() { 59 | if (!this.state.data) { 60 | return
loading
; 61 | } 62 | return this.props.children(this.state.data); 63 | } 64 | } 65 | 66 | storiesOf("Charts", module) 67 | .addDecorator( 68 | styles({ 69 | maxWidth: "600px", 70 | width: "100%", 71 | margin: "1rem auto", 72 | padding: "0 1rem" 73 | }) 74 | ) 75 | .add("Bar Chart", () => { 76 | const barData = new Array(5).fill(0).map((val, i) => ({ 77 | key: `Bar ${i + 1}`, 78 | value: getRandomInt(1, 40) 79 | })); 80 | const tooltip = ({ datum }) => ( 81 |
82 | {datum.key}: {datum.value} 83 |
84 | ); 85 | 86 | return ( 87 | 88 | This is a bar chart 89 | This is a description for the bar chart 90 | 91 | {chartProps => { 92 | return ( 93 | d.key} 96 | y={d => d.value} 97 | yAxisLabel="This is an axis label" 98 | margin={{ top: 10, left: 55, right: 10, bottom: 30 }} 99 | {...chartProps} 100 | /> 101 | ); 102 | }} 103 | 104 | This is a source for the bar chart 105 | 106 | ); 107 | }); 108 | 109 | storiesOf("Charts", module) 110 | .addDecorator( 111 | styles({ 112 | maxWidth: "600px", 113 | width: "100%", 114 | margin: "1rem auto", 115 | padding: "0 1rem" 116 | }) 117 | ) 118 | .add("Horizontal Bar Chart", () => { 119 | const barData = new Array(5).fill(undefined).map((val, i) => ({ 120 | key: `Bar ${i + 1}`, 121 | value: getRandomInt(1, 40) 122 | })); 123 | 124 | const tooltip = ({ datum }) => ( 125 |
126 | {datum.key}: {datum.value} 127 |
128 | ); 129 | 130 | return ( 131 | 132 | This is a title for the horizontal bar chart 133 | 134 | This is a description for the horizontal bar chart 135 | 136 | 137 | {chartProps => ( 138 | d.value} 141 | y={d => d.key} 142 | numTicksX={width => (width < 400 ? 4 : 6)} 143 | margin={{ top: 10, left: 40, right: 10, bottom: 20 }} 144 | {...chartProps} 145 | /> 146 | )} 147 | 148 | 149 | This is a source 150 | 151 | ); 152 | }); 153 | 154 | storiesOf("Charts", module) 155 | .addDecorator( 156 | styles({ 157 | maxWidth: "600px", 158 | width: "100%", 159 | margin: "1rem auto", 160 | padding: "0 1rem" 161 | }) 162 | ) 163 | .add("Scatterplot", () => { 164 | const scatterData = new Array(30).fill(undefined).map((val, i) => ({ 165 | name: i, 166 | x: getRandomInt(i, (i + 1) * 5), 167 | y: getRandomInt(i, (i + 1) * 5) 168 | })); 169 | return ( 170 | 171 | This is a scatterplot 172 | This is a description 173 | 174 |
Tooltip
} 178 | > 179 | {chartProps => ( 180 | d.x} 183 | y={d => d.y} 184 | xAxisLabel="This is an axis label" 185 | yAxisLabel="This is an axis label" 186 | numTicksX={width => (width < 400 ? 5 : 7)} 187 | margin={{ 188 | top: 10, 189 | bottom: 50, 190 | left: 55, 191 | right: 10 192 | }} 193 | {...chartProps} 194 | /> 195 | )} 196 |
197 | 198 | This is a source 199 |
200 | ); 201 | }); 202 | 203 | storiesOf("Charts", module) 204 | .addDecorator( 205 | styles({ 206 | maxWidth: "600px", 207 | width: "100%", 208 | margin: "1rem auto", 209 | padding: "0 1rem" 210 | }) 211 | ) 212 | .add("Line Chart", () => { 213 | const url = 214 | "https://na-data-projects.s3.amazonaws.com/data/nann/network_research.json"; 215 | 216 | const tooltip = ({ datum }) => ( 217 |
218 | {datum.year}: {datum.cumulative} 219 |
220 | ); 221 | return ( 222 | 223 | {data => ( 224 | 225 | This is a line chart 226 | 227 | Instead of a fixed height, this chart has an aspect ratio that it 228 | will maintain on all screen sizes 229 | 230 | 231 | {props => ( 232 | 233 | d.year} 236 | y={d => +d.cumulative} 237 | yAxisLabel="Label" 238 | margin={{ top: 10, left: 55, right: 10, bottom: 30 }} 239 | numTicksX={width => (width < 350 ? 3 : 8)} 240 | {...props} 241 | /> 242 | 256 | 257 | )} 258 | 259 | This is a source 260 | 261 | )} 262 | 263 | ); 264 | }); 265 | 266 | storiesOf("Charts", module) 267 | .addDecorator( 268 | styles({ 269 | maxWidth: "600px", 270 | width: "100%", 271 | margin: "1rem auto", 272 | padding: "0 1rem" 273 | }) 274 | ) 275 | .add("Horizontal Stacked Bar", () => ( 276 | 277 | This is a horizontal stacked bar chart 278 | This is a description 279 | ( 283 |
284 | {datum.key}: 285 | {datum.bar.data[datum.key]} 286 |
287 | )} 288 | > 289 | {props => ( 290 | d["date"]} 292 | keys={Object.keys(cityTemperature[0]).filter(d => d !== "date")} 293 | colors={[ 294 | colors.turquoise.light, 295 | colors.blue.light, 296 | colors.purple.light 297 | ]} 298 | margin={{ top: 35, left: 60, right: 40, bottom: 20 }} 299 | data={[...cityTemperature.slice(0, 10)]} 300 | {...props} 301 | /> 302 | )} 303 |
304 | 305 | This is a source 306 |
307 | )); 308 | 309 | storiesOf("Charts", module) 310 | .addDecorator( 311 | styles({ 312 | maxWidth: "600px", 313 | width: "100%", 314 | margin: "1rem auto", 315 | padding: "0 1rem" 316 | }) 317 | ) 318 | .add("Vertical Grouped Bar", () => ( 319 | 320 | This is a title 321 | This is a short description 322 |
Tooltip
}> 323 | {props => ( 324 | d["date"]} 327 | keys={Object.keys(cityTemperature[0]).filter(d => d !== "date")} 328 | colors={[ 329 | colors.turquoise.light, 330 | colors.blue.light, 331 | colors.purple.light 332 | ]} 333 | margin={{ top: 50, left: 25, right: 10, bottom: 30 }} 334 | {...props} 335 | /> 336 | )} 337 |
338 | This is a source 339 |
340 | )); 341 | 342 | storiesOf("Maps", module) 343 | .addDecorator( 344 | styles({ 345 | maxWidth: "850px", 346 | width: "100%", 347 | margin: "1rem auto", 348 | padding: "0 1rem" 349 | }) 350 | ) 351 | .add("Pindrop Map", () => { 352 | return ( 353 | 354 | {data => { 355 | return ( 356 | 357 | This is a title 358 | This is a description 359 |
Tooltip
} 363 | > 364 | {props => ( 365 | 372 | )} 373 |
374 | This is a source 375 |
376 | ); 377 | }} 378 |
379 | ); 380 | }); 381 | 382 | storiesOf("Maps", module) 383 | .addDecorator( 384 | styles({ 385 | maxWidth: "850px", 386 | width: "100%", 387 | margin: "1rem auto", 388 | padding: "0 1rem" 389 | }) 390 | ) 391 | .add("U.S. Choropleth", () => { 392 | return ( 393 | 394 | {data => { 395 | return ( 396 | 397 | This is a title for the map 398 | This is a description 399 |
Tooltip
}> 400 | {props => ( 401 | +d["average_net_price"]} 406 | idAccessor={d => d.id} 407 | mapStroke="#f5f5f5" 408 | {...props} 409 | /> 410 | )} 411 |
412 | 413 | This is a source 414 |
415 | ); 416 | }} 417 |
418 | ); 419 | }); 420 | 421 | storiesOf("Maps", module) 422 | .addDecorator( 423 | styles({ 424 | maxWidth: "850px", 425 | margin: "1rem auto", 426 | padding: "0 1rem" 427 | }) 428 | ) 429 | .add("World Choropleth", () => { 430 | return ( 431 | 432 | {data => { 433 | return ( 434 | 435 | This is a title for the map 436 | 437 | These maps are completely responsive. Try resizing your browser. 438 | 439 |
Tooltip
}> 440 | {props => ( 441 | +d["average_net_price"]} 446 | mapStroke="#f5f5f5" 447 | {...props} 448 | /> 449 | )} 450 |
451 | This is a source 452 |
453 | ); 454 | }} 455 |
456 | ); 457 | }); 458 | 459 | storiesOf("Maps", module) 460 | .addDecorator( 461 | styles({ 462 | maxWidth: "850px", 463 | margin: "1rem auto", 464 | padding: "0 1rem" 465 | }) 466 | ) 467 | .add("Cartogram", () => { 468 | return ( 469 | 470 | {data => { 471 | return ( 472 | 473 | This is a title for the map 474 | 475 | These maps are completely responsive. Try resizing your browser. 476 | 477 | +d["average_net_price"]} 482 | renderTooltip={() =>
Tooltip
} 483 | /> 484 | This is a source 485 |
486 | ); 487 | }} 488 |
489 | ); 490 | }); 491 | 492 | storiesOf("Timeline", module).add("Timeline", () => { 493 | const url = 494 | "https://na-data-projects.s3.amazonaws.com/data/isp/proxy_warfare.json"; 495 | return ( 496 | 497 | {data => { 498 | const _data = data.timeline.map((val, i) => ({ 499 | ...val, 500 | date: new Date(val.date) 501 | })); 502 | return ( 503 | 504 | This is a Title 505 | 506 | 507 | ); 508 | }} 509 | 510 | ); 511 | }); 512 | 513 | storiesOf("Data Table", module) 514 | .addDecorator( 515 | styles({ 516 | width: "100%" 517 | }) 518 | ) 519 | .add("Data Table", () => { 520 | return ( 521 | 522 | 533 | 534 | ); 535 | }); 536 | 537 | storiesOf("Components", module) 538 | .addDecorator( 539 | styles({ 540 | padding: "0 0.5rem" 541 | }) 542 | ) 543 | .add("Search", () => { 544 | return console.log(val)} />; 545 | }); 546 | 547 | storiesOf("Components", module) 548 | .addDecorator( 549 | styles({ 550 | padding: "0 0.5rem" 551 | }) 552 | ) 553 | .add("Select", () => { 554 | return ( 555 |